跳至正文
Starone

运维

分布式系统的可观测性

一份关于指标、日志和链路追踪的实用指南:三者各自回答什么问题,基数如何决定成本,以及 SLO 如何把遥测数据变成值得半夜起床处理的告警。

· 阅读约 5 分钟 · Starone 工程团队

监控告诉你一个已知的故障发生了。可观测性则是仅凭系统已经发出的遥测数据,去解释一个没人预料到的故障的能力。在分布式系统里这个区别很重要,因为真正值得关注的故障,往往出在那些单独看都很健康的服务之间的相互作用上。

我们的立场是,可观测性应当从用户体验出发倒推着设计。先定服务等级目标,再对请求路径做埋点,让一个请求可以被端到端地跟踪,其余的数据只有在能回答某个会有人提出的问题时才去采集。把所有能采的遥测都采下来、指望日后用得上,换来的只是一张高额账单和一次缓慢的排查。

三种信号,三类不同的问题

指标、日志和链路追踪并不能互相替代。每一种都只对某一类问题成本低廉,对其他问题则要么昂贵,要么毫无用处。

  • 指标是随时间预先聚合的数值。它能以很低的成本、在很长的时间跨度上回答是否出了问题以及有多严重。
  • 链路追踪记录单个请求跨服务的路径。它回答时间花在了哪里、是哪个依赖出了故障。
  • 日志是带有细节的离散事件。当你已经知道该看哪里时,它回答某一点上究竟发生了什么。

基数就是成本模型

指标的存储方式是:标签值的每一种唯一组合对应一条时间序列。一个按服务、端点和状态码打标签的请求计数器,只有几百条序列。加上一个用户 ID 标签,它就变成每个用户、每个端点、每个状态各一条序列,Prometheus 这类存储远在它能派上用场之前就会耗尽内存。

我们遵循的规则是:指标标签必须有界而且数量少。用路由模板,不用原始 URL。用状态码类别,不用错误信息。用户、租户、订单或请求 ID 这类高基数标识符应当放在链路追踪和结构化日志里,在那里每个事件单独存储,成本与数据量成正比,而不是与各维度的乘积成正比。Exemplar 在两者之间架起桥梁:它把一个采样的 trace ID 挂到指标的某个桶上,于是延迟图上的一个尖峰可以直接链接到一个亲历了它的请求。

服务看 RED,资源看 USE

两个小框架就能覆盖大多数仪表盘。RED 适用于任何处理请求的对象:速率、错误和耗时。USE 适用于任何可能被耗尽的对象:使用率、饱和度和错误,对象包括 CPU、磁盘、连接池、线程池和队列。

耗时必须以直方图记录,而不是平均值。平均值掩盖了用户真正在抱怨的慢请求,而按实例计算的百分位数事后又无法合并。桶边界设置合理的直方图可以跨实例聚合,然后再查询 p95 或 p99。饱和度是最常缺失的信号:一个连接池用满但无人等待是没问题的,同一个池子如果等待队列在不断增长,那就是一场即将发生的事故。

由 SLO 和错误预算决定什么才重要

服务等级指标是对用户可见行为的度量,例如在 500 ms 内成功完成的结账请求所占的比例。服务等级目标是它在一个时间窗口内要达到的目标值,比如 30 天内达到 99.9%。目标与完美之间的差距就是错误预算:也就是服务被允许出现的失败量。

有了这个预算,两类决策就不再那么依赖各方博弈。如果预算消耗得很快,可靠性工作就优先于新功能。如果预算几乎没动,团队就可以发布得更快,或者承担更多风险。它也为告警提供了有原则的依据。我们基于燃烧率告警:短窗口内的快速燃烧立即呼叫值班人员,长窗口内的缓慢燃烧则开一张工单。每条告警同时配一个短窗口和一个长窗口,既避免短暂抖动触发呼叫,也避免已经恢复的事故继续告警。

代价是投入。定义好的指标需要各方就用户在意什么达成一致,而选得不好的指标只会带来虚假的安心。对于没有交互用户的内部批处理系统,一个更简单的数据新鲜度或完成情况检查通常就够了。

追踪上下文必须在每一跳都存活下来

一条 trace 只有完整才有用。每个服务都必须读取传入的追踪上下文,以子节点的形式创建自己的 span,并把上下文传给它调用的每一个下游。W3C Trace Context 标准为此定义了 traceparent 请求头,OpenTelemetry 的埋点库已经为常见的 HTTP 和 gRPC 类库处理了这件事。

断点出现在自动埋点覆盖不到的边界上:一条发布到 Kafka 却没有在 header 里带上下文的消息、一个交给线程池的任务、一个定时任务、一次经过会剥掉未知请求头的代理的调用。每一处都会把一条 trace 切成互不相关的碎片。我们把跨异步边界的传播当作需要显式完成的工作,并在每一行结构化日志里写入 trace ID,让日志和链路可以关联起来。

采样是另一个需要做的决定。头部采样在请求开始时做选择,成本低,但会把大部分错误连同常规流量一起丢掉。尾部采样在 trace 完整之后再做决定,因此可以保留每一个错误和每一个慢请求,代价是要在 collector 中缓冲 span。对于故障罕见、又很有排查价值的系统,尾部采样值得多引入这一个组件。

告警疲劳是一种设计缺陷

值班工程师如果总是因为无需处理的事情被呼叫,就会学会忽略呼叫。根源几乎总是针对原因而不是针对症状告警:CPU 超过阈值、单个 Pod 重启、队列短暂地比平时长。这些值得画成图表,却很少值得为此把人叫醒。

  • 只对用户可见的症状发起呼叫,并通过 SLO 燃烧率来表达
  • 每一次呼叫都链接到运行手册,并且有一个人可以立即执行的第一步动作
  • 基于原因的信号进入仪表盘和工单,而不是发到手机上
  • 对触发后无需任何处理的告警进行复盘,然后修正或删除

什么时候少一点才是恰到好处

分布式追踪、指标流水线和日志平台都有实实在在的运营成本。一个只有一个数据库的模块化单体并不需要追踪后端:带请求 ID 的结构化日志、边缘层的 RED 指标和数据库查询耗时,就足以解释大多数问题。当一个请求经常要跨越好几个进程边界时,我们才会加上链路追踪,因为到了那个时候,光靠日志已经讲不清来龙去脉了。

正在做类似的事情

我们很乐意交流心得。欢迎与撰写本文的工程师聊聊。