HTTP/1.1協議是一個基於文本的傳輸協議。傳輸報文都是直接以文本的形式傳遞消息。所以本質上講,HTTP服務器就是負責解析文本,處理請求,然后組織文本並回傳客戶端。
Web開發剛剛興起的時候,HTTP服務器開發這塊各家都有自己的實現,有自己的特點。有些報文解析速度快,有一些處理請求速度快,有一些組織回傳結果的速度快。為了方便代碼復用,實現這些不同特點的服務器模塊的按需組織,一些語言就自行定義了一些協議,對Web服務器的開發做出一些建議性的規定。比如,Java的servlet協議。
Python則自定義了WSGI協議。
WSGI協議
Python官方在PEP-0333和PEP-3333中詳細定義了WSGI協議的細節。前者主要適用於Python2.x,后者根據Python3.x的語言細節變化對WSGI協議做了一些調整。
WSGI協議將整個Web服務器程序划分為應用框架端(后文簡稱為應用端)、服務器網關端(后文簡稱為服務器端)以及中間件。中間件,在應用端來看就是服務器端,在服務器端來看就是應用端。所以總體來講,WSGI就只規定了應用端和服務器端之間的交互協議。
在WSGI協議中,服務器端負責監聽套接字端口,接收報文,解析報文,向應用端發送解析結果,並接收應用端的響應結果,組織響應報文,向客戶端發送響應報文。而應用端,接收到服務器端的解析結果后,根據HTTP方法、URI以及其他報文,執行代碼,調用模版生成動態網頁,也許會向數據庫后端發送查詢請求。
WSGI協議的交互核心就是一個app(environ, start_response)
,是一個回調機制的實現。app()
方法由應用端實現,environ
參數是標准的Python built-in字典,其中包含了WSGI環境變量以及服務器對HTTP請求報文的解析結果;start_response
是一個類似於函數的可調用對象(任何實現了__call__
方法的實例都是可行的),由服務器端實現,負責組織響應報文。start_response
接收兩個參數status
, response_headers
。前者是應用端響應結果的狀態,例如"100 Continue"
、"200 OK"
、"404 Not Found"
;后者這是響應報文的報文頭,是一個數組,其中的每一個元素又是一個元組,例如[("Content-type", "text/plain")]
。
WSGI協議規定的是一個同步Web框架,因為要照顧某些無法異步實現的流程。
服務器端框架的底層機制實現
服務器端框架直接處理網絡IO,對整個Web服務器的性能影響非常大。常見的服務器端框架有多線程、異步兩種基礎機制。
bjoern的異步機制
bjoern是一個使用C語言實現的內存占用少而且擁有極強性能的Python服務器端框架。bjoern使用了事件驅動框架libev、高性能HTTP解析框架http-parser。bjoern使用了Python2的C語言API,因此只能用於CPython解釋器,且不支持Python3。原作者暫時也沒有支持Python3的打算,issue: Python 3 support (PEP 3333) #29。此外bjoern不支持SSL。
bjoern的異步機制是這樣的。首先注冊一個ev_io事件保持對主端口(80或者443)的監聽狀態。一旦套接字可讀,表示有新的連接請求,這時候立即調用回調函數,執行accept動作,並注冊另一個ev_io事件保持對客戶端連接的監聽,並持續接收報文(io可讀),調用WSGI協議,持續回傳響應(io可寫)等等。接收、回傳兩部均實現事件驅動機制。
同一個ev_loop,既保持對主端口的監聽,同時保持對所有客戶端口的報文收發,對其中的所有網絡IO事件協調調度。
cheroot的多線程機制
CherryPy是一個純Python實現的生產級高穩定高性能框架,包含了應用端和服務器端。而cheroot則是CherryPy框架解耦之后分離出來的服務器端框架,支持Python2和Python3,同時還支持PyPy。
cheroot的多線程機制簡單的說就是一個線程池。線程池使用數組維護。其中的線程進行了自定義,可以實現線程的狀態查詢和管理。cheroot有一個主線程監聽常用端口,主線程會將新的連接請求放入一個任務隊列當中。線程池中的線程從任務隊列中獲取任務並執行其余流程。這其實是一個經典的“生產者-消費者”模式。
CPython和PyPy都有GIL,所以線程池是一個相對低效的實現。無論是內存占用還是邏輯流的切換開銷,多線程機制遠比事件驅動機制的開銷要大。
看看評測
網上有一篇博文對幾個Python服務器端框架進行了評測:A Performance Analysis of Python WSGI Servers: Part 2,可以進行一下簡單的分析。
- 並發處理能力:bjoern優勢太明顯,顯得好像做過弊(當然了,原文已經排除這一可能)。這可以歸功於純C語言的實現和libev。驚喜的是,CherryPy的並發處理能力居然超過了meinheld。后者是部分使用C語言實現的,基於http-parser框架和picoev框架,一個原作者稱比libev更適合網絡IO的事件驅動框架。按照bjoern和meinheld的官方文檔描述,兩者具有相似的結構設計,計算密集型流程使用的性能相近的第三方框架,所以兩者應該水平相當。我還沒有看meinheld的源碼,不了解具體原因。 當然了,這次評測沒有模擬數據庫請求IO過程,所以和真實環境的並發並不完全一樣。
- 處理延遲:同時連接數不斷增加時,CherryPy的處理延遲非常低,幾乎為一條水平線。看上去,GIL並沒有構成瓶頸。bjoern隨着同時連接數的增加,處理延遲略有上升,但斜率很低。這里的處理延遲,表示的是從接收請求到發送響應之間的時間差。本次測試的應用端不涉及模版處理或網頁生成,而是直接返回預設參數。
- 內存占用:內存占用是異步框架的強項。bjoern,和meinheld一起處於第一梯隊,並和其他框架拉開了巨大的差距。
- 異常:異常是指服務器突然斷開連接或者連接超時。這個測試里,CherryPy展示出強大的實力,幾乎為0。bjoern也不錯,但同時連接數很高的時候,表現開始不那么穩定。
- CPU占用:這個因不同的底層機制而異。