python高性能服务器编写,Tornado的高性能服务器开发常用方法
最近一直開(kāi)發(fā)AI人臉識(shí)別相關(guān)的項(xiàng)目,需要提供給客戶(hù)一些服務(wù),所以我需要開(kāi)發(fā)一些服務(wù)端程序。由于AI算法都是用python3寫(xiě)的,所以我就索性用起了python開(kāi)發(fā)服務(wù)端,畢竟速度也快,以前用過(guò)Flask、Django,這次決定有Tornado來(lái)做,對(duì)該框架做了一系列的調(diào)用,癡迷于他的異步非阻塞的功能,項(xiàng)目開(kāi)發(fā)完之后有了一些經(jīng)驗(yàn),特此對(duì)以前的資料查詢(xún)做一個(gè)總結(jié),以便后面可以復(fù)用。
高性能源于Tornado基于Epoll(unix為kqueue)的異步網(wǎng)絡(luò)IO。因?yàn)閠ornado的單線(xiàn)程機(jī)制,一不小心就容易寫(xiě)出阻塞服務(wù)[block]的代碼。不但沒(méi)有性能提高,反而會(huì)讓性能急劇下降。因此,探索tornado的異步使用方式很有必要。
簡(jiǎn)而言之,Tornado的異步包括兩個(gè)方面,異步服務(wù)端和異步客戶(hù)端。無(wú)論服務(wù)端和客戶(hù)端,具體的異步模型又可以分為回調(diào)[callback]和協(xié)程[coroutine]。具體應(yīng)用場(chǎng)景,也沒(méi)有很明確的界限。往往一個(gè)請(qǐng)求服務(wù)里還包含對(duì)別的服務(wù)的客戶(hù)端異步請(qǐng)求。
服務(wù)端的異步方式
服務(wù)端異步,可以理解為一個(gè)tornado請(qǐng)求之內(nèi),需要做一個(gè)耗時(shí)的任務(wù)。直接寫(xiě)在業(yè)務(wù)邏輯里可能會(huì)block整個(gè)服務(wù)。因此可以把這個(gè)任務(wù)放到異步處理,實(shí)現(xiàn)異步的方式就有兩種,一種是yield掛起函數(shù),另外一種就是使用類(lèi)線(xiàn)程池的方式。
請(qǐng)看一個(gè)同步例子(借用的):
class SyncHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
# 耗時(shí)的代碼
os.system("ping -c 2 www.google.com")
self.finish('It works')
此時(shí)耗時(shí)動(dòng)作將嚴(yán)重阻塞系統(tǒng)的性能,導(dǎo)致并發(fā)量很小,因?yàn)樘幚硪粋€(gè)請(qǐng)求的時(shí)間就好幾秒。
一、我們將以上代碼改成異步的,使用回調(diào)函數(shù)
from tornado.ioloop import IOLoop
class AsyncHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self, *args, **kwargs):
IOLoop.instance().add_timeout(1, callback=functools.partial(self.ping, 'www.google.com'))
# do something others
self.finish('It works')
@tornado.gen.coroutine
def ping(self, url):
os.system("ping -c 2 www.google.com")
return 'after'
這種寫(xiě)法就使耗時(shí)的任務(wù)在后臺(tái)運(yùn)行了,從而顯著提高并發(fā),但是此時(shí),我們有兩個(gè)知識(shí)點(diǎn)需要了解:
1、裝飾器
@tornado.web.asynchronous
@tornado.gen.coroutine
兩個(gè)問(wèn)題:
為什么要使用這兩個(gè)裝飾器?
為什么要先用asynchronous在用coroutine呢?或著說(shuō)為什么要用這種調(diào)用順序?
這兩個(gè)裝飾器的作用:
1.1、@tornado.web.asynchronous
首先我們要明白同步和異步的作用
同步的情況下,web請(qǐng)求到來(lái)之后必須處理完成之后在返回,這是一個(gè)阻塞的過(guò)程。也就是說(shuō)當(dāng)一個(gè)請(qǐng)求被處理時(shí),服務(wù)器進(jìn)程會(huì)被掛起直至請(qǐng)求完成。而這會(huì)影響服務(wù)器的并發(fā)能力。
異步的情況下,web服務(wù)器進(jìn)程在等待請(qǐng)求處理的時(shí)候,會(huì)將IO循環(huán)打開(kāi),繼續(xù)來(lái)接受請(qǐng)求。而拿到處理結(jié)果之后會(huì)調(diào)用回調(diào)函數(shù),將結(jié)果返回。這要既不影響處理請(qǐng)求,也不影響接受請(qǐng)求,能夠顯著的提升并發(fā)能力。
我們必須要明白,在同步的情況下,web服務(wù)進(jìn)程,接受請(qǐng)求,處理請(qǐng)求,然后返回結(jié)果,最后自己來(lái)關(guān)閉連接。這個(gè)關(guān)閉的動(dòng)作是自動(dòng)的。
而異步的情況下,因?yàn)樵谔幚硪粋€(gè)請(qǐng)求的時(shí)候還沒(méi)有的到結(jié)果,所以需要保持連接的打開(kāi),最后返回結(jié)果之后,關(guān)閉連接,這個(gè)關(guān)閉動(dòng)作必須要手動(dòng)關(guān)閉。也就是必須手動(dòng)調(diào)用self.finish.
tornado中使用@tornado.web.asynchronous裝飾器作用是保持連接一直開(kāi)啟,
上面的例子中使用的回調(diào)函數(shù)的缺點(diǎn)是,可能引起回調(diào)深淵,系統(tǒng)將難以維護(hù),比如回調(diào)中調(diào)用回調(diào)等。
因?yàn)閷?shí)現(xiàn)異步需要保持連接一直打開(kāi),而不能在handler執(zhí)行完畢的時(shí)候關(guān)掉。
所以總的來(lái)說(shuō),@tornado.web.asynchronous的作用就是:把http連接變成長(zhǎng)連接,直到調(diào)用self.finish,連接都在等待狀態(tài)。
1.2、@tornado.gen.coroutine
這個(gè)函數(shù)的作用就是簡(jiǎn)化異步編程,讓代碼的編寫(xiě)更像同步代碼,同時(shí)實(shí)現(xiàn)的確實(shí)異步的。這樣避免了寫(xiě)回調(diào)函數(shù)。而且使用的是協(xié)程的方式來(lái)來(lái)實(shí)現(xiàn)異步編程。最新版的tornado,其實(shí)不一定需要寫(xiě)@tornado.web.asynchronous。
1.3、順序
@asynchronous會(huì)監(jiān)聽(tīng)@gen.coroutine的返回結(jié)果(Future),并在@gen.coroutine裝飾的代碼段執(zhí)行完成后自動(dòng)調(diào)用finish。從Tornado 3.1版本開(kāi)始,只使用@gen.coroutine就可以了。
2、函數(shù)
IOLoop.instance().add_timeout()
functools.partial()
2.1、IOLoop.instance().add_timeout()
首先我們需要了解IOLoop,以及IOLoop.instance()也就是實(shí)例化動(dòng)作。
IOLoop?是基于 epoll 實(shí)現(xiàn)的底層網(wǎng)絡(luò)I/O的核心調(diào)度模塊,用于處理 socket 相關(guān)的連接、響應(yīng)、異步讀寫(xiě)等網(wǎng)絡(luò)事件。每個(gè) Tornado 進(jìn)程都會(huì)初始化一個(gè)全局唯一的 IOLoop 實(shí)例,在 IOLoop 中通過(guò)靜態(tài)方法 instance() 進(jìn)行封裝,獲取 IOLoop 實(shí)例直接調(diào)用此方法即可。
Tornado 服務(wù)器啟動(dòng)時(shí)會(huì)創(chuàng)建監(jiān)聽(tīng) socket,并將 socket 的 file descriptor 注冊(cè)到 IOLoop 實(shí)例中,IOLoop 添加對(duì) socket 的IOLoop.READ 事件監(jiān)聽(tīng)并傳入回調(diào)處理函數(shù)。當(dāng)某個(gè) socket 通過(guò) accept 接受連接請(qǐng)求后調(diào)用注冊(cè)的回調(diào)函數(shù)進(jìn)行讀寫(xiě)。接下來(lái)主要分析IOLoop 對(duì) epoll 的封裝和 I/O 調(diào)度具體實(shí)現(xiàn)。
epoll是Linux內(nèi)核中實(shí)現(xiàn)的一種可擴(kuò)展的I/O事件通知機(jī)制,是對(duì)POISX系統(tǒng)中?select?和?poll?的替代,具有更高的性能和擴(kuò)展性,FreeBSD中類(lèi)似的實(shí)現(xiàn)是kqueue。Tornado中基于Python C擴(kuò)展實(shí)現(xiàn)的的epoll模塊(或kqueue)對(duì)epoll(kqueue)的使用進(jìn)行了封裝,使得IOLoop對(duì)象可以通過(guò)相應(yīng)的事件處理機(jī)制對(duì)I/O進(jìn)行調(diào)度。
IOLoop模塊對(duì)網(wǎng)絡(luò)事件類(lèi)型的封裝與epoll一致,分為READ / WRITE / ERROR三類(lèi)。
functools模塊用于高階函數(shù):作用于或返回其他函數(shù)的函數(shù)。一般而言,任何可調(diào)用對(duì)象都可以作為本模塊用途的函數(shù)來(lái)處理。
functools.partial返回的是一個(gè)可調(diào)用的partial對(duì)象,使用方法是partial(func,*args,**kw),func是必須要傳入的,而且至少需要一個(gè)args或是kw參數(shù)。
在這里就是添加一個(gè)回調(diào)函數(shù)的partial對(duì)象。
上面的這種寫(xiě)法不能獲取返回值。需要獲取返回值需要使用yield掛起函數(shù),并根據(jù)函數(shù)的return獲取返回值。
二、帶返回值的,同時(shí)使用協(xié)程來(lái)實(shí)現(xiàn)
class AsyncTaskHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self, *args, **kwargs):
# yield 結(jié)果
response = yield tornado.gen.Task(self.ping, 'www.google.com')
print 'response', response
self.finish('hello')
@tornado.gen.coroutine
def ping(self, url):
os.system("ping -c 2 {}".format(url))
return 'after'
可以看到結(jié)果值也被返回了。有時(shí)候這種協(xié)程處理,未必就比同步快。在并發(fā)量很小的情況下,IO本身拉開(kāi)的差距并不大。甚至協(xié)程和同步性能差不多。但是在大并發(fā)量的情況下就不一樣了,因?yàn)椴l(fā)請(qǐng)求很多,越來(lái)越多的請(qǐng)求如果被耗時(shí)的處理阻塞,將會(huì)長(zhǎng)時(shí)間得不到結(jié)果。
yield掛起函數(shù)協(xié)程,盡管沒(méi)有block主線(xiàn)程,因?yàn)樾枰幚矸祷刂?#xff0c;掛起到響應(yīng)執(zhí)行還是有時(shí)間等待,相對(duì)于單個(gè)請(qǐng)求而言。另外一種使用異步和協(xié)程的方式就是在主線(xiàn)程之外,使用線(xiàn)程池,線(xiàn)程池依賴(lài)于futures。Python2需要額外安裝。
我認(rèn)為這種用法應(yīng)該是一種比較常用的用法。
三、使用線(xiàn)程池的方式修改為異步處理
from concurrent.futures import ThreadPoolExecutor
class FutureHandler(tornado.web.RequestHandler):
executor = ThreadPoolExecutor(10)
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self, *args, **kwargs):
url = 'www.google.com'
tornado.ioloop.IOLoop.instance().add_callback(functools.partial(self.ping, url))
self.finish('It works')
@tornado.concurrent.run_on_executor
def ping(self, url):
os.system("ping -c 2 {}".format(url))
想要返回值也很容易。再切換一下使用方式接口。使用tornado的gen模塊下的with_timeout功能(這個(gè)功能必須在tornado>3.2的版本)。
如:
class Executor(ThreadPoolExecutor):
_instance = None
def __new__(cls, *args, **kwargs):
if not getattr(cls, '_instance', None):
cls._instance = ThreadPoolExecutor(max_workers=10)
return cls._instance
class FutureResponseHandler(tornado.web.RequestHandler):
executor = Executor()
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self, *args, **kwargs):
future = Executor().submit(self.ping, 'www.google.com')
response = yield tornado.gen.with_timeout(datetime.timedelta(10), future,quiet_exceptions=tornado.gen.TimeoutError)
if response:
print 'response', response.result()
@tornado.concurrent.run_on_executor
def ping(self, url):
os.system("ping -c 1 {}".format(url))
return 'after'
具體使用何種方式,更多的依賴(lài)業(yè)務(wù),不需要返回值的往往需要處理callback,回調(diào)太多容易出錯(cuò),當(dāng)然如果需要很多回調(diào)嵌套,首先優(yōu)化的應(yīng)該是業(yè)務(wù)或產(chǎn)品邏輯。yield的方式很優(yōu)雅,寫(xiě)法可以異步邏輯同步寫(xiě),快是快了一些,但也會(huì)損失一定的性能。
總結(jié)
以上是生活随笔為你收集整理的python高性能服务器编写,Tornado的高性能服务器开发常用方法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 安装飞利浦系统服务器,DOS系统安装Wi
- 下一篇: websocket python爬虫_p