分布式链路追踪记录请求在微服务间的完整调用路径,帮助定位性能瓶颈和故障根因。OpenTelemetry 正在统一 Metrics、Logs、Traces 三大信号标准,成为现代云原生可观测性的基础设施。一条 Trace 可以跨越数十个服务、数百个进程,横跨多个数据中心甚至云平台,因此标准化的链路追踪不仅影响故障排查效率,更决定了整个组织的技术运维水平。本章从核心概念出发,覆盖 SDK 埋点、上下文传递、采样策略、Collector 架构、服务网格集成以及生产迁移的完整实践链路。
1. 核心概念
| 概念 | 说明 |
|---|---|
| Trace | 一次请求的完整调用链,由唯一 TraceID 标识 |
| Span | 调用链中的单个操作单元,包含名称、起止时间、属性、事件、链接 |
| SpanContext | TraceID + SpanID + TraceFlags + TraceState,跨服务传递的最小上下文 |
| Baggage | 跨 Span 传播的业务自定义键值对,需经传输协议透传 |
Trace (user-request):
[Span: GET /api/order] 10ms
├── [Span: queryUser] 3ms
├── [Span: queryInventory] 2ms
└── [Span: createPayment] 4ms
└── [Span: callBankAPI] 3ms
1.1 SpanKind 语义约定
OpenTelemetry 将 SpanKind 分为五类,明确标注每个 Span 在调用中的角色,便于后端分析和依赖拓扑构建:
| SpanKind | 语义 | 典型场景 |
|---|---|---|
SERVER | 服务端接收请求 | HTTP Server、gRPC Server、消息消费者 |
CLIENT | 客户端发起请求 | HTTP Client、gRPC Client、数据库查询 |
PRODUCER | 消息生产者 | Kafka 发送消息、RabbitMQ publish |
CONSUMER | 消息消费者 | Kafka 订阅消费、RabbitMQ 消费端处理 |
INTERNAL | 内部处理 | 业务计算、本地缓存访问、内部函数调用 |
标注正确的 SpanKind 可以让 Jaeger/Grafana Tempo 等后端自动生成服务依赖图。例如,CLIENT 与 SERVER 配对出现时可以建立服务调用边,PRODUCER 与 CONSUMER 配对则建立消息流边。
1.2 StatusCode 与错误记录
每个 Span 拥有独立的执行状态:
| StatusCode | 含义 | 使用时机 |
|---|---|---|
UNSET | 默认状态,Span 正常执行 | 正常返回的 Span |
OK | 显式标记成功 | 异步任务完成、确认校验通过 |
ERROR | 执行失败 | 异常抛出、HTTP 5xx、超时、业务校验失败 |
当调用出错时,应同时调用 span.recordException(e) 将异常栈完整记录到 Span 的 Event 列表中。如果异常已被上层捕获处理不应标记 ERROR,但业务层面严重失败时也应标记为 ERROR。
1.3 Link:跨 Trace 的关联
Link 用于将一个 Span 与另一个 Trace 中的 Span 关联起来。典型场景是批量处理:用户上传一批文件触发异步任务,每个子任务有自己的 Trace,但需要关联回原始上传 Trace。
Span batchSpan = tracer.spanBuilder("processBatch")
.addLink(parentSpanContext)
.startSpan();
Link 与父子关系的区别在于父子关系属于同一 Trace,Link 则是跨 Trace 的弱引用,常用于回溯因果但不需要合并为一条链路。
1.4 Event:Span 内的时间线
Event 是 Span 上的结构化时间点记录,可以理解为 Span 内部的日志。与属性(Attribute)不同,Event 带有时间戳,能够精确标注某个时刻发生了什么。
span.addEvent("cache_miss", Attributes.of(AttributeKey.stringKey("cache.key"), key));
span.addEvent("db_query_start", Attributes.of(AttributeKey.stringKey("db.system"), "postgresql"));
span.addEvent("db_query_end");
Event 名称应使用小写加下划线风格(snake_case),属性应遵循语义约定。
1.5 Semantic Conventions 语义属性
OpenTelemetry 规范中定义了大量标准化属性,确保不同语言和框架产出的数据具备一致的维度。常用语义属性前缀:
| 前缀 | 含义 | 示例 |
|---|---|---|
http. | HTTP 协议相关 | http.method, http.status_code, http.url |
db. | 数据库操作 | db.system=[mysql](/posts/database/), db.statement, db.operation=SELECT |
messaging. | 消息队列 | messaging.system=[kafka](/posts/kafka/), messaging.destination=orders |
rpc. | RPC 调用 | rpc.system=grpc, rpc.method, rpc.service |
service. | 服务元信息 | service.name=order-service, service.version=1.2.0 |
host. | 主机信息 | host.name, host.arch |
process. | 进程信息 | process.pid, process.runtime.name=openj9 |
使用标准化属性的好处在于:Jaeger 和 Grafana 能够自动识别维度并生成预设图表,同时不同团队的数据可以在同一个查询界面中聚合分析。
2. OpenTelemetry 自动埋点与手动埋点
自动埋点(Auto Instrumentation)能够在不修改业务代码的前提下,通过运行时探针或字节码增强技术自动采集框架层面的 Span。手动埋点则是针对核心业务路径添加关键追踪点。二者结合可以达到覆盖率80%自动采集加关键路径精确追踪的最佳效果。
2.1 Java Agent 自动埋点
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.resource.attributes=deployment.environment=production \
-jar order-service.jar
Java Agent 支持自动埋点的主要框架包括 Spring Boot、JDBC、Kafka、gRPC、OkHttp、Apache HttpClient、Redis(Jedis/Lettuce)、Elasticsearch
、MongoDB
等。通过添加 -Dotel.javaagent.debug=true 可以输出埋点诊断日志,用于排查 SDK 未生效的问题。
2.2 Node.js 自动埋点
# 安装 auto-instrumentations-node 包
npm install --save @opentelemetry/auto-instrumentations-node
# 启动时通过 --require 加载
node --require @opentelemetry/auto-instrumentations-node/register app.js
环境变量配置:
export OTEL_SERVICE_NAME=payment-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
export OTEL_TRACES_EXPORTER=otlp
export OTEL_NODE_RESOURCE_DETECTORS=all
Node.js Agent 自动支持 Express、Fastify、Koa、NestJS、HTTP/HTTPS、gRPC、MySQL、PostgreSQL、Redis、MongoDB、ioredis 等框架与中间件。
2.3 Python 自动埋点
# 安装 opentelemetry-distro 和对应 instrumentations
opentelemetry-bootstrap -a install
启动命令:
opentelemetry-instrument \
--traces_exporter otlp \
--service_name inventory-service \
--exporter_otlp_endpoint http://otel-collector:4317 \
python main.py
Python Agent 支持 Flask、Django、FastAPI、Requests、urllib3、SQLAlchemy、Redis、Kafka、Celery、gRPC 等库。通过 OTEL_PYTHON_DISABLED_INSTRUMENTATIONS 可以禁用特定库的自动埋点,避免过度采集。
2.4 Go 手动埋点
Go 语言目前主要依赖显式手动埋点,官方提供了各框架的中间件支持。以 Gin 框架为例:
package main
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
ginotel "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
var tracer = otel.Tracer("order-service")
func createOrderHandler(c *gin.Context) {
ctx := c.Request.Context()
span := tracer.Start(ctx, "createOrder",
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
attribute.String("http.method", c.Request.Method),
attribute.String("http.route", "/api/v1/orders"),
),
)
defer span.End() // 最佳实践:总是 defer
// 业务处理
orderID, err := processOrder(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
span.SetAttributes(attribute.String("order.id", orderID))
c.JSON(http.StatusOK, gin.H{"order_id": orderID})
}
func processOrder(ctx context.Context) (string, error) {
_, childSpan := tracer.Start(ctx, "processOrder")
defer childSpan.End() // 子 Span 也 defer
// ... 业务逻辑
return "ORD-12345", nil
}
func main() {
r := gin.Default()
r.Use(ginotel.Middleware("order-service"))
r.POST("/api/v1/orders", createOrderHandler)
r.Run(":8080")
}
2.5 手动埋点最佳实践
必须 defer span.End()
Go、Java、Python 任何语言中,Span 的结束都必须放在 defer/finally/try-with-resources 中,确保异常退出时也会关闭。
Span span = tracer.spanBuilder("criticalOperation").startSpan();
try (Scope scope = span.makeCurrent()) {
doWork();
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end(); // 总是执行
}
避免在热路径上创建过多 Span
循环内部的操作不宜逐个创建 Span,否则会产生大量追踪数据拖垮 Collector。可以采用批量 Span 或仅记录循环整体耗时。
Span batchSpan = tracer.spanBuilder("batchProcess").startSpan();
try {
for (Item item : items) {
// 循环内只记录事件而非子 Span
batchSpan.addEvent("process_item",
Attributes.of(AttributeKey.stringKey("item.id"), item.getId()));
process(item);
}
} finally {
batchSpan.end();
}
命名规范
Span 名称应使用小写动词加名词,如 get_user、create_payment、send_kafka_message。不要包含动态参数,因为 Jaeger 会使用 Span 名称聚合统计。
属性值不要过大
单个属性值不宜超过 128 个字符,SQL 语句超过长度应截断。也不要将完整的请求体或响应体作为属性,避免传输和存储开销。
3. 上下文传递
SpanContext 需要在服务间传递,使得下游 Span 能够正确关联到父 Span。OpenTelemetry 定义了标准的 TextMapPropagator 接口,通过 Carrier 实现注入(Inject)和提取(Extract)。
3.1 HTTP 上下文注入与提取
// HTTP 客户端:注入上下文
TextMapSetter<HttpURLConnection> setter = (carrier, key, value) ->
carrier.setRequestProperty(key, value);
openTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), connection, setter);
// 服务端:提取上下文
TextMapGetter<HttpServletRequest> getter = new TextMapGetter<>() {
public String get(HttpServletRequest req, String key) {
return req.getHeader(key);
}
public Iterable<String> keys(HttpServletRequest req) {
return () -> req.getHeaderNames().asIterator();
}
};
Context ctx = openTelemetry.getPropagators().getTextMapPropagator()
.extract(Context.current(), request, getter);
W3C Trace Context 标准头部:
traceparent:00-{trace-id}-{span-id}-{flags}- trace-id: 32 位十六进制字符串
- span-id: 16 位十六进制字符串
- flags: 00 表示不采样,01 表示采样
tracestate: 厂商扩展信息,格式为vendor=value,vendor2=value2
3.2 gRPC 上下文传递
gRPC 基于 HTTP/2,天然支持头部传递。OpenTelemetry 提供了客户端和服务端的拦截器实现跨进程上下文传播:
// 客户端拦截器
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9090)
.intercept(new OpenTelemetryClientInterceptor(openTelemetry))
.build();
// 服务端拦截器
Server server = ServerBuilder.forPort(9090)
.addService(ServerInterceptors.intercept(
serviceImpl, new OpenTelemetryServerInterceptor(openTelemetry)))
.build();
Go 的 gRPC 拦截器配置类似:
import (
grpctrace "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
// 服务端
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(grpctrace.UnaryServerInterceptor()),
grpc.StreamInterceptor(grpctrace.StreamServerInterceptor()),
)
// 客户端
conn, _ := grpc.Dial(addr,
grpc.WithUnaryInterceptor(grpctrace.UnaryClientInterceptor()),
)
gRPC 元数据中的传播头部名称与 HTTP W3C Trace Context 完全一致,因此 gRPC 与 REST 服务混合部署时可以实现无缝链路追踪。
3.3 消息队列上下文传递
在异步消息队列场景下,生产者将 SpanContext 写入消息属性/头部,消费者从消息中读取并恢复上下文,继续链路追踪。
Kafka 上下文传递:
// 生产者:注入
ProducerRecord<String, String> record = new ProducerRecord<>("orders", orderId, payload);
KafkaHeadersSetter setter = new KafkaHeadersSetter();
openTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), record.headers(), setter);
producer.send(record);
```java
// KafkaHeadersSetter 的实现
public class KafkaHeadersSetter implements TextMapSetter<Headers> {
@Override
public void set(Headers headers, String key, String value) {
if (headers != null) {
headers.remove(key);
headers.add(key, value.getBytes(StandardCharsets.UTF_8));
}
}
}
RabbitMQ 上下文传递:
// 生产者:注入
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.headers(new HashMap<>())
.build();
channel.basicPublish("exchange", "routingKey", props, message.getBytes());
openTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), props.getHeaders(),
(carrier, key, value) -> carrier.put(key, value));
// 消费者:提取
Delivery delivery = consumer.nextDelivery();
Map<String, Object> headers = delivery.getProperties().getHeaders();
Context extractedCtx = openTelemetry.getPropagators().getTextMapPropagator()
.extract(Context.current(), headers,
(carrier, key) -> carrier != null ? (String) carrier.get(key) : null);
消息队列场景下,生产者和消费者分属不同的服务端点,SpanKind 应分别标注为 PRODUCER 和 CONSUMER。同时消息的属性中应包含 messaging.system=kafka 或 messaging.system=rabbitmq 等语义属性。
3.4 Baggage 使用与限制
Baggage 是附加在 SpanContext 上的用户自定义键值对,与 Trace Context 一起跨服务传播。适合传递轻量级的业务上下文,如租户 ID、用户类型、实验分组等。
// 写 Baggage
Baggage.current()
.toBuilder()
.put("tenant.id", tenantId)
.put("experiment.group", "A")
.build()
.makeCurrent();
// 读 Baggage
String tenantId = Baggage.current().getEntryValue("tenant.id");
Baggage 会被自动注入到所有下游传播中,无需手动在每个 Span 上标记。但需要特别注意以下限制:
- 大小限制:经过 HTTP 头部或消息队列属性传播时,总量受限于中间件的头部大小限制,如 Nginx 默认 8KB、Kafka Headers 过大影响吞吐量。
- 不加密:Baggage 以明文形式在网络中传输,不可放入敏感信息如密码、Token、PII 数据。
- 向后兼容:下游服务未接入 OTel 时,Baggage 会透传但不会解析,可能造成头部污染,应在网关层注入服务网格 sidecar 统一处理。
- 性能开销:Baggage 传播增加了每条请求的头部大小和序列化开销,建议总量控制在 1KB 以内。
4. 采样策略
采样策略决定哪些 Trace 被采集、存储和分析。生产环境中流量巨大,全量采集无论是在网络带宽、存储成本还是查询性能上都不可承受。采样策略需要在数据完整性和成本之间取得平衡。
4.1 内置 SDK 采样器
| 策略 | 说明 | 适用 |
|---|---|---|
| AlwaysOn | 全量采样,记录所有 Trace | 测试环境、本地开发 |
| AlwaysOff | 不采样,不记录任何 Trace | 紧急降级、生产关闭调试 |
| TraceIdRatio | 按 TraceID 哈希值比例采样 | 生产常用,如 0.01 表示 1% |
| ParentBased | 跟随父 Span 的采样决策 | 保证完整链路不被切断 |
| RateLimiting | 速率限制采样,如 100 spans/s | 流量极高且均匀场景 |
TraceIdRatio 的原理基于 TraceID 的确定性哈希,使得同一 TraceID 的采样决策始终一致。这在分布式场景下至关重要,确保父服务采样了,所有子服务也会采样,反之亦然。
// SDK 中配置 ParentBased + TraceIdRatio
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.setSampler(
Sampler.parentBased(
Sampler.traceIdRatioBased(0.01)
)
)
.build();
4.2 Head-based vs Tail-based 采样对比
| 维度 | Head-based | Tail-based |
|---|---|---|
| 决策时机 | 请求开始时 | 请求完成后 |
| 依据 | 概率、固定比例 | 错误状态、延迟、自定义规则 |
| 实现位置 | SDK 或 Collector 入口 | Collector 缓存后批量决策 |
| 内存需求 | 低 | 高(需缓存完整 Trace) |
| 异常捕获 | 可能丢失异常链路 | 能捕获异常链路 |
| 延迟影响 | 无 | 增加等待窗口 |
Head-based 采样在请求入口即决定是否采样,优点是简单无延迟,缺点是对异常慢请求无法 “事后捕获”。Tail-based 采样是收集完整 Trace 后根据规则决定是否保留,可以确保错误和慢请求 100% 被采样,代价是需要在 Collector 中缓存整个 Trace,消耗内存。
4.3 Tail-based 采样 Collector 配置
Tail-based 采样在 OpenTelemetry Collector 中通过 tail_sampling Processor 实现。需要配置 decision_wait 等待窗口,让每个 Trace 的所有 Span 到达后再做决策。
processors:
tail_sampling:
decision_wait: 10s # 等待 Span 到达的最大时间
num_traces: 100000 # 内存中缓存的最大 Trace 数
expected_new_traces_per_sec: 1000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow_requests
type: latency
latency: { threshold_ms: 1000 }
- name: probabilistic
type: probabilistic
probabilistic: { sampling_percentage: 10.0 }
- name: custom_attribute
type: string_attribute
string_attribute:
key: http.route
values:
- /api/v1/critical
enabled_regex_matching: true
Tail Sampling 的 decision_wait 不宜过大,否则会增加数据上报延迟和内存占用。常见配置为 5~10 秒,配合 Collector 的水平扩展可以有效分散缓存压力。
4.4 自适应采样(Adaptive Sampling)
Jaeger 提供自适应采样策略,Agent 根据流量动态调整每个服务端的采样率。其核心思路是根据过去一段时间的服务调用量和配置的目标采样率,自动计算每个服务应该采用的采样比例。这种方式避免了手动配置每个服务的采样率,特别适合服务端点众多的大型微服务架构。Jaeger 的自适应采样通过 Agent 与 Collector 的配合,将策略推送到客户端 Agent,SDK 定期获取新的采样策略并生效。
5. Jaeger 部署与存储后端
Jaeger 是 CNCF 孵化的链路追踪系统,支持多种存储后端和部署模式。“all-in-one” 模式适合本地开发和测试,生产环境应使用独立部署的 Collector、Query 和存储组件。
5.1 本地测试部署
## docker-compose.yaml
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.50
ports:
- "16686:16686" # UI 查询界面
- "4317:4317" # OTLP gRPC 接收端
- "4318:4318" # OTLP HTTP 接收端
- "14268:14268" # Jaeger Thrift HTTP
- "14250:14250" # Jaeger gRPC
environment:
- COLLECTOR_OTLP_ENABLED=true
- LOG_LEVEL=info
networks:
- tracing
otel-collector:
image: otel/opentelemetry-collector-contrib:0.88.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "8888:8888" # Prometheus 指标端点
- "8889:8889" # Collector 自身指标
depends_on:
- jaeger
networks:
- tracing
networks:
tracing:
driver: bridge
访问 http://localhost:16686 查看调用链。在 UI 中可以通过 TraceID、服务名、操作名、标签等多维度进行搜索,火焰图模式直观展示每个 Span 在链路中的耗时占比。
5.2 生产级存储后端选择
| 存储后端 | 适用规模 | 查询延迟 | 数据保留 | 特点 |
|---|---|---|---|---|
| Memory | 测试/演示 | 极低 | 进程存活 | all-in-one 默认 |
| Badger | 中小规模 | 低 | 可配 TTL | 嵌入式,无需外部依赖 |
| Elasticsearch | 大规模 | 中等 | 长期保留 | 全文检索、聚合能力强 |
| Cassandra | 超大规模 | 中等 | 长期保留 | 高写入吞吐、水平扩展 |
| Kafka | 缓冲/中转 | N/A | N/A | 作为 Collector 的缓冲区 |
生产环境推荐使用 Elasticsearch 或 Cassandra 作为 Span 存储,配合 Kafka 作为缓冲层。如果已有 Elasticsearch 日志平台,复用现有集群可以降低运维成本。
5.3 Grafana Tempo 替代方案
Grafana Tempo 是一个专注于可扩展性和成本优化的追踪后端,设计上采用对象存储(S3/GCS/MinIO)作为持久层,仅索引少量标签(TraceID、服务名等),查询时从对象存储按需拉取完整 Trace。
## tempo.yaml
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
ingester:
trace_idle_period: 10s
max_block_bytes: 1_000_000
max_block_duration: 5m
compactor:
compaction:
block_retention: 168h
storage:
trace:
backend: local
local:
path: /tmp/tempo/traces
# 生产环境切换为 S3:
# backend: s3
# s3:
# bucket: tempo-traces
# endpoint: s3.us-east-1.amazonaws.com
# access_key: ${S3_ACCESS_KEY}
# secret_key: ${S3_SECRET_KEY}
overrides:
defaults:
ingestion:
max_traces_per_user: 100000
Tempo 的优势在于存储成本远低于 Elasticsearch,特别适合只需按 TraceID 查询而不需要在 Span 标签上做复杂搜索的场景。配合 Grafana 的 TraceQL,可以在 Grafana 界面内实现 Log-to-Trace、Metric-to-Trace 的无缝跳转。
6. Metrics + Logs + Traces 关联
可观测性的终极目标是将 Metrics(发生了什么)、Logs(为什么发生)和 Traces(在哪里发生)关联起来,形成统一的故障诊断体验。三条信号通过共同的维度(TraceID、服务名、时间戳)实现自然连接。
6.1 Exemplar 详解
Exemplar 是 Prometheus 2.26+ 和 OpenTelemetry 支持的特性,允许在时序聚合的 Histogram/Summary 中保存代表性的原始样本,每个样本包含一个 TraceID。这样在观察慢请求或高延迟时段时,可以直接从指标点跳转到对应的完整 Trace。
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.view import View
## 配置 MeterProvider
meter_provider = MeterProvider()
metrics.set_meter_provider(meter_provider)
meter = metrics.get_meter(__name__)
request_duration = meter.create_histogram(
name="http.request.duration",
description="HTTP request duration in ms",
unit="ms",
)
## 记录请求时 Exemplar 自动关联当前 SpanContext
import time
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("handle_request") as span:
trace_id = format(span.get_span_context().trace_id, "032x")
request_duration.record(
duration_ms,
attributes={"http.method": "GET", "http.route": "/api/orders"},
)
使用 Exemplar 后,Grafana 中查看 Histogram 时,每个柱子旁会显示可点击的 Trace 链接,直接跳转查看该延迟样本的完整调用链,无需再在海量 Trace 中手动搜索。Exemplar 的存储消耗极小,一个时间序列上通常只保留最近几条样本,不会显著增加存储成本。
6.2 结构化日志 OTLP 导出
Log 中嵌入 TraceID 和 SpanID 是实现 Log-to-Trace 跳转的基础。现代日志采集系统如 Fluent Bit、Vector、Promtail 都支持从日志 JSON 中提取 trace_id 字段。
import logging
import structlog
from opentelemetry import trace
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)
logger = structlog.get_logger()
tracer = trace.get_tracer(__name__)
current_span = trace.get_current_span()
span_ctx = current_span.get_span_context()
logger.info(
"order_processing_complete",
trace_id=format(span_ctx.trace_id, "032x"),
span_id=format(span_ctx.span_id, "016x"),
trace_flags=span_ctx.trace_flags,
order_id="ORD-789",
amount=199.99,
)
在 Grafana Loki 中搜索时,可以用 {job="order-service"} |= "trace_id=abc123" 快速过滤。OpenTelemetry 还支持通过 OTLP Log Exporter 直接从 SDK 发送日志到 Collector,无需经过本地文件采集,延迟更低且格式统一。
6.3 三信号关联模式总结
| 关联方向 | 维度 | 工具 |
|---|---|---|
| Log -> Trace | trace_id | Grafana Loki / ELK Trace Appender |
| Metric -> Trace | Exemplar (trace_id) | Prometheus/Grafana |
| Trace -> Log | trace_id span_id | Jaeger/Tempo 日志关联面板 |
| Trace -> Metric | 同时间轴对齐 | Grafana 统一时间选择器 |
7. OpenTelemetry Collector 架构与配置详解
OpenTelemetry Collector 是 OpenTelemetry 生态的核心组件,承担数据接收、处理、导出的全部职责。它位于应用和后端存储之间,解耦了 SDK 与具体监控系统的依赖。一个服务只需通过 OTLP 协议将数据发送到 Collector, Collector 再负责将数据路由到 Jaeger、Tempo、Prometheus、Loki 等任意后端。
7.1 Collector 核心架构
Collector 的架构由三大组件构成:
| 组件类型 | 职责 | 示例 |
|---|---|---|
| Receiver | 接收遥测数据 | OTLP、Jaeger、Zipkin、Prometheus Remote Write |
| Processor | 转换、过滤、增强数据 | Batch、Memory Limit、Tail Sampling、Resource |
| Exporter | 将数据发送到后端 | OTLP、Jaeger、Prometheus、Kafka、File |
数据在 Collector 内部的流动路径为:Receiver -> Processor(s) -> Exporter(s)。一个 Collector 实例可以配置多条 pipelines,例如 traces pipeline 和 metrics pipeline 独立运行。
7.2 完整 Collector 配置示例
## otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: "otel-collector"
scrape_interval: 10s
static_configs:
- targets: ["0.0.0.0:8888"]
processors:
batch:
timeout: 1s
send_batch_size: 1024
send_batch_max_size: 2048
memory_limiter:
check_interval: 5s
limit_mib: 512
spike_limit_mib: 128
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
- key: host.name
from_attribute: host.name
action: upsert
tail_sampling:
decision_wait: 10s
num_traces: 100000
expected_new_traces_per_sec: 1000
policies:
- name: errors-and-slow
type: and
and:
and_sub_policy:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 500 }
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write
tls:
insecure: true
logging:
verbosity: detailed
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
zpages:
endpoint: 0.0.0.0:55679
service:
extensions: [health_check, pprof, zpages]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch, resource, tail_sampling]
exporters: [otlp/jaeger, otlp/tempo, logging]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch, resource]
exporters: [prometheusremotewrite, logging]
配置中的 extensions 是 Collector 的运维扩展组件,health_check 用于 Kubernetes 健康探针,zpages 提供 Web 诊断页面查看 Collector 内部状态和延迟分布。pprof 用于 Go 性能分析,排查 Collector 自身的 CPU/Memory 热点。
7.3 Processor 详解
Batch Processor:将多个小请求合并为一批发送,减少网络开销。send_batch_size 是触发发送的条目数,timeout 是最大等待时间。建议 timeout 设置为 1s,既能及时发送又避免频繁RPC。
Memory Limiter:防止 Collector 因上游数据突增导致 OOM。当内存超过 limit_mib 时,开始丢弃新数据;低于 spike_limit_mib 时恢复接收。生产环境必须配置此项。
Resource Processor:为所有数据添加或修改 Resource 属性,统一打上集群名、环境名、版本号等标签,便于多环境数据区分。
Tail Sampling Processor:前文已详细介绍,是捕获异常链路的利器。
8. 生产部署:高可用 Collector、Batch Processor、Resource Processor
8.1 Collector 的水平扩展
生产环境中 Collector 不应以单实例运行。最佳实践是:
- Agent 模式:每个节点或每个 Pod 运行一个 DaemonSet Collector,负责接收本机应用的 OTLP 数据并转发到 Gateway。
- Gateway 模式:独立部署一组 Collector 实例(StatefulSet 或 Deployment),接收来自 Agent 的数据,执行 Tail Sampling、Batching 等重处理,再导出到后端。
## Kubernetes Agent DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-agent
spec:
selector:
matchLabels:
app: otel-agent
template:
metadata:
labels:
app: otel-agent
spec:
containers:
- name: otel-agent
image: otel/opentelemetry-collector-contrib:0.94.0
command: ["--config=/conf/otel-agent-config.yaml"]
volumeMounts:
- name: config
mountPath: /conf
ports:
- containerPort: 4317
- containerPort: 4318
volumes:
- name: config
configMap:
name: otel-agent-config
8.2 Kubernetes 中的 Sidecar 注入
对于需要隔离资源或混合语言部署的场景,可以将 Collector 作为 Sidecar 注入到应用 Pod 中。应用将数据发送到 localhost:4317,由 Sidecar Collector 处理后转发到集群内的 Gateway Collector。
## Pod 中的 Sidecar 示例
spec:
containers:
- name: my-app
image: my-app:1.0
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://localhost:4317
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.94.0
volumeMounts:
- name: otel-config
mountPath: /etc/otel
8.3 资源规划与限流
| 场景 | CPU 请求/限制 | 内存请求/限制 | 副本数 |
|---|---|---|---|
| Agent(DaemonSet) | 100m / 500m | 128Mi / 512Mi | 每节点 1 |
| Gateway(Deployment) | 500m / 2 | 512Mi / 2Gi | 2-6 |
| 高吞吐场景 | 2 / 4 | 2Gi / 8Gi | 6+ |
当后端临时不可用时,Collector 会缓存数据到内存和磁盘(通过 file_storage extension)。但缓存不是无限的,超过限制后新数据会被丢弃。通过 Prometheus 指标 otelcol_exporter_enqueue_failed_spans 监控队列溢出情况。
9. 服务网格(Istio/Linkerd)自动注入 Trace
服务网格(Service Mesh)无需应用代码改造即可实现链路追踪,因为所有服务间通信都经过 Sidecar 代理(Envoy),代理天然可以记录请求起点、终点、耗时和状态码。
9.1 Istio 自动链路追踪
Istio 通过 Envoy Sidecar 自动收集所有服务间调用的 Span,并注入 W3C Trace Context 头部。Istio 支持多种追踪后端:Jaeger、Zipkin、Lightstep、Datadog。
## Istio 全局配置
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
enableTracing: true
defaultConfig:
tracing:
sampling: 1.0 # 100% 采样,生产环境建议 0.01
custom_tags:
cluster:
literal:
value: "prod-cluster-01"
extensionProviders:
- name: otel-collector
opentelemetry:
service: otel-collector.monitoring.svc.cluster.local
port: 4317
在 Istio 中开启链路追踪后,Envoy 自动为每个进出 Pod 的请求创建 Span。应用本身无需任何 OTel 依赖。但如果应用也接入了 OTel,则需要确保 Istio 的头部传播与 OTel SDK 的传播器兼容。Istio 默认支持 W3C Trace Context,而 OTel SDK 默认也使用 W3C,因此二者可以无缝协作。
9.2 Linkerd 自动链路追踪
Linkerd 2.x 通过 OpenCensus 代理实现链路追踪。配置方式类似:
## Linkerd 配置
apiVersion: v1
kind: ConfigMap
metadata:
name: linkerd-config
namespace: linkerd
data:
values.yaml: |
tracing:
enabled: true
collector:
svcAddr: otel-collector.monitoring.svc.cluster.local:55678
Linkerd 的追踪粒度比 Istio 更轻量,因为 Linkerd 使用 Rust 编写的代理,资源占用比 Envoy 低。但 Linkerd 的追踪生态相对 Istio 更封闭,自定义扩展能力较弱。
9.3 服务网格与 OTel SDK 的协作
在网格环境中,最佳实践是将服务网格的自动追踪作为 “第一层” 覆盖所有服务间通信,同时让应用 OTel SDK 负责 “第二层” 的业务级 Span(如数据库查询、缓存访问、外部 API 调用)。两层数据合并后形成最完整的链路视图。
需要注意端口配置,Istio Sidecar 和应用 SDK 不要同时导出到同一个 Collector 端口造成重复采集。建议 Istio 的 Collector 与应用的分属不同实例或 pipeline。
10. 从 Zipkin/Jaeger 迁移到 OpenTelemetry 指南
许多存量系统使用 Zipkin Brave 或 Jaeger Client 进行链路追踪,随着 OpenTelemetry 成为事实标准,迁移是必然趋势。
10.1 迁移策略选择
| 策略 | 适用场景 | 风险 | 周期 |
|---|---|---|---|
| 大爆炸式 | 小型系统、团队集中 | 高,回滚困难 | 1-2 周 |
| 逐步替换 | 中大型系统、多团队 | 低,可逐步验证 | 2-6 个月 |
| Collector 桥接 | 超大型系统、风险厌恶 | 极低,无感知 | 随时 |
推荐采用 “Collector 桥接 + 逐步替换” 的策略。Collector 可以同时接收 Zipkin、Jaeger 和 OTLP 三种格式的数据,在迁移期间旧系统继续工作,新系统接入 OTLP,后端无需立即改动。
10.2 Collector 桥接配置
## 接收 Zipkin 和 Jaeger 数据,统一导出到 Jaeger OTLP
receivers:
zipkin:
endpoint: 0.0.0.0:9411
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
grpc:
endpoint: 0.0.0.0:14250
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp, zipkin, jaeger]
processors: [batch]
exporters: [otlp/jaeger]
10.3 SDK 迁移检查清单
从 Zipkin Brave 迁移:
- 移除
zipkin-reporter、brave依赖 - 引入
opentelemetry-api、opentelemetry-sdk、对应语言的自动 instrumentations - 将
BraveTracer替换为OpenTelemetry.getTracer() - HTTP 传播器从 B3 格式切换为 W3C Trace Context(可通过配置保留 B3 向后兼容)
- 自定义 tags 改为 OTel Attributes,Custom Span 改为 OTel Span
从 Jaeger Client 迁移:
- 移除
jaeger-client依赖 - 引入 OTel SDK
io.jaegertracing.spi.Reporter逻辑不再需要,由 OTLP Exporter 替代jaeger.propagation.Format.B3等传播格式替换为 W3Cjaeger.samplers.ProbabilisticSampler替换为 OTelTraceIdRatioBasedSampler
// 迁移前:Jaeger Client
try (Scope scope = tracer.buildSpan("processOrder").startActive(true)) {
tracer.activeSpan().setTag("order.id", orderId);
}
// 迁移后:OpenTelemetry
Span span = tracer.spanBuilder("processOrder").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.id", orderId);
} finally {
span.end();
}
10.4 向后兼容策略
在过渡期间,可以让 Collector 同时输出两种格式的传播器,确保新旧服务间的链路不中断:
## Collector 同时注入 W3C 和 B3 头部
propagators:
text:
- tracecontext # W3C
- b3 # Zipkin B3
- b3multi # B3 多头部格式
- jaeger # Jaeger Uber 头部
这样旧服务使用 B3 格式传播 Trace 时,新服务通过 W3C 也能正确提取并关联。
总结
- OpenTelemetry 是标准:新项目直接使用 OTel SDK
- 自动埋点覆盖 80%:Java/Python/Node.js Agent 自动收集,Go/Rust 等语言依赖手动埋点
- Collector 是中枢:统一接收、处理、导出三信号,是生产部署的关键基础设施
- 采样是生产关键:Head-based 控制总量,Tail-based 捕获异常链路
- 服务网格加速落地:Istio/Linkerd 自动注入无需改造业务代码
- 三信号关联:TraceID 串联 Metrics/Logs/Traces,Exemplar 实现 Metrics 到 Trace 的一键跳转
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。