同一次故障在仪表板上看不见
目标
对同样的 12 小时数据提出同样的问题,只改变 rate 窗口和 step,用数字测量答案会有怎样的变化,并修复从生产环境仪表板上照搬来的三个坏面板后提交。
为什么重要
信任一个仪表板,应当意味着知道那个面板做了什么样的摘要。然而大多数面板,在有人创建之后,就再也没有人去打开查询看一看。如果窗口是一小时,20 分钟的事故会被压低到三分之一的高度显示;如果窗口比抓取间隔还短,明明有数据,屏幕上却会显示“无数据”。如果对比率求平均,凌晨的一小时和白天的一小时就有了相同的权重;如果对分位数求平均,得到的就是毫无意义的数字。本实验的目的不是学习更多 PromQL,而是学会看到一个面板时,能够问一句“这幅图抹掉了什么”。
步骤
- 创建
/root/obs-graph-traps/qr.py。以python3 qr.py '<PromQL>' <step초> [<범위 시간>] [<임계값>](占位符依次为 step 秒数、范围时长、阈值)调用时,向/api/v1/query_range传入start=지금-범위、end=지금、step=step초(占位符依次为当前时刻与范围、当前时刻、step 秒数)发出一次查询,输出一行points=<돌아온 값의 총 개수> max=<최댓값> over=<임계값보다 큰 값의 개수>(占位符依次为返回值的总数、最大值、大于阈值的值的个数)。范围默认值为 12(小时),阈值默认值为 0.01。如果一个值都没有,就打印points=0 max=NA over=0。最大值写到小数点后六位。 - 把询问 5xx 比率的表达式
sum(rate(http_requests_total{job="shop-api",status="500"}[창])) / sum(rate(http_requests_total{job="shop-api"}[창]))(占位符为窗口)只改变窗口,发出四次。窗口为15s、30s、1m、5m,step 为 60,范围为 12 小时。在/root/obs-graph-traps/window-points.tsv中写四行,没有表头,每行是以制表符分隔的两个字段<창> <점 수>(占位符依次为窗口、点数)。然后在/root/obs-graph-traps/02-why.txt中,用以reason=开头、至少 40 个字符的一行,说明为什么最短的窗口没有点。 - 对同样的 5xx 比率表达式,用窗口
1m、5m、15m、30m、1h发出五次(step 60,范围 12 小时,阈值 0.01)。在/root/obs-graph-traps/window-shape.tsv中写五行,没有表头,每行是以制表符分隔的三个字段<창> <최댓값> <0.01 을 넘은 점 수>(占位符依次为窗口、最大值、超过 0.01 的点数)。最大值保留六位小数,点数为整数。 - Grafana 按
max($__interval + 스크레이프간격, 4 * 스크레이프간격)(占位符为抓取间隔)计算$__rate_interval。这个 Pod 的抓取间隔是 15 秒。分别计算 step 为15、108、600、3600秒时的 rate 窗口,然后用该窗口、以同样的 step 发出 5xx 比率表达式(范围 12 小时)。在/root/obs-graph-traps/rate-interval.tsv中写四行,没有表头,每行是以制表符分隔的四个字段<step초> <rate 창 초> <점 수> <최댓값>(占位符依次为 step 秒数、rate 窗口的秒数、点数、最大值)。然后在/root/obs-graph-traps/04-note.txt中,用以reason=开头、至少 40 个字符的一行,说明为什么只有在 step 为 3600 时,每次重新发出同样的查询,最大值都会不同。 - 用两种方法求同样 12 小时的 5xx 比率。一种是 1 分钟间隔比率的简单平均
avg_over_time((sum(rate(http_requests_total{job="shop-api",status="500"}[5m])) / sum(rate(http_requests_total{job="shop-api"}[5m])))[12h:1m]),另一种是按流量加权的整体比率sum(increase(http_requests_total{job="shop-api",status="500"}[12h])) / sum(increase(http_requests_total{job="shop-api"}[12h]))。在/root/obs-graph-traps/weighted.txt中写三行——avg_of_ratio=<소수 여섯 자리>、weighted_ratio=<소수 여섯 자리>、gap_pct=<소수 두 자리>(占位符依次为保留六位小数的值、保留六位小数的值、保留两位小数的值)。gap_pct是 (前面的值 − 后面的值) ÷ 后面的值 × 100。 - 把
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="shop-api"}[창])))(占位符为窗口)用窗口5m、15m、12h发出三次(step 60,范围 12 小时)。在/root/obs-graph-traps/p99-window.tsv中写三行,没有表头,每行是以制表符分隔的两个字段<창> <12시간 최댓값>(占位符依次为窗口、12 小时最大值)。然后在/root/obs-graph-traps/06-note.txt中,用以reason=开头、至少 40 个字符的一行,说明为什么长窗口的分位数会小于短窗口分位数的最大值。 /opt/lab/graph/panels.yml中的面板p1询问的是“过去 12 小时 5xx 的次数”,却给出带小数点的值。在/root/obs-graph-traps/fix-count.promql中写出能把同样 12 小时的 5xx 次数以整数给出的 PromQL(可以保留注释#)。然后在/root/obs-graph-traps/count-panel.txt中写两行——broken_value=<소수 세 자리>(占位符为保留三位小数的值)是 p1 的表达式目前给出的值,fixed_value=<정수>(占位符为整数)是修复后的查询给出的值。评分器会真正发出修复后的查询来查看值。/opt/lab/graph/panels.yml中的面板p2(错误比率)和p3(p99 延迟)都回答不了“最高的时候是多少”。请把修复后的查询分别写入/root/obs-graph-traps/fix-ratio.promql和/root/obs-graph-traps/fix-p99.promql。然后在/root/obs-graph-traps/verdict.tsv中写两行,没有表头,每行是以制表符分隔的四个字段<패널id> <고치기 전 12시간 최댓값> <고친 뒤 12시간 최댓값> <무엇이 틀렸었나>(占位符依次为面板 id、修复前 12 小时最大值、修复后 12 小时最大值、原先错在哪里)。第一行是p2,第二行是p3,最大值按 step 60、范围 12 小时测量,保留六位小数。第四个字段至少 20 个字符,并且必须包含至少一个数字。
参考
- 工作目录是
/root/obs-graph-traps。如果不存在,请先创建。 - 面板原件在
/opt/lab/graph/panels.yml中。第 7、8 步使用这个文件的expr。 - 查询可以用
promq "<PromQL>"直接发出试试。范围查询用第 1 步制作的qr.py来看。 - 这个 Pod 的抓取间隔是 15 秒,回填区间的分辨率是 30 秒,数据是 12 小时的。
- 不要启动 Grafana。本实验只看面板背后的查询——仪表板的制作会在其他实验中涉及。
- 常见错误:只看最大值来选窗口。缩小窗口,峰值会保留下来,噪声也会一起保留下来。
- 常见错误:把
increase()的结果当作精确的次数来读。它是在两端外推出的估计值。 - HTTP API — range queries、Query functions、Histograms and summaries、Grafana — $__rate_interval、Monitoring Distributed Systems (SRE Book 第 6 章)
制作把范围查询汇总成数字的工具
创建 /root/obs-graph-traps/qr.py。以 python3 qr.py '<PromQL>' <step초> [<범위 시간>] [<임계값>](占位符依次为 step 秒数、范围时长、阈值)调用时,向 /api/v1/query_range 传入 start=지금-범위、end=지금、step=step초(占位符依次为当前时刻与范围、当前时刻、step 秒数)发出一次查询,输出一行 points=<돌아온 값의 총 개수> max=<최댓값> over=<임계값보다 큰 값의 개수>(占位符依次为返回值的总数、最大值、大于阈值的值的个数)。范围默认值为 12(小时),阈值默认值为 0.01。如果一个值都没有,就打印 points=0 max=NA over=0。最大值写到小数点后六位。
范围响应中的值以字符串形式位于 data.result[].values[][1]。如果有多条时间序列,请全部合并后统计。可能会混入 NaN 字符串,所以必须用 math.isnan 过滤,最大值才不会被破坏。只使用标准库(urllib.request、json、time、math)。
空图不意味着没有流量
把询问 5xx 比率的表达式 sum(rate(http_requests_total{job="shop-api",status="500"}[창])) / sum(rate(http_requests_total{job="shop-api"}[창]))(占位符为窗口)只改变窗口,发出四次。窗口为 15s、30s、1m、5m,step 为 60,范围为 12 小时。在 /root/obs-graph-traps/window-points.tsv 中写四行,没有表头,每行是以制表符分隔的两个字段 <창> <점 수>(占位符依次为窗口、点数)。然后在 /root/obs-graph-traps/02-why.txt 中,用以 reason= 开头、至少 40 个字符的一行,说明为什么最短的窗口没有点。
这个 Pod 的抓取间隔是 15 秒。rate 必须在窗口内至少有两个样本才能算出变化率,如果只有一个,返回的不是错误,而是空结果。所以官方文档建议把窗口设为抓取间隔的四倍以上。回填区间的分辨率是 30 秒,所以 30 秒的窗口也几乎是空的。
加宽窗口,峰值会降低,宽度会变宽
对同样的 5xx 比率表达式,用窗口 1m、5m、15m、30m、1h 发出五次(step 60,范围 12 小时,阈值 0.01)。在 /root/obs-graph-traps/window-shape.tsv 中写五行,没有表头,每行是以制表符分隔的三个字段 <창> <최댓값> <0.01 을 넘은 점 수>(占位符依次为窗口、最大值、超过 0.01 的点数)。最大值保留六位小数,点数为整数。
step 固定为 60,所以一个点就是 1 分钟。第三个字段可以读作“图所表示的事故时长(分钟)”。请同时看两个方向:高度缩小为几分之一的同时,宽度扩大了几倍。
面板宽度决定窗口——手工计算 $__rate_interval
Grafana 按 max($__interval + 스크레이프간격, 4 * 스크레이프간격)(占位符为抓取间隔)计算 $__rate_interval。这个 Pod 的抓取间隔是 15 秒。分别计算 step 为 15、108、600、3600 秒时的 rate 窗口,然后用该窗口、以同样的 step 发出 5xx 比率表达式(范围 12 小时)。在 /root/obs-graph-traps/rate-interval.tsv 中写四行,没有表头,每行是以制表符分隔的四个字段 <step초> <rate 창 초> <점 수> <최댓값>(占位符依次为 step 秒数、rate 窗口的秒数、点数、最大值)。然后在 /root/obs-graph-traps/04-note.txt 中,用以 reason= 开头、至少 40 个字符的一行,说明为什么只有在 step 为 3600 时,每次重新发出同样的查询,最大值都会不同。
$__interval 是面板的时间范围除以像素宽度所得的值,所以在这里 step 就是 $__interval。108 是把 12 小时的面板画成 400 像素时得到的值,3600 则是在查看宽得多的范围时得到的。窗口可以像 3615s 这样,直接以秒为单位的字符串放入。网格从 start 开始,以 step 为间隔排布,而 start 会随着“现在”而移动——当 step 比事故时长还长时,网格点落在峰值的哪个位置,就会改变最大值。
比率的平均不是整体比率
用两种方法求同样 12 小时的 5xx 比率。一种是 1 分钟间隔比率的简单平均 avg_over_time((sum(rate(http_requests_total{job="shop-api",status="500"}[5m])) / sum(rate(http_requests_total{job="shop-api"}[5m])))[12h:1m]),另一种是按流量加权的整体比率 sum(increase(http_requests_total{job="shop-api",status="500"}[12h])) / sum(increase(http_requests_total{job="shop-api"}[12h]))。在 /root/obs-graph-traps/weighted.txt 中写三行——avg_of_ratio=<소수 여섯 자리>、weighted_ratio=<소수 여섯 자리>、gap_pct=<소수 두 자리>(占位符依次为保留六位小数的值、保留六位小数的值、保留两位小数的值)。gap_pct 是 (前面的值 − 后面的值) ÷ 后面的值 × 100。
这两个值可以用 promq 直接发出试试。前一个表达式把流量少的凌晨 1 分钟和繁忙的白天 1 分钟按相同的权重统计,后一个表达式则按请求数赋予权重。如果事故发生在清闲的时段,请先想一想哪一个会更大。
长窗口的 p99 不是最糟时的 p99
把 histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="shop-api"}[창])))(占位符为窗口)用窗口 5m、15m、12h 发出三次(step 60,范围 12 小时)。在 /root/obs-graph-traps/p99-window.tsv 中写三行,没有表头,每行是以制表符分隔的两个字段 <창> <12시간 최댓값>(占位符依次为窗口、12 小时最大值)。然后在 /root/obs-graph-traps/06-note.txt 中,用以 reason= 开头、至少 40 个字符的一行,说明为什么长窗口的分位数会小于短窗口分位数的最大值。
直方图的桶也是计数器,所以 rate 的窗口同样适用。窗口一长,缓慢的请求和快速的请求就混在一起,尾部被稀释。所以仅凭一个“过去 12 小时 p99”面板,回答不了“最糟的时候是多少”。阈值保持默认值即可——这一步只看最大值。
应用 ① — 次数面板显示出小数点
/opt/lab/graph/panels.yml 中的面板 p1 询问的是“过去 12 小时 5xx 的次数”,却给出带小数点的值。在 /root/obs-graph-traps/fix-count.promql 中写出能把同样 12 小时的 5xx 次数以整数给出的 PromQL(可以保留注释 #)。然后在 /root/obs-graph-traps/count-panel.txt 中写两行——broken_value=<소수 세 자리>(占位符为保留三位小数的值)是 p1 的表达式目前给出的值,fixed_value=<정수>(占位符为整数)是修复后的查询给出的值。评分器会真正发出修复后的查询来查看值。
increase() 和 rate() 因为样本并不恰好落在窗口边界上,所以会在两端外推。因此本应是整数的次数会以小数出现。如果想要精确的次数,就必须用不外推的方法来问——想一想计数器当前值与 12 小时前的值之差。offset 修饰符会有帮助。
应用 ② — 修复错误比率和 p99 面板后提交
/opt/lab/graph/panels.yml 中的面板 p2(错误比率)和 p3(p99 延迟)都回答不了“最高的时候是多少”。请把修复后的查询分别写入 /root/obs-graph-traps/fix-ratio.promql 和 /root/obs-graph-traps/fix-p99.promql。然后在 /root/obs-graph-traps/verdict.tsv 中写两行,没有表头,每行是以制表符分隔的四个字段 <패널id> <고치기 전 12시간 최댓값> <고친 뒤 12시간 최댓값> <무엇이 틀렸었나>(占位符依次为面板 id、修复前 12 小时最大值、修复后 12 小时最大值、原先错在哪里)。第一行是 p2,第二行是 p3,最大值按 step 60、范围 12 小时测量,保留六位小数。第四个字段至少 20 个字符,并且必须包含至少一个数字。
p2 不是表达式写错,而是窗口太长。p3 有两处错误——窗口也太长,而且在对已经算出的分位数求平均。直方图必须先按 le 汇总桶,再求分位数。修复后的值应当与第 3 步、第 6 步中已经看到的数字相同,才算正常。