不能在 asyncio.run() 中调用 start_http_server(),因其基于同步阻塞式 http.server,会抢占事件循环导致业务路由无响应;应改用 asgi 方式挂载 /metrics,如 prometheus_fastapi_instrumentator 或 make_asgi_app(),并确保指标操作线程/协程安全。

不能在 asyncio.run() 里调用 start_http_server(),它会卡死事件循环——这是异步服务集成 Prometheus 最常见的阻塞点。
为什么 start_http_server() 在 async 里直接用就崩
start_http_server() 启动的是同步阻塞式 HTTP 服务器(基于 http.server),它会独占主线程、抢占事件循环调度权。你在 asyncio.run() 或 uvicorn.run() 启动后立刻调它,结果就是:应用看似启动了,但所有协程都不执行,/metrics 看似能访问,而业务路由完全无响应,甚至抛出 RuntimeError: asyncio.run() cannot be called from a running event loop。
这不是 bug,是设计使然:它根本不是为异步环境准备的。
- 现象:服务启动后 CPU 占用低、请求无日志、
print()不输出、WebSocket 连不上 - 验证方式:注释掉
start_http_server(),看业务是否恢复 - 替代方案唯一路径:走 ASGI 路由暴露指标,而非另起一个 HTTP 服务
FastAPI / Starlette 正确挂载 /metrics 的两种方式
核心原则:指标端点必须是 ASGI callable,和主应用共享同一个事件循环。
✅ 推荐用 prometheus_fastapi_instrumentator(FastAPI)或 prometheus_client.make_asgi_app()(Starlette 及通用 ASGI):
- FastAPI 用户:
pip install prometheus-fastapi-instrumentator,然后两行:from prometheus_fastapi_instrumentator import Instrumentator Instrumentator().instrument(app).expose(app, include_in_schema=False)
- 纯 Starlette / 自定义 ASGI 应用:
pip install prometheus-client>=0.16,然后:from prometheus_client import make_asgi_app metrics_app = make_asgi_app() app.mount("/metrics", metrics_app) - 注意:旧版
prometheus-client 没有 <code>make_asgi_app(),必须升级,否则只能自己写轻量 ASGI handler
Counter 和 Gauge 在协程里怎么才安全
默认的 Counter 和 Gauge 内部用的是 threading.Lock,但在 asyncio 单线程高密度协程切换下,锁完全失效——高频更新(如每秒上千请求、WebSocket 心跳)会导致 .inc() 丢数、.set() 值错乱。
两种稳妥解法,选其一:
- 显式加
asyncio.Lock:对关键指标包裹操作,比如lock = asyncio.Lock() async with lock: REQUEST_COUNT.inc() - 换用
prometheus_async:它用asyncio.Queue缓冲变更,再批量 flush,天然适配协程调度;但注意它不支持Histogram,耗时类指标仍得用原生库 - 绝对避免:在
async for循环、后台create_task()、中间件dispatch函数里直接调.inc()或.set()
测接口耗时必须用 Histogram,别碰 Summary
Summary 在客户端本地维护滑动窗口算分位数(P95/P99),异步场景下每个协程/worker 都独立算,没共享状态,结果不可靠且性能差——尤其在高并发或长周期任务中,内存和 CPU 开销会随时间线性增长。
Histogram 是唯一推荐方案,但要注意两点:
- 必须显式配置
buckets,别依赖默认(太宽泛,P99 会不准);例如 API 延迟常用:buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] - 务必用
with histogram.time():包裹逻辑,而不是手动time.time()相减——前者自动处理异常路径、确保 observe 被调用 - 后台任务耗时也一样:用
Summary记录单次执行时间是错的,该用Histogram+time()上下文管理器
最常被忽略的一点:即使你用了 ASGI 挂载,如果中间件(比如 MetricsMiddleware)没过滤 /metrics 自身,每次 Prometheus 抓取都会触发一次计数,造成指标虚高。记得设 filter_unhandled_paths=True 并排除该路径。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











