Flask想上線? 你還需要一些酷東西

Posted by MingLun Allen Wu on Friday, January 22, 2021

前言

從研究所開始接觸 Flask 也有兩三年的時間了。

這個輕量級的框架的確很適合拿來「快速」建立網頁或是API,如果需要擴充功能,也有不少第三方工具可以支援: 例如透過 Flask-login 來處理會員登入/登出功能。

不過 Flask 的負載量是個考驗,一個人在本地端測試通常都沒有問題,但要上線讓多人使用時,心中總是忐忑不安,研究所時用Flask做了一個平台,最害怕人家問我:

欸!阿你網頁卡住了怎麼辦?

有使用過 Flask 的人一定知道,啟動服務時, Flask總是會「友善」的跳出下方提醒:

連官方都提醒你要記得更換 WSGI Server 了,這到底是什麼東西?

接下來讓我們從 Flask 為起點,依序介紹 Application Server, WSGI Server, Web Server的概念,以及如何設定服務,提高 Flask 的負載率。


Application Server

在旅程的一開始,我們先在地圖放下第一個元件: Application Server

常見的 Flask, Django框架都屬於這個層級,主要是負責:

接受客製化的Request,執行程式碼後,回傳客製化的Response

Application Server接受使用者傳送的Request,將其轉送(Routing)至對應的程式碼進行處理、運算,最後回傳客製化的結果

這段話看起來有點拗口,但其實就是一般API所做的任務。

由於 Application Server 可以根據使用者的需求(參數)進行不同的運算,例如:資料庫的存取、資料的匯總,從而回傳不同的結果,我們稱之為「動態伺服器」。


WSGI Server

WSGI Server 是用來處理 WSGI 協定的伺服器

WSGI Server 加入的位置在UserApplication Server之間,加入後的地圖長這樣:

介紹 WSGI Server 前,我們必須先說明何謂 WSGI (備註:發音跟英文的威士忌一樣!)

WSGI 協定

定義HTTP Request(字串) 如何與 Application Server 互動

WSGI協定的全名是: Python Web Server Gateway Interface

這個協定制定了一套規則,規定 HTTP Request 要如何與 Application Server (請見上節)溝通。

我們透過下面這張圖來說明 WSGI 協定的流程:

接下來依序說明每一個步驟:

1. HTTP Request

瀏覽器造訪服務、呼叫API時,會發送HTTP Request,可視為「有特定格式」的字串,通常會包含:

  • Request Header
  • Request Method
  • Request URL
  • Message Body

細節部分不贅述,有興趣的讀者可以自行Google

2. Parse, 封裝Environ

WSGI Server 接收到 HTTP Request 後,會將這些字串解析成Key-Value的形式,儲存至environ變數之中:

{
    'REQUEST_HEADER': 'GET',
    'PATH_INFO': '/url/',
    'SERVER_PROTOCL': 'HTTP/1.1',
    'HTTP_EXAMPLE_HEADER': 'example value',
    'wsgi.input': <_io.BytesIO>,
    ...
}    

environ 除了使用者資訊(例如表單)外,還會附加些許系統資訊,這些內容將成為 Application Server 啟動函式的依據。

3. 調用App

WSGI Server 會將封裝好的變數 environ 送至 Application Server

此外,還會同時傳送一個 callback function,讓 Application Server 完成運算後,能夠知道要將訊息送至何處,將於第五步驟進一步說明。

4. 邏輯處理

Application Server 接收到 environ 後,會以這些資訊做為環境變數,呼叫特定的程式碼進行運算。

5. 回傳 HTTP Status Header

當前一步驟的「邏輯處理」完成後,在回傳結果前,會先透過步驟3的 callback functionResponse Header狀態碼(Ex: 200成功, 500失敗…)先傳回瀏覽器。

6. Response Body

在這個步驟才會將運算後的結果傳回 WSGI Server

7. HTTP Response

在步驟2中 WSGI ServerHTTP Request字串轉換為類似Dictionary的格式。

在此步驟則是反向轉換,將前一步驟回傳的結果轉譯成 HTTP Response(字串格式)。

替換 WSGI Server

了解 WSGI 協定的基本流程後,我們可以將WSGI Server理解成處理 HTTP Request(字串) 與 Python 可理解的 Input/Output 的中繼站(Middleware)。

所有支援 WSGI 協定的 Server 都可稱為 WSGI Server,現在比較常見的WSGI Servergunicornuwsgi

回到一開始所提出的問題: 為什麼 Flask 會要求我們替換 WSGI Server 呢?

Flask身為一個輕量級的框架,為了讓使用者不需要進行過多設定就能使用,所以已內建較為陽春的WSGI Server (Werkzeug),負責處理HTTP RequestFlask間資料的轉換。

然而,Flask官方文件有提到Werkzeug過於簡陋,只能算是WSGI工具包(Toolkit),所以在處理「短時間多個Request」時的負載能力不佳,如果有較大量的流量需求,建議使用額外的WSGI Server來取代Werkzeug

使用 gunicorn 等較為成熟的WSGI Server,能夠使用 Multithreading, Multiprocessing的機制來增加負載能力。

如何使用 gunicorn 來替換 Flask 內建的 Werkzeug? 將在稍後的章節介紹。

讓我們先繼續完成地圖!


Web Server

最後,讓我們在 WSGI Server 前方加入下一個元件: Web Server

常見的 Apache, Nginx 都是屬於 Web Server 的範疇,它的功能有下列三項:

  1. 靜態檔案快取:

    將大型的文件暫存在使用者的瀏覽器,以降低重複造訪時的讀取時間

    快取的目的是讓系統的回應速度變快,減少等待 Response 的時間。 如果網站中包含了大量的靜態檔案(圖片、js、css 檔案),設置快取可以讓瀏覽器緩存這些文件。 當使用第二次造訪網站時就不需要重新下載這些檔案,達到加速的效果。

    值得一提的是:這些快取僅限於 「靜態檔案」,在發送 Request 的過程中不涉及運算,任何使用者造訪都將取得相同內容(例如首頁的封面圖)。 如果發送Request時有額外的參數、需要進行客製化的運算,則屬於「動態」請求,這是屬於前一小節 Application Server 處理的範疇。

  2. 負載平衡(Load Balancer):

    扮演門神,所有的Request將依循其指引,前往該去的地方

    當服務流量太高時,單靠一台 Server 可能不足以負載,會同時有多台 Server 提供服務。

    每一台Server的位置都不同,我們可不能請使用者自動分流:

    注意:請身分證字號最後一碼是奇數的,使用 xxx.xxx.xx.xxx 位置、最後一碼是偶數的,則使用 yyy.yyy.yyy.yy 位置。

    這好嗎?這不好。 :) 會出事的

    這時候我們就需要透過 Web Server 扮演看門人,所有的 Request 都會經過它,由其判斷該將 Request 導向哪一台 Server,通常會有幾種策略:

    • 輪循(Round Robin):

      假設共有三台Server(A,B,C)提供服務,每一個Request依照 A, B, C, A, B, C…的順序分配。

    • 最小負載:

      將當前 Request 導向目前負載量最小的 Server。

    • IP Hashtable:

      將發送 Request 的 IP 送入雜湊表中,決定該送往哪一台 Server。特性是當同一個 IP 位置再次造訪時,能夠導向同一台 Server。

    這些策略相當繁多,在此不多做停留。

  3. 反向代理:

    隱藏真正的Server位置

    儘管負載平衡機制會指派不同的 Server 處理 Request,但對於客戶端來說,所有的 Request 都是同一台 Server 在處理(下圖中的Web Server),不需要也不會知道背後真正處理的 Server 是哪一台。 換言之: 真正的 Server 位置被隱藏了

    不管今天是由圖中的 Server1, Server2 還是 Server3 提供服務,對於使用者來說,所有的 Request 都是送往 123.45.67.89 這個位置,使用者無從得知真正提供服務的Server路徑為何。


統整

目前我們介紹了三種不同類型的Server:

  1. Application Server:

    • 代表服務: Flask, Django
    • 特色: 負責商業邏輯處理、根據URL、參數不同,執行不同的程式碼
  2. WSGI Server:

    • 代表服務: gunicorn, uWsgi
    • 特色:根據WSGI協定,負責「HTTP協定的內容(字串)」和「Application Server能理解的內容」之間的轉換
  3. Web Server:

    • 代表服務: Nginx, Apache
    • 特色:靜態檔案快取、負載平衡、反向代理

如何設定gunicorn

安裝

pip install gunicorn

建立一個簡易的Flask App

先建立一個簡易的 run.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

如果要以 Flask 內建的 Werkzeug 作為 WSGI Server,只要執行下列指令即可啟動:

python run.py

以gunicorn作為WSGI Server

首先我們要先建立一個新的 wsgi.py,並在其中載入 run.py建構的 app:

from run import app

接著在 Bash Terminal 執行下列指令

# gunicorn --workers=<整數> --threads=<整數> <wsgi檔名>:<app名稱> 
gunicorn --workers=4 --threads=4 wsgi:app

如果沒有噴錯,就已經成功替換 WSGI Server了!恭喜!

如果希望 gunicorn 能在背景執行,只需要在上方執行指令加上 -d 標籤。

此時如果使用 ps -aux | grep gunicorn 指令搜尋 Process,應該可以看到同時有多個Process正在執行。

結語

使用 Flask 作為首選框架已經好長一段時間,對於要將服務部署到正式環境總是忐忑不安。

終於有機會花了點時間,整理這部分的架構及實作方式,對於 WSGI 協定部分的 HTTP 機制不甚熟悉,如果有這部分專業的朋友,歡迎指教xD。

近年Python有一個更快速簡潔的框架 FastAPI 正在興起,目前正在研究,如果有興趣的朋友也歡迎點擊收看:

Fast API 入門筆記 (一)

希望這篇文章對大家有幫助!下次再見!



See Also

監控資料品質的旅程