理解 Python 後端技術:從 CGI 談到 WSGI, uWSGI 與 uwsgi
Posted on May 29, 2024 in Python 程式設計 - 中階 by Amo Chen ‐ 9 min read
開發 Flask, Django 等後端應用時,最後一定會遇到部署的問題,這些框架多半告訴我們要使用 WSGI HTTP server 部署,例如 gunicorn, uWSGI 等等都是一時之選。
但你有想過為什麼要使用 WSGI HTTP server 嗎?你理解什麼是 WSGI 嗎?跟 uWSGI 之間又有什麼差異?(這些其實也是面試 Python 後端工程師時常見的問題)
本文將從 CGI 開始,一路認識 WSGI, uWSGI 以及 uwsgi, 把各種常見的問題都釐清!
本文環境
$ pip install gunicorn
$ brew install nginx
CGI (Common Gateway Interface) 簡介
我們都知道 Apache2, Nginx, Lighttpd 等這些 Web 伺服器,能夠跟各種程式語言開發的後端伺服協作,做到動態網頁的功能,那它們之間具體是怎麼溝通協作的呢?
通常談到溝通協作這件事,就會有所謂的「協定 / Protocol 」或「介面 / Interface 」,簡單來說,都是規定雙方要用哪些方式溝通、用什麼格式傳送資料、呼叫什麼方法達到目的等等。
而 Web 伺服器與其他程式語言之間的溝通方式,最早是 1990 年早期 1 項稱為 CGI (Common Gateway Interface) 的介面。
這個介面規定 Web 伺服器在收到 request 時,如果要將 request 交由其他程式(或稱外部程式 / CGI script / CGI program )處理,就需要建立 1 個新的 process 執行該程式,該程式處理完 request 之後,就必須將 response 輸出到 STDOUT, Web 伺服器就會把這個 STDOUT 回應給使用者。如果這個外部程式需要接收資料,則可以透過讀取 STDIN 得到資料,如果是需要讀取 request 相關資料,則是透過讀取環境變數取得,這些環境變數會在 Web 伺服器建立新的 process 時一併建立好。
CGI 的出現,使得各種程式語言都能夠與 Web 伺服器進行協作,提供動態網頁的功能,譬如 PHP 預設可以使用 CGI 模式與 Web 伺服器協作。
以圖表示的話,應該能夠更清楚 Web Server, CGI 與其他程式的關係,居中協調的介面就是 CGI , Web Server 與外部程式都必須實作 CGI 才能夠順利溝通:
做 1 個簡單的 Python CGI 程式
知道 CGI 大致樣貌之後,試看看做個會回傳 Hello, World 的 Python Script 吧!
查閱 RFC 3875: The Common Gateway Interface (CGI) Version 1.1 之後,可以知道最簡單的回應格式為:
Content-Type: <Content Type>\n\n
<Response Body>
加上 CGI 是透過 STDOUT 回傳資料,所以只需要用到 print()
就可以做 1 個超簡單的 Python CGI 程式:
hello.cgi
#!/usr/bin/env python3
print(
"Content-Type: text/html"
"\n\n"
"<html><body><h1>Hello, World!</h1></body></html>"
)
搞定 CGI 程式之後,我們還需要 1 個 Web Server 驗證一下 2 者是否能夠協作成功。
為了方便,我們使用 macOS 內建的 Apache2(如果沒有 macOS 也沒有關係,本章節僅是為了說明 CGI 如何運作), 首先將 hello.cgi
放到 /Library/WebServer/CGI-Executables
資料夾內,並且賦予它可執行的權限,因為 Web Server 需要執行該程式才能取得回應:
$ sudo mv hello.cgi /Library/WebServer/CGI-Executables/
$ sudo chmod +x /Library/WebServer/CGI-Executables/hello.cgi
p.s. /Library/WebServer/CGI-Executables/
是 macOS Apache2 預設放 CGI 程式的地方,預設也有開啟 CGI 模組
接著,編輯 macOS Apache2 的設定檔(本文使用 Vim 編輯器,各位可以換成自己慣用的編輯器):
$ sudo vim /etc/apache2/httpd.conf
搜尋字串 <Directory "/Library/WebServer/CGI-Executables">
後,會發現以下設定:
<Directory "/Library/WebServer/CGI-Executables">
AllowOverride None
Options None
Require all granted
</Directory>
我們需要將 Options None
改為 Options +ExecCGI
後存檔,以賦予 Apache2 執行 CGI 程式的能力:
<Directory "/Library/WebServer/CGI-Executables">
AllowOverride None
Options +ExecCGI
Require all granted
</Directory>
最後,使用以下指令啟動 Apache2:
$ sudo apachectl start
p.s. 停止的指令為 sudo apachectl stop
打開瀏覽器輸入網址 http://127.0.0.1/cgi-bin/hello.cgi
查看以 Python 寫成的 CGI 程式否能夠運作,正常的話將會看到以下畫面:
這就是 CGI 的威能!賦予 Web Server 執行各種程式、各種程式語言達到協作的能力!
加碼 — 從環境變數取的 request 的內容
前文提到 CGI 協定規定 request 的內容會放在環境變數中,因此我們也可以在 Python CGI script 中使用環境變數讀到 request 的內容,可以使用的環境變數也都記錄在 RFC 3875: The Common Gateway Interface (CGI) Version 1.1 中,包含:
CONTENT_TYPE
SCRIPT_NAME
QUERY_STRING
REQUEST_METHOD
- 其他
p.s. 如果有寫過 Flask, Django 的話,應該都會對這些字串感到熟悉吧?
我們可以將 hello.cgi
改成下列程式碼,取得 request 的內容:
#!/usr/bin/env python3
import os
print(
"Content-Type: text/html"
"\n\n"
"<html><body>"
"<h1>Hello, World!</h1>"
f"<p>REQUEST_METHOD: {os.getenv('REQUEST_METHOD')}</p>"
f"<p>SCRIPT_NAME: {os.getenv('SCRIPT_NAME')}</p>"
"</body></html>"
)
再打開瀏覽器輸入網址 http://127.0.0.1/cgi-bin/hello.cgi
,運作正常的話將可以看到以下畫面:
至此,你應該已經知道 CGI 具體如何運作了!說 CGI 此技術解放 Web Server 產生動態網頁的能力也不為過!
疑?那 WSGI 與 uWSGI 又是什麼?
實際上, CGI 作為最早出現的溝通介面,其最致命的缺陷就是效能差。
原因在於每次接到 1 個新的 request, 就得建立 1 個新的 process 進行處理,這種做法天生就無法處理大量 requests, 這也是為什麼我們現在少使用 CGI 的緣故。
為了解決 CGI 的效能問題,開始出現各種不同的 Interfaces 的替代方案,例如 Nginx 的 FastCGI 。
而 Python 也在 PEP-333 提出 WSGI (Web Server Gateway Interface),同樣也是可以讓 Web Server 與 Web 應用(application)與框架(framework)溝通協作的 Interface 。
This PEP, therefore, proposes a simple and universal interface between web servers and web applications or frameworks: the Python Web Server Gateway Interface (WSGI).
注意, WSGI 只是 1 份規格(specification)而已,而 uWSGI 則是有實作 WSGI 這份規格的 project 。
我們同樣以圖表示 WSGI, Server 與 Application 之間的關係:
注意,上圖中的 Web Server 變成 gunicorn, uWSGI 等有實作 WSGI 規格的 HTTP server!
它們從 Client 端的角度來看是 Web Server, Clients 可以用 HTTP 協定進行溝通,但它們與外部程式溝通則是使用 WSGI, 接下來我們會以 WSGI HTTP server 稱呼這些實作 WSGI 規格的 Web servers 。
再小結一次:
- WSGI 是規格,規定 Web Server 與 Application 之間的溝通方式
- uWSGI 是有實作 WSGI 規格的 Project
- WSGI HTTP server 是有實作 WSGI 的 Web server
WSGI 寫了些什麼?
首先,在 2003 年提出 WSGI 時,當時沒有任何 Python Web 應用/框架支援 WSGI, 因此 WSGI 在設計時就考慮到,必須讓未來所有 Python Web 應用/框架都能輕易實作 WSGI, 所以「簡單實作」是 WSGI 的 1 大特點!
WSGI 規格中的規定主要分為 2 端:
- Application / Framework Side
- Server / Gateway Side
如果要開發像 Flask, Django 之類的 Framework 或 Application, 只要遵守 Application / Framework Side 的規定就好。
如果要開發像 gunicorn, uWSGI 之類的 WSGI HTTP server, 則是遵守 Server / Gateway Side 的規定。
以下會分開進行解釋。
Application / Framework Side
Application / Framework Side 規定應用或者框架如何定義符合 WSGI 規範,而且能夠被 Server Side 呼叫的 object (或稱 callable object),該 object 可以是 1 個 function, method, class 或者是有實作 __call__
方法的 instance 。
The application object is simply a callable object that accepts two arguments. The term “object” should not be misconstrued as requiring an actual object instance: a function, method, class, or instance with a
__call__
method are all acceptable for use as an application object.
Application / Framework Side 也是一般開發者最熟悉的,開發 Flask, Django 等應用都是在做 Application / Framework Side 的事情,只是因為 Flask, Django 包裝得很好,所以我們不知道它們實際遵循 WSGI 規格。
Application / Framework Side 規定 callable object 必須接受 2 個參數,分別是 environ
與 start_response
。
environ
代表從 WSGI server side 傳進來的 request 內容, environ
是 1 個 dictionary, 裡面存著與 CGI 環境變數相容的 key 與 value, 例如 CONTENT_TYPE
, SCRIPT_NAME
等等,除此之外,還有 WSGI 獨有的 key 與 value, 例如 wsgi.version
, wsgi.input
等等,詳細可以閱讀文件 environ Variables。
p.s. 與 CGI 相容的好處是可以讓 CGI programs 很容易進行改寫
p.s. 這也是為什麼 Flask, Django 等都能看到 CONTENT_TYPE
, SCRIPT_NAME
等 keys 的原因
start_response
則是 1 個 callable object, 呼叫 start_response
代表可以開始回應 HTTP response, start_response
的形式如下所示,接受 2 個參數與 1 個 optional 參數:
start_response(status, response_headers, exc_info=None)
status
是我們熟知的200 OK
,404 Not Found
等字串。response_headers
是我們要回傳的 HTTP headers, 是 1 個 list of tuples, 每 1 個 tuple 格式為(header_name, header_value)
,其中header_name
必須是合法的 HTTP header 。exc_info
主要用於錯誤處理,預設為None
。如果在呼叫start_response()
之前程式有任何錯誤,可以傳入與 sys.exc_info() 相同格式的值,或者直接放sys.exc_info()
的回傳值也可以。
最後, Application / Framework Side 規定 callable object 必須回傳 iterable bytes 作為回應的內容,也就是 HTTP response 的 body 。
知道 WSGI 規定的 application 要怎麼寫之後,我們就可以做 1 個像前文 CGI 範例同樣簡單的 Python WSGI Application:
app.py
def app(environ, start_response):
print(f'{environ=}')
start_response("200 OK", [('Content-Type', 'text/html'),])
return [
b"<html>",
b"<body>",
b"<h1>Hello, WSGI</h1>",
b"</body>",
b"</html>",
]
接著,我們使用 Python WSGI HTTP Server — gunicorn 做為我們的 Web server, 讓 gunicorn 呼叫我們寫的 WSGI application:
$ gunicorn -w 1 app:app
p.s. -w 1
則代表只需要 1 個 worker process
p.s. app:app
代表呼叫 app.py
裡的 app
callable object
成功之後,可以看到類似以下的訊息,訊息中可以看到 gunicorn 正在監聽 8000 port:
[2024-05-28 23:19:44 +0800] [87015] [INFO] Starting gunicorn 22.0.0
[2024-05-28 23:19:44 +0800] [87015] [INFO] Listening at: http://127.0.0.1:8000 (87015)
[2024-05-28 23:19:44 +0800] [87015] [INFO] Using worker: sync
[2024-05-28 23:19:44 +0800] [87090] [INFO] Booting worker with pid: 87090
我們可以使用瀏覽器打開網址 http://127.0.0.1:8000
。
成功的話,可以看到以下畫面,代表我們成功透過 WSGI HTTP server 呼叫我們寫的 Python WSGI application 囉:
知道 Application Side 是怎麼回事之後,進一步看看一些 Python Web Framework 怎麼做的,我們以 Django 為例, Django 也在 WSGIHandler 中實作 WSGI application, 它的運作邏輯也十分類似前述的 app.py
:
而負責建立 WSGIHandler 的地方就在 get_wsgi_application() 裡,這也是為什麼使用指令 django-admin startproject <project name>
建立 1 個專案之後,專案資料夾裡有個 wsgi.py
會呼叫 get_wsgi_application()
的原因,目的就是為了建立 1 個 callable object 準備給 WSGI HTTP server 使用:
$ django-admin startproject mysite
$ tree -L 3 mysite
mysite
├── manage.py
└── mysite
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
wsgi.py
的內容:
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_wsgi_application()
上述程式碼裡的 application
就是 WSGI 需要的 callable object, 所以我們可以透過 gunicorn 指令,告訴 guincorn 去呼叫名稱為 application
的 callable object 處理 requests:
$ cd mysite
$ gunicorn -w 1 mysite.wsgi:application
p.s. mysite.wsgi:application
代表呼叫 mysite/wsgi.py
裡的 application
object, 之所以使用 .
分隔是因為 mysite
是個 Python module
讀到此處,大家應該都能理解為什麼 Flask, Django 等文件都提到應該使用 gunicorn, uWSGI 等 WSGI HTTP server 進行部署,因為我們開發的 Python 程式可以透過 WSGI 進行呼叫。
WSGI Application 與 WSGI HTTP Server 分開的好處,可以讓開發者專注在開發應用,而 WSGI HTTP server 就專注在執行效能與如何處理更多 requests ,而且開發者可以不用為了特別的 Web Server 開發特定的 application 邏輯,只要雙方都遵守 WSGI ,那就可以任意搭配,例如我們寫的 Django 應用,可以選擇搭配 uWSGI 也可以選擇 gunicorn, 完全不需要為了換 Web Server 而改程式碼。
只要對 WSGI application side 理解夠清楚,之後不管使用什麼 WSGI HTTP server 都可以,因為只要告訴 WSGI HTTP server 我們的 WSGI callable object 在哪裡就行!
Server / Gateway Side
不僅 Application / Framework Side, WSGI 也有規定 Server / Gateway 怎麼呼叫應用(Application)或框架(Framework),以及如何回應 HTTP response 給使用者等等。
WSGI Server Side 的重點在於:
- 把
environ
整理好,準備呼叫 application 時使用 - 提供
start_response
callable object, 同樣準備呼叫 application 時使用 - 取得呼叫 application 之後的回傳值,並回應給使用者
有興趣的話,可以看 PEP-333 The Server/Gateway Side ,該章節有提供 1 個 CGI 版的 WSGI server, 它的程式邏輯主要就是做上述所提到的 3 件事,同樣的模式也可以在 gunicorn 的 source code 中看到:
這就是 Sever / Gateway Side 的所有規定!
為什麼部署 WSGI application 還需要 Nginx?
目前為止,我們所談到的架構如下圖所示:
但實際上,我們很可能會遇到在 WSGI HTTP server 前面還加了 Nginx, Apache2 等 Web Server 的情況,如下圖所示:
這是因為 Nginx, Apache2 等 Web Server 仍有其強項,例如處理靜態檔案(static files)、負載平衡、反向代理(reverse proxy)、快取(cache)等能力,不僅可以減小 WSGI HTTP server 的壓力,還可以提升整體效能,透過妥善利用各自的強項,也讓 WSGI HTTP server 不必重新發明輪胎又實作一遍 Nginx, Apache2 等已經具備的功能,也因此上圖在現今的後端架構相當常見。
等等,我還看到有個 uwsgi (全小寫)
如果選擇使用 uWSGI 作為 WSGI HTTP server 的話,或者使用 Nginx 作為更前端的 Web Server 的話,就會看到有個 uwsgi (全小寫) 的關鍵字。
uwsgi (全小寫) 實際上是 1 個協定(protocol), uwsgi是 uWSGI 所使用的特殊協定,官方也承認這是個不好的名字,很容易造成混淆⋯⋯。
uWSGI natively speaks HTTP, FastCGI, SCGI and its specific protocol named “uwsgi” (yes, wrong naming choice). The best performing protocol is obviously uwsgi, already supported by nginx and Cherokee (while various Apache modules are available).
uwsgi 協定是 1 個用來傳送各種類型的 binary 資料的協定,用於 Nginx, Apache2 這些有提供支援 uwsgi 協定的 Web server, 是 Web Server 與 uWSGI 之間的溝通協定,如下圖紅色部分所示:
uWSGI 支援 HTTP, FastCGI, SCGI 與 uwsgi 等協定,作為 Web Server 與 uWSGI 之間的溝通方式,其中 uwsgi 是效能最好的,也因此你可以用 --socket
參數讓 uWSGI 使用 uwsgi 協定:
$ uwsgi --socket 127.0.0.1:3031 --wsgi-file app.py --master --processes 4 --threads 2
然後在 Nginx 或其他 Web Sever 設定與 uWSGI 溝通時使用 uwsgi 協定,舉 Nginx 為例,設定如下:
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:3031;
}
這就是 uwsgi (全小寫) 與 uWSGI 的差異!
總結
本文從 CGI 一路談到 WSGI 與 uWSGI, uwsgi 等經常會看到的技術名詞,實際用 Python 實作過一遍,而且探訪了幾個實際 WSGI application / framework 與 WSGI HTTP server 的原始碼,以期讓彼此能夠真正了解這些名詞的來龍去脈,並提升對相關技術的理解程度,提高掌握力!
以上!
Enjoy!
References
RFC 3875: The Common Gateway Interface (CGI) Version 1.1
How to deploy with WSGI | Django documentation
PEP-333 - Python Web Server Gateway Interface v1.0