直接在 asyncio.run() 中启动 start_http_server() 会因阻塞主线程、抢占事件循环导致应用无响应或 RuntimeError;正确做法是使用 make_asgi_app()(v0.16+)返回 ASGI 应用并挂载,或采用 starlette_exporter 等 ASGI 兼容方案。

直接用 prometheus_client 暴露指标端点,配合异步框架的生命周期管理,就能跑起来;但不处理事件循环绑定和并发计数器竞争,指标会不准甚至崩溃。
为什么不能直接在 asyncio.run() 里启动 start_http_server()
start_http_server() 是阻塞式 HTTP 服务器,会抢占主线程、阻断事件循环。在 FastAPI 或 Starlette 中硬塞它,会导致应用无法响应请求,或者抛出 RuntimeError: asyncio.run() cannot be called from a running event loop。
- 正确做法是用
ASGI兼容的中间件或独立异步 HTTP 服务(如starlette_exporter) - 若坚持用
prometheus_client,得改用make_asgi_app()(v0.16+)返回 ASGI app,再挂载到主路由下 - 旧版本(make_asgi_app(),必须换库或自己写一个轻量 ASGI handler
FastAPI 里注册 Prometheus 中间件的典型陷阱
很多人直接把 starlette_exporter 的 MetricsMiddleware 加进 app.add_middleware(),却发现 http_requests_total 计数翻倍、延迟指标偏高。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- 默认会统计所有路径,包括
/metrics自身——每次拉指标都触发一次计数,形成自增循环 - 解决方法:设置
filter_unhandled_paths=True,并显式排除/metrics路径 - 若用了
BaseHTTPMiddleware自定义中间件,注意它和MetricsMiddleware的执行顺序:后者应在最外层,否则可能漏掉异常路径 - 并发请求下,
Counter类型安全,但自定义Gauge若在协程中非原子更新(比如先读再写),会产生竞态——应改用asyncio.Lock或改用Summary统计耗时
如何让 Prometheus 正确抓取异步任务的运行时指标
Web 请求指标容易做,但后台任务(如 asyncio.create_task() 启动的定时清理)的活跃数、执行时长、失败次数,需要手动打点,且不能依赖全局事件循环状态。
立即学习“Python免费学习笔记(深入)”;
- 用
prometheus_client.Gauge('task_active', 'Active background tasks'),配合task.add_done_callback()增减数值 - 避免在 task 内部直接调用
gauge.set(1)—— 任务 cancel 后回调可能失效,应统一由任务启动/结束处管理 - 对长时间运行的协程,用
Summary记录单次执行耗时:task_duration = Summary('task_execution_seconds', 'Time spent in background task', ['name']),并在try/except/finally中打点 - 别用
time.time()手动算耗时——asyncio 中可能被 await 切走,应改用asyncio.get_event_loop().time()
异步框架里指标采集的关键不是“有没有”,而是“谁在什么时候改了哪个指标”。Prometheus 本身不关心协程,但你的打点逻辑必须跟事件循环节奏对齐,否则看到的数字只是幻觉。

















