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

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

前言

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

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

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

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

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

WARNING: This is a development server. Do not use it in a production deployment.
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 的範疇,它的功能有下列三項:

  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