1. 程式人生 > >深入理解uwsgi和gunicorn網路模型

深入理解uwsgi和gunicorn網路模型

前言:

       去年10月份建了一個python技術群,到現在為止人數已經漲到700人了。最一開始我經常在群裡迴應大家的問題,不管是簡單還是困難的,我都會根據自己的經驗來交流。 讓人新奇的是一些初學者關注最多的話題不是怎麼學好python,反而是高併發,高效能這類高大上的話題。

       記得有次幾個不懂網路io、io多路複用含義網友,居然在群裡吵了有半個小時,說出來的理論實在是讓人哭笑不得。 群裡當然有人在反駁,後來越聊越歡。 群裡不少人在問我 uwsgi、gevent、tornado的一些設計,先前我儘量詳細的作答,後來發現問我這些問題的朋友,並沒有實際網路程式設計的經驗,而我的回答更偏重於底層的實現,其實這樣的解答不利於別人的理解的。我常常自己也在想 如何更好的回答別人的問題,一方面讓人聽著好懂,另一方面不讓人覺得是在裝逼。 

       沒有人喜歡被別人說裝逼,我同樣也不喜歡,我是個謙虛善良的人。 我現在算是找到了自己的路數,我在回答問題的時候,習慣性的問別人幾個問題,這樣能把握好對方的深淺。 記得以前在騰訊的時候,名叫 院長 的前輩總是跟我說一句話,沒有場景瞎談高併發流氓行為。 胡粗理不粗呀,要避免自己紙上談兵。   

上面有點偏遠這次的話題,這次主要講解 uwsgi 、gunicorn的網路方面的設計。我會圍繞下面幾個問題講解uwsgi、gunicorn的設計。

uwsgi 、gunicorn 有啥區別?  

uwsgi、gunicorn的Master Worker程序模型?

有這麼多worker模型,我們應該怎麼選擇?

uwsgi、gunicorn作為閘道器角色的意義?

這類框架怎麼組合效能最高 ?

在架構上是這樣的,nginx負責動態的轉發和靜態檔案的直接訪問,gunicorn和uwsgi作為閘道器服務用來解析http請求,後面的flask只是個application而已,沒有server的服務特徵。


先說簡單幹練的gunicorn講起吧,下面是gunicorn的啟動方式:

Python
12gunicorn-w3-b127.0.0.1:5000app:app-kgevent

通過strace得知 gunicorn 預設的網路模型是 select , 當我們worker 替換成 gevent 後, 改為 epoll 監聽模型 .  select 和 epoll之間的區別我們就不再囉嗦了。 

下面是gunicorn 、uwsgi 的 Master Worker的模型,大體實現是這樣的。


如果我們的app是flask寫得,那麼用gevent做worker的意義在於什麼?

Gevent worker 它提供了一種機制,讓你可以監聽到多個事件,epoll wait呼叫是阻塞的,但是可以設定超時事件,在超時事件內,如果有事件準備好就返回。比如採用epoll事件處理模型,當事件沒準備好時,放到epoll裡面,事件準備好了,我們就去讀寫,當讀寫返回EAGAIN時,我們將它再次加入到epoll裡面。這樣,只要有事件準備好了,我們就去處理它,當所有fd沒有發生讀寫時,epoll才會阻塞等待。這樣,我們就可以併發處理大量的併發了,當然,這裡的併發請求,是指未處理完的請求,執行緒只有一個,所以同時能處理的請求當然只有一個了,只是在請求間進行不斷地切換而已,切換也是因為非同步事件未準備好,而主動讓出的。工作流之間會產生切換的,但這裡的切換消耗遠沒有多執行緒上下文切換大。

gunicorn根據Master Worker來fork出子程序,Master在這裡不用做處理對外的http請求,而用來管理這些子程序,比如 升級、過載配置、kill程序避免oom 等。這些worker(子程序)繼承了主程序的listening fd,這時候從accept、parse http protocol、response 都是在一個gevent協程裡面的,也就說 在協程池的數目允許下,每個連線就是一個gevent協程。  如果你的app的業務邏輯是阻塞模式的,又沒相容gevent的patch,那麼可想而知,結果是同步阻塞了。  

gunicorn框架對外服務的模式下有http、tcp socket和unix domain socket,這跟uwsgi的模式一樣一樣的。

對於高併發的場景下,如果支援unix domain socket 模式,最少可以省略tcp的計算校驗,這樣效能有不少的提升。gunicorn wsgi相比uwsgi的協議相比,可以使傳輸的協議層更加的緊湊。

下面是uwsgi的啟動方式:

Python
1 2 /usr/local/bin/uwsgi--gevent500--gevent-monkey-patch--http127.0.0.1:5000--callableapp--wsgi-fileapp.py--http-keepalive--master

上面是 uwsgi http 服務模式,但是uwsgi會啟動兩組埠port, 一個是5000 ,一個是5300x ,  埠5000是我們已知的,這個埠用來直接對外接收請求的,他在構建完一個請求協議包之後,會connect 到 5300x 埠, 平白的多消耗了一些網路io。這種模式是 rep req模型,我能想到的優點是,他避免了因為listen fd事件的到來把其他程序喚醒的問題。 也就是說,只有5000對外,5300x是真正的worker。 埠5000根據一定的演算法來選擇worker。 5000 和 5300x的資料互動方式是 可壓縮可序列化的tcp報文,有興趣的可以抓包看看。

在核心2.6就早沒有accept驚群這個說法了,但是當我們多個程序各自把listen fd放到epoll監聽池裡面時,其實會造成事件的喚醒,雖然最終只會被一次accept,但平白無故喚醒了多個程序也不是值得驕傲的。 

題外話,nginx是通過多個程序輪流持鎖的方式來避免epoll accept喚醒問題。

下面是 pull req 模型.

改成 –socket :5000 ,  只會監聽5000 port , 因為uwsgi協議比較特殊,測試起來很是麻煩。 我這裡開源了一個uwsgi客戶端。uwsgi client http://xiaorui.cc/?p=4205

改成  –socket /path/to/xiaorui.cc.sock ,線上經驗表明 unix domain socket 模式要比tcp socket效能有提升的。

uwsgi 和 gunicorn 是長連線麼?  怎麼測試uwsgi的長連線 ?  uwsgi 長連線實現方法?

gunicorn是長連線的,uwsgi要啟用 –http-keepalive 模式才是長連線請求。 不要用curl測試,因為當你curl關閉的時候,已經出發了tcp四次揮手。 你可以根據strace和tcpdump來分析,在curl獲取列印資料後,會發起close請求。  正確的測試方法是,你寫個python requests請求,當請求完畢後,不要急著退出指令碼,加一個sleep等待後再次去請求。 我們會發現連線始終是一個,tcpdump沒有抓到建立新連線的報文。  uwsgi、gunicorn如何實現的長連線 ?  不只是在 返回的http加入 Connection:keep-alive 欄位就標明是長連線,還需要藉助select、epoll這樣的io多路複用模型,用來監聽各個fd讀寫事件。 簡單說只要server不主動去close(),客戶端client也不去close(),既然沒有人去close(),這個連線自然就是長連線了,反之就是短連線。  

flask 是長連線麼? 我負責的說 是,長連線。既然長連線是藉助select、epoll模型來實現,那麼為毛flask是阻塞模式,隨意加一個 time.sleep(xxx) 就io阻塞了。 

這是Python flask的框架介紹…   Werkzeug 是 Flask的wsgi server ,gunicorn 跟 flask做結合時,gunicorn可以理解為是 flask 的wsgi server。 

Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions. And before you ask: It’s BSD licensed!

    簡單說作為 wsgi server 他的意義在於 讓我們專心去寫web application,而不用專注於網路底層實現。 我們拿 flask 的Werkzeug來說,Werkzeug使用 Thread Local來實現的,所以才會有flask.request 、 flask.g 、 flask.session 這麼便利的模組。引用的時候就像使用單例物件一樣,但實際上對它們的所有賦值操作都只會影響到當前請求(當前執行緒),另外生存的週期也僅僅是這次請求而已。Thread Local 模式的實現一定要有一個 Thread Identity 作為標識。  為了測試我寫一個測試發包程式,一個完整的http請求報文被切分成好幾份,特意緩慢的發給Werkzeug服務端,直到結束,期間另開幾個執行緒用來不斷模擬正常的併發請求訪問,可以成功獲取結果。  接著我們在Flask的業務邏輯里加入io阻塞,發現這時候阻塞了。   通過不斷strace追查查明,Werkzeug 在接收http請求和返回response結果的時候是非同步非阻塞的。 隨著我們單步除錯的資料報文的到達,可以看到epoll wait由阻塞變為成功返回。

    我通過閱讀gunicorn、uwsgi 的程式碼得知,他們在單程序單執行緒下是和Werkzeug一樣的。預設情況下,gunicorn會非同步非阻塞的積攢tcp報文,通過http協議來解決tcp粘包的問題,當構建出一個完整http包,才會讓這些worker來處理下一步的邏輯,也就是業務邏輯。  到此為止,我已經解釋了 flask 使用Werkzeug epoll還是會發生阻塞的原因,也解釋情況了gunicorn、uwsgi如何處理http請求 。

gunicorn、uwsgi遇到普遍的問題是502 504問題, 一說到502 ,我們知道後端處理過慢需要擴充套件worker,一說到504,我們知道處理超時,一般調整timeout就可以。那麼502,504該問題的根本原因是什麼?   socket 內部是有兩個佇列,一個syn佇列,一個是accept佇列,這兩個佇列都在accept()之間就有了。 backlog是syn和accept佇列之和,當你後端處理不及時,backlog又到限制時,會出現502,也就是說新的客戶端不能建立,因為沒有syn的槽位供你三次握手。  504 就很好理解了,處理超時,中斷處理,直接範圍錯誤資訊。

Python
1 2 3 4 5 6 7 # xiaorui.cc [program:test] command=/usr/bin/gunicorn-w16app:app-blocalhost:8100 timeout=60*60 backlog=10000


     Uwsgi、gunicorn 應該如何做選擇?  這裡需要注意的是 uwsgi http模式一定要慎用,這個rep req模式實在是奇葩呀。試想一想,linux做請求發起時,往大里說一共可以建立65535-1000的連線。Nginx通常是跟uwsgi一臺伺服器的,那麼nginx收到一個請求時,會主動跟uwsgi建立連線,然後uwsgi跟worker又發起連線,那麼連線數降到3w的理論值 。  又因為uwsgi轉發了一個請求造成時間消耗。

    uwsgi的功能是要比gunicorn豐富的多,通過豐富的配置引數就知道了。 但根據我幾個專案的線上測試結果,gunicorn要比uwsgi穩定。 單筆效能的化,gevent效能是最好的。 推薦的配置是 unix domain socket、多程序、gevent協程池 組合。 執行緒池的方式不太推薦使用,pyhton的執行緒是核心的pthread執行緒,在繁多的執行緒數目下,對比協程的消耗可想而知。

    後面我會依次分析下 uwsgi、gunicorn實現的原始碼,你會發現這些實現還是很精妙的。

     沒寫完。。。。