asyncio.create_task() 与 asyncio.sleep() 组合实现非阻塞轮询,需用 while true + await asyncio.sleep() 并独立启动任务,避免主线程阻塞;必须配合 asyncio.wait_for() 控制超时,使用连接池复用、异步 dns 等优化才能达到 sub-10ms 级别延迟。

asyncio.create_task() 与 asyncio.sleep() 组合才是轮询主线
直接用 while True + await asyncio.sleep(1) 是最简可行路径,但必须配合 create_task() 启动,不能在 main() 中阻塞等待。否则整个事件循环会被单次耗时操作拖住,延迟立刻失控。
常见错误是把健康检查逻辑写成同步函数再用 loop.run_in_executor() 包裹——这引入线程调度开销,实测平均延迟增加 8–15ms;纯异步 HTTP 客户端(如 aiohttp 或 httpx.AsyncClient)才能压到 sub-10ms 级别。
- 轮询间隔设为
0.1秒时,asyncio.sleep(0.1)实际调度误差通常在 ±3ms 内;设为0.01则误差可能跳到 ±8ms,不建议低于 50ms - 多个服务并行轮询,每个应独立
create_task(),避免共用一个循环体导致某次超时拖累全部 - 首次检查前加
await asyncio.sleep(0),让事件循环先调度其他 pending task,避免冷启动抖动
超时控制必须用 asyncio.wait_for(),不是 try/except TimeoutError
try/except asyncio.TimeoutError 无效——它只捕获由 wait_for() 主动抛出的异常,不会中断正在运行的协程。没加 wait_for() 的请求会一直挂起,后续轮询全被堵死。
健康检查这类 I/O 密集型任务,超时阈值建议设为预期延迟的 2.5 倍。比如目标 P95 是 40ms,就设 timeout=100,太紧会误判,太松失去意义。
-
asyncio.wait_for(coro, timeout=0.1)是唯一能真正中断挂起协程的机制 - 不要对
wait_for()外层再套asyncio.shield(),否则超时失效 - 如果检查逻辑里含子协程调用(如重试),每个子调用也得单独套
wait_for(),父级超时不管用
连接池复用比重连快 3–7 倍,aiohttp.TCPConnector 配置很关键
每次轮询都新建 TCP 连接,光是三次握手+TLS 握手就吃掉 30–60ms。用连接池后,复用连接的健康检查可稳定在 5–12ms(局域网内)。
aiohttp.TCPConnector 默认 limit=100、limit_per_host=0,看似宽松,但实际中 limit_per_host=30 更稳——避免单 host 占满连接池导致其他服务饿死。
- 务必设
keepalive_timeout=30(默认 15s),匹配服务端 idle timeout -
force_close=False必须显式指定,否则每次响应后强制关连接 - 不用
httpx的默认连接池?它默认不复用 HTTP/1.1 连接,要手动传httpx.AsyncClient(pool=httpx.AsyncConnectionPool(...))
轮询任务崩溃后自动恢复需靠 asyncio.current_task() + 异常捕获闭环
一个轮询 task 因网络闪断或解析失败抛出未捕获异常,就会静默退出,整个健康检查就此中断——没人会告诉你它停了。
正确做法是在 task 内部用 try/except Exception 包住全部逻辑,并在 except 块里打日志 + await asyncio.sleep(1) 后 return,让外层 while 循环继续下一轮。靠外层监控 task 状态反而增加复杂度。
- 不要用
asyncio.ensure_future(),它不返回 task 对象,无法做状态判断 - 崩溃后立即重试?不行。至少 sleep 100ms,否则连续失败会打爆目标服务
- 日志里必须带
task.get_name(),否则多个轮询混在一起根本分不清谁挂了
run_in_executor(仅当 payload > 1MB 且 CPU 密集时才值得)。这些细节漏掉任意一项,P99 延迟就可能从 20ms 跳到 200ms。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











