前言
從研究所開始接觸 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
加入的位置在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
正在興起,目前正在研究,如果有興趣的朋友也歡迎點擊收看:
Fast API 入門筆記 (一)
希望這篇文章對大家有幫助!下次再見!