Skip to content

微服务可观测性:OpenTelemetry、Trace传播、采样、指标基数与SLO

可观测性不是“装了Prometheus、ELK和Jaeger”,而是系统输出的证据能否回答:谁受影响、从什么时候开始、请求经过哪里、时间花在哪、哪个版本或依赖异常、修复后是否真正恢复。工具只是存储和查询载体,真正困难的是信号语义、上下文传播、采样、标签基数、时间关联和采集链路自身的可靠性。

本页从一次HTTP请求进入Java服务开始,追踪Trace、Metrics、Logs如何产生,怎样经过OpenTelemetry SDK和Collector,最后进入后端;并讲清W3C Trace Context、异步Span Link、Head/Tail Sampling、Exemplar、OTLP、Collector背压、SLO与错误预算。Kubernetes对象、容器日志和节点取证见Kubernetes可观测性专栏

一、学习目标

  1. 区分Metrics、Logs、Traces、Events和Profiles的证据价值。
  2. 解释OpenTelemetry API、SDK、Instrumentation、Exporter、Collector和Backend。
  3. 解析W3C traceparent并说明tracestate与Baggage边界。
  4. 解释一次同步HTTP调用和异步MQ消费怎样形成调用关系。
  5. 区分Span Parent和Span Link。
  6. 区分Head Sampling、Tail Sampling和Parent-based Sampling。
  7. 理解Counter、Gauge、Histogram、Temporality和Exemplar。
  8. 解释高基数标签怎样造成时间序列爆炸。
  9. 设计Collector Pipeline、批处理、队列、重试和限流。
  10. 用RED、USE、Golden Signals、SLI/SLO和Burn Rate建立告警。

二、五类信号分别回答什么

信号擅长回答不足
Metrics多少、趋势、范围、是否超阈值很难解释单次请求代码细节
Logs某时刻代码或组件记录了什么没记录不代表没发生,搜索成本受结构影响
Traces一次请求经过哪里、每段耗时与状态采样会丢请求,属性不等于业务事实
Events控制器或系统做了什么判断常被聚合和过期,不是永久审计日志
ProfilesCPU、堆、锁和线程时间集中在哪里不直接说明用户影响和业务结果

正确排查通常是:Metrics发现范围和开始时间,Trace定位慢在哪个服务,Logs查看具体错误和业务键,Profile确认CPU或锁热点,平台Event验证是否与发布、重启、调度有关。

三、RT、P95和P99到底是什么

线上排查里经常听到“RT升高了”“P99很差”“平均耗时没问题但用户还是慢”。这些词都在描述请求耗时,但含义不同。

3.1 RT是什么

RT通常指Response Time,也就是响应时间。它表示一次请求从开始到结束花了多长时间。

在HTTP接口里,常见口径是:

text
RT = 服务端收到请求的时间 -> 服务端写完响应的时间

如果从客户端视角看,还可能包含DNS、建连、TLS、网关、网络传输和客户端读取时间:

mermaid
flowchart TD
    A["客户端开始请求"] --> B["DNS、TCP、TLS"]
    B --> C["Gateway接收和转发"]
    C --> D["应用服务处理"]
    D --> E["数据库、Redis、下游接口"]
    E --> F["响应返回客户端"]

所以讨论RT时一定要先问清“在哪一层测的”:

观测位置RT包含什么常见用途
浏览器或App客户端网络、网关、服务端、响应下载用户真实体验
Nginx或Gateway入口排队、路由、上游响应入口排查和SLA
应用服务Controller、Service、DB、下游调用应用性能分析
Feign或RPC客户端选址、连接池、网络、下游处理、解码下游依赖分析
数据库慢日志SQL执行和锁等待SQL优化

如果不说明口径,RT很容易被误解。例如Gateway看到RT 3秒,但订单服务日志只显示800ms,剩下时间可能花在网关连接池等待、网络、下游响应写回或客户端慢读上。

3.2 平均RT为什么会骗人

平均RT是所有请求耗时求平均:

text
平均RT = 所有请求耗时总和 / 请求数量

假设10个请求耗时如下:

text
20ms, 21ms, 22ms, 22ms, 23ms, 24ms, 25ms, 26ms, 27ms, 1000ms

平均值约是:

text
(20+21+22+22+23+24+25+26+27+1000) / 10 = 121ms

平均RT看起来只有121ms,但有一个用户等了1秒。平均值会把少量很慢的请求“摊平”,因此排查线上体验不能只看平均RT。

3.3 P95和P99是什么

P95、P99是百分位延迟。它们回答的是:

把一段时间内所有请求按耗时从小到大排序,排在95%位置或99%位置的请求耗时是多少。

直觉理解:

指标含义
P5050%的请求不超过这个耗时,也叫中位数
P9090%的请求不超过这个耗时
P9595%的请求不超过这个耗时,剩下5%更慢
P9999%的请求不超过这个耗时,剩下1%更慢
P99999.9%的请求不超过这个耗时,剩下0.1%更慢

举例:某接口一分钟10000次请求,P99=800ms,意思是:

text
约9900次请求耗时 <= 800ms
约100次请求耗时 > 800ms

它不是“99个请求的平均值”,也不是“最慢请求”。P99关注的是尾部慢请求,但仍会忽略最极端的0.几%。

3.4 P95/P99怎么算

简单教学算法:

  1. 收集一段时间内的请求耗时。
  2. 从小到大排序。
  3. 找到百分位 * 样本数附近的位置。
  4. 该位置的值就是对应百分位。

例如20个请求耗时:

text
10, 11, 12, 13, 14,
15, 16, 17, 18, 19,
20, 21, 22, 23, 24,
25, 26, 27, 500, 900

P95位置约为:

text
20 * 0.95 = 19

第19个值是500ms,所以P95约为500ms。P99位置接近第20个值,所以P99接近900ms。

生产系统通常不会真的保存所有原始请求再排序,而是使用Histogram、DDSketch、TDigest、HdrHistogram或Prometheus Histogram这类结构估算分位数。不同系统算法、桶边界和聚合方式不同,所以看P99时还要确认采集口径和计算方式。

3.5 为什么P99比平均值更适合排查线上问题

用户感知的是自己的那一次请求,而不是全站平均值。一个接口平均RT 50ms,但P99 3s,说明大部分请求很快,但少量用户体验很差。

常见原因:

P99升高原因解释
某个实例慢负载均衡仍把部分请求打到慢实例
数据库锁等待少量请求命中热点行或大事务
慢SQL特定参数、租户或时间范围触发差查询
GC或Safepoint实例短暂停顿,部分请求被拖慢
连接池等待请求还没发出或还没拿到DB连接
下游P99升高上游Feign等待下游响应
队列堆积线程池、MQ、限流延迟队列把请求排在后面
重试放大第一次失败后又重试,整体耗时变长

3.6 P95、P99和最大值怎么一起看

指标组合可能含义
平均RT升高,P95/P99也升高整体都变慢,可能容量不足或依赖整体变慢
平均RT正常,P99很高大多数请求正常,少量尾部请求异常
P95正常,P99很高只有最后1%左右请求很慢,常见于单实例、锁、GC、热点租户
P99高,最大值更离谱存在极端卡顿、超时、长事务或网络尖峰
P50高,P95/P99更高基础耗时已经变慢,同时尾部更差

不要只看最大值。最大值可能是一个孤立异常,也可能是采集误差;但如果P95/P99持续升高,就说明有稳定比例的用户受到影响。

3.7 和QPS、并发、错误率一起看

RT不能单独看。高QPS下RT升高,通常代表系统接近容量边界;低QPS下RT升高,可能是锁、GC、下游抖动、冷启动或单租户大查询。

mermaid
flowchart TD
    A["接口变慢"] --> B["看QPS是否升高"]
    A --> C["看P95/P99是否升高"]
    A --> D["看错误率是否升高"]
    A --> E["看线程池、连接池和队列"]
    E --> F["定位是排队、执行慢还是下游慢"]

常见组合:

现象判断方向
QPS升高、P99升高、错误率升高可能过载或限流不足
QPS没升、P99升高单实例慢、DB锁、GC、下游局部故障
P99升高、线程池队列增长请求在应用内排队
P99升高、DB连接池等待增长线程等数据库连接,不是CPU不够
P99升高、下游Feign P99也升高慢点可能在下游
P99升高、CPU不高可能在等待IO、锁、连接池或远程依赖

3.8 商业场景怎么使用

订单接口可以这样定义SLO:

text
99%的创建订单请求在800ms内完成,错误率低于0.1%。

这句话比“平均响应时间小于200ms”更贴近用户体验。因为平均值达标不代表尾部用户不慢。

医疗数据采集平台可以这样看:

场景重点指标
登录和权限接口P95/P99、401/403、认证中心RT
采集入库接口P99、DB连接池等待、批次失败率
字典服务调用Feign P95/P99、本地缓存命中率
ES查询P95/P99、慢查询、分片耗时
MQ消费消费RT、Lag、最老消息年龄
定时任务单批耗时P95/P99、失败批次数

3.9 面试标准回答

text
RT是Response Time,表示一次请求从开始到结束的响应耗时,但要说明观测口径,
比如客户端、Gateway、应用服务、Feign客户端或数据库层。P95/P99是百分位延迟,
把一段时间内请求耗时从小到大排序,P95表示95%的请求不超过这个耗时,
P99表示99%的请求不超过这个耗时。它们不是平均值,也不是最大值。

平均RT容易掩盖少量慢请求,而线上用户体验经常受尾延迟影响,所以排查接口慢
要看P95/P99、QPS、错误率、线程池、连接池、GC、慢SQL和下游调用耗时。
如果平均值正常但P99很高,通常说明大部分请求正常,但存在慢实例、锁等待、
GC、热点参数、连接池等待或下游长尾问题。

四、OpenTelemetry组件关系

mermaid
flowchart TD
    A["自动或手工Instrumentation"] --> B["OpenTelemetry API创建Span、Metric与Log关联"]
    B --> C["SDK Provider与Processor处理遥测"]
    C --> D["Exporter通过OTLP等协议发送"]
    D --> E["Agent或Gateway Collector接收"]
    E --> F["Processor批处理、过滤、采样与资源补充"]
    F --> G["Exporter写入Trace、Metric、Log后端"]
    G --> H["查询、Dashboard、告警和事故分析"]
组件作用关键边界
API供库和业务创建遥测的稳定接口只有API没有SDK可能不导出数据
SDK采样、聚合、处理和导出实现配置不当会增加业务开销
Instrumentation拦截HTTP、JDBC、MQ等操作自动埋点不了解领域语义
Resource描述service、instance、cluster等实体不应放请求级高基数值
Exporter将信号发送到Collector或后端失败必须有界,不能无限阻塞业务
Collector接收、处理、路由和导出遥测自身也需要容量、监控和HA
Backend存储、索引、查询与展示后端查不到不证明请求未发生

OpenTelemetry是规范、API、SDK和Collector生态,不是一个固定存储数据库。Tempo、Jaeger、Zipkin、Prometheus兼容存储、Loki/Elasticsearch等承担不同后端角色。

五、一次同步HTTP请求怎样形成Trace

mermaid
flowchart TD
    A["Gateway读取或创建Trace Context"] --> B["创建Server Span"]
    B --> C["订单服务处理本地逻辑并创建内部Span"]
    C --> D["HTTP客户端创建Client Span"]
    D --> E["把traceparent注入下游请求头"]
    E --> F["库存服务提取上下文并创建Server Span"]
    F --> G["JDBC Instrumentation创建数据库Span"]
    G --> H["各Span结束并由Processor批量导出"]

一条Trace共享同一个Trace ID,每个操作有不同Span ID。库存Server Span的Parent通常是订单Client Span,而不是订单Server Span;Client和Server两个Span分别表达调用方视角和服务方视角,网络时间与时钟偏差可能使两边耗时不完全相等。

Span应该记录:

  • 稳定操作名,例如POST /orders/{id},而不是包含真实ID的完整URL。
  • Span Kind:SERVER、CLIENT、PRODUCER、CONSUMER、INTERNAL。
  • 状态和异常事件。
  • 低到中基数属性:service、route、method、status category、peer。
  • 必要且脱敏的业务定位字段。

HTTP 500通常应标记错误;HTTP 404是否是错误要看接口语义。业务返回HTTP 200但code=PAYMENT_FAILED时,自动埋点无法自动理解,应用必须补领域事件或业务指标。

六、W3C Trace Context原理

典型traceparent

text
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

四部分分别是:

字段示例含义
version00格式版本
trace-id32个十六进制字符整条Trace身份,不能全0
parent-id16个十六进制字符上游当前Span身份,不能全0
trace-flags01低位常表示sampled

5.1 提取与注入

  • Extract:服务入口从HTTP Header、gRPC Metadata或消息属性读取远端Context。
  • Attach:让当前线程或异步任务在作用域中使用该Context。
  • Inject:调用下游前把当前Context写入载体。
  • Detach:请求结束清理作用域,防止线程复用串链。

不能无条件信任公网客户端传入的任意Trace ID和Baggage。入口应限制长度、格式和值域,必要时保留外部关联ID后创建内部可信Trace,避免日志注入、超长Header和跨租户数据关联。

5.2 tracestate

tracestate允许不同追踪厂商在标准上下文旁传播受限状态。它有格式、成员数和长度约束,代理应按规范转发和更新;不能当无限业务Header使用。

七、Baggage为什么危险

Baggage是随上下文跨服务传播的键值,不会自动成为每个Span属性,也不应承载密码、Token、身份证、病历或完整订单对象。

风险包括:

  • 每跳都增加Header字节和序列化成本。
  • 未限制来源时可被客户端伪造。
  • 自动复制到Span或日志会造成敏感信息扩散。
  • tenantId、userId等若变成Metric Label会造成高基数爆炸。
  • 异步消息长期保存Baggage可能让过期权限上下文被错误复用。

只传播真正需要跨进程的少量字段,定义白名单、长度、字符集、信任边界和生命周期。租户身份应来自认证结果,而不是只信Baggage。

八、线程池、异步任务和MQ怎样关联

7.1 线程池上下文丢失

Trace Context通常与当前执行上下文关联。把任务提交到普通线程池后,执行线程不是提交线程,ThreadLocal上下文不会天然正确传播。框架的TaskDecorator、Context Propagation或Instrumentation需要在提交时捕获、执行时恢复、结束后清理。

只复制不清理会出现线程池串链:请求B复用了请求A遗留的Trace ID。

同步RPC通常有清晰的父子关系;异步消费可能:

  • 一个生产Span对应多个消费者。
  • 一条批量消费Span处理多条不同Trace的消息。
  • 消息在队列中等待很久,消费是新的处理阶段。
mermaid
flowchart TD
    A["订单请求Trace A"] --> B["Producer Span写入消息上下文"]
    B --> C["消息在Broker等待"]
    C --> D["Consumer创建新的消费Span"]
    D --> E["通过Parent或Span Link关联生产上下文"]
    E --> F["消费事务、重试和ACK"]

单消息可用生产Context作为远端Parent;批量消费或一个处理关联多个因果来源时,Span Link更准确。消息重试应保留稳定eventId,并区分“同一消息的多次消费Attempt”,不能每次覆盖原始生产上下文。

7.3 JDK 8 Demo:线程池上下文传播与清理

下面的 Demo 用最小代码模拟 Trace Context。它不是要替代 OpenTelemetry,而是帮助理解为什么异步线程会断链,以及为什么只传播不清理会串链。

java
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TraceContextThreadPoolDemo {

    static final class TraceContext {
        private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<String>();

        static void set(String traceId) {
            TRACE_ID.set(traceId);
        }

        static String get() {
            return TRACE_ID.get();
        }

        static void clear() {
            TRACE_ID.remove();
        }
    }

    static Executor contextAware(Executor delegate) {
        return new Executor() {
            public void execute(final Runnable command) {
                final String capturedTraceId = TraceContext.get();
                delegate.execute(new Runnable() {
                    public void run() {
                        String oldTraceId = TraceContext.get();
                        try {
                            TraceContext.set(capturedTraceId);
                            command.run();
                        } finally {
                            if (oldTraceId == null) {
                                TraceContext.clear();
                            } else {
                                TraceContext.set(oldTraceId);
                            }
                        }
                    }
                });
            }
        };
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        Executor tracedPool = contextAware(pool);

        TraceContext.set("trace-" + UUID.randomUUID().toString());
        tracedPool.execute(new Runnable() {
            public void run() {
                System.out.println("async traceId = " + TraceContext.get());
            }
        });

        Thread.sleep(200);
        TraceContext.clear();
        pool.shutdown();
    }
}

这段代码里有三个关键点:

原理不这样会怎样
提交时捕获异步任务提交线程里有当前Trace Context工作线程拿不到父请求Trace,Trace断链
执行时恢复工作线程执行任务前把Context放回ThreadLocal日志MDC、Client Span、下游Header都可能缺Trace
finally清理或恢复线程池线程会复用,任务结束必须清理旧上下文下一个请求复用同一线程,可能带上上一个用户或Trace

生产里不要自己手写这套作为最终方案。Spring Boot 2存量项目常见 TaskDecorator、Sleuth/Brave 自动装饰;Boot 3主线是 Micrometer Context Propagation、Observation 和 OTel Instrumentation。原则始终一样:捕获、恢复、执行、清理

九、采样:为什么查不到那条故障Trace

8.1 Head Sampling

在Trace开始时决定是否采样。优点是成本可预测、后续服务可遵循父决策;缺点是当时不知道请求最后会不会超时或失败,可能漏掉最需要的故障。

8.2 Parent-based Sampling

子服务遵循上游采样决定,减少一条Trace只保存中间几段的碎片。但入口决策错误会影响整条链,跨信任域时也不能盲从不可信客户端的sampled位。

8.3 Tail Sampling

Collector等待Trace的Span到达后,按最终状态、时延、属性或概率决定保留。例如保留全部错误、P99慢请求和特定灰度版本,再对普通成功请求按1%采样。

成本与边界:

  • 需要缓存未完成Trace,消耗内存。
  • 同一Trace的Span应尽量路由到同一采样决策点。
  • 决策等待时间过短会漏掉迟到Span,过长增加内存与查询延迟。
  • Collector重启或过载可能丢未决Trace。
  • 100%保留所有错误在错误风暴时也会压垮后端,需要容量上限。

8.4 采样不等于不计指标

Trace未采样不应导致RED指标也消失。Metrics用于全量聚合趋势,Trace用于抽样解释单次请求。二者需要独立但可关联的管道。

8.5 采样决策链:为什么错误Trace可能查不到

排查“为什么这次错误没有Trace”时,不要只看采样比例。一次Trace是否能查到,至少经过下面这条链:

mermaid
flowchart TD
    A["入口创建或提取Context"] --> B["Head Sampler决策"]
    B --> C["下游Parent-based继续传播"]
    C --> D["应用SDK生成Span"]
    D --> E["BatchSpanProcessor排队"]
    E --> F["Exporter发送Collector"]
    F --> G["Collector Tail Sampling决策"]
    G --> H["后端写入和索引"]
    H --> I["查询条件命中"]

常见断点如下:

断点现象证据
Head Sampling未采整条链大概率没有Span保留入口采样配置、trace-flags sampled位、SDK采样指标
Parent-based继承未采下游都遵循上游不采样决策上游Header、下游Sampler配置
业务错误没标记Trace有但状态是OK,Tail Sampling没保留Span status、异常记录、业务code映射
Batch队列满应用生成了Span但导出前丢弃SDK dropped spans、队列长度、导出失败日志
Collector过载Collector接收或处理阶段丢弃Receiver refused、processor dropped、memory limiter指标
Tail Sampling等待不够早到Span被保留决策漏掉,迟到Span缺失tail sampling latency、decision wait、trace incomplete指标
后端写入失败Collector导出成功率下降或后端拒绝exporter failed、后端ingest错误
查询条件错误数据存在但按服务名、时间、环境查不到resource attributes、service.name、namespace、时区

trace-flags 的 sampled 位只能表达“上游建议或决定采样”,不能当业务可靠性依据。它不能证明请求成功,也不能证明请求失败,更不能当幂等键、审计号或订单号使用。来自公网的 sampled 位还要经过入口信任边界校验,避免外部用户强行要求高采样压垮观测系统。

Tail Sampling 的关键前提是:同一条 Trace 的 Span 尽量到达同一个采样决策点。多 Collector、多机房、多队列路由时,如果按随机方式分散,同一Trace被拆到不同Collector,每个Collector都只看到一部分Span,就很难按“整条Trace是否错误或慢”做正确决策。生产要按 traceId 做一致性路由,或者接受Tail Sampling只能局部判断。

十、traceId、requestId、幂等键和业务单号区别

这些ID都能“查东西”,但语义完全不同,不能混用。

ID表示什么生命周期能不能做幂等常见用途
traceId一次观测链路一次请求链路或一次异步因果链不能串日志、Trace、指标Exemplar
spanIdTrace中的一个操作片段一次HTTP调用、SQL、MQ生产或消费不能定位哪一段慢或失败
requestId一次接口请求或一次调用尝试一次入口请求,重试可能变化通常不能直接做业务幂等网关日志、接口排查、客户端重试定位
幂等键一次业务意图同一业务意图跨重试保持不变可以,且必须有唯一约束或幂等表防重复下单、重复支付、重复扣库存
业务单号领域事实编号业务对象生命周期可辅助幂等,但不等同于调用Trace订单号、支付单号、批次号
eventId一条业务事件消息投递、重试和消费全过程消费侧去重常用MQ重复消费去重
XID分布式事务上下文一个全局事务生命周期不是业务幂等键Seata、XA/JTA、事务排查

错误用法示例:

text
把traceId当幂等键:同一业务重试如果生成了新Trace,就挡不住重复写。
把orderNo当Trace:能查订单事实,但看不到请求经过Gateway、库存、支付哪一段慢。
把requestId每次重试都换掉:业务层无法判断这些请求是不是同一个业务意图。

商业系统建议这样组合:

  1. 网关生成或校验 requestId,用于一次入口请求排查。
  2. 链路追踪生成 traceId,贯穿Gateway、Feign、MQ、DB访问日志。
  3. 核心写操作由客户端或服务端生成稳定幂等键,数据库唯一约束兜底。
  4. 订单、支付、库存等领域对象生成业务单号,作为事实源查询入口。
  5. MQ事件使用稳定 eventId,消费者用 consumer_group + event_id 去重。

十一、Trace断链深度排查:从入口到后端逐层取证

Trace断链不是一个错误,而是一类现象。要先定义断在哪里:入口没有Trace、Gateway到服务断、Feign下游断、异步任务断、MQ消费断,还是后端没查到。

mermaid
flowchart TD
    A["发现Trace不完整"] --> B["确定最后一个有Span的服务"]
    B --> C["确定第一个缺失的调用边"]
    C --> D["查Header是否注入和透传"]
    D --> E["查客户端或RPC拦截器"]
    E --> F["查异步和MQ上下文"]
    F --> G["查SDK、Exporter、Collector"]
    G --> H["用日志、指标和业务事实交叉验证"]

逐层检查:

查什么常见问题
Gateway入口是否提取外部 traceparent,是否创建内部可信Trace外部Header格式非法、超长、被直接信任
Header转发traceparent、B3、Baggage是否被代理清洗或覆盖Nginx、Gateway、Mesh白名单漏配
Feign/RestTemplate/WebClient是否使用被自动埋点管理的Client,拦截器顺序是否正确自定义Client绕过Instrumentation
gRPC/DubboMetadata/Attachment是否注入和提取Filter顺序错误、只传业务Header不传Trace
线程池提交时是否捕获Context,执行后是否清理ThreadLocal丢失或串链
Reactor/异步流是否使用Context而不是普通ThreadLocal假设切线程后MDC为空
MQ生产消息属性是否写入Trace Context,是否记录eventId只写日志不写消息Header
MQ消费单消息用Parent还是批量用Link,重试attempt是否保留批量消费把多个Trace错误塞成父子
SDK导出Span是否创建、结束、入队、flush进程退出太快、队列满、Exporter失败
Collectoraccepted、dropped、tail decision、export failed内存限制、采样规则、后端拒绝
查询后端service.name、namespace、时间范围、时区查错环境或服务名

要特别注意:Trace平台没有数据,不能证明请求没发生。正确证据链应该是:

  1. Metrics证明某接口在该时间段有流量、错误或延迟变化。
  2. Gateway日志证明请求到达,包含 requestId、用户、租户和路由。
  3. 应用日志证明业务方法开始或结束,包含 traceIdrequestId
  4. 业务事实源证明是否写入订单、支付、库存、消息或Outbox。
  5. Collector和后端指标证明遥测是否被丢弃或写入失败。

对于核心交易和审计,Trace只能作为排查证据,不能作为唯一事实源。事实源永远应是数据库状态机、幂等表、消息表、审计日志和对账结果。

十二、Metrics数据模型

类型适合例子
Counter只增累计量请求总数、失败数
UpDownCounter可增可减数量活跃任务数
Gauge某时刻观测值队列长度、连接池Active
Histogram值分布请求耗时、消息大小

Histogram不是“算一个平均值”。它按桶或指数结构累计分布,让后端估算分位数。跨实例聚合P99时,客户端Summary的预计算分位数通常难以正确聚合;Histogram更适合服务级聚合,但桶边界要符合SLO。

9.1 Temporality

  • Cumulative:从起点累计到当前值。
  • Delta:只报告本周期变化。

采集器与后端需要正确转换和处理进程重启,否则Counter重置可能被误判成负增长或流量骤降。

9.2 Exemplar

Exemplar在Histogram样本与具体Trace之间保存少量关联,例如P99延迟桶中附带一个Trace ID。Dashboard看到尖峰后可直接跳到代表性慢Trace,而不是把Trace ID作为每个Metric Label。

十三、高基数为什么会压垮指标系统

时间序列数量近似为标签取值组合的乘积:

text
service 50
× route 100
× status 5
× instance 20
= 500000条潜在序列

若再加入百万级orderId,序列数量会失控,带来内存、索引、存储和查询成本。

适合Metric Label:service、namespace、cluster、version、规范化route、method、状态类别。通常不适合:userId、orderId、traceId、完整URL、异常全文、SQL全文、IP。

高基数业务ID应脱敏后进入结构化日志或受控Trace属性,并设置保留期和访问权限。

十四、结构化日志与Trace关联

推荐JSON等结构化字段:

json
{
  "timestamp": "2026-07-18T10:00:00.123+08:00",
  "level": "ERROR",
  "service": "order-service",
  "version": "3.2.1",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "event": "inventory_reserve_failed",
  "error_type": "DEPENDENCY_TIMEOUT"
}

日志关联常通过MDC完成。异步线程、Reactor和虚拟线程场景不能只假设传统ThreadLocal始终正确,需使用对应上下文传播方案并在作用域结束清理。

不要记录密码、Token、完整身份证、病历、密钥和支付敏感数据。异常堆栈应有去重、采样或速率限制,避免故障时日志风暴反过来占满磁盘和CPU。

十五、Collector Pipeline完整过程

mermaid
flowchart TD
    A["Receiver接收OTLP、Prometheus等数据"] --> B["内存限制与拒绝保护"]
    B --> C["Processor补资源、过滤、变换和批处理"]
    C --> D["可选Tail Sampling与路由"]
    D --> E["Exporter发送到一个或多个后端"]
    E --> F{"后端是否接受"}
    F -- "是" --> G["记录导出成功"]
    F -- "否" --> H["有界队列、退避重试或持久队列"]

12.1 Agent与Gateway

  • Agent模式靠近工作负载,补充节点信息、接收本机数据并减少跨网络连接。
  • Gateway模式集中执行Tail Sampling、路由和后端认证。
  • 二者可组合,但每层都批处理和重试会增加延迟、内存与排查复杂度。

12.2 Collector故障不能拖垮业务

应用Exporter应使用异步有界队列和批处理。Collector不可达时,不能无限阻塞请求线程或无限堆内存。允许有界丢弃并记录Dropped Telemetry指标,通常比让核心交易一起宕机更合理。

对于强审计数据,不能依赖普通Trace“尽力而为”语义,应进入专门审计日志、数据库或可靠消息链路。

十六、观测系统也要被观测

至少监控:

  • Receiver accepted/refused数量。
  • Processor dropped数量和Tail Sampling决策。
  • Batch大小与队列占用。
  • Exporter成功、失败、重试和永久丢弃。
  • Collector CPU、内存、GC、磁盘和网络。
  • 后端写入延迟、索引失败和查询延迟。
  • 每服务遥测到达的新鲜度。

“Dashboard没有数据”可能是应用没生成、SDK没注册、采样丢弃、Collector拒绝、Exporter失败、后端写入失败或查询条件错误。必须沿遥测数据面逐层取证。

十七、RED、USE和Golden Signals

RED:面向请求服务

  • Rate:请求速率。
  • Errors:错误比例,按业务有效请求定义。
  • Duration:耗时分布,重点看P95/P99和SLO桶。

USE:面向资源

  • Utilization:资源忙碌比例。
  • Saturation:排队或超额需求。
  • Errors:资源错误。

CPU 40%不代表系统不饱和:数据库连接池等待、线程队列和单核热点都可能已满。Utilization必须针对真正瓶颈资源。

Golden Signals

延迟、流量、错误、饱和度。它是组织信号的框架,不是四个固定PromQL。每项都要映射到具体服务和业务语义。

十八、从用户目标定义SLI和SLO

示例SLO:

text
30天滚动窗口内,99.9%的有效订单查询请求成功且低于500ms。

必须定义:

  • 什么是有效请求,是否排除客户端参数错误。
  • 成功按HTTP状态还是业务状态。
  • 延迟从Gateway入口还是服务内部开始。
  • 哪些租户、地区和接口包含在内。
  • 维护窗口是否排除。

错误预算:

text
budget = 1 - SLO

99.9%约允许0.1%的失败或不达标事件,但不能简单转换后就忽略流量权重和统计窗口。

十九、Burn Rate为什么比固定错误率更适合SLO告警

Burn Rate表示错误预算被消耗的速度:

text
实际错误比例 / 允许错误比例

SLO 99.9%允许0.1%,当前错误率1%,Burn Rate约为10,意味着预算消耗速度是目标的10倍。

生产常用多窗口告警:短窗口快速发现剧烈事故,长窗口确认持续影响。只有短窗口容易被瞬时抖动误报;只有长窗口发现太慢。具体倍率和窗口应按SLO、值班响应时间和流量校准,不机械复制模板。

二十、Java 8/Boot 2与Java 17+/Boot 3

基线常见追踪主线迁移重点
JDK 8 / Boot 2.xSpring Cloud Sleuth、Brave、Zipkin或Java Agent明确B3/W3C格式、采样和MDC兼容
Java 17+ / Boot 3.xMicrometer Observation/Tracing、OpenTelemetry桥接或AgentSleuth不再是新主线,迁移Observation与Exporter

Boot 3的Observation把一次操作的指标与追踪生命周期统一抽象,但不意味着自动解决业务Span命名、高基数和敏感字段。Java 21虚拟线程、Reactor和普通线程池的上下文传播方式不同,必须按实际运行模型验证。

不要在同一应用重复启用Java Agent、框架自动埋点和手写拦截器而不做去重,否则可能出现重复Span、重复指标和双倍Exporter流量。

二十一、JDK 8 Demo:解析并校验traceparent

java
public class TraceparentDemo {
    static String[] parse(String value) {
        if (value == null || !value.matches(
                "^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$")) {
            throw new IllegalArgumentException("invalid traceparent format");
        }
        String[] parts = value.split("-");
        if ("00000000000000000000000000000000".equals(parts[1])) {
            throw new IllegalArgumentException("trace-id cannot be zero");
        }
        if ("0000000000000000".equals(parts[2])) {
            throw new IllegalArgumentException("parent-id cannot be zero");
        }
        return parts;
    }

    public static void main(String[] args) {
        String[] p = parse(
                "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
        int flags = Integer.parseInt(p[3], 16);
        System.out.println("version=" + p[0]);
        System.out.println("traceId=" + p[1]);
        System.out.println("parentId=" + p[2]);
        System.out.println("sampled=" + ((flags & 1) == 1));
    }
}

这是帮助理解版本00格式的教学代码,生产应使用OpenTelemetry Propagator等标准实现,因为规范还包含未来版本、长度、大小写和转发行为等完整规则。

二十二、商业场景:支付成功但资产页没更新

排查链路:

  1. 业务SLI确认影响比例、租户、版本和开始时间。
  2. 用支付业务流水在脱敏日志中找到Trace ID。
  3. Trace显示支付回调成功、Outbox写入成功、MQ Producer Span成功。
  4. Consumer Span Link显示资产消费者多次重试。
  5. Metrics显示某消费组Lag和数据库连接等待升高。
  6. 消费日志按eventId定位唯一键冲突或SQL超时。
  7. 修复后观察Lag、最老消息年龄、资产同步成功率和错误预算恢复。

Trace只能证明已观测的调用行为,最终资产状态仍要查询数据库事实和消费幂等记录。

二十三、生产Runbook

20.1 Trace断链

  1. 找到最后一个有Span的服务和第一个缺失服务。
  2. 检查HTTP/gRPC/MQ载体是否注入和提取标准Header。
  3. 检查异步线程、Reactor、线程池是否捕获并清理Context。
  4. 检查网关或代理是否删除、重写Header。
  5. 检查采样决策是否不同或Span导出失败。

20.2 Trace很多但关键错误查不到

  1. 查看Head Sampling比例和Parent-based决策。
  2. 检查错误是否被业务包装成HTTP 200而未标记Span。
  3. 检查Tail Sampling策略、等待时间和Collector内存拒绝。
  4. 查看Exporter dropped和后端写入失败。
  5. 用Metrics和日志证明问题存在,不能因无Trace否定事故。

20.3 Prometheus或指标后端内存暴涨

  1. 统计时间序列增长最快的Metric和Label。
  2. 查找orderId、userId、完整URL、异常文本等高基数值。
  3. 先阻断新增高基数,再评估存储清理和保留期。
  4. 把定位字段迁移到日志/Trace,路由使用模板化名称。
  5. 在代码Review和CI中加入基数规则。

20.4 Collector队列满或丢数据

  1. 查看Receiver接收、Processor丢弃和Exporter失败速率。
  2. 区分后端变慢、网络故障、采样成本和Collector资源不足。
  3. 启用有界队列、批处理、退避和必要的持久队列。
  4. 扩容前检查是否单个高基数服务制造遥测风暴。
  5. 确保业务Exporter不因Collector故障阻塞交易线程。

二十四、常见误区

误区后果
有Trace ID就算链路追踪没有Span关系、状态和耗时只能做日志关联
100%采样永远最好成本失控,故障时先压垮观测链路
未采样表示请求没发生只能说明没有保留Trace
Baggage放完整用户上下文Header膨胀、泄密和高基数扩散
orderId做Metric Label时间序列爆炸
Collector故障时无限重试遥测拖垮业务应用
HTTP 200都算成功掩盖业务失败码
只按CPU告警漏掉连接池、锁和队列饱和
固定错误率阈值等于SLO不反映预算消耗速度和业务窗口

二十五、面试回答主线

先区分Metrics、Logs、Traces和Profiles;再沿Instrumentation → API/SDK → Processor/Exporter → Collector → Backend解释数据链;用traceparent讲同步传播,用Span Link讲异步因果;补充Head/Tail Sampling、Histogram/Exemplar、高基数、Collector失效边界;最后用SLI/SLO和Burn Rate说明告警如何服务用户目标。

标准回答见微服务可观测性面试题

二十六、关联知识点

本章小结

真正的可观测性是一条有损、异步且需要容量治理的数据链。既要理解请求Context和Span关系,也要理解Metric聚合、日志结构、采样决策、Collector队列和后端写入。只有能从用户SLO一路追到单次Trace、业务日志和资源Profile,并知道“没有数据”可能在哪一层丢失,才具备生产排查能力。