Python 程序运行状态的可观测性设计

6次阅读

python进程需主动暴露运行状态:用psutil采集CPU/内存/线程数,结合threading.active_count()和gc.get_stats()补充应用层指标;http接口应分三级(/health、/ready、/metrics),避免耗时操作;日志与监控需统一OpenTelemetry或通过threading.local()关联;异步任务须用自定义registry+异常安全清理防内存泄漏。

Python 程序运行状态的可观测性设计

怎么让 Python 进程主动暴露运行状态

Python 默认不对外暴露运行时指标,得靠自己加钩子。核心思路是:用 psutil 抓进程级基础数据(CPU、内存、线程数),再配合 threading.active_count()gc.get_stats()(3.12+)或 len(gc.get_objects()) 补充应用层状态。别依赖 sys.getsizeof()对象大小——它不递归,结果严重偏低。

常见错误是只采集启动时的 PID,后续 fork 或 reload 后失效。正确做法是在采集逻辑里每次调用 os.getpid() 动态获取,尤其在多进程(如 gunicorn worker)场景下。

HTTP 接口暴露健康与指标是否足够可靠

单纯加个 /health 返回 {"status": "ok"} 没用,它不反映真实负载。生产环境至少要分三级:

  • /health:只检查自身 socket 可连、主线程存活(用 threading.main_thread().is_alive()
  • /ready:额外验证下游依赖(DB 连接池可用、redis ping 通),超时严格控制在 1s 内
  • /metrics:输出 prometheus 格式,含 python_gc_collected_totalprocess_cpu_seconds_total 等标准指标

注意:不要在 /metrics 里执行耗时操作(如遍历所有 request 对象),否则会拖慢抓取,导致监控系统误判。

立即学习Python免费学习笔记(深入)”;

日志里埋点为什么总对不上监控曲线

根本原因是日志打点和指标采集时间窗口不一致。比如用 Logging.info("req_time=%.3f", time.time() - start) 记耗时,但 Prometheus 的 http_request_duration_seconds 是直方图聚合,两者统计口径不同。

解决办法只有两个:

  • 统一用 OpenTelemetry SDK 上报 trace + metric,让 http.server.request.duration 和日志里的 trace_id 关联
  • 如果不用 OTel,至少把关键耗时存进 threading.local(),在日志 handler 里读取并附加到 log record,再用 Loki 做日志指标联动

别手动在日志里拼 "cpu: {}/mem: {}".format(...) —— 这会让日志解析器崩溃,也污染结构化字段。

异步任务(Celery/AIOHTTP)的状态怎么不丢不重

Celery 的 task-senttask-received 事件默认不持久化,Broker 重启就丢失。必须配 worker_send_task_events = True 并用 celery events 实时消费,或者直接对接 celery.app.control.inspect().active() 定期拉取。

AIOHTTP 场景更麻烦:每个 request handler 是独立 task,但 asyncio.all_tasks() 会混入系统 task(如 dns resolver)。安全做法是用 asyncio.current_task().get_name() 打标记,或在 middleware 里把 task 加入自定义 registry:

active_requests = set() # middleware 中 task = asyncio.current_task() active_requests.add(task) try:     await handler(request) finally:     active_requests.discard(task)

这里容易忽略的是 task cancel 异常——如果没在 finally 清理,集合会持续膨胀,最终吃光内存。

text=ZqhQzanResources