| name | observability-and-instrumentation |
| description | 对基础设施组件(分布式存储、网络服务、数据面组件)进行仪表化,使集群的生产行为可见且可诊断。在添加日志、指标、追踪或告警时使用。在发布任何运行在集群上的功能且你需要证据证明它有效时使用。当出现节点/磁盘/链路异常但从现有数据中无法定位是哪台机器、哪块盘、哪条链路时使用。 |
可观测性与仪表化
概述
你无法观察的组件就是无法运维的组件。可观测性是使用组件发出的遥测数据,从外部回答"集群在做什么以及为什么?"的能力。仪表化不是发布后的附加组件——它与功能一起编写,就像测试一样。如果一个复制、均衡或故障转移功能在没有遥测的情况下上线,第一次慢节点投诉就变成了考古学而非一次查询。
何时使用
- 构建任何将在生产集群中运行的组件(存储引擎、复制协议、调度器、网关)
- 添加新的节点角色、副本/分片路径、后台任务(compaction、rebalance、GC)或跨节点调用
- 一次集群事故花了太长时间才定位("我们不知道是哪个节点、哪块盘还是哪条链路")
- 设置或审查告警规则
- 审查添加了磁盘 I/O、网络 RPC、重试、队列或副本间通信的 PR
不用于:
- 诊断现在正在发生的故障——使用
debugging-and-error-recovery 技能(可观测性是让该技能下次变快的东西)
- 分析和优化测量到的缓慢——使用
performance-optimization 技能
- 发布日的监控检查清单和回滚触发条件——参见
shipping-and-launch 技能;此技能涵盖输入它们的仪表化
流程
1. 在仪表化之前定义"正常工作"
没有问题的遥测就是噪音。在添加任何仪表化之前,写下值班工程师将对此组件提出的 2-4 个问题:
组件:副本同步(replica catch-up)
值班工程师会问的问题:
1. 多少比例的副本落后于 leader?落后多少条日志/多少字节?
2. 追赶速度是被什么限制的——磁盘写、网络带宽还是 CPU?
3. 某个 follower 永久落后时,为什么?(慢盘?链路丢包?源节点限流?)
→ 下面的每个信号都必须帮助回答以上问题之一。
如果你不能说出这些问题,你还没有准备好进行仪表化——你会记录一切,学到零。
2. 为每个问题选择正确的信号
| 信号 | 回答 | 成本概况 | 示例 |
|---|
| 结构化日志 | "在这个具体副本/分片上发生了什么?" | 每事件;随流量和节点数增长 | replica_lagged 带 shard ID 和落后字节数 |
| 指标 | "总体上,多频繁 / 多快 / 多满?" | 每系列固定;查询便宜 | 磁盘 await p99、网卡重传率 |
| 追踪 | "一个请求跨节点的时间花在哪里?" | 每请求;通常是采样 | 一次慢写入,按 leader→副本→落盘逐跳细分 |
经验法则:指标告诉你某事出错了,追踪告诉你在哪个节点/哪一跳,日志告诉你为什么。
3. 结构化日志
记录事件,而非散文。每行日志都是一个 JSON 对象,带有稳定的事件名和机器可读的字段:
// 坏:字符串插值——不可查询、无法按节点/分片聚合
log("replica " + replicaId + " on node " + nodeId + " fell behind by " + lagBytes);
// 好:稳定的事件名 + 结构化字段
log.warn({
event: 'replica_lagged',
shardId: shard,
nodeId: node,
peerNodeId: leader,
lagBytes: lag,
diskDevice: 'nvme1n1',
});
日志级别——一致地使用它们:
| 级别 | 含义 | 值班行动 |
|---|
error | 不变量被破坏(副本数据损坏、写入丢失、一致性校验失败);可能有人需要行动 | 调查 |
warn | 降级但已处理(重试成功、切换到备用副本、绕过慢盘) | 观察趋势 |
info | 重要的集群事件(leader 选举完成、rebalance 结束、节点加入/退出) | 无 |
debug | 诊断细节 | 默认在生产中关闭 |
关联 ID 是强制的。 在请求进入集群的边界生成(或接受)一个请求 ID,并将其附加到每行日志、每个 span 和每次跨节点 RPC。没有它,你无法从几十个节点交错的日志中重建单个请求在副本/分片间的路径。每条日志还必须自带定位三要素:nodeId、shardId(或 replicaId)、设备/接口名——在分布式系统里,一条不知道自己在哪台机器上的日志等于没写。
rpc_set_header(req, "x-request-id", request_id);
rpc_set_header(req, "x-shard-id", shard_id);
永远不要记录密钥、认证 Token 或完整的用户数据负载。 遥测管道是经典的数据泄露路径。记录偏移量、长度、校验和;不要记录整条 record 或整个 block 的内容。
高吞吐路径必须采样。 数据面上的每请求日志(每次读盘、每个包)会在故障高峰期恰好压垮日志管道本身。对正常路径按固定比例采样(如 1/1000),对错误、超时和慢操作(超过阈值)保留 100%——这和追踪的尾部采样是同一个思想。
4. 指标
基础设施的核心方法论是 USE 方法:对每一种资源,逐一仪表化 Utilization(利用率)、Saturation(饱和度)、Errors(错误)。对请求驱动的入口(客户端 RPC 面)再叠加 RED(Rate、Errors、Duration),但集群内部的健康主要由 USE 回答。
按资源逐一应用:
| 资源 | Utilization | Saturation | Errors |
|---|
| 磁盘 | util%(设备忙的时间占比) | await(I/O 平均等待)、队列深度(aqu-sz) | 设备错误、I/O 超时、重试计数、SMART 重分配扇区增长 |
| 网卡/链路 | 吞吐量 / 链路带宽 | 发送队列堆积、TCP 重传率、丢包率 | NIC 硬件错误、CRC 错误、链路 flap |
| CPU | 利用率(区分 user/sys/iowait/steal) | run queue 长度、调度延迟 | 机器检查、节流(throttling)事件 |
| 内存 | 已用 / 总量 | swap 换入换出速率、页回收扫描频率 | OOM kill、分配失败 |
util% 接近 100% 而吞吐没有同步增长,是设备排队或过载的经典信号;await 突增而 util% 不高,往往指向慢盘或设备级错误——这两个指标必须一起看。
可以作为标签: node_role="follower" device="nvme1n1" shard_state="catching_up" error_class="timeout"
永远不要作为标签:node_id、shard_id、request_id、peer 地址、错误消息文本
基数是失败模式。 每个唯一的标签组合都是一个单独的时间序列,而基础设施天然想要按节点、按盘、按分片打标签——一个 1000 节点 × 24 盘 × 5000 分片的标签组合就是数亿条序列,会直接打垮指标后端。标签必须来自小型、固定的集合(节点角色、设备类型、分片状态、错误类别)。按节点/分片维度的下钻属于日志和追踪,或者在采集端做受控的 per-node 导出(每个节点导出自己的盘指标,标签里不含全局唯一 ID),由存储后端按实例维度天然区分。
永远不要追踪平均值,始终追踪百分位数:平均延迟隐藏了 1% 的请求正落在慢节点/慢盘上的事实。使用直方图并读取 p50/p95/p99——对基础设施尤其要读 p99.9,因为慢节点离群点(见步骤 6)藏在最深的尾部。
5. 分布式追踪
这里追踪的不是一次 HTTP 请求经过几个微服务,而是一个请求在集群内的物理路径:客户端 → leader 节点 → 各副本/分片节点 → 落盘/确认 → 返回。每一跳都是一个 span,span 属性必须携带值班工程师定位用的字段:nodeId、shardId、rpcType、diskDevice(在 IO span 上)。
使用 OpenTelemetry(或等效的供应商中立方案);自动埋点覆盖常见 RPC 框架,但存储引擎内部的异步路径几乎总是需要手动埋点——写入在内存队列里排队、在后台线程落盘、经网络线程发给副本,上下文跨这些边界会自动断裂。仅在围绕有意义的内部工作单元(例如 appendToLog、fsync、replicateToFollowers、compaction)添加手动 span。
在每个跨节点和异步边界(RPC 头部、复制消息、队列任务元数据)传播上下文——否则追踪在第一个线程切换处就断了。默认以低速率进行头采样;如果你的后端支持尾部采样,保留 100% 的错误和慢请求。一条能完整展示"请求在三个副本间的路径、每跳的排队与落盘耗时"的追踪,就是慢节点定位的终极答案。
6. 慢节点/慢盘离群检测
分布式系统的延迟尾部长度通常由最慢的那台节点决定,而不是平均水平——一个请求要等待所有副本确认时,p99 就是集群里最慢副本的延迟。把离群检测当作一等公民来仪表化:
- 按节点分组的延迟直方图(在采集端按实例维度区分,不在标签里放节点 ID),值班查询是"所有节点 p99 的 top-N",而不是集群平均
- 按设备分组的磁盘
await / util%——找出那一块比其他盘慢 10 倍的盘
- 副本落后量(lag)按节点分布——找出永远追不上的 follower
- 对链路:TCP 重传率按节点对(peer)分布——找出那条丢包的链路
检测到一个离群点后,系统应当已经留下回答"为什么是它"的日志和追踪(步骤 3/5);否则你只知道有一台慢机器,又要从头猜。
7. 告警
对用户/上层服务感受到的症状告警,而非原因:
症状(值得发告警): 原因(放仪表盘,不发告警):
客户端写入 p99 > SLO,持续 5 min 某节点 CPU 在 85%
副本落后超过 N 分钟且持续增长 某块盘 util% 在 70%
读错误率 > 0.1% 一台节点重启了
跨可用区复制延迟 > 阈值 TCP 重传率升高
基于原因的告警在一切正常时也会触发(一台 CPU 85% 的节点可能服务得好好的),并错过你没有预测到的故障。基于症状的告警恰好在集群用户受到伤害时触发,无论原因是慢盘、丢包还是热点分片。
你创建的每个告警的规则:
- 它必须是可操作的。 如果响应是"忽略它,rebalance 完成后会自愈",删除该告警。
- 它链接到一个 Runbook——即使只有三行:意味着什么、第一个要运行的查询(通常是"找出离群节点/盘")、升级路径。
- 它有基于 SLO 或历史数据的阈值和持续时间,而非猜测。
- 仅使用两个严重级别:page(数据面受损或有数据丢失风险,现在行动)和 ticket(容量/冗余降级,本周行动)。第三个级别会变成噪音,训练人们忽略一切。
8. 验证遥测本身
仪表化就是代码;它可能出错。在宣布工作完成之前,触发这些路径并查看实际输出:
- 在测试集群中拔掉一块盘/给一个节点注入延迟 → 按
requestId 在日志中重建请求路径,确认字段是结构化的(不是 %v 打出来的地址)
- 施加测试负载 → 确认磁盘/网卡/CPU 的 USE 指标序列以预期标签和合理值出现
- 在追踪 UI 中跟踪一个跨副本请求 → 每个节点一跳,没有断裂的 span
- 制造一个慢节点 → 离群检测面板/top-N 查询能把它指出来
- 触发每个新告警一次(临时降低阈值)→ 确认它到达了正确的渠道且 Runbook 链接有效
常见合理化借口
| 合理化借口 | 现实 |
|---|
| "等集群跑稳了我再加指标" | "之后"变成了"第一次丢数据之后",那是发现你处于盲区的最昂贵的时刻。仪表化随构建一起进行。 |
| "机器指标 node-exporter 都有了,够了" | 主机指标回答不了"哪个分片慢、哪跳 RPC 卡住了"。没有应用层仪表化,你只能看到生病,看不到病灶。 |
| "出问题了登上去 iostat 一下就行" | 事故发生时故障现场可能已消失(rebalance 完成、节点已重启),而且你不可能在 500 台机器上逐台 iostat。数据必须在事发时就被采集。 |
| "每条请求都记一条日志,信息最全" | 数据面全量日志会在故障高峰期压垮日志管道和磁盘,恰恰在你最需要它的时候。正常路径采样,错误和慢操作全保留。 |
| "按分片 ID 打标签,查问题多方便" | 几千个分片 × 几百个节点就是基数炸弹,会把指标后端先打垮。按分片的下钻属于日志和追踪。 |
| "所有节点指标都告警,阈值以后再调" | 嘈杂的告警器训练人们忽视它。调优永远不会发生;错过真正的告警会。 |
| "三个节点的集群搞追踪是小题大做" | 三个节点已经意味着日志无法回答的跨节点延迟问题——慢的是哪一跳,不追踪就是猜。 |
| "平均延迟正常,集群就是健康的" | 平均值隐藏离群点。决定长尾的是最慢的那台节点,而它可能正在悄悄坏掉。 |
红旗警告
- 一个带有复制、重试、队列或跨节点 RPC 的功能 PR,却没有新的遥测数据
- 日志行通过字符串插值构建,而非结构化字段
- 日志没有
nodeId/shardId/设备名——无法定位是哪台机器、哪块盘
- 没有关联 ID——一个请求跨副本的路径无法从交错日志中重建
- 指标标签使用节点 ID、分片 ID、peer 地址或错误消息文本(基数炸弹)
- 只有平均值,没有直方图——p99/p99.9 不可查询
- 有磁盘吞吐指标却没有
await/util%/错误计数——USE 三缺一,慢盘不可见
- 数据面高吞吐路径无采样全量打日志
- 每天触发的告警被确认而不采取行动
- 对 CPU/磁盘利用率告警,而客户端可见的错误率和延迟未受监控
- 无法回答"现在集群里最慢的是哪台节点/哪块盘"——没有离群视角
- 密钥、Token 或完整数据负载出现在日志中
验证
对组件进行仪表化后,确认:
有关此列表的一目了然版本,包括发布前仪表化门禁,参见 references/observability-checklist.md。