给 asyncio 爬虫加一个 Semaphore 似乎是标准动作,但写法很容易悄悄失效。这篇用本机服务器统计“同时在处理的请求峰值”,看几种写法到底限住了没有,排队时间算不算超时,以及一次建出几十万个任务要付多少内存。
Semaphore( 5):服务器同时在处理的峰值 5,总耗时 2148 ms(理论下限 2000 ms) Semaphore( 20):服务器同时在处理的峰值 20,总耗时 557 ms(理论下限 500 ms) Semaphore(100):服务器同时在处理的峰值 100,总耗时 138 ms(理论下限 100 ms)
Semaphore(N) 包住整个请求流程时,服务器看到的并发峰值恰好是 N,总耗时紧贴 请求数 / N × 单次耗时。
Semaphore(5),50 个请求,信号量包住整个 async with(含 read):服务器同时在处理的峰值 5,总耗时 3069 ms Semaphore(5),50 个请求,信号量只包住 await session.get():服务器同时在处理的峰值 50,总耗时 328 ms
await session.get() 在收到响应头时就返回,这时释放信号量,响应体还没读。如果服务器的响应体很慢,实际的在途连接数会远超信号量。正确的写法是让 async with sem 包住 async with session.get(...) 的整个块,包括 await r.read()。
TCPConnector(limit=10),total 超时 1.0 秒:200 个请求(每个 0.1 秒,10 并发理论上要 2 秒)→ 成功 90,超时 110,总耗时 1011 ms TCPConnector(limit=0) + Semaphore(10),total 超时 1.0 秒:200 个请求(每个 0.1 秒,10 并发理论上要 2 秒)→ 成功 200,超时 0,总耗时 2154 ms
TCPConnector(limit=10) 让多出来的请求在连接器里排队,而这个排队时间算在 ClientTimeout(total=…) 里:1 秒的总超时下,200 个请求只有 90 个来得及完成。用 limit=0 加自己的 Semaphore,排队发生在超时计时之前,200 个全部成功。
20000 个任务(每个只 sleep 1 ms,信号量/worker 都是 100):gather:峰值内存 51.2 MiB(起点 23.5),耗时 0.31 s;queue:峰值内存 23.6 MiB(起点 23.5),耗时 0.25 s 200000 个任务(每个只 sleep 1 ms,信号量/worker 都是 100):gather:峰值内存 310.8 MiB(起点 23.5),耗时 3.41 s;queue:峰值内存 23.5 MiB(起点 23.3),耗时 2.39 s
gather 先把所有协程创建成任务,哪怕它们大部分只是在等信号量:20 万个任务峰值内存 310.8 MiB,耗时 3.41 s。改成固定数量的 worker 从有界队列取任务,内存不随任务总数增长(23.5 MiB),也更快(2.39 s)。这里每个任务只是 sleep(0.001),真实任务持有的数据会让差距更大。
Semaphore(2),12 个任务按 0..11 顺序创建,实际获得信号量的顺序:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 与创建顺序一致:True
实测等待信号量的任务按到达顺序获得它。要按优先级处理,需要优先级队列而不是信号量。