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