7AA7 Blog

可观测性三支柱:日志、指标与链路的协同

码同客 UID:88
2026-09-08 09:10
14 1 0

线上出问题时的第一诉求永远是”发生了什么”。而常见的处境是:监控面板一片绿,用户投诉却有几十条;或者告警显示某个接口错误率上升,翻遍日志找不到一条异常。缺的不是数据,是数据之间的关联,以及据此回答未知问题的能力。

三者各自回答什么

三类数据的结构决定了各自的能力边界,用错地方就会觉得”什么都没有”。

指标(Metrics) 是聚合后的数值时间序列:QPS、错误率、P99 延迟、队列深度。它的强项是趋势与告警——体量小,可以低成本长期保留,能支撑任意时间范围的回溯对比。弱项是没有细节:错误率从 0.1% 涨到 5%,指标本身无法告诉你错的是哪些请求。

日志(Logs) 是离散的事件记录,带完整上下文。它回答”具体发生了什么”:这个请求的参数、走到的分支、抛出的异常栈。弱项是成本高、无法聚合查询。用日志算错误率意味着全量扫描,代价远大于读一条预聚合的时序。

链路(Traces) 记录一次请求在多个服务间的完整路径与耗时分布。它回答”慢在哪一跳”:网关 3 毫秒、订单服务 200 毫秒、其中一次数据库查询占 180 毫秒。弱项是它只覆盖被埋点的调用,且全量采集的体积在大规模下难以承受。

对应到排查过程,三者是串起来的:指标发现异常(哪个服务、哪个接口、什么时间点)→链路定位到具体环节(哪次调用、哪一跳、哪个依赖)→日志还原细节(参数、分支、堆栈)。缺少任何一环,排查都会在中间断掉。

TraceID 如何串联三者

串联的前提是全链路唯一的标识。做法是在入口生成 TraceID,通过上下文在每个服务间透传,同时写入日志和 span。

GET /api/orders/123  X-Request-Id: 7f3a9c1e...
  ├─ 网关          3ms    trace_id=7f3a9c1e span_id=a1
  ├─ 订单服务    180ms    trace_id=7f3a9c1e span_id=b2  parent=a1
  │   ├─ DB 查询  165ms   trace_id=7f3a9c1e span_id=b3  parent=b2
  │   └─ 缓存     <1ms    trace_id=7f3a9c1e span_id=b4  parent=b2
  └─ 库存服务      8ms    trace_id=7f3a9c1e span_id=c1  parent=a1

落地时要处理几件容易出错的事:

透传要贯穿异步与队列。同步调用靠 HTTP header 或 RPC 上下文比较容易,容易断的是消息发送与消费——生产者要把 TraceID 放进消息头,消费者取出作为父 span。批量消费时一条消息一个 span,但都挂在同一个父之下。

日志必须携带 TraceID。日志框架里统一注入,而不是让业务代码手动打印。常见做法是 MDC(映射调试上下文)在入口处写入,日志模板统一带上 %X{traceId}。

<!-- 日志模板统一携带链路标识,避免依赖业务代码手动输出 -->
<pattern>%d{ISO8601} %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n</pattern>

指标与链路的关联靠 exemplar。在指标的某个时间桶上附带一个典型 TraceID 样本,让”错误率上升”能一键跳到对应的那条链路。没有这层关联,两个系统之间就差一次人工按时间比对。

一致性命名。同一个服务在三个系统里必须叫同一个名字,否则无法互相跳转。服务名、接口名建议在系统边界处统一定义,而不是各处自由填写。

采样策略的取舍

全量链路采集的成本随流量线性增长,通常不现实。采样决策要解决的是”保住有价值的样本”。

头部采样在请求入口就决定是否采集,决策早、开销小。问题是它随机丢弃:正常运行时的慢请求、出错请求大概率被丢掉,而它们恰恰是最需要的样本。

尾部采样把决策推迟到整条链路结束后,在收集端根据结果筛选。可以做到”错误链路全采、慢链路全采、正常链路按比例采”,样本价值最高。代价是需要在链路数据的汇聚侧做缓冲等待所有 span 到齐,实现复杂度和资源占用都更高。

# 尾部采样的常见组合规则
tail_sampling:
  decision_wait: 10s          # 等待 span 到齐的时间
  policies:
    - name: keep-all-errors
      type: status_code
      status_code: { status_codes: [ERROR] }
    - name: keep-slow-traces
      type: latency
      latency: { threshold_ms: 1000 }
    - name: baseline
      type: probabilistic
      probabilistic: { sampling_percentage: 5 }

工程上的常见组合是:头部采样保底(低比例,比如 1%~5%,保证任何时刻都有样本)+ 特定强制采样(入口按规则标记,例如 VIP 租户、特定压测标记的请求)。资源允许时再上尾部采样。

采样率不是一次性定的。通常先按存储预算倒推:每天允许多少条链路数据、平均每条多大(span 数量 × 单 span 体积),得出可接受的采样率,再按业务重要性给不同接口分配不同比例——核心链路比例高,内部巡检类接口低。

高基数标签导致的崩溃

这是指标系统最常见的容量事故来源。假设给请求延迟打上 user_id 标签:一千万用户 × 若干时间桶 = 上亿条时间序列,索引膨胀、内存吃满、查询超时,最终整个指标系统不可用。

判断标签合理性的经验边界:取值有限且有业务含义的维度适合作为标签——服务名、接口、状态码、机房、租户等级。取值随实体数量增长的一律不适合——用户 ID、订单号、URL 路径中的 ID 段、随机 UUID。

处理方式有三类:把高基数值放进日志或链路(它们适合存明细),把高基数维度降级为聚合维度(例如把 user_id 换成 user_tier),或者针对少量必须观测的大客户单独建一路受限的指标。

防护手段也要做在系统侧:给单个指标的序列数量设上限,超限拒绝写入并告警,而不是让整个存储被拖垮。

成本控制的现实手段

可观测数据的成本增长速度通常快于业务本身。有效手段集中在分层:

分层保留。原始日志保留较短时间(例如 7~30 天),降采样或聚合后长期保留(例如一年)。指标原始数据降到低分辨率长期存(1 分钟粒度保留若干天,5 分钟粒度保留数月)。

冷热分离。近期数据放高速存储支撑排障查询,历史数据放廉价对象存储,只在需要做年度对比时取回。

按需采集。DEBUG 日志默认关闭,出问题期间对特定服务或特定 TraceID 临时开启,恢复后自动关闭。这比长期全量记录的成本低一个数量级。

按价值分级。核心交易链路保留全字段,内部异步任务只保留错误和摘要。

成本要做得可持续,关键在于把”数据成本”变成一个可见的指标——按服务统计写入量与存储量,让产生数据的团队看见自己的账单。没有这层反馈,削减成本永远只能靠运维单方面限流。

从监控到可观测

两者处理的其实是不同类别的问题。

监控面向已知未知:你已经知道哪些情况会出问题(磁盘满、连接池耗尽、错误率上升),预先定义指标和阈值,等待它们触发。它的效率高,但只能覆盖预先想到的失败模式。

可观测面向未知未知:系统出现了你没预料到的现象,需要提出之前没有想过的问题并在现有数据中找到答案。它要求数据具备足够的维度和上下文——能在事后按任意维度切分,而不需要提前埋点。

这个差别决定了建设方向。只做监控时,每遇到一个新问题就加一个面板,最终面板数量膨胀而诊断能力没有提升。真正的改进是让任意一次具体请求都能被还原:从指标发现异常开始,能下钻到链路,再从链路跳到日志,中间不需要猜测时间窗口、不需要重新部署加日志。

衡量这条链路是否打通的方式很直接:随机挑一个用户报障的单据号,看多久能还原出它经历的每一步。如果这个时间从几十分钟降到几分钟,投入就是有效的;如果每次都要重新加日志、重新部署才查得动,那么缺的不是工具,而是数据之间的连接。

390Views
14Likes
1Comments
1Participants
9 minread time

Comments (1)

  • Vector 陈 · 2026-09-08 11:38

    写得挺克制,没有吹成银弹,这点难得。

    Reply