Python后端中间件专题27:告警响了先看哪里——Grafana 看板、SLO 与故障手册
凌晨告警标题写着“TicketFlow HTTP error ratio exceeded 5%”。值班人员首先需要判断它是 API 故障、上游 PostgreSQL 不可用、某条搜索路径故障,还是根本没有足够流量。看板如果只有彩色折线而没有流量、错误、长尾与异步积压的关系,不能帮人决定下一步。
本课目标与前置
目标是用 provisioned Dashboard 的五组真实 PromQL 找出候选故障域,区分告警阈值和业务 SLO,并按 runbook 做可回滚的核查。前置为 26 的 RED/低基数、14 的 Outbox 与 23 的双 API 入口。能离线验证 JSON、YAML 与内存 mock;“Grafana 已加载、Prometheus 已抓取 API×2/relay/worker”只能由授权远端 integration selector 证明。
上一课练习答案
答案 EX-26-01 [code]
在project/保存为ex26_metrics_probe.py,内存 registry 避免污染默认进程指标:
fromprometheus_clientimportCollectorRegistryfromticketflow.observability.metricsimportTicketFlowMetrics registry=CollectorRegistry()metrics=TicketFlowMetrics(registry=registry)for_inrange(97):metrics.observe_http(method="GET",route="/tickets/{ticket_id}",status_code=200,duration_seconds=0.02)for_inrange(3):metrics.observe_http(method="GET",route="/tickets/{ticket_id}",status_code=503,duration_seconds=1.0)metrics.set_outbox_pending(7)labels=lambdastatus:{"method":"GET","route":"/tickets/{ticket_id}","status_class":status}ok=registry.get_sample_value("ticketflow_http_requests_total",labels("2xx"))bad=registry.get_sample_value("ticketflow_http_requests_total",labels("5xx"))pending=registry.get_sample_value("ticketflow_outbox_pending")assert(ok,bad,pending)==(97.0,3.0,7.0)samples=[sampleforfamilyinregistry.collect()forsampleinfamily.samples]assertall("ticket_id"notinsample.labelsand"tenant_id"notinsample.labelsforsampleinsamples)print(f"2xx={int(ok)}5xx={int(bad)}pending={int(pending)}")运行命令:PYTHONPATH=src python ex26_metrics_probe.py(PowerShell:$env:PYTHONPATH="src"; python ex26_metrics_probe.py)。
预期输出:
2xx=97 5xx=3 pending=7这是人为喂给内存 Counter 的数,不是 Prometheus 抓到的真实流量。
答案 EX-26-02 [prose]
先看 Request rate 与 Prometheus targets:极少量请求时百分比容易剧烈波动。再看 Error ratio 与 p95,并按 route/status_class 下钻;若两台 API/health也失败,先查 PostgreSQL 与迁移,若主要是搜索 503,则核查 ES 降级合同。Outbox pending=120 时同时看 oldest age:年龄持续增大与 relay publish retryable/failed 上升,先查 RabbitMQ/relay;pending 降而 Worker outcome 失败增加,转查队列、DLQ 与消费 receipt。这里的业务 SLO 假设是 99.5% 成功率,即 100000 个有效请求约 500 个失败预算;配置的>5%、五分钟窗口且for: 5m是一条事故告警阈值,并不是 0.5% 的预算计算或正式多窗口 burn-rate 策略。低流量下clamp_min把分母抬高并不能证明用户没有受害,还要看绝对错误数、请求量和 scrapeup。
仪表盘不是截图清单
ticketflow-overview.json固定 Prometheus datasource UIDticketflow-prometheus与 Dashboard UIDticketflow-overview。五组面板对应:
| 面板 | 查询回答的问题 | 使用时需避开的误判 |
|---|---|---|
| Request rate | 五分钟内入口吞吐有多少 | 没流量时“错误率零”不等于可用 |
| Error ratio | 5xx 占总完成请求比例 | 低流量分母 clamp 后仍需看绝对量 |
| p95 latency | 最慢 5% 附近的桶估计 | 全局聚合会掩盖少量慢 route |
| Outbox backlog | SQL pending 数和最老未发年龄 | 同一图里计数和秒是不同单位;不是 broker queue depth |
| Cache hit ratio | 详情缓存命中趋势 | 命中率高也不证明数据库/通知健康 |
看板暂未显示 RabbitMQ 队列深度、Worker outcome 或搜索延迟,事故时必须回到 Prometheus 查询和组件运行状态。不要根据面板标题推测不存在的指标。Grafana provisioning 还安装两个告警:TicketFlowHighErrorRatio查询五分钟 5xx 比率,阈值 >0.05 且for: 5m;TicketFlowOutboxBacklog查询max_over_time(ticketflow_outbox_pending[15m]),阈值 >100 且for: 15m。后者是滚动窗口中的最大值,一次峰值可能留在窗口中影响后续多次评估,不等价于“连续 15 分钟每个采样都大于 100”;runbook 中应结合 oldest age/当前 pending 核查,不应只依据 summary 文字删除 Outbox 行。
SLO 先定义合格请求,再谈预算
若团队批准“30 天内合格公共 API 请求成功率 ≥99.5%”,就必须写清 SLI 的分子、分母与排除项。例如分母可以是完成的公共 API 请求,分子是其中非 5xx 的请求;Nginx 自身 502/504 是否能进入应用 Counter、客户端取消和计划维护如何计入,都需由入口指标补足并评审。100000 个合格请求的失败预算是 500,不是“任意五分钟最多失败 5%”。当前项目没有 30 天 recording rule、预算存储或多窗口 burn-rate 告警,现有>5% for 5m只是一条快速事故信号;本课不会把告警组名ticketflow-slo当成 SLO 已实施的证据。真实 SLO 还要由产品/运维共同批准,并用覆盖入口错误的可审计 SLI 验证。
一次事故的操作路线
收到 HTTP 告警先确认两个ticketflow-apitarget、Request rate 与 p95,再用 23 的代理日志看 upstream 和 request ID,分别检查/live与/health,确认 PostgreSQL 是否可读;只搜索失败时保留工单详情与写入能力。收到积压告警先对 SQL pending/oldest age 做只读查询,核对 RabbitMQ 健康、relay publisher confirms、Worker/队列/DLQ;修复依赖或进程后观察积压回落与业务 effect,不手动把未确认事件标记已发。项目提供docs/runbooks/http-errors.md与docs/runbooks/outbox-backlog.md;告警runbook_url指向http://127.0.0.1:8080/runbooks/...,只适用于入口在同一主机或有正确端口转发的浏览器,远端操作者不应把自己的 localhost 当成远端机器。
从课程根运行正式本地命令python tools/verify_executable_revisions.py --lesson 27 --python python:独立安装并执行 baseline 与 09–27 全部历史合同,解析静态 JSON/YAML、scrape targets、alert 和 runbook;这不是 live Grafana。真实服务需单独在授权隔离环境运行python scripts/run_remote_selector.py --checkpoint 27 --selector tests/integration/test_grafana_provisioning.py::test_running_prometheus_and_grafana_loaded_ticketflow_resources,抓/api/v1/targets、PromQL 样本、dashboard 与 alert rules;未运行保持 PENDING。不要用假造 API JSON 当远端验收输出。
本课练习
练习 EX-27-01 [code]
写project/ex27_dashboard_audit.py解析docker/grafana/dashboards/ticketflow-overview.json,断言 UID、五个唯一面板 ID、标题中同时有Error ratio和Outbox backlog,且所有查询没有ticket_id=或tenant_id=。打印 UID 与面板数量。说明这个静态验证不能证明 Grafana 已加载资源。
练习 EX-27-02 [prose]
若 HTTP 错误比率恢复正常,但 pending=120、oldest age 继续增长,按 runbook 给出安全排查/恢复顺序;说明max_over_time告警为什么不能独自证明持续积压,以及哪项证据才说明 Worker 通知确已产生一份业务效果。
结论与下一站
看板使范围收敛,runbook 把查询变成有边界的动作;仍可能遇到“API 201 很快返回,通知半分钟后才完成”的个例。28 用 Outbox 中的传播上下文把一次异步因果关系画成 span,而不是只看统计平均。
完整核心模块:Grafana provisioned 看板
{"annotations":{"list":[]},"editable":false,"panels":[{"datasource":{"type":"prometheus","uid":"ticketflow-prometheus"},"fieldConfig":{"defaults":{"unit":"reqps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":0},"id":1,"targets":[{"expr":"sum(rate(ticketflow_http_requests_total[5m]))","refId":"A"}],"title":"Request rate","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"ticketflow-prometheus"},"fieldConfig":{"defaults":{"unit":"percentunit"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":0},"id":2,"targets":[{"expr":"sum(rate(ticketflow_http_requests_total{status_class=~\"5xx\"}[5m])) / clamp_min(sum(rate(ticketflow_http_requests_total[5m])), 0.000001)","refId":"A"}],"title":"Error ratio","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"ticketflow-prometheus"},"fieldConfig":{"defaults":{"unit":"s"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":8},"id":3,"targets":[{"expr":"histogram_quantile(0.95, sum by (le) (rate(ticketflow_http_request_duration_seconds_bucket[5m])))","refId":"A"}],"title":"p95 latency","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"ticketflow-prometheus"},"gridPos":{"h":8,"w":12,"x":12,"y":8},"id":4,"targets":[{"expr":"ticketflow_outbox_pending","refId":"A"},{"expr":"ticketflow_outbox_oldest_age_seconds","refId":"B"}],"title":"Outbox backlog","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"ticketflow-prometheus"},"fieldConfig":{"defaults":{"unit":"percentunit"},"overrides":[]},"gridPos":{"h":8,"w":24,"x":0,"y":16},"id":5,"targets":[{"expr":"sum(rate(ticketflow_cache_operations_total{outcome=\"hit\"}[5m])) / clamp_min(sum(rate(ticketflow_cache_operations_total{operation=\"ticket_get\"}[5m])), 0.000001)","refId":"A"}],"title":"Cache hit ratio","type":"timeseries"}],"schemaVersion":39,"tags":["ticketflow","operations"],"title":"TicketFlow Overview","uid":"ticketflow-overview","version":1}