python高性能服务器编写,Tornado的高性能服务器开发常用方法
最近一直開發AI人臉識別相關的項目,需要提供給客戶一些服務,所以我需要開發一些服務端程序。由于AI算法都是用python3寫的,所以我就索性用起了python開發服務端,畢竟速度也快,以前用過Flask、Django,這次決定有Tornado來做,對該框架做了一系列的調用,癡迷于他的異步非阻塞的功能,項目開發完之后有了一些經驗,特此對以前的資料查詢做一個總結,以便后面可以復用。
高性能源于Tornado基于Epoll(unix為kqueue)的異步網絡IO。因為tornado的單線程機制,一不小心就容易寫出阻塞服務[block]的代碼。不但沒有性能提高,反而會讓性能急劇下降。因此,探索tornado的異步使用方式很有必要。
簡而言之,Tornado的異步包括兩個方面,異步服務端和異步客戶端。無論服務端和客戶端,具體的異步模型又可以分為回調[callback]和協程[coroutine]。具體應用場景,也沒有很明確的界限。往往一個請求服務里還包含對別的服務的客戶端異步請求。
服務端的異步方式
服務端異步,可以理解為一個tornado請求之內,需要做一個耗時的任務。直接寫在業務邏輯里可能會block整個服務。因此可以把這個任務放到異步處理,實現異步的方式就有兩種,一種是yield掛起函數,另外一種就是使用類線程池的方式。
請看一個同步例子(借用的):
class SyncHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
# 耗時的代碼
os.system("ping -c 2 www.google.com")
self.finish('It works')
此時耗時動作將嚴重阻塞系統的性能,導致并發量很小,因為處理一個請求的時間就好幾秒。
一、我們將以上代碼改成異步的,使用回調函數
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'
這種寫法就使耗時的任務在后臺運行了,從而顯著提高并發,但是此時,我們有兩個知識點需要了解:
1、裝飾器
@tornado.web.asynchronous
@tornado.gen.coroutine
兩個問題:
為什么要使用這兩個裝飾器?
為什么要先用asynchronous在用coroutine呢?或著說為什么要用這種調用順序?
這兩個裝飾器的作用:
1.1、@tornado.web.asynchronous
首先我們要明白同步和異步的作用
同步的情況下,web請求到來之后必須處理完成之后在返回,這是一個阻塞的過程。也就是說當一個請求被處理時,服務器進程會被掛起直至請求完成。而這會影響服務器的并發能力。
異步的情況下,web服務器進程在等待請求處理的時候,會將IO循環打開,繼續來接受請求。而拿到處理結果之后會調用回調函數,將結果返回。這要既不影響處理請求,也不影響接受請求,能夠顯著的提升并發能力。
我們必須要明白,在同步的情況下,web服務進程,接受請求,處理請求,然后返回結果,最后自己來關閉連接。這個關閉的動作是自動的。
而異步的情況下,因為在處理一個請求的時候還沒有的到結果,所以需要保持連接的打開,最后返回結果之后,關閉連接,這個關閉動作必須要手動關閉。也就是必須手動調用self.finish.
tornado中使用@tornado.web.asynchronous裝飾器作用是保持連接一直開啟,
上面的例子中使用的回調函數的缺點是,可能引起回調深淵,系統將難以維護,比如回調中調用回調等。
因為實現異步需要保持連接一直打開,而不能在handler執行完畢的時候關掉。
所以總的來說,@tornado.web.asynchronous的作用就是:把http連接變成長連接,直到調用self.finish,連接都在等待狀態。
1.2、@tornado.gen.coroutine
這個函數的作用就是簡化異步編程,讓代碼的編寫更像同步代碼,同時實現的確實異步的。這樣避免了寫回調函數。而且使用的是協程的方式來來實現異步編程。最新版的tornado,其實不一定需要寫@tornado.web.asynchronous。
1.3、順序
@asynchronous會監聽@gen.coroutine的返回結果(Future),并在@gen.coroutine裝飾的代碼段執行完成后自動調用finish。從Tornado 3.1版本開始,只使用@gen.coroutine就可以了。
2、函數
IOLoop.instance().add_timeout()
functools.partial()
2.1、IOLoop.instance().add_timeout()
首先我們需要了解IOLoop,以及IOLoop.instance()也就是實例化動作。
IOLoop?是基于 epoll 實現的底層網絡I/O的核心調度模塊,用于處理 socket 相關的連接、響應、異步讀寫等網絡事件。每個 Tornado 進程都會初始化一個全局唯一的 IOLoop 實例,在 IOLoop 中通過靜態方法 instance() 進行封裝,獲取 IOLoop 實例直接調用此方法即可。
Tornado 服務器啟動時會創建監聽 socket,并將 socket 的 file descriptor 注冊到 IOLoop 實例中,IOLoop 添加對 socket 的IOLoop.READ 事件監聽并傳入回調處理函數。當某個 socket 通過 accept 接受連接請求后調用注冊的回調函數進行讀寫。接下來主要分析IOLoop 對 epoll 的封裝和 I/O 調度具體實現。
epoll是Linux內核中實現的一種可擴展的I/O事件通知機制,是對POISX系統中?select?和?poll?的替代,具有更高的性能和擴展性,FreeBSD中類似的實現是kqueue。Tornado中基于Python C擴展實現的的epoll模塊(或kqueue)對epoll(kqueue)的使用進行了封裝,使得IOLoop對象可以通過相應的事件處理機制對I/O進行調度。
IOLoop模塊對網絡事件類型的封裝與epoll一致,分為READ / WRITE / ERROR三類。
functools模塊用于高階函數:作用于或返回其他函數的函數。一般而言,任何可調用對象都可以作為本模塊用途的函數來處理。
functools.partial返回的是一個可調用的partial對象,使用方法是partial(func,*args,**kw),func是必須要傳入的,而且至少需要一個args或是kw參數。
在這里就是添加一個回調函數的partial對象。
上面的這種寫法不能獲取返回值。需要獲取返回值需要使用yield掛起函數,并根據函數的return獲取返回值。
二、帶返回值的,同時使用協程來實現
class AsyncTaskHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self, *args, **kwargs):
# yield 結果
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'
可以看到結果值也被返回了。有時候這種協程處理,未必就比同步快。在并發量很小的情況下,IO本身拉開的差距并不大。甚至協程和同步性能差不多。但是在大并發量的情況下就不一樣了,因為并發請求很多,越來越多的請求如果被耗時的處理阻塞,將會長時間得不到結果。
yield掛起函數協程,盡管沒有block主線程,因為需要處理返回值,掛起到響應執行還是有時間等待,相對于單個請求而言。另外一種使用異步和協程的方式就是在主線程之外,使用線程池,線程池依賴于futures。Python2需要額外安裝。
我認為這種用法應該是一種比較常用的用法。
三、使用線程池的方式修改為異步處理
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功能(這個功能必須在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'
具體使用何種方式,更多的依賴業務,不需要返回值的往往需要處理callback,回調太多容易出錯,當然如果需要很多回調嵌套,首先優化的應該是業務或產品邏輯。yield的方式很優雅,寫法可以異步邏輯同步寫,快是快了一些,但也會損失一定的性能。
總結
以上是生活随笔為你收集整理的python高性能服务器编写,Tornado的高性能服务器开发常用方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安装飞利浦系统服务器,DOS系统安装Wi
- 下一篇: centos7下qt creator p