asyncio 是Python 標准庫里的一個異步 I/O 框架。在本文中,我們將介紹 uvloop : 這是 asyncio 默認事件循環的一個代替品,實現的功能完整,且即插即用。uvloop 是用 Cython 寫的,建於 libuv 之上。
uvloop 可以使 asyncio 更快。事實上,它至少比 nodejs、gevent 和其他 Python 異步框架要快 兩倍 。基於 uvloop 的 asyncio 的速度幾乎接近了 Go 程序的速度。
asyncio & uvloop
Asyncio 模塊在 PEP 3156 中引入,是一個網絡傳輸、協議和流量抽象化等的集合,帶有一個可插換的事件循環。這個事件循環是 asyncio 的核心。它給以下功能提供了 API:
- 安排函數調用,
- 通過網絡傳輸數據,
- 執行 DNS 詢問,
- 處理 OS 信號,
- 可創建服務器和連接的方便抽象類
- 異步地處理 subprocess
截止目前,uvloop 還智能在 *nix 平台 和 Python 3.5 中使用。
uvloop是一個對 asyncio 默認事件循環的代替品。你可以用 pip 安裝它:
$ pip install uvloop
在 asyncio 代碼里面使用 uvloop 也很簡單:
實例1:
1 import asyncio 2 import uvloop 3 4 5 asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
上面這段代碼使得任何對 asyncio.get_event_loop()
的調用都將返回一個 uvloop 的實例。
實例2:
1 import asyncio 2 import uvloop 3 4 5 # 聲明使用 uvloop 事件循環 6 asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 7 loop = uvloop.new_event_loop() 8 9 asyncio.set_event_loop(loop)
實例3:
1 import sys 2 import signal 3 import asyncio 4 import logging 5 import uvloop 6 from typing import Coroutine, Any 7 from commons.databases.cache import RedisCache 8 from commons.databases.mongo import MongoCollectionManager 9 from commons.databases.elasticsearch import ElasticSearchManager 10 from .databases.mysql import MysqlConnection 11 from .sdks.th_data import THData 12 from .kafka.producers import KafkaProducerManager 13 14 logger = logging.getLogger(__name__) 15 16 17 def shutdown(sig: signal.Signals) -> None: 18 # stop other tasks 19 logging.info(f"Received exit signal {sig}...") 20 logging.info("Nacking outstanding messages") 21 for task in asyncio.all_tasks(): 22 if task is not asyncio.current_task(): 23 task.cancel() 24 logging.info('bye, exiting in a minute...') 25 26 27 def signal_handler_register(loop: asyncio.AbstractEventLoop) -> None: 28 if sys.platform != 'win32': 29 loop.add_signal_handler(signal.SIGTERM, lambda: shutdown(signal.SIGTERM)) 30 loop.add_signal_handler(signal.SIGHUP, lambda: shutdown(signal.SIGHUP)) 31 loop.add_signal_handler(signal.SIGINT, lambda: shutdown(signal.SIGINT)) 32 33 34 async def startup() -> None: 35 loop = asyncio.get_event_loop() 36 signal_handler_register(loop=loop) 37 # prepare database 38 RedisCache.set_lock() 39 KafkaProducerManager.set_lock() 40 41 # start all sdk 42 THData.create_session() 43 44 45 async def teardown() -> None: 46 # stop all database 47 logging.info("Closing database connections") 48 await RedisCache.close_all() 49 logger.info("Cache are all stoped") 50 MongoCollectionManager.close_all() 51 logger.info("Mongo are all stoped") 52 await ElasticSearchManager.close_all() 53 logger.info("Elasticsearch are all stoped") 54 await KafkaProducerManager.close_all() 55 logger.info("Kafka producer are all stoped") 56 await MysqlConnection.close_all() 57 logger.info("Mysql are all stoped") 58 59 # stop all sdk 60 await THData.close_session() 61 logging.info(f"Flushing metrics") 62 63 64 async def kick(coro: Coroutine) -> Any: 65 try: 66 await startup() 67 return await coro 68 except asyncio.CancelledError: 69 logging.info(f'task {coro} was cannceled!') 70 finally: 71 await teardown() 72 73 74 def set_default_event_loop_policy(): 75 if sys.platform == 'win32' and hasattr(asyncio, 'WindowsSelectorEventLoopPolicy'): 76 asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 77 else: 78 asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 79 80 81 def asyncio_graceful_run(coro: Coroutine, debug: bool = False) -> None: 82 set_default_event_loop_policy() 83 asyncio.run(kick(coro), debug=debug)
架構
uvloop是用 Cython 寫的,其基礎是 libuv。
libuv 是 nodejs 使用的一個高性能、多平台的異步 I/O 庫。由於 nodejs 使用很廣也很流行,使得 libuv 又快又穩定。
uvloop 實現了所有 asyncio 里面的事件循環 API 。高級 Python 對象封裝了底層的 libuv 結構體和函數。為了讓代碼干凈、不重復,並保證手動內存管理都和 libuv 的原語生命周期保持一致,uvloop 使用了子類繼承的方法。
基准測試
為了比較 uvloop 實現和其他實現方法的性能區別,我們創建了一個工具平台(tool bench),來對 TCP 和 UNIX 套接字 I/O 和 HTTP 協議的性能進行測試。
基准測試服務器在一個 Docker 容器里面運行,外面有一個負載生成工具(測試 HTTP 協議則使用 wrk),負責評估請求吞吐量和延遲。
本文提到的所有基准測試都是在一台裝了 Ubuntu 的 Linux 機器上運行的。機器搭載了英特爾 Xeon CPU E5-1620 v2 @ 3.70GHz。我們用的是 Python 3.5,並且所有服務器都是單線程的。除此之外,對 Go 代碼我們設定 GOMAXPORCS=1
, nodejs 不使用集群,並且所有 Python 服務器都是單進程的。每個基准測試都設置了 TCP_NODELAY
標志。
在Mac OS X上運行的基准測試得到了類似的結果。
TCP
該基准測試通過不同大小的消息,對一個簡單的 echo 服務器的性能進行了檢測。我們使用了 1、10和 100KB 的包。並發級別設為 10,每個基准測試運行30秒。
參見完整的TCP基准測試報告
簡單評論一下每個位置的情況:
-
asyncio-steams。 使用內置的純 Python 事件循環的 asyncio 。在這個基准測試里,我們測試了高級別流抽象的性能。我們用
asyncio.create_server()
來創建一個服務器。這個服務器回傳一對(reader, writer)
給客戶端的協同程序。 -
tornado。 這個服務器實現了一個簡單的 Tornado 協議,可以立即傳回它收到的任何數據。
-
curio-steams。Curio 是Python 異步庫中的新生兒。和 asyncio-steams 一樣,在本次基准測試里我們打算測試 curio 的數據流。我們使用
curio.make_steams()
來創建一對(reader, writer)
, 提供了一些高級API,比如readline()
。 -
twisted。和Tornado類似,這里我們測試了一個最簡單的 echo 的協議。
-
curio。 這個基准測試檢驗 curio 套接字的性能:一個由
sock.recv()
和sock.sendall()
協同程序組成的緊湊循環。 -
uvloop-streams。 就像第二個(tornado)一樣,這里我們測試 asyncio 高級數據流的性能,只不過這次我們使用的是 uvloop 。
-
gevent。我們用
gevent.SteamServer
和一個 gevent 套接字,在緊湊循環中來發送/接收數據。 -
asyncio。看起來原生的 asyncio 也很快!和第 2 個(tornado)和第 4 個(twisted)類似,這里我們測試一個最簡的 echo 協議的性能。這個協議是用純 Python 的 asyncio 實現的。
-
nodejs。我們用
net.createServer
API來測試 nodejs v4.2.6 中的數據流性能。 -
uvloop。 這個基准測試中,我們用以 uvloop 為基礎的 asyncio 實現一個最簡單的 echo 協議(像第2、4、8個一樣),並對該協議的性能進行測試。用 1KB 的信息, uvloop是最快的實現,每秒達到了 105,000 次請求!對於 100KB 的信息來說,uvloop 的傳輸速度可以做到 2.3 GB/s。
-
Go。使用由
net.Conn.Read/Write
調用組成的緊湊循環。Golang 的性能和 uvloop 十分相似,對於 10KB 和 100KB 的信息來說性能稍好一些。
HTTP
一開始,我們想比較搭建在 asyncio 和 uvloop 之上的 aiohttp 與 nodejs、Go 的性能差別。aiohttp 是用 asyncio 搭建異步 HTTP 服務器最流行的框架。
然而,aiohttp 的性能瓶頸竟然是它的 HTTP 解析器。這個解析器的速度非常慢,導致底層 I/O 庫再快也沒有用。為了讓事情更有趣一點,我們為 http-parser (nodejs 中的 HTTP 解析器,用 C 編寫,一開始為 Nginx 開發) 創建了一個 Python 綁定。這個庫叫作 httptools ,可在 Github 和 PyPI上找到。
對於 HTTP ,所有的基准測試都使用的 wrk 來生成負載。並發級別設置為 300。每次基准測試的時間為30秒。
出人意料的是,有了高性能 HTTP 解析器的幫助,純 Python 的 asyncio 的速度超過了nodejs,而后者用的也是同一種 HTTP 解析器!
Go在 1KB 的響應上的性能比較快,但是 uvloop+asyncio 的實現在 10/100KB 的表現上明顯比較快。對於用 httptools 的 asyncio 和 uvloop 而言,它們的性能非常棒。Go 語言也是一樣。
不可否認的是,基於 httptools 的服務器非常的簡單,而且比起其他實現方法來,也不包括其他的路由邏輯。然而,這次的基准測試證明了配合一個實現得很有效率的協議,uvloop 可以變得非常之快。
結論
我們可以安全地下結論說,有了 uvloop,我們可以寫出每秒每 CPU 核心可以推送上萬次請求的 Python 網絡互連代碼。在一個多核心系統上,用上進程池,也許還可以進一步地提高性能。
在 Python 3.5 中,配合async/awit的力量, uvloop 和 asyncio 使得用 Python 寫出高性能的網絡互連代碼比以前任何時候都簡單。