凌晨三点,交接文档里的命令是错的
目标
为接手的运维团队构建“指标 → 告警条件 → 运行手册条目 → 可执行的检查命令”这条链,并由评分器启动模拟服务的多种故障模式,通过演练确认这条链指向了正确的条目。
为什么重要
交接文档只在写成的那天是对的。如果告警在什么条件下触发、触发后用什么确认,与代码分开写,服务一变文档就过时,而这件事要到凌晨值班人员敲命令时才会暴露。所以告警规则要做成脚本读取的数据,运行手册的检查行要写成用退出码作答的命令,再用检查器和演练反复确认这些命令真的能运行。不把沉默的仪表板(抓取不到指标)当作正常,也是这条链的一部分。
预计 75 分钟。请在默认 60 分钟会话结束前用“+时间”延长(最长 180 分钟)。会话结束后 /root/drill 中的文件会消失,想保留的代码请在结束前另外保存。
材料
- 交接备忘录(告警表、运行手册规则):
/opt/lab/drill/brief.md - 模拟服务:
python3 /opt/lab/drill/fakesvc.py --port 9311 --mode normal(用--help查看故障模式) - 供应商留下的旧运行手册:
/opt/lab/drill/vendor-runbook.md - 评分器不使用你启动的服务器。它会在空闲端口上用多种模式、多个种子亲自启动模拟服务,并运行你的脚本。把提供的数字背下来填进去是通不过的。
步骤
- 以正常模式启动模拟服务,把
/metrics响应不加处理地保存到/root/drill/normal.prom。从原文的# TYPE行中,把이름 형식(占位符依次为名称和类型)每行一个地写到/root/drill/families.txt。 - 编写
/root/drill/promtext.py。python3 promtext.py FILE对每个样本输出{"name","labels","value","timestamp"}对象的 JSON 数组。值为数字,无穷大和 NaN 是字符串"+Inf"、"-Inf"、"NaN",没有时间戳则为 null。跳过注释和空行,正确读取标签值中的逗号、花括号以及\\、\"、\n转义。只要有一个错误的样本行,就以退出码 2 结束。 - 把交接备忘录中的六个告警转写到
/root/drill/alerts.json。格式是{"alerts": [{"alert","severity","runbook","rule"}]},rule.kind 是threshold(metric、op、value)、ratio(metric、denominator、op、value)、expires_within(metric、seconds)、scrape、absent(metrics)之一。评分器会在包含边界值的多种情形下判定你的规则。 - 编写
/root/drill/diagnose.py。python3 diagnose.py --url URL [--rules 경로](占位符为规则文件的路径;默认为/root/drill/alerts.json)抓取一次URL/metrics,用规则判定,并输出{"target": URL, "firing": [{"alert","severity","runbook","labels"}]}。labels 是该样本的标签。退出码在没有告警时为 0,有告警时为 1。阈值要从规则文件中读取。 - 修改
/root/drill/diagnose.py,让它把抓取失败升级为告警。连接失败、HTTP 不是 200、格式错误、没有orders_up时,只发出一个OrdersTargetDown(labels 为{}),退出码为 2。缺少必需指标时,每个缺失的指标发出一个OrdersMetricAbsent,其 labels 为{"metric": 이름}(占位符为指标名称)。 - 把六个告警的运行手册条目写入
/root/drill/runbook.md。条目由## 런북이름(占位符为运行手册名称)标题以及- 경보:、- 확인:、- 판단:、- 조치:、- 에스컬레이션:五行组成。检查行是用反引号包起来的一行命令,用$ORDERS_URL指向服务,用bash -o pipefail -c运行时,正常为 0,属于该故障则以非 0 值立即结束。 - 编写
/root/drill/check_runbook.py。python3 check_runbook.py 런북.md --url URL(占位符依次为运行手册文件名和服务地址)对每个条目,在环境变量ORDERS_URL下用bash -o pipefail -c在 5 秒限制内运行检查命令,并按运行手册的顺序输出[{"id","check","exit","status","reason"}]。status 在退出码为 0 时是ok,否则是broken。reason 是ok、timeout、not-found(127)、exit N、no-check(没有检查行)之一。只要有一个 broken,就以退出码 1 结束。对供应商运行手册运行它,找出已坏的行。 - 编写值班演练脚本
/root/drill/oncall.sh。bash oncall.sh URL通过 diagnose.py 得到告警,对每个告警找到 runbook.md 中的条目并运行检查命令,输出{"target": URL, "incidents": [{"alert","severity","runbook","labels","check_exit","confirmed","escalation"}]}。confirmed 在检查命令以非 0 值结束(故障得到确证)时为 true,escalation 原样取自运行手册的升级上报行。有事件时退出码为 1,没有则为 0。
参考
- 保存原文:
curl -fsS http://127.0.0.1:9311/metrics -o 파일(占位符为文件名)——用管道加工的话,最后的换行符或注释可能会丢失。 - 同时启动多个服务做比较时,要更换端口(9312、9313……)。结束的服务要像
pkill -f 'fakesvc.py --port 9312'这样连端口一起指定来停止。 - 常见错误 1:按逗号切分标签。在
note="handoff \"v2\", see RB"处就会出错。 - 常见错误 2:把抓取失败当作空列表吞掉。这样服务挂掉的夜里,告警数量就是 0 个。
- 常见错误 3:在检查命令中使用
curl ... | grep -q。grep 在第一次匹配时结束,curl 收到 SIGPIPE(退出码 23),在 pipefail 下,明明正常也可能显示为失败。要使用grep -c ... >/dev/null或jq -e这类把输入读完的形式。 - 常见错误 4:只给 bash 设置时间限制。管道后面的 curl 会活下来,检查器也会跟着卡住。要在新会话中启动,并连同进程组一起切断。
原样抓取指标原文
把正常模式模拟服务的 /metrics 响应保存到 /root/drill/normal.prom,把 TYPE 行中的 이름 형식 列表(占位符依次为名称和类型)保存到 /root/drill/families.txt。
# TYPE <이름> <형식>(占位符依次为名称和类型)这一行,每个指标族只有一行。对于 histogram 族,以 TYPE 行中的名称为准,样本中会带着 _bucket、_sum、_count 出现。用 awk 选出前两列是 # 和 TYPE 的行即可。
不被逗号和引号欺骗的解析器
编写把 Prometheus 文本暴露格式转换为样本对象 JSON 数组的 /root/drill/promtext.py。有错误的样本行时,以退出码 2 结束。
标签部分不能用 split(',') 切分。在引号之内,逗号和 } 是值的一部分,反斜杠之后的字符是转义。请做一个逐字符读取的小状态机(是否在引号内、是否紧跟在反斜杠之后)。值用 float() 读取,但要把 JSON 中没有的无穷大和 NaN 转成字符串。
把交接备忘录中的六个告警变成数据
把 /opt/lab/drill/brief.md 中的告警表转写成 /root/drill/alerts.json 规则。
“低于”与“不超过”、“超过”与“不低于”是不同的 op。证书指标是过期时刻,所以用 expires_within;队列不用深度,而用最老消息的年龄。抓取失败写 scrape,必需指标列表写在 absent 的 metrics 中。
抓取一次并用规则判定
编写抓取指标、用 alerts.json 判定并以 JSON 输出触发的告警的 /root/drill/diagnose.py(无告警为 0,有告警为 1)。
把样本按名称分组后,就可以只取出每种规则所需的部分。ratio 要把分子和分母按相同标签配对。expires_within 比较的是“指标值 − 当前时刻”。如果导入上一步的 promtext.py,就不需要重写解析器。
不要把沉默的仪表板当作正常
修改 /root/drill/diagnose.py,把抓取失败以 OrdersTargetDown 和退出码 2 升级为告警,把缺失的必需指标以 OrdersMetricAbsent 升级为告警。
urlopen 对连接失败抛出 URLError,对非 200 的响应抛出 HTTPError。解析器的 ParseError 也属于抓取失败。如果把这三种情况变成空的样本列表,所有比较就都会为假,告警数量变成 0 个——这本身就应该是一个告警。absent 按“是否一个以该名称命名的样本都没有”来判定。
用退出码作答的运行手册
把六个告警的条目写入 /root/drill/runbook.md。检查命令必须实际做到:正常为 0,出现对应故障时为非 0。
诊断端点(/debug/deps、/debug/disk、/debug/tls、/debug/queue)是 JSON,所以 jq -e 很合适。-e 在结果为 false 或 null 时给出退出码 1。curl 要加上 -f(把 4xx、5xx 视为失败)和 -m(时间限制)。target-down 还要能抓住返回 200 的维护页面,所以要检查响应中是否有 orders_up 样本。
运行供应商运行手册,找出已坏的行
编写实际运行运行手册中的检查命令并加以判定的 /root/drill/check_runbook.py,并对 /opt/lab/drill/vendor-runbook.md 运行。
subprocess.run(timeout=…) 在超时后只会杀掉 bash,管道后面留下的 curl 抓着输出管道,等待可能永远不会结束。请用 Popen(start_new_session=True) 启动,超时后用 os.killpg 连同进程组一起切断。退出码 127 表示“找不到命令”。
从告警到升级上报的值班演练
编写一次性跑完 diagnose.py → runbook.md → 检查命令 → 升级上报的 /root/drill/oncall.sh。
diagnose.py 的退出码 1、2 不是错误,而是判定结果,所以不要用 set -e 让脚本中断。运行手册的解析和带时间限制的运行,可以导入第 7 步 check_runbook.py 中的函数重复使用。confirmed 表示“检查命令是否确证了故障”,所以退出码不为 0 时为 true。