TT Lab
开始
学习 学习路径 课程

可观测性

日志全都留下,反而什么都找不到

在 TT Lab 中继续学习

一句话总结

日志不是按行计价的。是按请求保留还是消失,决定了能否调查,而级别和采样率是叠加其上的成本旋钮。

为什么需要它

SRE Book 的故障排查一章所说的调查第一步,是找出留下症状的请求,而在故障调查中会发生这样的事。已经查到了失败请求的 request_id。把这个 ID 输入日志存储,只出现两行——“request started”和“request failed”。中间发生了什么,什么都没有。因为上个月的成本会议之后,把 DEBUG 关掉了。

相反的情况同样糟糕。重新打开 DEBUG 的团队,一个月后存储费用涨到了四倍,搜索变慢,已经没法用于调查。接下来顺手的选择是“只保留 10%”。然而,如果按行抽样,同一个请求只剩下一半,没有一个请求能从头读到尾。成本降下来了,可调查性变成了 0。

三次犯的都是同一个错误。把日志的单位看成行。人在调查时读取的单位不是行,而是一个请求留下的一组行。

工作原理

所以采样要按请求来抽。对 request_id 求哈希,除以 N 的余数为 0 的请求才保留,这样一个请求的各行要么整体保留,要么整体消失。这称为一致性头部采样(consistent head sampling)。哈希只要 Python 标准库的 hashlib 就足够了。哈希做的事只有一件——对同一个 ID,无论何时何地计算,都给出同样的答案。所以如果多个服务使用同样的规则,一个请求的流程就能跨越服务边界延续下去。

在此之上再叠加一条规则。以错误结束的请求,无论采样率是多少,全部保留。 值得调查的请求通常是失败的请求,而这类请求只占整体的百分之几,所以保留它们几乎不会增加成本。同时采用 10% 的采样率和保留错误,行数会从 9.6% 略微增加到 14.3%,但失败请求的保留率会从 0% 提高到 100%。投入产出比比这更高的旋钮,很少见。

剩下的两个旋钮是处理重复的。陷入重试循环的代码,每秒会打印几百次同样的行。把连续相同的行折叠成一行,并附上 repeated=<k>,就能在不丢失信息的前提下缩小体积。如果还是有剩余,就设置每秒上限——不过上限只应该加在 DEBUG 上。如果因为触及上限而使 ERROR 消失,成本是降了,调查却变得不可能。

旋钮 减少的 失去的
降低级别 体积的大部分 失败请求的整个流程
按请求采样 与体积成比例 没被抽中的全部请求
保留错误 增加(少量) 无
折叠重复 重试循环的体积 无(次数会保留)
每秒上限 暴增区间 超出上限的行

在策略文档中,除了这些旋钮,还要按级别写明保留期限。几乎没有理由把 DEBUG 保留 90 天,而如果 ERROR 只保留 3 天,在季度复盘时就什么也找不到。如果把级别统一成一个保留期限,就会落入两者之一——迁就 DEBUG 而丢失 ERROR,或者迁就 ERROR 而让 DEBUG 的费用翻好几倍。

要让所有这些旋钮生效,有一个前提。行中必须有 request_id。OpenTelemetry 的日志数据模型把 trace_id 和 span_id 作为日志记录的一等字段,原因就在这里。没有 ID 的日志,既无法按请求归组,也无法按请求抽样,最终只能按行切割。降低成本这件事的一半,在埋点阶段就已经决定了。

在现场相遇的样子

有个团队把采样率降到 1%,并汇报说“把成本降低了 99%”。三个月后调查支付失败时,日志里一条相关请求都没有。因为没有保留错误的规则。加上这条规则后,成本回升到 1.3%,失败请求全部保留了下来。用 1.3% 与 1% 之间的差距买到的,是“可调查”。

在另一个团队里,每秒上限是不分级别地设置的。平时没有任何问题,可当真正的故障发生、每秒涌入数千行的那一刻,触及了上限,ERROR 行被截掉了。偏偏缺失的是最需要的时刻的日志。这个设计忘记了,上限生效的不是平时,而是最糟糕的时刻。

第三个案例与折叠有关。陷入重试循环的批处理作业,一天打印了 4 亿行,而这些行一个字符都没有不同。加入一个连续折叠之后,这 4 亿行减少到了 2 万行,而且多亏了 repeated= 的数字,“重试了多少次”这个问题仍然可以回答。丢弃和汇总是两回事。

下一项实验要做什么

用 Pod 中预先放好的固定日志文件制作按级别划分的成本表,并亲自统计关掉 DEBUG 之后,出事故的请求的九行中还剩下几行。然后制作一个用 request_id 哈希抽取按请求采样的过滤器,再加上保留以错误结束的请求的规则,把采样率与可调查性之间的权衡做成表。再制作一个折叠重复并设置每秒上限的过滤器,用把两个过滤器串联起来的流水线,在减少 88% 的同时,确认那九行全部保留,最后写出包含级别、采样率、保留期限和预期成本的策略文件。