深色模式
分布式追踪实战:OpenTelemetry 与 Jaeger 在 K8s 中的落地
摘要:本文面向生产 SRE / 平台工程师,讲清分布式追踪"为什么需要、怎么采、怎么存、怎么排障"。以 OpenTelemetry(厂商中立标准)为采集面、Jaeger(CNCF 毕业)为后端,给出 OTel Collector 的两层部署(DaemonSet 采集 + Gateway 处理)、自动埋点、context 透传、tail-based sampling 与断链排查。适用 Kubernetes v1.28+(控制面 span 导出自 v1.25+ 可用)。
适用版本与前提
- Kubernetes:v1.28+(控制面 OTLP span 导出见
[版本相关]说明) - 组件:
opentelemetry-collector/opentelemetry-operator、jaeger、OTel SDK(应用侧) - 前提:应用可引入 OTel SDK 或通过 Operator 自动注入;集群内可达 Collector 的 OTLP 端口
背景与问题
单体时代"慢"能在单进程内用 profiler 定位。微服务下,一次用户请求穿越 10+ 服务,延迟可能卡在任意一段:某个下游超时、某个 gRPC 序列化慢、某次重试放大。没有追踪,你只能靠猜和逐个 grep 日志。
追踪的核心是 span:一个操作(一次 RPC、一次 DB 查询)的时间片段,含开始/结束时间、属性、事件;多个 span 通过 trace_id 串联成一条调用链,parent/child 表达嵌套关系。价值在于:一眼看出"这次请求 2.4s 里,支付服务占了 1.9s,其中 DB 查询 1.7s"。
OTel 为什么是事实标准
OpenTelemetry 由 CNCF 托管,统一了 Metrics/Logs/Traces 的采集 API 与协议(OTLP),厂商中立。应用只埋 OTel SDK,后端可换 Jaeger/Tempo/商业产品而不改代码——这正是它取代旧 Jaeger 客户端(jaeger-client)的原因。新项目不要再直接依赖厂商私有 SDK。
核心概念
- Trace / Span / Context:trace_id 串联整条链;context 携带 trace_id + span_id + 采样标志,在进程间靠 propagator(如 W3C
tracecontext+baggage)透传。 - OTel Collector:接收(receiver)→ 处理(processor)→ 导出(exporter)的管道,解耦应用与后端,做批处理、采样、脱敏、加 K8s 元数据。
- Exporter:应用把 span 推给 Collector(OTLP/gRPC 4317、OTLP/HTTP 4318)。追踪是 push 模型(与指标 pull 相反)。
- Jaeger:后端,负责存储(memory/Cassandra/Elasticsearch/Badger)与查询 UI。
架构与原理
生产推荐两层 Collector:节点级 DaemonSet 做轻量采集(加 k8s 元数据、批处理),中心 Gateway Deployment 做重处理(tail-based sampling、路由)。tail sampling 需要"看到整条 trace 再决定保留",所以必须在 Gateway 层(汇聚所有节点流量),而非 DaemonSet。
为何 context 透传是追踪的灵魂
若跨服务调用没把 traceparent header 透传下去,链路就"断"成多段独立 trace。HTTP 用 tracecontext propagator 自动注入/提取 header;消息队列需在消息头里手动传播;gRPC 用 metadata。任何一处漏传都会导致断链。K8s 中若用 Service Mesh(Istio/Linkerd),sidecar 可自动做一部分透传,但仍需应用侧 SDK 配合。
生产实践:OTel Collector 部署(Operator 模式)
用 opentelemetry-operator 以 CRD OpenTelemetryCollector 声明部署,两层形态:
yaml
# 节点级采集(DaemonSet)+ k8s 元数据 enrichment
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel-collector
namespace: monitoring
spec:
mode: daemonset
config: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch: {}
memory_limiter:
check_interval: 1s
limit_mib: 512
k8sattributes: # 注入 k8s.pod.name/namespace/node 等
auth_type: serviceAccount
exporters:
otlp:
endpoint: otel-collector-gateway.monitoring:4317
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlp]
---
# 中心 Gateway:tail-based sampling + 落 Jaeger
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel-collector-gateway
namespace: monitoring
spec:
mode: deployment
replicas: 3
config: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch: {}
tail_sampling:
decision_wait: 10s
num_traces: 50000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 1000 }
- name: probabilistic
type: probabilistic
probabilistic: { sampling_percentage: 10 }
exporters:
otlp/jaeger:
endpoint: jaeger-collector.monitoring:4317
tls: { insecure: true }
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, batch]
exporters: [otlp/jaeger]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
tail-based sampling 的成本权衡
100% 采样在生产不可持续(存储与网络成本)。head sampling(采前决定)简单但会漏掉偶发错误;tail sampling(采后按错误/慢请求决定保留)能"只留有用的 10%",但要求 Gateway 汇聚全量流量、内存与延迟更高。错误/慢请求务必保留,普通请求概率采样即可。
自动埋点(零改业务代码)
用 Instrumentation CRD 让 Operator 向 Pod 注入 auto-instrumentation agent:
yaml
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: auto-instrumentation
namespace: monitoring
spec:
exporter:
endpoint: http://otel-collector.monitoring:4317
propagators:
- tracecontext
- baggage
java:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.9.0
python:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.48b0
---
# 业务 Deployment 加注解即可被注入
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: demo
annotations:
instrumentation.opentelemetry.io/inject-java: "true"
spec:
template:
metadata:
annotations:
instrumentation.opentelemetry.io/inject-java: "true"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
版本相关
Instrumentation CRD 当前为 v1alpha1,auto-instrumentation 镜像与支持的语言随 OTel Operator 版本演进;Java/Python/Node.js/Go 支持度与版本号请以目标 OTel Operator release 为准。生产引入前请在预发验证注入是否干扰启动。
后端:Jaeger 部署(all-in-one 仅演示,生产用 storage 后端)
yaml
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger
namespace: monitoring
spec:
strategy: production
storage:
type: elasticsearch
options:
es:
server-urls: https://es.monitoring:9200
ingress:
enabled: false1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
生产危险
Jaeger all-in-one(内存存储)仅适合本地演示,重启即丢、容量极小,禁止上生产。生产必须配持久化存储(ES/Cassandra),并设置采样与留存,否则存储与费用失控。
验证
bash
# 1. Collector 接收端口在监听
kubectl exec -n monitoring ds/otel-collector -- ss -ltn | grep -E '4317|4318'
# 2. 发一条测试 trace 并查 Jaeger
kubectl port-forward -n monitoring svc/jaeger-query 16686 &
# 打开 http://localhost:16686,按 service=api-server 查 trace
# 3. 查 OTel Collector 是否接收到 span(metrics 自观测)
kubectl exec -n monitoring deploy/otel-collector-gateway -- \
curl -s localhost:8889/metrics | grep otelcol_receiver_accepted_spans1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
回滚与清理
生产危险
删除 Jaeger/ES 存储前确认已备份。清理演示:
bash
kubectl delete instrumentation auto-instrumentation -n monitoring
kubectl delete -n demo deploy/api-server
# 切勿对生产 monitoring 命名空间执行 kubectl delete ns monitoring1
2
3
2
3
故障排查
| 现象 | 根因 | 处理 |
|---|---|---|
| 链路断成多段 | context 未透传(缺 propagator / 消息队列未传 header) | 确认 propagators 含 tracecontext;检查跨服务 HTTP header traceparent 是否透传 |
| 收不到 span | 应用 endpoint 指向错 / 端口不对 | 核对 exporter.endpoint 为 otel-collector:4317;kubectl get svc 确认 |
| 采样后看不到错误 | tail sampling 配置未含 errors 策略 | 检查 tail_sampling.policies 有 status_code: [ERROR] |
| Collector OOM | 无 memory_limiter 或队列过大 | 每个 pipeline 加 memory_limiter 且放首位;调 batch/sending_queue |
| 元数据缺失(无 pod 名) | 缺 k8sattributes 或 RBAC 不足 | DaemonSet 需 ClusterRole 读 pod;确认 k8sattributes.auth_type: serviceAccount |
性能、容量与成本
- 采样是第一成本杠杆:tail sampling 把存储压到 10% 左右,错误/慢请求 100% 保留。
- Collector 资源:
memory_limiter必须启用且置于 processor 链首位,防止 OOM;batch减少网络包。 - Jaeger 存储:ES 索引量大,需 ILM;或用 Badger(单副本本地)仅适合中小规模。
- 成本权衡:全量采样 > tail 采样 > head 采样 的成本依次降,但可观测性依次降;按业务关键路径分级。
常见坑
- 只用 head sampling 导致偶发错误被漏采。
- propagator 只配
baggage没配tracecontext→ 断链。 - Collector 未启用
memory_limiter→ 流量尖峰 OOM。 - 把 Jaeger all-in-one 当生产后端。
- 应用日志里没打 trace_id,指标↔日志↔追踪三向关联断裂(应在日志结构化里输出
trace_id)。
替代方案与权衡
- Grafana Tempo:与 Loki/Prometheus 同源,trace_id 与日志/指标关联体验好,存储成本低(只索引 trace_id)。
- Jaeger vs Tempo:Jaeger 生态成熟、查询强;Tempo 与 Grafana 全家桶整合更顺、成本低。新 Grafana 栈优先 Tempo。
- Service Mesh 内置追踪:Istio/Linkerd 可自动生成服务间 span,但应用内 span 仍需 OTel SDK。
FAQ
Q:trace 和 log 怎么关联? A:应用日志结构化输出 trace_id;Loki 配 derived fields 提取 trace_id 生成跳转 Tempo/Jaeger 的链接;指标开 exemplar 也能跳 trace。
Q:自动埋点够吗? A:HTTP/gRPC/DB 通用路径够;业务自定义 span(如"下单"内部子步骤)仍需手动 tracer.startSpan。
参考资料
- OpenTelemetry 官方文档 - Collector configuration best practices,访问日期:2026-10-08。
- OpenTelemetry 官方文档 - Kubernetes 安装 Collector,访问日期:2026-10-08。
- OpenTelemetry Operator - Instrumentation / OpenTelemetryCollector CRD,访问日期:2026-10-08。
- Jaeger 官方文档 - Deployment,访问日期:2026-10-08。
- W3C - Trace Context 标准,访问日期:2026-10-08。