
aiohttp 大请求的性能挑战
在使用 aiohttp 发送大量(例如 50 个)大型(例如每个 5mb)http post 请求时,开发者可能会遇到显著的性能问题。核心挑战在于 aiohttp 内部处理 json 参数时,json 序列化过程(如 json.dumps)是同步且耗时的。对于大型数据负载,这可能导致事件循环被阻塞数十毫秒甚至更长时间。在 asyncio.gather 等并发场景下,这意味着所有请求的序列化操作会串行执行,导致请求并非“可用即发送”,而是等待所有序列化完成后才批量发送,从而显著增加整体延迟。
此外,DNS 解析也是潜在的性能瓶颈,尤其是在高并发或延迟敏感的应用中,每次新的连接都可能涉及 DNS 查询。
解决方案一:优化 JSON 数据序列化
aiohttp 的 ClientSession.post 方法提供了一个 json 参数,方便地处理 JSON 数据的序列化和 Content-Type 头设置。然而,对于大型数据,这种便利性是以阻塞事件循环为代价的。为了避免这种情况,我们应该手动进行 JSON 序列化,并确保这个阻塞操作在独立的线程中执行,不影响主事件循环。
1.1 手动预序列化数据
不再使用 json 参数,而是将 Python 对象预先序列化为字节串,并通过 data 参数传递。同时,手动设置 Content-Type 头。
import json
import asyncio
import aiohttp
async def prepare_data_in_thread(obj) -> bytes:
"""在单独的线程中同步执行 JSON 序列化,避免阻塞事件循环。"""
# json.dumps 是同步操作,对于大对象会阻塞,因此使用 asyncio.to_thread
return await asyncio.to_thread(lambda: json.dumps(obj).encode('utf-8'))
async def send_large_post_request(session: aiohttp.ClientSession, url: str, payload: dict):
"""
发送一个大型 POST 请求,数据预先序列化。
"""
# 1. 在单独的线程中序列化数据
serialized_data = await prepare_data_in_thread(payload)
# 2. 使用 data 参数发送预序列化后的字节数据
# 并手动设置 Content-Type 头
headers = {"Content-Type": "application/json"}
async with session.post(url, data=serialized_data, headers=headers) as response:
response.raise_for_status() # 检查 HTTP 状态码
return await response.text()
async def main():
target_url = "http://example.com/api/upload" # 替换为你的 API 地址
# 构造一个大型的模拟数据负载
large_payload = {"key": "value" * 1000000, "data": [i for i in range(100000)]}
async with aiohttp.ClientSession() as session:
tasks = []
for i in range(50): # 发送 50 个大请求
print(f"准备发送请求 {i+1}...")
tasks.append(send_large_post_request(session, target_url, large_payload))
print("所有请求已准备,等待完成...")
responses = await asyncio.gather(*tasks)
print("所有请求完成。")
# print(responses) # 根据需要处理响应
if __name__ == "__main__":
asyncio.run(main())1.2 注意事项
- asyncio.to_thread 的重要性:json.dumps 是一个同步的、CPU 密集型操作。如果直接在主协程中调用它,即使数据准备好了,事件循环依然会被阻塞。asyncio.to_thread 确保了 json.dumps 在一个单独的线程池中执行,从而释放主事件循环,使其能够继续处理其他 I/O 任务(例如,一旦一个请求的序列化完成,它就可以立即被发送出去,而不是等待所有请求都序列化完毕)。
- 数据不变性:在将数据传递给 asyncio.to_thread 进行序列化时,请确保原始 Python 对象在序列化过程中不会被修改。最好使用不可变的数据结构或在传递前创建数据的副本。
- 编码:确保将字符串编码为字节,通常使用 UTF-8。json.dumps(...).encode('utf-8') 是标准做法。
解决方案二:优化 DNS 解析速度
DNS 解析是网络请求生命周期中的一个关键步骤。如果 DNS 解析速度慢,即使后端服务器响应迅速,整体延迟也会增加。
2.1 安装 aiohttp[speedups]
aiohttp 提供了可选的“加速包”,其中包含了 aiodns 库,它是一个异步 DNS 解析器。安装 aiohttp[speedups] 可以显著提升 DNS 解析性能。
pip install aiohttp[speedups]
安装后,aiohttp 会自动检测并使用 aiodns 进行 DNS 解析,无需额外的代码修改。
2.2 直接使用 IP 地址
如果你的应用场景允许,并且你知道目标服务的 IP 地址,可以直接在 URL 中使用 IP 地址而不是域名。这将完全跳过 DNS 解析步骤,从而消除 DNS 带来的延迟。
# 替换为实际的 IP 地址和端口 target_url_with_ip = "http://192.168.1.100:8080/api/upload"
然而,这种方法通常不适用于生产环境,因为 IP 地址可能会变化,并且不利于负载均衡和服务发现。
2.3 复用 ClientSession
这是一个非常重要的性能优化点,无论是否涉及大请求或 DNS 优化。每次创建 aiohttp.ClientSession 都会建立新的连接池和 DNS 缓存。重复创建会丢弃之前的 DNS 缓存和 TCP 连接,导致性能下降。始终复用同一个 ClientSession 实例,尤其是在发送多个请求时。
# 错误示例:每次请求都创建新的 session
# async def bad_example():
# for _ in range(50):
# async with aiohttp.ClientSession() as session:
# await session.post(...)
# 正确示例:复用 session
async def good_example():
async with aiohttp.ClientSession() as session: # session 只创建一次
tasks = []
for _ in range(50):
tasks.append(send_large_post_request(session, "...", {}))
await asyncio.gather(*tasks)总结
优化 aiohttp 处理大量大型请求的性能,关键在于识别并解决阻塞事件循环的操作。通过以下策略,可以显著提升应用的响应性和吞吐量:
- 预序列化 JSON 数据:避免使用 json 参数,手动将 Python 对象序列化为字节串,并通过 data 参数传递。
- 利用 asyncio.to_thread:将耗时的同步 JSON 序列化操作放入单独的线程中执行,防止阻塞主事件循环。
- 优化 DNS 解析:安装 aiohttp[speedups] 以利用 aiodns 进行异步 DNS 解析,或在特定场景下直接使用 IP 地址。
- 复用 ClientSession:这是最基本的性能优化,确保连接池和 DNS 缓存得到有效利用。
通过实施这些优化措施,您的 aiohttp 应用程序将能够更高效、更快速地处理高并发的大型 HTTP 请求。











