本文將測試python aiohttp的極限,同時測試其性能表現,以分鐘發起請求數作爲指標。大家都知道,當應用到網絡操作時,異步的代碼表現更優秀,但是驗證這個事情,同時搞明白異步到底有多大的優勢以及爲什麼會有這樣的優勢仍然是一件有趣的事情。爲了驗證,我將發起1000000請求,用aiohttp客戶端。aiohttp每分鐘能夠發起多少請求?你能預料到哪些異常情況以及崩潰會發生,當你用比較粗糙的腳本去發起如此大量的請求?面對如此大量的請求,哪些主要的陷阱是你需要去思考的?
初識 asyncio/aiohttp
異步編程並不簡單。相比平常的同步編程,你需要付出更多的努力在使用回調函數,以事件以及事件處理器的模式進行思考。同時也是因爲asyncio相對較新,相關的教程以及博客還很少的緣故。官方文檔非常簡陋,只有最基本的範例。在我寫本文的時候,Stack Overflow上面,只有410個與asyncio相關的話題(相比之下,twisted相關的有2585)。有個別關於asyncio的不錯的博客以及文章,比如這個、這個、這個,或者還有這個以及這個。
簡單起見,我們先從基礎開始 —— 簡單HTTP hello world —— 發起GET請求,同時獲取一個單獨的HTTP響應。
同步模式,你這麼做:
import requests
def hello()
return requests.get("http://httpbin.org/get")
print(hello())
接着我們使用aiohttp:
#!/usr/local/bin/python3.5
import asyncio
from aiohttp import ClientSession
async def hello():
async with ClientSession() as session:
async with session.get("http://httpbin.org/headers") as response:
response = await response.read()
print(response)
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
好吧,看上去僅僅一個簡單的任務,我寫了很多的代碼……那裏有“async def”、“async with”、“await”—— 看上去讓人迷惑,讓我們嘗試弄懂它們。
你使用async以及await關鍵字將函數異步化。在hello()中實際上有兩個異步操作:首先異步獲取相應,然後異步讀取響應的內容。
Aiohttp推薦使用ClientSession作爲主要的接口發起請求。ClientSession允許在多個請求之間保存cookie以及相關對象信息。Session(會話)在使用完畢之後需要關閉,關閉Session是另一個異步操作,所以每次你都需要使用async with關鍵字。
一旦你建立了客戶端session,你可以用它發起請求。這裏是又一個異步操作的開始。上下文管理器的with語句可以保證在處理session的時候,總是能正確的關閉它。
要讓你的程序正常的跑起來,你需要將他們加入事件循環中。所以你需要創建一個asyncio loop的實例, 然後將任務加入其中。
看起來有些困難,但是隻要你花點時間進行思考與理解,就會有所體會,其實並沒有那麼複雜。
訪問多個鏈接
現在我們來做些更有意思的事情,順序訪問多個鏈接。
同步方式如下:
for url in urls:
print(requests.get(url).text)
很簡單。不過異步方式卻沒有這麼容易。所以任何時候你都需要思考,你的處境是否有必要用到異步。如果你的app在同步模式工作的很好,也許你並不需要將之遷移到異步方式。如果你確實需要異步方式,這裏會給你一些啓示。我們的異步函數hello()還是保持原樣,不過我們需要將之包裝在asyncio的Future對象中,然後將Future對象列表作爲任務傳遞給事件循環。
loop = asyncio.get_event_loop()
tasks = [] # I'm using test server localhost, but you can use any url
url = "http://localhost:8080/{}"
for i in range(5):
task = asyncio.ensure_future(hello(url.format(i)))
tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
現在假設我們想獲取所有的響應,並將他們保存在同一個列表中。目前,我們沒有保存響應內容,僅僅只是打印了他們。讓我們返回他們,將之存儲在一個列表當中,最後再打印出來。
爲了達到這個目的,我們需要修改一下代碼:
#!/usr/local/bin/python3.5
import asyncio
from aiohttp import ClientSession
async def fetch(url):
async with ClientSession() as session:
async with session.get(url) as response:
return await response.read()
async def run(loop, r):
url = "http://localhost:8080/{}"
tasks = []
for i in range(r):
task = asyncio.ensure_future(fetch(url.format(i)))
tasks.append(task)
responses = await asyncio.gather(*tasks)
# you now have all response bodies in this variable
print(responses)
def print_responses(result):
print(result)
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(run(loop, 4))
loop.run_until_complete(future)
注意asyncio.gather()的用法,它蒐集所有的Future對象,然後等待他們返回。
常見錯誤
現在我們來模擬真實場景,去調試一些錯誤,作爲演示範例。
看看這個:
# WARNING! BROKEN CODE DO NOT COPY PASTE
async def fetch(url):
async with ClientSession() as session:
async with session.get(url) as response:
return response.read()
如果你對aiohttp或者asyncio不夠了解,即使你很熟悉Python,這段代碼也不好debug。
上面的代碼產生如下輸出:
pawel@pawel-VPCEH390X ~/p/l/benchmarker> ./bench.py
[<generator object ClientResponse.read at 0x7fa68d465728>,
<generator object ClientResponse.read at 0x7fa68cdd9468>,
<generator object ClientResponse.read at 0x7fa68d4656d0>,
<generator object ClientResponse.read at 0x7fa68cdd9af0>]
發生了什麼?你期待獲得響應對象,但是你得到的是一組生成器。怎麼會這樣?
我之前提到過,response.read()是一個異步操作,這意味着它不會立即返回結果,僅僅返回生成器。這些生成器需要被調用跟運行,但是這並不是默認行爲。在Python34中加入的yield from以及Python35中加入的await便是爲此而生。它們將迭代這些生成器。以上代碼只需要在response.read()前加上await關鍵字即可修復。如下:
# async operation must be preceded by await
return await response.read()
# NOT: return response.read()
我們看看另一個例子。
# WARNING! BROKEN CODE DO NOT COPY PASTE
async def run(loop, r):
url = "http://localhost:8080/{}"
tasks = []
for i in range(r):
task = asyncio.ensure_future(fetch(url.format(i)))
tasks.append(task)
responses = asyncio.gather(*tasks)
print(responses)
輸出結果如下:
pawel@pawel-VPCEH390X ~/p/l/benchmarker> ./bench.py
<_GatheringFuture pending>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>
wait_for=<Future pending cb=[Task._wakeup()]>
cb=[gather.<locals>._done_callback(0)()
at /usr/local/lib/python3.5/asyncio/tasks.py:602]>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>
wait_for=<Future pending cb=[Task._wakeup()]>
cb=[gather.<locals>._done_callback(1)()
at /usr/local/lib/python3.5/asyncio/tasks.py:602]>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>
wait_for=<Future pending cb=[Task._wakeup()]>
cb=[gather.<locals>._done_callback(2)()
at /usr/local/lib/python3.5/asyncio/tasks.py:602]>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>
wait_for=<Future pending cb=[Task._wakeup()]>
cb=[gather.<locals>._done_callback(3)()
at /usr/local/lib/python3.5/asyncio/tasks.py:602]>
發生了什麼?查看本地日誌,你會發現沒有任何請求到達服務器,實際上沒有任何請求發生。打印信息首先打印<_Gathering pending>對象,然後警告等待的任務被銷燬。又一次的,你忘記了await。
修改
responses = asyncio.gather(*tasks)
到
responses = await asyncio.gather(*tasks)
即可解決問題。
經驗:任何時候,你在等待什麼的時候,記得使用await。