高效能伺服器Tornado
Python的web框架名目繁多,各有千秋。正如光榮屬於希臘,偉大屬於羅馬。Python的優雅結合WSGI的設計,讓web框架介面實現千秋一統。WSGI 把應用(Application)和伺服器(Server)結合起來。Django 和 Flask 都可以結合 gunicon 搭建部署應用。
與 django 和 flask 不一樣,tornado 既可以是 wsgi 應用,也可以是 wsgi 服務。當然,選擇tornado更多的考量源於其單程式單執行緒非同步IO的網路模式。高效能往往吸引人,可是有不少朋友使用之後會提出疑問,tornado號稱高效能,實際使用的時候卻怎麼感受不到呢?
實際上,高效能源於Tornado基於Epoll(unix為kqueue)的非同步網路IO。因為tornado的單執行緒機制,一不小心就容易寫出阻塞服務(block)的程式碼。不但沒有效能提高,反而會讓效能急劇下降。因此,探索tornado的非同步使用方式很有必要。
Tornado 非同步使用方式
簡而言之,Tornado的非同步包括兩個方面,非同步服務端和非同步客戶端。無論服務端和客戶端,具體的非同步模型又可以分為回撥(callback)和協程(coroutine)。具體應用場景,也沒有很明確的界限。往往一個請求服務裡還包含對別的服務的客戶端非同步請求。
服務端非同步方式
服務端非同步,可以理解為一個tornado請求之內,需要做一個耗時的任務。直接寫在業務邏輯裡可能會block整個服務。因此可以把這個任務放到非同步處理,實現非同步的方式就有兩種,一種是yield掛起函式,另外一種就是使用類執行緒池的方式。請看一個同步例子:
1 2 3 4 5 6 |
class SyncHandler(tornado.web.RequestHandler): def get(self, *args, **kwargs): # 耗時的程式碼 os.system("ping -c 2 www.google.com") self.finish('It works') |
使用ab測試一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
ab -c 5 -n 5 http://127.0.0.1:5000/sync Server Software: TornadoServer/4.3 Server Hostname: 127.0.0.1 Server Port: 5000 Document Path: /sync Document Length: 5 bytes Concurrency Level: 5 Time taken for tests: 5.076 seconds Complete requests: 5 Failed requests: 0 Total transferred: 985 bytes HTML transferred: 25 bytes Requests per second: 0.99 [#/sec] (mean) Time per request: 5076.015 [ms] (mean) Time per request: 1015.203 [ms] (mean, across all concurrent requests) Transfer rate: 0.19 [Kbytes/sec] received |
qps 僅有可憐的 0.99,姑且當成每秒處理一個請求吧。
下面祭出非同步大法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class AsyncHandler(tornado.web.RequestHandler): @tornado.web.asynchronous @tornado.gen.coroutine def get(self, *args, **kwargs): tornado.ioloop.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 {}".format(url)) return 'after' |
儘管在執行非同步任務的時候選擇了timeout 1秒,主執行緒的返回還是很快的。ab壓測如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Document Path: /async Document Length: 5 bytes Concurrency Level: 5 Time taken for tests: 0.009 seconds Complete requests: 5 Failed requests: 0 Total transferred: 985 bytes HTML transferred: 25 bytes Requests per second: 556.92 [#/sec] (mean) Time per request: 8.978 [ms] (mean) Time per request: 1.796 [ms] (mean, across all concurrent requests) Transfer rate: 107.14 [Kbytes/sec] received |
上述的使用方式,通過tornado的IO迴圈,把可以把耗時的任務放到後臺非同步計算,請求可以接著做別的計算。可是,經常有一些耗時的任務完成之後,我們需要其計算的結果。此時這種方式就不行了。車道山前必有路,只需要切換一非同步方式即可。下面使用協程來改寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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' |
可以看到非同步在處理,而結果值也被返回了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Server Software: TornadoServer/4.3 Server Hostname: 127.0.0.1 Server Port: 5000 Document Path: /async/task Document Length: 5 bytes Concurrency Level: 5 Time taken for tests: 0.049 seconds Complete requests: 5 Failed requests: 0 Total transferred: 985 bytes HTML transferred: 25 bytes Requests per second: 101.39 [#/sec] (mean) Time per request: 49.314 [ms] (mean) Time per request: 9.863 [ms] (mean, across all concurrent requests) Transfer rate: 19.51 [Kbytes/sec] received |
qps提升還是很明顯的。有時候這種協程處理,未必就比同步快。在併發量很小的情況下,IO本身拉開的差距並不大。甚至協程和同步效能差不多。例如你跟博爾特跑100米肯定輸給他,可是如果跟他跑2米,鹿死誰手還未定呢。
yield掛起函式協程,儘管沒有block主執行緒,因為需要處理返回值,掛起到響應執行還是有時間等待,相對於單個請求而言。另外一種使用非同步和協程的方式就是在主執行緒之外,使用執行緒池,執行緒池依賴於futures。Python2需要額外安裝。
下面使用執行緒池的方式修改為非同步處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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)) |
再執行ab測試:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Document Path: /future Document Length: 5 bytes Concurrency Level: 5 Time taken for tests: 0.003 seconds Complete requests: 5 Failed requests: 0 Total transferred: 995 bytes HTML transferred: 25 bytes Requests per second: 1912.78 [#/sec] (mean) Time per request: 2.614 [ms] (mean) Time per request: 0.523 [ms] (mean, across all concurrent requests) Transfer rate: 371.72 [Kbytes/sec] received |
qps瞬間達到了1912.78。同時,可以看到伺服器的log還在不停的輸出ping的結果。
想要返回值也很容易。再切換一下使用方式介面。使用tornado的gen模組下的with_timeout功能(這個功能必須在tornado>3.2的版本)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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' |
執行緒池的方式也可以通過使用tornado的yield把函式掛起,實現了協程處理。可以得出耗時任務的result,同時不會block住主執行緒。
1 2 3 4 5 6 7 8 9 10 |
Concurrency Level: 5 Time taken for tests: 0.043 seconds Complete requests: 5 Failed requests: 0 Total transferred: 960 bytes HTML transferred: 0 bytes Requests per second: 116.38 [#/sec] (mean) Time per request: 42.961 [ms] (mean) Time per request: 8.592 [ms] (mean, across all concurrent requests) Transfer rate: 21.82 [Kbytes/sec] received |
qps為116,使用yield協程的方式,僅為非reponse的十分之一左右。看起來效能損失了很多,主要原因這個協程返回結果需要等執行完畢任務。
好比打魚,前一種方式是撒網,然後就完事,不聞不問,時間當然快,後一種方式則撒網之後,還得收網,等待收網也是一段時間。當然,相比同步的方式還是快了千百倍,畢竟撒網還是比一隻只釣比較快。
具體使用何種方式,更多的依賴業務,不需要返回值的往往需要處理callback,回撥太多容易暈菜,當然如果需要很多回撥巢狀,首先優化的應該是業務或產品邏輯。yield的方式很優雅,寫法可以非同步邏輯同步寫,爽是爽了,當然也會損失一定的效能。
非同步多樣化
Tornado非同步服務的處理大抵如此。現在非同步處理的框架和庫也很多,藉助redis或者celery等,也可以把tonrado中一些業務非同步化,放到後臺執行。
此外,Tornado還有客戶端非同步功能。該特性主要是在於 AsyncHTTPClient的使用。此時的應用場景往往是tornado服務內,需要針對另外的IO進行請求和處理。順便提及,上述的例子中,呼叫ping其實也算是一種服務內的IO處理。接下來,將會探索一下AsyncHTTPClient的使用,尤其是使用AsyncHTTPClient上傳檔案與轉發請求。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式