python websocket异步高并发_高并发异步uwsgi+web.py+gevent
為什么用web.py?
python的web框架有很多,比如webpy、flask、bottle等,但是為什么我們選了webpy呢?想了好久,未果,硬要給解釋,我想可能原因有兩個:第一個是兄弟項目組用webpy,被我們組拿來主義,直接用了;第二個是我可能當(dāng)時不知道有其他框架,因為剛工作,知識面有限。但是不管怎么樣,webpy還是好用的,所有API的URL和class在一個文件中進(jìn)行映射,可以很方便地查找某個class是為了哪個API服務(wù)的。(webpy的其中一個作者是Aaron Swartz,這是個牛掰的小伙,英年早逝)
這里對webpy、flask、bottle性能進(jìn)行了測試,測試結(jié)果詳見:webpy/flask/bottle性能測試
wsgi,這是python開發(fā)中經(jīng)常遇到詞(以前只管用了,趁寫博客之際,好好學(xué)習(xí)下細(xì)節(jié))。
wsgi協(xié)議
WSGI的官方文檔參考http://www.python.org/dev/peps/pep-3333/。WSGI是the Python Web Server Interface,它的作用是在協(xié)議之間進(jìn)行轉(zhuǎn)換。WSGI是一個橋梁,一邊是web服務(wù)器(web server),一邊是用戶的應(yīng)用(wsgi app)。但是這個橋梁功能非常簡單,有時候還需要別的橋(wsgi middleware)進(jìn)行幫忙處理,下圖說明了wsgi這個橋梁的關(guān)系。
一個簡單的WSGI應(yīng)用:
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [HELLO_WORLD]
這個是最簡單的WSGI應(yīng)用,那么兩個參數(shù)environ,start_response是什么?
evniron是一系列環(huán)境變量,參考https://www.python.org/dev/peps/pep-3333/#id24,用于表示HTTP請求信息。(為了讓理解更具體一點,下表給出一個例子,這是uWSGI傳遞給wsgi app的值)
{
'wsgi.multiprocess': True,
'SCRIPT_NAME': '',
'REQUEST_METHOD': 'GET',
'UWSGI_ROUTER': 'http',
'SERVER_PROTOCOL': 'HTTP/1.1',
'QUERY_STRING': '',
'x-wsgiorg.fdevent.readable': ,
'HTTP_USER_AGENT': 'curl/7.19.7(x86_64-unknown-linux-gnu)libcurl/7.19.7NSS/3.12.7.0zlib/1.2.3libidn/1.18libssh2/1.2.2',
'SERVER_NAME': 'localhost.localdomain',
'REMOTE_ADDR': '127.0.0.1',
'wsgi.url_scheme': 'http',
'SERVER_PORT': '7012',
'uwsgi.node': 'localhost.localdomain',
'uwsgi.core': 1023,
'x-wsgiorg.fdevent.timeout': None,
'wsgi.input': ,
'HTTP_HOST': '127.0.0.1: 7012',
'wsgi.multithread': False,
'REQUEST_URI': '/index.html',
'HTTP_ACCEPT': '*/*',
'wsgi.version': (1,0),
'x-wsgiorg.fdevent.writable': ,
'wsgi.run_once': False,
'wsgi.errors': ,
'REMOTE_PORT': '56294',
'uwsgi.version': '1.9.10',
'wsgi.file_wrapper': ,
'PATH_INFO': '/index.html'
}
start_response是個函數(shù)對象,參考https://www.python.org/dev/peps/pep-3333/#id26,其定義為為start_response(status, response_headers, exc_info=None),其作用是設(shè)置HTTP status碼(如200 OK)和返回結(jié)果頭部。
WSGI服務(wù)器
wsgi服務(wù)器就是為了構(gòu)建一個讓W(xué)SGI app運行的環(huán)境。運行時需要傳入正確的參數(shù),以及正確地返回結(jié)果,最終把結(jié)果返回客戶端。工作流程大致是,獲取客戶端的request信息,封裝到environ參數(shù),然后把執(zhí)行結(jié)果返回給客戶端。
對于webpy而言,其內(nèi)置了一個CherryPy服務(wù)器。CheckPy在運行時,采用多線程模式,主線程負(fù)責(zé)accept客戶端請求,將請求放入一個request Queue里面,然后有N個子線程負(fù)責(zé)從Queue中取出request,然后處理后將結(jié)果返回給客戶端。不過看起來,這個內(nèi)置的服務(wù)器看起來是為了開發(fā)調(diào)試用,因為webpy并未開放這個默認(rèn)容器的參數(shù)調(diào)節(jié),例如線程數(shù)目,所以為了尋求高效地托管WSGI app,不建議用這個默認(rèn)的容器。這可能也是使用uWSGI的原因,因為我們的線上系統(tǒng)有幾個API的訪問量很大,目前大概是250萬次/天。(當(dāng)然對于業(yè)務(wù)量不是很大的服務(wù),可能這個默認(rèn)的CheckPy也就夠了)
uWSGI
我們知道,Python有把大鎖GIL,會將多個線程退化為串行執(zhí)行,所以一個多線程python進(jìn)程,并不能充分使用多核CPU資源,所以對于Python進(jìn)程,可能采用多進(jìn)程部署方式比較有利于充分利用多核的CPU資源。
uWSGI就是這么一個項目,可以以多進(jìn)程方式執(zhí)行WSGI app,其工作模式為 1 master進(jìn)程 + N worker進(jìn)程(N*m線程),主進(jìn)程負(fù)責(zé)accept客戶端request,然后將請求轉(zhuǎn)發(fā)給worker進(jìn)程,因此最終是worker進(jìn)程負(fù)責(zé)處理客戶端request,這樣很方便的將WSGI app以多進(jìn)程方式進(jìn)行部署。以下給出uwsgi響應(yīng)客戶端請求的執(zhí)行流程圖:
值得注意的是,在master進(jìn)程接收到客戶端請求時,以round-bin方式分發(fā)給worker進(jìn)程,所以多個process在處理前端請求時,所承受的負(fù)載相對還是均衡的。(這是我測試時的經(jīng)驗,改天再扒一下uwsgi的源代碼確認(rèn)一下 TODO)。關(guān)于uWSGI的使用,可能并不是這里的重點,不再贅述。
(其實看到uWSGI的多進(jìn)程模型,我想到了Nginx,它也是多進(jìn)程模型,這個也很有意思,由此了解了thunder herd問題,扯遠(yuǎn)了,繼續(xù)往下說)
考慮一個應(yīng)用場景:client向serverM(uwsgi)發(fā)起一個HTTP請求,serverM在處理這次請求時,需要訪問另一個服務(wù)器serverN,直到serverN返回數(shù)據(jù),serverM才會返回結(jié)果給client,即wsgi app是同步的。假如serverM訪問serverN花費時間比較久,那么若是client請求數(shù)量比較多的情況下,(N*m)線程都會被占用,可想而知,大容量下的并發(fā)處理能力就受(N*m)的限制。
如果碰上了這種情況,怎么解決呢?
(1)增大N,即worker的數(shù)量:在增加進(jìn)程的數(shù)量的時候,進(jìn)程是要消耗內(nèi)存的,并且如果進(jìn)程數(shù)量太多的情況下(并且進(jìn)程均處于活躍狀態(tài)),進(jìn)程間的切換會消耗系統(tǒng)資源的,所以N并不是越大越好。一般情況下,可能將進(jìn)程數(shù)目設(shè)置為CPU數(shù)量的2倍。
(2)增大m,即worker的線程數(shù)量:在創(chuàng)建線程的時候,最大能夠多大呢?由于線程棧是要消耗內(nèi)存的,因此線程的數(shù)量跟系統(tǒng)設(shè)置(virtual memory)和(stack size)有關(guān)。線程數(shù)量太大會不會不太好?(這個肯定不好,我答不上來 TODO)
由此在大并發(fā)需求的情況下,我了解到了C10K問題,并進(jìn)一步學(xué)習(xí)到I/O的多路復(fù)用的epoll,可以避免阻塞調(diào)用在某個socket上。比如libevent就是封裝了多個平臺的高效地I/O多路復(fù)用方法,在linux上用的就是epoll。但是這里我們不討論epoll或者libevent的使用,我們這里引入gevent模塊。
gevent協(xié)程
gevent在使用時,跟thread的接口很像,但是thread是由操作系統(tǒng)負(fù)責(zé)調(diào)度,而gevent是用戶態(tài)的“線程”,也叫協(xié)程。gevent的好處就是無需等待I/O,當(dāng)發(fā)生I/O調(diào)用是,gevent會主動切換到另一gevent進(jìn)行運行,這樣在等待socket數(shù)據(jù)返回時,可以充分利用CPU資源。
在使用gevent內(nèi)部實現(xiàn):
1. gevent 協(xié)程切換使用greenlet項目,greenlet其實就是一個函數(shù),及保存函數(shù)的上下文(也就是棧),greenlet的切換由應(yīng)用程序自己控制,所以非常適合對于I/O型的應(yīng)用程序,發(fā)生I/O時就切換,這樣能夠充分利用CPU資源。
2. gevent在監(jiān)控socket事件時,使用了libevent,就是高級的epoll。
3. python中有個猴子補(bǔ)丁(monkey patch)的說法,在python進(jìn)程中,python的函數(shù)都是對象,存在于進(jìn)程的全局字典中,因此,開發(fā)者可以通過替換這些對象,來改變標(biāo)準(zhǔn)庫函數(shù)的實現(xiàn),這樣還不用修改已有的應(yīng)用程序。在gevent里,也有這樣的monkey patch,通過gevent.monkey.patch_all()替換掉了標(biāo)準(zhǔn)庫中阻塞的模塊,這樣不用修改應(yīng)用程序就能充分享受gevent的優(yōu)勢了。(真是方便啊)
使用gevent需注意的問題
1. 無意識地引入阻塞模塊
我們知道gevent通過monkey patch替換掉了標(biāo)準(zhǔn)庫中阻塞的模塊,但是有的時候可能我們會“無意識”地引入阻塞模塊,例如MySQL-Python,pylibmc。這兩個模塊是通過C擴(kuò)展程序?qū)崿F(xiàn)的,都需要進(jìn)行socket通信,由于調(diào)用的底層C的socket接口,所以超出了gevent的管控范圍,這樣就在使用這兩個模塊跟mysql或者memcached進(jìn)行通信時,就退化為了阻塞調(diào)用。
這樣一個應(yīng)用場景:在一個gevent進(jìn)程中,基于MySQL-Python模塊,創(chuàng)建一個跟mysql有10個連接的連接池,當(dāng)并發(fā)量大的情況下,我們期望這10個連接可以同時處理對mysql的10個請求。實際如我們期望的這樣么?的確跟期望不一樣,因為在conn.query()的時候,阻塞了進(jìn)程,這樣意味著同一時刻不可能同時有2個對mysql的訪問,所以并發(fā)不起來了。如mysql響應(yīng)很慢的話,那么整個process跟hang住了沒什么兩樣(這個時候,便不如多線程的部署模式了,因為多個線程的話,一個線程hang住,另一個線程還是有機(jī)會執(zhí)行的)。
如何解決:在這些需要考慮并發(fā)的效率的場景,盡量避免引入阻塞模塊,可以考慮純python實現(xiàn)的模塊,例如MySQL-Python->pymysql, pylibmc->memcached(這個python實現(xiàn)的memcached client可能不太完備,例如一致性hash目前都沒實現(xiàn))。
2. gevent在遇到I/O訪問時,會進(jìn)行g(shù)reenlet切換。若是某個greenlet需要占用大量計算,那么若是計算任務(wù)過多(激進(jìn)一點,陷入死循環(huán)),可能會導(dǎo)致其他greenlet沒有機(jī)會執(zhí)行。若是一個gevent進(jìn)程需要執(zhí)行多個任務(wù)時,若某個任務(wù)計算過多,可能會影響其他任務(wù)的執(zhí)行。例如我曾遇到一個進(jìn)程中,采用生產(chǎn)者任務(wù)(統(tǒng)計數(shù)據(jù),將結(jié)果放入內(nèi)存)+消費者任務(wù)(將計算結(jié)果寫入磁盤),然而當(dāng)數(shù)據(jù)很大的時候,生產(chǎn)者任務(wù)占用大量的CPU資源,然而消費者任務(wù)不能及時將統(tǒng)計結(jié)果寫入磁盤,即生產(chǎn)太快,消費太慢,這樣內(nèi)存占用越來越多,一度高達(dá)2G內(nèi)存。所以鑒于此,需要根據(jù)任務(wù)的特點(I/O密集或者CPU密集),合理分配進(jìn)程任務(wù)。
參考:
以上為工作經(jīng)驗總結(jié),在整理成文的時候,才發(fā)現(xiàn)有些知識點只是一知半解,所以需要繼續(xù)完善該文。
總結(jié)
以上是生活随笔為你收集整理的python websocket异步高并发_高并发异步uwsgi+web.py+gevent的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 陈涉世家重点字词翻译
- 下一篇: 文字的网名73个