不能在主Web服务中直接用generate_latest()或asyncio.run()调start_http_server(),因会导致指标不准、服务卡死;正确做法是metrics独立运行:开发用start_http_server(),生产用make_wsgi_app()或ASGI挂载,且须规避中间件自增、竞态更新等陷阱。

直接在主 Web 服务里用 generate_latest() 暴露指标,或在 asyncio.run() 里调 start_http_server(),基本等于给监控埋雷——不是指标不准,就是服务卡死、抓不到数据。
为什么不能把 metrics 塞进主路由里
FastAPI、Starlette、Flask 等框架的主事件循环/线程池是为业务请求优化的。一旦你在里面注册一个同步的 generate_latest() 路由,所有 metrics 请求都会走同一套调度逻辑:高并发时锁竞争剧烈,Counter 计数可能丢、Gauge 值跳变;更糟的是,若用 start_http_server(8001) 却没隔离进程,多 worker(如 Gunicorn)会抢端口报 Address already in use。
正确路径只有一条:metrics 必须独立于主服务生命周期运行。
- 开发调试:用
start_http_server(8001)单独起一个轻量 HTTP server(基于http.server,非阻塞) - 生产部署(Gunicorn/uWSGI):改用
make_wsgi_app()+ WSGI middleware,让每个 worker 自己管自己的 metrics - ASGI 框架(FastAPI/Starlette):优先选
starlette_exporter,它返回标准 ASGI app,可直接挂载到app.mount("/metrics", ...)
FastAPI 中 MetricsMiddleware 的两个致命陷阱
starlette_exporter.MetricsMiddleware 是最常用方案,但默认配置下极易翻车:
librosa 是一个用于音频和音乐分析的 Python 库。它提供了特征提取、频谱分析、节拍跟踪、起始检测和音频可视化的工具,基于 NumPy 和 SciPy 构建,用于科学音频计算。
立即学习“Python免费学习笔记(深入)”;
- 不设
filter_unhandled_paths=True→ 它会统计/metrics自身的访问,每次 Prometheus 抓取都触发一次http_requests_total自增,指标虚高 - 没排除
/metrics路径 → 在app.add_middleware(MetricsMiddleware, filter_unhandled_paths=True)后,必须显式加一条:app.add_route("/metrics", make_asgi_app(), include_in_schema=False),否则中间件仍会拦截该路径 - 若同时用了自定义
BaseHTTPMiddleware,确保MetricsMiddleware是第一个注册的中间件,否则 404 或异常路径可能漏统计
指标命名和 label 的硬性约束
Prometheus 对指标格式零容忍,错一个字符就拒收,且 prometheus_client 不报错,只能靠日志排查。
- 指标名必须全小写 + 下划线,例如
http_request_duration_seconds,不能是HttpRequestDurationSec或http-request-duration-seconds - label 名同样不能大写,且禁用保留字:
name、help、type、unit都不行;推荐用endpoint、method、status_code - 避免在
Gauge更新中做“读-改-写”操作(比如先gauge.get() + 1再gauge.set()),协程并发下必然竞态;改用gauge.inc()或gauge.dec()原子方法
异步后台任务的指标怎么打点才可靠
Web 请求有中间件自动包住,但 asyncio.create_task() 启的清理任务、定时拉取等,得手动管理生命周期。
- 用
Gauge('task_active', 'Active background tasks', ['name'])跟踪活跃数:任务启动时gauge.labels(name="cleanup").inc(),结束时.dec() - 别在 task 内部直接
gauge.set(0)—— 任务被 cancel 后回调失效,数值就卡住了 - 耗时统计用
Summary('task_execution_seconds', 'Time spent in background task', ['name']),配合try/except/finally打点,不用time.time()手算 - 如果任务本身是长周期协程(如 WebSocket 连接维持),记得在连接断开时显式
gauge.dec(),否则指标永远不降
最容易被忽略的其实是环境适配:本地 localhost:8001 能 curl 通,不代表容器里 Prometheus 就能访问;K8s 里必须用 service-name:8001,Docker Desktop 用 host.docker.internal:8001,且 scrape_configs 的 targets 得严格匹配——少一个字母,http://localhost:9090/targets 里状态就是 DOWN。

















