尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

第24章:Python可观测性——日志、指标与追踪

第24章:Python可观测性——日志、指标与追踪 1. 项目背景业务场景食光集市技术团队在某个周二晚上 20:15 接到了第一条告警——“API 响应时间 P99 超过 3 秒”。当时值班运维小李查了 Grafana 仪表盘发现 CPU、内存、数据库连接数都正常。他怀疑是某个第三方服务变慢了但没有证据——因为日志里看不出每次请求的耗时分布也没有链路追踪告诉他是支付回调慢还是地图 API 慢。一个小时后用户投诉量从 5 条涨到 80 条。CTO 亲自打电话拉人排查。最终原因是当天下午一个骑手 App 更新把心跳上报的间隔从 5 秒改成了 0.5 秒一个 bug导致 Redis 的单线程被 Sorted Set 写入打满。这个影响通过链路传播到订单服务的库存查询因为库存也在同一个 Redis 实例上最终表现为订单创建接口变慢。整个排查过程耗时 2 小时——不是因为问题复杂而是因为缺乏观测数据。有监控但没追踪有日志但没关联。运维说感觉慢开发说不是我的模块产品说用户崩了。CTO 会后拍板“可观测性三件套——结构化日志、Prometheus 指标、OpenTelemetry 追踪——下个 sprint 必须全员落地。”痛点没有可观测性的系统如同夜间在高速公路上闭着眼睛开车告警感觉慢没有 SLO服务水平目标不知道什么算正常什么算异常。告警阈值靠拍脑袋要么被噪点淹没过度告警要么漏掉真实故障。日志是散文不是数据print(f订单创建失败: {order_id})没有统一格式——想统计过去一小时有多少订单创建失败得 grep 日志文件再数行数。故障无法定界用户说下单失败开发 A 说我支付的接口返回 200开发 B 说我库存的接口返回 200运维说我 Nginx 日志也显示 200——没人能证明到底哪个链路节点出了问题。没有性能基线不知道 P50/P90/P99 延迟的日常值是多少。有一天 P99 从 200ms 涨到了 300ms——这是正常波动还是系统退化无从判断。2. 项目设计场景2 小时排查事故的复盘会上小李把排查过程中手动 grep 了 47 次日志的截图贴了出来。小胖“大师我感觉’可观测性’是一个筐——什么都能往里装。日志、指标、追踪三者的关系是什么我需要三个都用吗还是 overlapping了”小白“经典问题。我的理解是——日志是’某时刻发生了什么’指标是’过去一分钟发生了什么’追踪是’一个请求完整经历了什么’。但我困惑的是——这三个是不是 Overkill 了我们当前就 6 个微服务全上 OpenTelemetry 是不是杀鸡用牛刀”大师“小白的总结基本正确但有细微差别。三者的关系不是’三选一’而是互补——它们组成了可观测性的’三层金字塔’”层级工具回答什么问题成本粒度日志structlog / loguru“刚才发生了什么”低细单事件指标Prometheus Grafana“系统整体健康吗”中粗聚合值追踪OpenTelemetry Jaeger“这个请求到底卡在哪”高细全链路三者如何配合指标告诉你’有问题’——P99 飙升告警。追踪帮你找到’哪段链路有问题’——火焰图显示POST /orders的 80% 耗时在redis.hgetall。日志帮你找出’具体是哪条数据导致的问题’——追踪里的trace_id关联到日志里的ridabc123看到具体是哪个 Redis key。“对于 6 个微服务的规模——我推荐全上但渐进式落地第一步结构化日志本周第二步 Prometheus 指标下周第三步 OpenTelemetry 追踪下个月——而不是’等全准备好了一起上’。”技术映射指标 仪表盘上的油表和速度表——你一眼就知道车况。追踪 GPS 导航记录——你知道从出发到到达经过的每条路和每个红绿灯。日志 行车记录仪——事后回放具体某一秒发生了什么。小胖“结构化日志我大概懂了——就是把print换成structlog每条日志带上order_id和request_id。但 Prometheus 指标怎么采集我的 FastAPI 接口怎么暴露/metrics端点”大师“Prometheus 的 Python 客户端prometheus-client提供开箱即用的指标采集。FastAPI 集成用prometheus-fastapi-instrumentator一行就搞定”fromprometheus_fastapi_instrumentatorimportInstrumentator appFastAPI()Instrumentator().instrument(app).expose(app)# 访问 /metrics 即可看到 request_total, request_duration_seconds 等指标“但默认指标只覆盖 HTTP 层——如果你想采集业务指标如’当前库存剩余量’需要自定义 Gauge/Counter/Histogram。”三大指标类型Counter只增不减——请求总数、错误总数total_paymentsGauge可增可减——当前库存、在线用户数active_ridersHistogram分布统计——请求延迟 P50/P90/P99order_latency_seconds技术映射Counter 里程表——开了就是开了跑得越多数字越大。Gauge 油量表——加满就上去跑一跑就下来。Histogram 测速雷达——记录每辆车的速度分布算平均/中位/99 百分位。3. 项目实战订单服务可观测性环境准备依赖版本说明Python3.13.14基准版本fastapi0.115Web 框架structlog24结构化日志prometheus-client0.21Prometheus 指标prometheus-fastapi-instrumentator7.0FastAPI 自动指标mkdirfoodmarket-ch24cdfoodmarket-ch24 python-mvenv .venv .venv\Scripts\activate pipinstallfastapi uvicorn structlog prometheus-client prometheus-fastapi-instrumentator pytest httpx分步实现步骤1结构化日志 请求 ID 关联src/logging_setup.py结构化日志——structlog 请求 ID 传播importstructlogimportuuidfromcontextvarsimportContextVar request_id_var:ContextVar[str]ContextVar(request_id,default)order_id_var:ContextVar[str]ContextVar(order_id,default)defadd_global_context(logger,method_name,event_dict):自动追加请求上下文到每条日志ridrequest_id_var.get()oidorder_id_var.get()ifrid:event_dict[request_id]ridifoid:event_dict[order_id]oidreturnevent_dictdefsetup_logging():structlog.configure(processors[structlog.stdlib.filter_by_level,add_global_context,structlog.stdlib.add_log_level,structlog.processors.TimeStamper(fmtiso),structlog.dev.ConsoleRenderer()if__import__(sys).stdout.isatty()elsestructlog.processors.JSONRenderer(),],wrapper_classstructlog.stdlib.BoundLogger,context_classdict,logger_factorystructlog.stdlib.LoggerFactory(),)defget_logger(name:str__name__):returnstructlog.get_logger(name)步骤2FastAPI 集成——指标 健康检查src/main.pyFastAPI 服务——可观测性集成importtimeimportuuidfromfastapiimportFastAPI,Requestfromprometheus_fastapi_instrumentatorimportInstrumentatorfromprometheus_clientimportCounter,Histogram,Gaugefromsrc.logging_setupimportsetup_logging,get_logger,request_id_var setup_logging()loggerget_logger()appFastAPI(title食光集市·订单服务)# ── Prometheus 指标自动采集 ──Instrumentator().instrument(app).expose(app)# ── 自定义业务指标 ──order_created_totalCounter(order_created_total,订单创建总数,[status])order_latency_secondsHistogram(order_latency_seconds,订单创建延迟,buckets[0.01,0.05,0.1,0.5,1,3,5])# ── 请求 ID 中间件 ──app.middleware(http)asyncdefadd_request_id(request:Request,call_next):ridrequest.headers.get(X-Request-ID,str(uuid.uuid4())[:8])request_id_var.set(rid)starttime.perf_counter()responseawaitcall_next(request)elapsedtime.perf_counter()-start response.headers[X-Request-ID]rid response.headers[X-Response-Time]f{elapsed:.3f}slogger.info(request_completed,methodrequest.method,pathrequest.url.path,status_coderesponse.status_code,duration_msround(elapsed*1000,2))returnresponseapp.get(/health)asyncdefhealth():return{status:healthy}app.post(/api/v1/orders)asyncdefcreate_order(request:Request):bodyawaitrequest.json()ifawaitrequest.body()else{}order_idfSG-{uuid.uuid4().hex[:12].upper()}starttime.perf_counter()# 模拟业务处理time.sleep(0.05)elapsedtime.perf_counter()-start order_latency_seconds.observe(elapsed)order_created_total.labels(statussuccess).inc()logger.info(order_created,order_idorder_id,store_idbody.get(store_id,),duration_msround(elapsed*1000,2))return{order_id:order_id,status:created,latency_ms:round(elapsed*1000,2)}app.get(/api/v1/orders/{order_id})asyncdefget_order(order_id:str):logger.info(order_queried,order_idorder_id)return{order_id:order_id,status:pending}步骤3编写测试tests/test_observability.pyimportpytestfromhttpximportAsyncClient,ASGITransportfromsrc.mainimportapppytest.fixtureasyncdefclient():asyncwithAsyncClient(transportASGITransport(appapp),base_urlhttp://test)asac:yieldacpytest.mark.asyncioasyncdeftest_health(client):respawaitclient.get(/health)assertresp.status_code200assertresp.headers.get(X-Request-ID)isnotNonepytest.mark.asyncioasyncdeftest_create_order_returns_request_id(client):respawaitclient.post(/api/v1/orders,json{store_id:ST001})assertresp.status_code200dataresp.json()assertdata[order_id].startswith(SG-)assertX-Request-IDinresp.headersassertX-Response-Timeinresp.headerspytest.mark.asyncioasyncdeftest_create_order_increments_counter(client):# 先获取当前计数器值fromsrc.mainimportorder_created_total beforeorder_created_total.labels(statussuccess)._value.get()awaitclient.post(/api/v1/orders,json{store_id:ST001})awaitclient.post(/api/v1/orders,json{store_id:ST002})afterorder_created_total.labels(statussuccess)._value.get()assertafterbefore2pytest.mark.asyncioasyncdeftest_metrics_endpoint(client):respawaitclient.get(/metrics)assertresp.status_code200textresp.textassertorder_created_totalintextassertorder_latency_secondsintextasserthttp_requests_totalintext# 自动指标运行uvicorn src.main:app--port8000python-mpytest tests/-vcurlhttp://localhost:8000/metrics# 查看 Prometheus 指标curlhttp://localhost:8000/health# 验证请求 ID完整代码清单foodmarket-ch24/ ├── src/ │ ├── __init__.py │ ├── logging_setup.py # 结构化日志 │ └── main.py # FastAPI 指标 ├── tests/ │ └── test_observability.py └── requirements.txt4. 项目总结优点 缺点维度structlog Prometheusprint 无监控ELK StackDatadog / New Relic部署成本★★★★ 低★★★★★★★★★★ (SaaS免部署但贵)查询能力★★★★★★★★★★★★★★告警能力★★★★★★★★★★★★★★学习曲线★★★★★★★★★★★★★★费用★★★★★ 免费★★★★★★★★ 大存储贵★ 按量计费适用场景所有后端服务结构化日志是最低门槛的可观测性——从print迁移到structlog只需 10 行代码。SLA/SLO 监控Prometheus 指标定义 SLI如 P99 延迟 500msGrafana 告警触发 SLO 燃烧率预警。分布式系统故障定位OpenTelemetry 追踪串联所有下游调用。容量规划QPS 增长率 资源使用率的历史趋势预测扩容节点。每周健康报告自动化从 Prometheus 拉取指标生成 PDF 周报替代手工维护 Excel。不适用场景一次性运行脚本日志到 stdout 即可不需要 Prometheus 指标。嵌入式中低资源设备Prometheus client 的 HTTP server 开销在 IoT 设备上难以接受。注意事项日志中不要打敏感信息密码、token、身份证号——在处理器中加正则替换逻辑。Histogram的 bucket 设置要贴合业务如果 P99 是 3 秒但 bucket 只设到 1 秒那么所有超过 1 秒的请求都被归入Inf——分布信息丢失。Counter重启后归零Prometheus 的rate()函数会自动处理计数器重置。Grafana 面板的 PromQL 不要用rate(xxx[1m])如果 scrape interval 是 30 秒——rate的窗口至少是 scrape interval 的 2-4 倍。常见踩坑经验故障案例1structlog 输出 JSON 但 ELK 不解析现象JSON 日志发送到 ELK但所有字段都在message字段中没有自动解析。根因Python 的logginghandler 默认把 structlog 的 JSON 当作普通字符串再包了一层{message: ...}。修复logging.basicConfig的 handler 使用logging.Formatter(%(message)s)——直接输出 structlog 的原始 JSON。故障案例2Prometheus cardinality 爆炸现象request_total的 label 里包含了user_id用户量 100 万时time series 从 100 条暴增到 100 万条——Prometheus 服务器 OOM。根因High cardinality label——Prometheus 中每个 label 组合都是一个独立 time series。修复用户 ID 这种高基数字段只在日志和追踪中记录不要在 label 中使用。指标 label 的基数应控制在 100 以内。故障案例3链路追踪的trace_id在线程池中丢失现象主协程设置了trace_id但ThreadPoolExecutor提交的任务中trace_id为空。根因contextvars只在线程/协程创建时自动继承ThreadPoolExecutor中不会自动传播。修复在executor.submit()前手动复制 contextctx contextvars.copy_context(); executor.submit(ctx.run, func)。思考题rate(http_requests_total[5m])和increase(http_requests_total[5m])返回的值有什么不同它们各自适合什么场景如果同一台机器上运行了 4 个 Python 进程gunicorn workers每个都暴露/metrics端点Prometheus 如何聚合它们的指标哪种指标Counter / Gauge / Histogram会被错误地聚合答案见中级篇综合实战章附录。延伸阅读与资源Python 3实战精进从脚本到高并发订单引擎MongoDB 实战进阶与内核修炼python入门Rquests从菜鸟脚本到企业级SDK的网络实战圣经Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析
返回列表