微服务可观测性: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可观测性专栏。
一、学习目标
- 区分Metrics、Logs、Traces、Events和Profiles的证据价值。
- 解释OpenTelemetry API、SDK、Instrumentation、Exporter、Collector和Backend。
- 解析W3C
traceparent并说明tracestate与Baggage边界。 - 解释一次同步HTTP调用和异步MQ消费怎样形成调用关系。
- 区分Span Parent和Span Link。
- 区分Head Sampling、Tail Sampling和Parent-based Sampling。
- 理解Counter、Gauge、Histogram、Temporality和Exemplar。
- 解释高基数标签怎样造成时间序列爆炸。
- 设计Collector Pipeline、批处理、队列、重试和限流。
- 用RED、USE、Golden Signals、SLI/SLO和Burn Rate建立告警。
二、五类信号分别回答什么
| 信号 | 擅长回答 | 不足 |
|---|---|---|
| Metrics | 多少、趋势、范围、是否超阈值 | 很难解释单次请求代码细节 |
| Logs | 某时刻代码或组件记录了什么 | 没记录不代表没发生,搜索成本受结构影响 |
| Traces | 一次请求经过哪里、每段耗时与状态 | 采样会丢请求,属性不等于业务事实 |
| Events | 控制器或系统做了什么判断 | 常被聚合和过期,不是永久审计日志 |
| Profiles | CPU、堆、锁和线程时间集中在哪里 | 不直接说明用户影响和业务结果 |
正确排查通常是:Metrics发现范围和开始时间,Trace定位慢在哪个服务,Logs查看具体错误和业务键,Profile确认CPU或锁热点,平台Event验证是否与发布、重启、调度有关。
三、RT、P95和P99到底是什么
线上排查里经常听到“RT升高了”“P99很差”“平均耗时没问题但用户还是慢”。这些词都在描述请求耗时,但含义不同。
3.1 RT是什么
RT通常指Response Time,也就是响应时间。它表示一次请求从开始到结束花了多长时间。
在HTTP接口里,常见口径是:
RT = 服务端收到请求的时间 -> 服务端写完响应的时间如果从客户端视角看,还可能包含DNS、建连、TLS、网关、网络传输和客户端读取时间:
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是所有请求耗时求平均:
平均RT = 所有请求耗时总和 / 请求数量假设10个请求耗时如下:
20ms, 21ms, 22ms, 22ms, 23ms, 24ms, 25ms, 26ms, 27ms, 1000ms平均值约是:
(20+21+22+22+23+24+25+26+27+1000) / 10 = 121ms平均RT看起来只有121ms,但有一个用户等了1秒。平均值会把少量很慢的请求“摊平”,因此排查线上体验不能只看平均RT。
3.3 P95和P99是什么
P95、P99是百分位延迟。它们回答的是:
把一段时间内所有请求按耗时从小到大排序,排在95%位置或99%位置的请求耗时是多少。
直觉理解:
| 指标 | 含义 |
|---|---|
| P50 | 50%的请求不超过这个耗时,也叫中位数 |
| P90 | 90%的请求不超过这个耗时 |
| P95 | 95%的请求不超过这个耗时,剩下5%更慢 |
| P99 | 99%的请求不超过这个耗时,剩下1%更慢 |
| P999 | 99.9%的请求不超过这个耗时,剩下0.1%更慢 |
举例:某接口一分钟10000次请求,P99=800ms,意思是:
约9900次请求耗时 <= 800ms
约100次请求耗时 > 800ms它不是“99个请求的平均值”,也不是“最慢请求”。P99关注的是尾部慢请求,但仍会忽略最极端的0.几%。
3.4 P95/P99怎么算
简单教学算法:
- 收集一段时间内的请求耗时。
- 从小到大排序。
- 找到
百分位 * 样本数附近的位置。 - 该位置的值就是对应百分位。
例如20个请求耗时:
10, 11, 12, 13, 14,
15, 16, 17, 18, 19,
20, 21, 22, 23, 24,
25, 26, 27, 500, 900P95位置约为:
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、下游抖动、冷启动或单租户大查询。
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:
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 面试标准回答
RT是Response Time,表示一次请求从开始到结束的响应耗时,但要说明观测口径,
比如客户端、Gateway、应用服务、Feign客户端或数据库层。P95/P99是百分位延迟,
把一段时间内请求耗时从小到大排序,P95表示95%的请求不超过这个耗时,
P99表示99%的请求不超过这个耗时。它们不是平均值,也不是最大值。
平均RT容易掩盖少量慢请求,而线上用户体验经常受尾延迟影响,所以排查接口慢
要看P95/P99、QPS、错误率、线程池、连接池、GC、慢SQL和下游调用耗时。
如果平均值正常但P99很高,通常说明大部分请求正常,但存在慢实例、锁等待、
GC、热点参数、连接池等待或下游长尾问题。四、OpenTelemetry组件关系
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
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:
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01四部分分别是:
| 字段 | 示例 | 含义 |
|---|---|---|
| version | 00 | 格式版本 |
| trace-id | 32个十六进制字符 | 整条Trace身份,不能全0 |
| parent-id | 16个十六进制字符 | 上游当前Span身份,不能全0 |
| trace-flags | 01 | 低位常表示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。
7.2 MQ为什么常用Span Link
同步RPC通常有清晰的父子关系;异步消费可能:
- 一个生产Span对应多个消费者。
- 一条批量消费Span处理多条不同Trace的消息。
- 消息在队列中等待很久,消费是新的处理阶段。
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,而是帮助理解为什么异步线程会断链,以及为什么只传播不清理会串链。
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是否能查到,至少经过下面这条链:
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 |
spanId | Trace中的一个操作片段 | 一次HTTP调用、SQL、MQ生产或消费 | 不能 | 定位哪一段慢或失败 |
requestId | 一次接口请求或一次调用尝试 | 一次入口请求,重试可能变化 | 通常不能直接做业务幂等 | 网关日志、接口排查、客户端重试定位 |
| 幂等键 | 一次业务意图 | 同一业务意图跨重试保持不变 | 可以,且必须有唯一约束或幂等表 | 防重复下单、重复支付、重复扣库存 |
| 业务单号 | 领域事实编号 | 业务对象生命周期 | 可辅助幂等,但不等同于调用Trace | 订单号、支付单号、批次号 |
eventId | 一条业务事件 | 消息投递、重试和消费全过程 | 消费侧去重常用 | MQ重复消费去重 |
XID | 分布式事务上下文 | 一个全局事务生命周期 | 不是业务幂等键 | Seata、XA/JTA、事务排查 |
错误用法示例:
把traceId当幂等键:同一业务重试如果生成了新Trace,就挡不住重复写。
把orderNo当Trace:能查订单事实,但看不到请求经过Gateway、库存、支付哪一段慢。
把requestId每次重试都换掉:业务层无法判断这些请求是不是同一个业务意图。商业系统建议这样组合:
- 网关生成或校验
requestId,用于一次入口请求排查。 - 链路追踪生成
traceId,贯穿Gateway、Feign、MQ、DB访问日志。 - 核心写操作由客户端或服务端生成稳定幂等键,数据库唯一约束兜底。
- 订单、支付、库存等领域对象生成业务单号,作为事实源查询入口。
- MQ事件使用稳定
eventId,消费者用consumer_group + event_id去重。
十一、Trace断链深度排查:从入口到后端逐层取证
Trace断链不是一个错误,而是一类现象。要先定义断在哪里:入口没有Trace、Gateway到服务断、Feign下游断、异步任务断、MQ消费断,还是后端没查到。
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/Dubbo | Metadata/Attachment是否注入和提取 | Filter顺序错误、只传业务Header不传Trace |
| 线程池 | 提交时是否捕获Context,执行后是否清理 | ThreadLocal丢失或串链 |
| Reactor/异步流 | 是否使用Context而不是普通ThreadLocal假设 | 切线程后MDC为空 |
| MQ生产 | 消息属性是否写入Trace Context,是否记录eventId | 只写日志不写消息Header |
| MQ消费 | 单消息用Parent还是批量用Link,重试attempt是否保留 | 批量消费把多个Trace错误塞成父子 |
| SDK导出 | Span是否创建、结束、入队、flush | 进程退出太快、队列满、Exporter失败 |
| Collector | accepted、dropped、tail decision、export failed | 内存限制、采样规则、后端拒绝 |
| 查询后端 | service.name、namespace、时间范围、时区 | 查错环境或服务名 |
要特别注意:Trace平台没有数据,不能证明请求没发生。正确证据链应该是:
- Metrics证明某接口在该时间段有流量、错误或延迟变化。
- Gateway日志证明请求到达,包含
requestId、用户、租户和路由。 - 应用日志证明业务方法开始或结束,包含
traceId或requestId。 - 业务事实源证明是否写入订单、支付、库存、消息或Outbox。
- 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。
十三、高基数为什么会压垮指标系统
时间序列数量近似为标签取值组合的乘积:
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等结构化字段:
{
"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完整过程
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:
30天滚动窗口内,99.9%的有效订单查询请求成功且低于500ms。必须定义:
- 什么是有效请求,是否排除客户端参数错误。
- 成功按HTTP状态还是业务状态。
- 延迟从Gateway入口还是服务内部开始。
- 哪些租户、地区和接口包含在内。
- 维护窗口是否排除。
错误预算:
budget = 1 - SLO99.9%约允许0.1%的失败或不达标事件,但不能简单转换后就忽略流量权重和统计窗口。
十九、Burn Rate为什么比固定错误率更适合SLO告警
Burn Rate表示错误预算被消耗的速度:
实际错误比例 / 允许错误比例SLO 99.9%允许0.1%,当前错误率1%,Burn Rate约为10,意味着预算消耗速度是目标的10倍。
生产常用多窗口告警:短窗口快速发现剧烈事故,长窗口确认持续影响。只有短窗口容易被瞬时抖动误报;只有长窗口发现太慢。具体倍率和窗口应按SLO、值班响应时间和流量校准,不机械复制模板。
二十、Java 8/Boot 2与Java 17+/Boot 3
| 基线 | 常见追踪主线 | 迁移重点 |
|---|---|---|
| JDK 8 / Boot 2.x | Spring Cloud Sleuth、Brave、Zipkin或Java Agent | 明确B3/W3C格式、采样和MDC兼容 |
| Java 17+ / Boot 3.x | Micrometer Observation/Tracing、OpenTelemetry桥接或Agent | Sleuth不再是新主线,迁移Observation与Exporter |
Boot 3的Observation把一次操作的指标与追踪生命周期统一抽象,但不意味着自动解决业务Span命名、高基数和敏感字段。Java 21虚拟线程、Reactor和普通线程池的上下文传播方式不同,必须按实际运行模型验证。
不要在同一应用重复启用Java Agent、框架自动埋点和手写拦截器而不做去重,否则可能出现重复Span、重复指标和双倍Exporter流量。
二十一、JDK 8 Demo:解析并校验traceparent
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等标准实现,因为规范还包含未来版本、长度、大小写和转发行为等完整规则。
二十二、商业场景:支付成功但资产页没更新
排查链路:
- 业务SLI确认影响比例、租户、版本和开始时间。
- 用支付业务流水在脱敏日志中找到Trace ID。
- Trace显示支付回调成功、Outbox写入成功、MQ Producer Span成功。
- Consumer Span Link显示资产消费者多次重试。
- Metrics显示某消费组Lag和数据库连接等待升高。
- 消费日志按eventId定位唯一键冲突或SQL超时。
- 修复后观察Lag、最老消息年龄、资产同步成功率和错误预算恢复。
Trace只能证明已观测的调用行为,最终资产状态仍要查询数据库事实和消费幂等记录。
二十三、生产Runbook
20.1 Trace断链
- 找到最后一个有Span的服务和第一个缺失服务。
- 检查HTTP/gRPC/MQ载体是否注入和提取标准Header。
- 检查异步线程、Reactor、线程池是否捕获并清理Context。
- 检查网关或代理是否删除、重写Header。
- 检查采样决策是否不同或Span导出失败。
20.2 Trace很多但关键错误查不到
- 查看Head Sampling比例和Parent-based决策。
- 检查错误是否被业务包装成HTTP 200而未标记Span。
- 检查Tail Sampling策略、等待时间和Collector内存拒绝。
- 查看Exporter dropped和后端写入失败。
- 用Metrics和日志证明问题存在,不能因无Trace否定事故。
20.3 Prometheus或指标后端内存暴涨
- 统计时间序列增长最快的Metric和Label。
- 查找orderId、userId、完整URL、异常文本等高基数值。
- 先阻断新增高基数,再评估存储清理和保留期。
- 把定位字段迁移到日志/Trace,路由使用模板化名称。
- 在代码Review和CI中加入基数规则。
20.4 Collector队列满或丢数据
- 查看Receiver接收、Processor丢弃和Exporter失败速率。
- 区分后端变慢、网络故障、采样成本和Collector资源不足。
- 启用有界队列、批处理、退避和必要的持久队列。
- 扩容前检查是否单个高基数服务制造遥测风暴。
- 确保业务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,并知道“没有数据”可能在哪一层丢失,才具备生产排查能力。
