前言
從研究所開始接觸 Flask 也有兩三年的時間了。
這個輕量級的框架的確很適合拿來「快速」建立網頁或是API,如果需要擴充功能,也有不少第三方工具可以支援: 例如透過 Flask-login 來處理會員登入/登出功能。
不過 Flask 的負載量是個考驗,一個人在本地端測試通常都沒有問題,但要上線讓多人使用時,心中總是忐忑不安,研究所時用Flask做了一個平台,最害怕人家問我:
欸!阿你網頁卡住了怎麼辦?

有使用過 Flask 的人一定知道,啟動服務時, Flask總是會「友善」的跳出下方提醒:
Use a production WSGI server instead.
連官方都提醒你要記得更換 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 加入的位置在User 跟 Application 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 function 將Response Header及狀態碼(Ex: 200成功, 500失敗…)先傳回瀏覽器。
6. Response Body
在這個步驟才會將運算後的結果傳回 WSGI Server。
7. HTTP Response
在步驟2中 WSGI Server 將HTTP Request由字串轉換為類似Dictionary的格式。
在此步驟則是反向轉換,將前一步驟回傳的結果轉譯成 HTTP Response(字串格式)。
替換 WSGI Server
了解 WSGI 協定的基本流程後,我們可以將WSGI Server理解成處理 HTTP Request(字串) 與 Python 可理解的 Input/Output 的中繼站(Middleware)。
所有支援 WSGI 協定的 Server 都可稱為 WSGI Server,現在比較常見的WSGI Server是gunicorn 及 uwsgi。
回到一開始所提出的問題: 為什麼 Flask 會要求我們替換 WSGI Server 呢?
Flask身為一個輕量級的框架,為了讓使用者不需要進行過多設定就能使用,所以已內建較為陽春的WSGI Server (Werkzeug),負責處理HTTP Request及Flask間資料的轉換。
然而,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 的範疇,它的功能有下列三項:
靜態檔案快取:
將大型的文件暫存在使用者的瀏覽器,以降低重複造訪時的讀取時間
快取的目的是讓系統的回應速度變快,減少等待 Response 的時間。 如果網站中包含了大量的靜態檔案(圖片、js、css 檔案),設置快取可以讓瀏覽器緩存這些文件。 當使用第二次造訪網站時就不需要重新下載這些檔案,達到加速的效果。
值得一提的是:這些快取僅限於 「靜態檔案」,在發送 Request 的過程中不涉及運算,任何使用者造訪都將取得相同內容(例如首頁的封面圖)。 如果發送Request時有額外的參數、需要進行客製化的運算,則屬於「動態」請求,這是屬於前一小節
Application Server處理的範疇。負載平衡(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。
這些策略相當繁多,在此不多做停留。
反向代理:
隱藏真正的Server位置
儘管負載平衡機制會指派不同的 Server 處理 Request,但對於客戶端來說,所有的 Request 都是同一台 Server 在處理(下圖中的Web Server),不需要也不會知道背後真正處理的 Server 是哪一台。 換言之: 真正的 Server 位置被隱藏了。

不管今天是由圖中的
Server1,Server2還是Server3提供服務,對於使用者來說,所有的 Request 都是送往123.45.67.89這個位置,使用者無從得知真正提供服務的Server路徑為何。
統整
目前我們介紹了三種不同類型的Server:
Application Server:
- 代表服務:
Flask,Django - 特色: 負責商業邏輯處理、根據URL、參數不同,執行不同的程式碼
- 代表服務:
WSGI Server:
- 代表服務:
gunicorn,uWsgi - 特色:根據
WSGI協定,負責「HTTP協定的內容(字串)」和「Application Server能理解的內容」之間的轉換
- 代表服務:
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 正在興起,目前正在研究,如果有興趣的朋友也歡迎點擊收看:
希望這篇文章對大家有幫助!下次再見!