三个假设里有两个,用数据 30 分钟就能排除
目标
按流程调查 Pod 的 Prometheus 12 小时数据中的一次事故。用相对时刻钉死时间,统计影响,先写下三个假设和预测,再排除其中两个,用其他指标确证剩下的那一个,留下时间线和事后分析,最后写一个能更快发现问题的检测器,并用同样的数据验证。
为什么重要
故障调查之所以花很长时间,通常不是因为没有数据,而是因为没有顺序。如果先说假设、后看数据,解释就会被假设牵着走。反过来,如果先写下“如果这个假设成立,应该看到什么”,当数据不是那个样子时,就能放弃假设,而证伪比确证便宜得多。在此之前必须做的是钉死时刻——没有开始和结束,像“事故区间的请求率”这样的话根本无法计算。而且所有时刻都用相对时刻来写。绝对时刻会搭上时区、夏令时和日志服务器时钟的便车,毁掉复盘。最后要留下的不只是原因——被排除的假设和被排除的依据,以及检测晚了多少分钟,决定了下个季度要修复什么。
步骤
- 把检测条件写入
/root/obs-incident-triage/onset.promql——5 分钟窗口中 5xx 所占的比例(阈值比较可以加,也可以不加)。然后以 1 分钟间隔的范围查询扫描最近 12 小时,找出这个比例超过 0.05 的区间,在/root/obs-incident-triage/01-onset.txt中写四行——start_minutes_ago=(首次超过的时刻是距现在多少分钟之前,整数)、end_minutes_ago=(最后一次超过的时刻)、duration_min=(超过的分钟数)、peak_ratio=(该区间的最大比例,保留到小数点后第四位)。不要写绝对时刻。 - 只挑出条件为真的分钟,把这些分钟的
increase(...[1m])值相加。在/root/obs-incident-triage/02-impact.txt中写三行failed_requests=(5xx 次数,整数)、total_requests=(全部请求数,整数)、failed_share=(两者之比,保留到小数点后第四位),并在/root/obs-incident-triage/02-handlers.tsv中,把四个处理器每行一个,写成以制表符分隔的三个字段<handler><탭><5xx 건수><탭><전체 5xx 중 비중>(占位符依次为 handler、制表符、5xx 次数、制表符、占全部 5xx 的比重)(比重保留到小数点后第四位)。如果按时刻切割窗口来统计,边界上的次数会整个缺失——请挑出分钟再相加。 - 在
/root/obs-incident-triage/03-hypotheses.tsv中写三行。每行是以制表符分隔的四个字段<id><탭><가설><탭><예측><탭><검증에 쓸 PromQL>(占位符依次为 id、制表符、假设、制表符、预测、制表符、用于验证的 PromQL),id 是h1、h2、h3。假设分别为 h1 = 特定处理器的代码缺陷,h2 = 流量激增导致的过载,h3 = 订单队列下游依赖的拥塞。在预测字段中,用至少 25 个字符写下“如果这个假设成立,数据中应该看到什么”,最后一个字段写能够确认它的查询。h1 的查询必须按handler分开,h2 要看http_requests_total的rate,h3 要看queue_depth。 - 从三个假设中挑出能用数据排除的两个,用两行写入
/root/obs-incident-triage/04-refuted.tsv。每行是以制表符分隔的四个字段<id><탭><측정값><탭><판정><탭><근거>(占位符依次为 id、制表符、测量值、制表符、判定、制表符、依据),判定是refuted。测量值因 id 而定——处理器假设是各处理器 5xx 比例(5 分钟窗口)的 12 小时最大值之间的最大值−最小值(保留到小数点后第四位),流量假设是事故区间的平均每秒请求数 ÷ 事故开始前一小时(去掉最后 5 分钟)的平均值(保留到小数点后第四位)。依据至少 25 个字符,并且必须包含数字。 - 在
/root/obs-incident-triage/05-confirm.txt中写五行——hypothesis=(剩下的假设的 id)、queue_peak=(12 小时内queue_depth的最大值,整数)、queue_baseline=(同样 12 小时的中位数,整数)、overlap_min=(队列深度超过 100 的分钟中,错误条件也为真的分钟数,整数)、evidence=(至少 40 个字符且包含数字的一行)。确证必须使用与最初条件表达式不同的指标。 - 在
/root/obs-incident-triage/06-timeline.tsv中写四行。每行是以制表符分隔的三个字段<event><탭><minutes_ago><탭><근거>(占位符依次为 event、制表符、minutes_ago、制表符、依据),event 有impact_start、detect、impact_end、latency_tail_start四种。detect是目前正在使用的告警(5 分钟窗口的 5xx 比例超过 0.10 的状态持续 10 分钟)响起的时刻,latency_tail_start是 p99 延迟开始超过 1 秒的时刻(与错误事故是不同的事件)。minutes_ago 是整数,依据字段中要用至少 10 个字符写出产生该值的指标或查询名称。 - 在
/root/obs-incident-triage/07-postmortem.txt中写五行——time_to_detect_min=(从影响开始到告警响起是多少分钟,整数)、time_to_recover_min=(从影响开始到影响结束是多少分钟,整数)、impact=(至少 40 个字符,包含数字)、why_late=(检测晚的原因,至少 60 个字符)、next_change=(下次要改变的内容,至少 60 个字符)。这两个数字可以直接从第 6 步的时间线中得出。 - 在
/root/obs-incident-triage/rules/faster.yml中写faster组和FastErrorRatio告警——必须有expr、for、labels.severity、annotations.runbook_url,for的格式是<정수>m(占位符为整数)。这条规则必须比目前的告警(5 分钟窗口、0.10、持续 10 分钟)提前至少 3 分钟响起,并且在同样的 12 小时内,在事故区间之外响起的分钟数不得超过 10 分钟。然后在/root/obs-incident-triage/08-gain.txt中用整数写三行baseline_detect_min_ago=、new_detect_min_ago=、gain_min=(两个值之差)。
参考
- 工作目录是
/root/obs-incident-triage。如果不存在,请先创建。 - 单次查询用
promq "<PromQL>"发出。本实验大量使用范围查询,所以请熟悉curl -sG --data-urlencode "query=..." --data-urlencode "start=..." --data-urlencode "end=..." --data-urlencode "step=60" http://127.0.0.1:9090/api/v1/query_range这种形式。 - Pod 的 Prometheus 中预先放入了 12 小时的数据,其中包含一次事故。另外还有一段只有尾部延迟突起的区间,那是与这次事故不同的事件。
- 不要使用绝对时刻。 所有时刻都写成“距现在多少分钟之前”。评分器也是这样看的。
- 常见错误:按时刻切割事故区间来统计。边界一旦错位,失败次数就会整个缺失——请挑出条件为真的分钟再相加。
- 常见错误:用最初写的条件表达式重新确认剩下的假设。那不是确证,而是同一句话的重复。
- Postmortem Culture (SRE Book 第 15 章)、Effective Troubleshooting (SRE Book 第 12 章)、Managing Incidents (SRE Book 第 14 章)、Alerting rules、Query API (range queries)
用数据钉死开始时刻
把检测条件写入 /root/obs-incident-triage/onset.promql——5 分钟窗口中 5xx 所占的比例(阈值比较可以加,也可以不加)。然后以 1 分钟间隔的范围查询扫描最近 12 小时,找出这个比例超过 0.05 的区间,在 /root/obs-incident-triage/01-onset.txt 中写四行——start_minutes_ago=(首次超过的时刻是距现在多少分钟之前,整数)、end_minutes_ago=(最后一次超过的时刻)、duration_min=(超过的分钟数)、peak_ratio=(该区间的最大比例,保留到小数点后第四位)。不要写绝对时刻。
范围查询是向 /api/v1/query_range 附上 start、end、step 发出的。step=60 时每分钟得到一个值。用 date +%s 得到以秒为单位的现在,减去 43200 就是 12 小时之前。结果 JSON 可以用 jq -r '.data.result[0].values[] | "\(.[0])\t\(.[1])"' 做成表格。
统计影响范围——有多少次失败,集中在哪里
只挑出条件为真的分钟,把这些分钟的 increase(...[1m]) 值相加。在 /root/obs-incident-triage/02-impact.txt 中写三行 failed_requests=(5xx 次数,整数)、total_requests=(全部请求数,整数)、failed_share=(两者之比,保留到小数点后第四位),并在 /root/obs-incident-triage/02-handlers.tsv 中,把四个处理器每行一个,写成以制表符分隔的三个字段 <handler><탭><5xx 건수><탭><전체 5xx 중 비중>(占位符依次为 handler、制表符、5xx 次数、制表符、占全部 5xx 的比重)(比重保留到小数点后第四位)。如果按时刻切割窗口来统计,边界上的次数会整个缺失——请挑出分钟再相加。
如果从第 1 步制作的比例表中,只取出值超过 0.05 的时间戳,就可以与其他查询结果按这些时间戳对齐后相加(awk 'NR==FNR{m[$1]=1;next} ($1 in m){s+=$2}')。处理器名称是 /api/orders、/api/users、/api/search、/healthz。
先写预测,再写假设
在 /root/obs-incident-triage/03-hypotheses.tsv 中写三行。每行是以制表符分隔的四个字段 <id><탭><가설><탭><예측><탭><검증에 쓸 PromQL>(占位符依次为 id、制表符、假设、制表符、预测、制表符、用于验证的 PromQL),id 是 h1、h2、h3。假设分别为 h1 = 特定处理器的代码缺陷,h2 = 流量激增导致的过载,h3 = 订单队列下游依赖的拥塞。在预测字段中,用至少 25 个字符写下“如果这个假设成立,数据中应该看到什么”,最后一个字段写能够确认它的查询。h1 的查询必须按 handler 分开,h2 要看 http_requests_total 的 rate,h3 要看 queue_depth。
预测只要把“看到什么,就能说这个假设错了”反过来写,就很容易得出。例如,如果是某一个处理器的缺陷,其他处理器就应该是平时的样子,而如果全部一起突起,这个假设就被排除。这三个查询必须真的有结果——评分器会亲自发出它们。
用数据排除两个假设
从三个假设中挑出能用数据排除的两个,用两行写入 /root/obs-incident-triage/04-refuted.tsv。每行是以制表符分隔的四个字段 <id><탭><측정값><탭><판정><탭><근거>(占位符依次为 id、制表符、测量值、制表符、判定、制表符、依据),判定是 refuted。测量值因 id 而定——处理器假设是各处理器 5xx 比例(5 分钟窗口)的 12 小时最大值之间的最大值−最小值(保留到小数点后第四位),流量假设是事故区间的平均每秒请求数 ÷ 事故开始前一小时(去掉最后 5 分钟)的平均值(保留到小数点后第四位)。依据至少 25 个字符,并且必须包含数字。
处理器有四个,最大值也就有四个。如果只有一个处理器坏了,这四个值会相差很大;如果是共同原因,它们几乎相同。流量之比如果接近 1,就意味着“不是激增”。这两个数字都可以通过扫描 12 小时的范围查询来求得,事故区间原样使用第 1 步挑出的那些分钟。
用其他信号确证剩下的假设
在 /root/obs-incident-triage/05-confirm.txt 中写五行——hypothesis=(剩下的假设的 id)、queue_peak=(12 小时内 queue_depth 的最大值,整数)、queue_baseline=(同样 12 小时的中位数,整数)、overlap_min=(队列深度超过 100 的分钟中,错误条件也为真的分钟数,整数)、evidence=(至少 40 个字符且包含数字的一行)。确证必须使用与最初条件表达式不同的指标。
中位数只要把值取出来排序,再选中间那个即可。重叠分钟的数量,是第 1 步挑出的分钟集合与队列超过 100 的分钟集合的交集大小。如果两个区间几乎完全重叠,就很可能是同一个事件,这就成了与错误指标相互独立的第二个证据。
机器可读的事故时间线
在 /root/obs-incident-triage/06-timeline.tsv 中写四行。每行是以制表符分隔的三个字段 <event><탭><minutes_ago><탭><근거>(占位符依次为 event、制表符、minutes_ago、制表符、依据),event 有 impact_start、detect、impact_end、latency_tail_start 四种。detect 是目前正在使用的告警(5 分钟窗口的 5xx 比例超过 0.10 的状态持续 10 分钟)响起的时刻,latency_tail_start 是 p99 延迟开始超过 1 秒的时刻(与错误事故是不同的事件)。minutes_ago 是整数,依据字段中要用至少 10 个字符写出产生该值的指标或查询名称。
持续条件(for)的意思是“条件连续为真的分钟累积到那个数量之后”——在 1 分钟间隔的表中统计连续为真的个数,数到第 11 个的那一分钟,就是告警响起的时刻。p99 用 histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) 求得。
事后分析——检测为什么晚了
在 /root/obs-incident-triage/07-postmortem.txt 中写五行——time_to_detect_min=(从影响开始到告警响起是多少分钟,整数)、time_to_recover_min=(从影响开始到影响结束是多少分钟,整数)、impact=(至少 40 个字符,包含数字)、why_late=(检测晚的原因,至少 60 个字符)、next_change=(下次要改变的内容,至少 60 个字符)。这两个数字可以直接从第 6 步的时间线中得出。
检测延迟是由告警的窗口长度和持续条件造成的值——5 分钟窗口本身就会产生延迟,而 for 会再加上相应的时间。在 next_change= 中,如果写下下一步将要实际制作的规则的方向,就能衔接起来。恢复时间指的是事故持续了多久,而不是人做了什么。
写一个能更快发现问题的检测器,并用同样的数据验证
在 /root/obs-incident-triage/rules/faster.yml 中写 faster 组和 FastErrorRatio 告警——必须有 expr、for、labels.severity、annotations.runbook_url,for 的格式是 <정수>m(占位符为整数)。这条规则必须比目前的告警(5 分钟窗口、0.10、持续 10 分钟)提前至少 3 分钟响起,并且在同样的 12 小时内,在事故区间之外响起的分钟数不得超过 10 分钟。然后在 /root/obs-incident-triage/08-gain.txt 中用整数写三行 baseline_detect_min_ago=、new_detect_min_ago=、gain_min=(两个值之差)。
缩短窗口会更快,但噪声也会变大——所以必须用同样的数据一并确认“有没有无效呼叫”。从第 1 步的表中看看平时的错误比例是多少,选一个比它高得多的阈值,即使在短窗口上也会保持安静。请先用 promtool check rules <파일>(占位符为文件)确认格式。