用 asyncio.Semaphore(20) 控制下游调用并发,跑批到一半所有协程都不再前进,日志停在等待信号量,重启后同样的批次数小时后必现。

一开始怀疑是「同一个信号量在嵌套调用里重复 acquire 两次」,查了一圈,方向不对。

原因不止一个,得分开看。超时异常在 acquire 之后抛出,release 写在 try 里被跳过。少数情况下是协程被取消时没有 finally 释放信号量。

另外,release 次数多于 acquire,计数被抬到上限后不再阻塞。

上来先看当前阻塞的协程数量与等待时间

python -c "import asyncio;print(len(asyncio.all_tasks()))"

再往下把 acquire 与 release 改成异步上下文管理器

python -c "import asyncio;print(asyncio.Semaphore)"

接着在超时路径里显式记录是否释放

python batch.py --guard-release --timeout 10

剩下的就是用 200 个任务压测确认无泄漏

python -c "import asyncio;print(asyncio.run(main()))"

恢复的判定其实很直接。200 个任务并发跑完后信号量计数回到初始值,等待队列为空,超时任务也没有让后续请求卡住。

信号量泄漏的表现是所有协程静止而不是报错,acquire 与 release 必须成对放进同一个 try/finally 边界。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。

作者 9823189