基于OpenTelemetry的全链路追踪微服务可观测性实践
一、概述
1.1 背景介绍
微服务拆分到第三年,我们的服务数量从最初的5个膨胀到了47个。一个用户下单请求要经过API Gateway -> 用户服务 -> 商品服务 -> 库存服务 -> 订单服务 -> 支付服务,中间还有消息队列和缓存的调用。某天线上出现下单接口P99延迟从200ms飙到了3秒,排查了两个小时才定位到是库存服务调用Redis超时导致的。
这就是没有链路追踪的痛。每个服务自己的日志都正常,但串起来看整个调用链,问题藏在哪个环节根本看不出来。
2019年我们先上了Jaeger + OpenTracing SDK,后来OpenTracing和OpenCensus合并成了OpenTelemetry(简称OTel),2021年开始逐步迁移到OTel。OTel的定位是可观测性的统一标准,一套SDK同时产生Trace、Metric、Log三种信号,不再需要为每种信号单独接入不同的库。
迁移到OTel后,排障效率提升明显。同样的问题,现在从Grafana的Trace面板点进去,直接看到整个调用链的瀑布图,哪个Span耗时异常一目了然,定位时间从小时级降到了分钟级。
1.2 技术特点
厂商中立的统一标准:OTel是CNCF的毕业项目,不绑定任何后端。今天用Jaeger,明天想换成Tempo或者Zipkin,只需要改Collector的exporter配置,业务代码一行不用动。我们从Jaeger迁移到Grafana Tempo的过程中,47个服务的代码零改动。
三信号统一(Trace/Metric/Log):一套OTel SDK同时产生分布式追踪、指标和日志,三种数据通过Trace ID关联。在Grafana里可以从一个异常Trace跳转到对应时间段的指标图表和日志,排障不需要在多个系统之间来回切换。
自动注入能力:Java、Python、Node.js等语言支持自动注入(auto-instrumentation),不需要修改业务代码就能自动采集HTTP请求、数据库调用、Redis操作等Span。我们的Java服务用javaagent方式接入,开发团队完全无感知。
1.3 适用场景
微服务架构的请求链路追踪:服务数量超过10个时,没有链路追踪基本等于盲人摸象。OTel可以自动记录每个服务间的调用关系、耗时、状态码,快速定位慢请求和错误请求的根因。
性能瓶颈分析:通过Trace数据可以看到每个环节的耗时占比,比如一个请求总耗时500ms,其中数据库查询占了300ms,就知道该优化SQL了。配合采样策略,可以只采集慢请求的Trace,减少存储开销。
服务依赖关系梳理:OTel Collector可以根据Trace数据自动生成服务拓扑图,哪个服务调用了哪个服务、调用频率多少、错误率多少,一张图全看清。新人入职看这张图就能快速了解系统架构。
1.4 环境要求
| 组件 | 版本要求 | 说明 |
|---|---|---|
| 操作系统 | CentOS 7+ / Ubuntu 20.04+ | 推荐Ubuntu 22.04 LTS |
| OTel Collector | 0.96+ | 本文以0.96.0为例 |
| Jaeger | 1.53+ | 作为Trace后端存储和查询 |
| Java | JDK 11+ | Java自动注入需要JDK 11以上 |
| Go | 1.21+ | Go SDK需要1.21以上 |
| Python | 3.8+ | Python自动注入需要3.8以上 |
| 内存 | Collector最低2GB,Jaeger最低4GB | 生产环境各8GB+ |
二、详细步骤
2.1 准备工作
2.1.1 架构说明
我们采用的架构是:
应用服务(OTel SDK) --> OTel Collector(Agent模式) --> OTel Collector(Gateway模式) --> Jaeger/Tempo --> Prometheus --> Loki
两层Collector的设计:
Agent模式:以DaemonSet或Sidecar部署在每个节点/Pod旁边,负责接收本地应用的数据,做初步处理(采样、过滤、添加属性)
Gateway模式:集中部署,接收所有Agent的数据,做聚合处理后发送到后端存储
这样设计的好处是Agent挂了只影响单个节点,Gateway可以水平扩展,后端存储的变更只需要改Gateway配置。
2.1.2 安装OTel Collector
# 创建用户和目录
sudo useradd --system --no-create-home --shell /usr/sbin/nologin otelcol
sudo mkdir -p /etc/otelcol /var/log/otelcol /data/otelcol
sudo chown -R otelcol:otelcol /var/log/otelcol /data/otelcol
# 下载OTel Collector Contrib版本(包含更多receiver/exporter)
OTEL_VERSION="0.96.0"
cd /tmp
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_VERSION}/otelcol-contrib_${OTEL_VERSION}_linux_amd64.tar.gz
tar xzf otelcol-contrib_${OTEL_VERSION}_linux_amd64.tar.gz
sudo mv otelcol-contrib /usr/local/bin/otelcol
sudo chmod +x /usr/local/bin/otelcol
# 验证安装
otelcol --version
2.1.3 安装Jaeger
# 用Docker部署Jaeger All-in-One(测试环境) docker run -d --name jaeger -p 16686:16686 -p 4317:4317 -p 4318:4318 -p 14250:14250 -e COLLECTOR_OTLP_ENABLED=true -e SPAN_STORAGE_TYPE=badger -e BADGER_EPHEMERAL=false -e BADGER_DIRECTORY_VALUE=/badger/data -e BADGER_DIRECTORY_KEY=/badger/key -v /data/jaeger:/badger jaegertracing/all-in-one:1.53 # 生产环境用Elasticsearch后端 docker run -d --name jaeger-collector -p 14250:14250 -p 4317:4317 -p 4318:4318 -e SPAN_STORAGE_TYPE=elasticsearch -e ES_SERVER_URLS=http://elasticsearch:9200 -e ES_INDEX_PREFIX=jaeger -e ES_NUM_SHARDS=3 -e ES_NUM_REPLICAS=1 jaegertracing/jaeger-collector:1.53
2.2 核心配置
2.2.1 OTel Collector Agent模式配置
# /etc/otelcol/agent-config.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 max_recv_msg_size_mib: 16 http: endpoint: 0.0.0.0:4318 cors: allowed_origins: ["*"] # 采集主机指标(替代node_exporter) hostmetrics: collection_interval: 30s scrapers: cpu: metrics: system.cpu.utilization: enabled: true memory: disk: filesystem: load: network: paging: processes: processors: batch: send_batch_size: 1024 send_batch_max_size: 2048 timeout: 5s # 攒够1024条或等待5秒就发送一批 # 默认的8192太大,延迟高;太小又增加网络开销 memory_limiter: check_interval: 5s limit_mib: 1536 spike_limit_mib: 512 # 内存限制1.5GB,超过后开始丢弃数据 # 这个必须配,不配的话Collector OOM是迟早的事 resource: attributes: - key: host.name from_attribute: host.name action: upsert - key: deployment.environment value: production action: upsert - key: service.cluster value: prod-bj-01 action: upsert # 尾部采样在Agent层不做,放到Gateway层统一处理 # Agent层只做概率采样作为兜底 probabilistic_sampler: sampling_percentage: 100 # Agent层默认采集100%,采样策略在Gateway层控制 exporters: otlp: endpoint: otel-gateway:4317 tls: insecure: true retry_on_failure: enabled: true initial_interval: 5s max_interval: 30s max_elapsed_time: 300s sending_queue: enabled: true num_consumers: 10 queue_size: 5000 logging: loglevel: warn # 只在debug时改成info或debug extensions: health_check: endpoint: 0.0.0.0:13133 zpages: endpoint: 0.0.0.0:55679 service: extensions: [health_check, zpages] pipelines: traces: receivers: [otlp] processors: [memory_limiter, resource, batch] exporters: [otlp] metrics: receivers: [otlp, hostmetrics] processors: [memory_limiter, resource, batch] exporters: [otlp] logs: receivers: [otlp] processors: [memory_limiter, resource, batch] exporters: [otlp] telemetry: logs: level: warn metrics: address: 0.0.0.0:8888
2.2.2 OTel Collector Gateway模式配置
# /etc/otelcol/gateway-config.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 max_recv_msg_size_mib: 32 http: endpoint: 0.0.0.0:4318 processors: batch: send_batch_size: 2048 timeout: 10s memory_limiter: check_interval: 5s limit_mib: 6144 spike_limit_mib: 1024 # 尾部采样:根据Trace的完整信息决定是否采样 tail_sampling: decision_wait: 30s # 等待30秒收集完整Trace后再决定是否采样 num_traces: 100000 # 内存中最多保留10万条Trace expected_new_traces_per_sec: 1000 policies: # 策略1:所有错误的Trace都保留 - name: errors-policy type: status_code status_code: status_codes: [ERROR] # 策略2:延迟超过1秒的Trace保留 - name: latency-policy type: latency latency: threshold_ms: 1000 # 策略3:正常请求按10%概率采样 - name: probabilistic-policy type: probabilistic probabilistic: sampling_percentage: 10 # 策略4:特定服务100%采样(比如支付服务) - name: payment-always type: string_attribute string_attribute: key: service.name values: [payment-service] # 给Span添加额外属性 attributes: actions: - key: collector.version value: "0.96.0" action: upsert # Span指标生成:从Trace数据中提取RED指标 spanmetrics: metrics_exporter: prometheusremotewrite dimensions: - name: http.method - name: http.status_code - name: service.name histogram: explicit: buckets: [5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s] exporters: otlp/jaeger: endpoint: jaeger-collector:4317 tls: insecure: true prometheusremotewrite: endpoint: http://prometheus:9090/api/v1/write resource_to_telemetry_conversion: enabled: true loki: endpoint: http://loki:3100/loki/api/v1/push default_labels_enabled: exporter: false job: true logging: loglevel: warn extensions: health_check: endpoint: 0.0.0.0:13133 zpages: endpoint: 0.0.0.0:55679 service: extensions: [health_check, zpages] pipelines: traces: receivers: [otlp] processors: [memory_limiter, tail_sampling, attributes, spanmetrics, batch] exporters: [otlp/jaeger] metrics: receivers: [otlp] processors: [memory_limiter, batch] exporters: [prometheusremotewrite] logs: receivers: [otlp] processors: [memory_limiter, batch] exporters: [loki] telemetry: logs: level: warn metrics: address: 0.0.0.0:8888
关键配置说明:
tail_sampling的decision_wait设为30秒,意味着一个Trace的所有Span必须在30秒内到达Collector,否则可能被截断。对于大多数同步调用场景够用了,但如果有异步消息队列的场景,可能需要调大到60秒。
spanmetrics处理器会从Trace数据中自动生成请求速率(Rate)、错误率(Error)、延迟分布(Duration)三个指标,也就是RED指标。这样不需要在应用代码里手动埋点Prometheus指标,Trace数据自动转换。
memory_limiter必须放在processors列表的第一个,这样内存超限时最先触发限流,避免后续处理器继续消耗内存。
2.2.3 Systemd服务配置
# /etc/systemd/system/otelcol.service [Unit] Description=OpenTelemetry Collector Documentation=https://opentelemetry.io/docs/collector/ After=network-online.target Wants=network-online.target [Service] Type=simple User=otelcol Group=otelcol ExecStart=/usr/local/bin/otelcol --config=/etc/otelcol/agent-config.yaml ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5 LimitNOFILE=65536 LimitMEMLOCK=infinity StandardOutput=journal StandardError=journal Environment=GOMAXPROCS=4 [Install] WantedBy=multi-user.target
2.3 启动和验证
2.3.1 启动服务
# 验证配置文件 otelcol validate --config=/etc/otelcol/agent-config.yaml # 启动Collector sudo systemctl daemon-reload sudo systemctl start otelcol sudo systemctl enable otelcol # 检查状态 sudo systemctl status otelcol # 检查日志 sudo journalctl -u otelcol -f --no-pager
2.3.2 功能验证
# 检查健康状态
curl -s http://localhost:13133/
# 预期输出:{"status":"Server available"...}
# 检查Collector自身指标
curl -s http://localhost:8888/metrics | head -30
# 发送测试Trace(用curl模拟OTLP HTTP协议)
curl -X POST http://localhost:4318/v1/traces
-H "Content-Type: application/json"
-d '{
"resourceSpans": [{
"resource": {
"attributes": [{"key": "service.name", "value": {"stringValue": "test-service"}}]
},
"scopeSpans": [{
"scope": {"name": "test"},
"spans": [{
"traceId": "5b8aa5a2d2c872e8321cf37308d69df2",
"spanId": "051581bf3cb55c13",
"name": "test-span",
"kind": 1,
"startTimeUnixNano": "1704067200000000000",
"endTimeUnixNano": "1704067200100000000",
"status": {"code": 1}
}]
}]
}]
}'
# 在Jaeger UI中查看:http://jaeger-host:16686
# 搜索service=test-service应该能看到刚才的Trace
2.3.3 SDK接入示例
Java自动注入(推荐,零代码改动):
# 下载OTel Java Agent wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.1.0/opentelemetry-javaagent.jar -O /opt/otel/opentelemetry-javaagent.jar # 启动Java应用时加上javaagent参数 java -javaagent:/opt/otel/opentelemetry-javaagent.jar -Dotel.service.name=order-service -Dotel.exporter.otlp.endpoint=http://localhost:4317 -Dotel.exporter.otlp.protocol=grpc -Dotel.traces.sampler=parentbased_always_on -Dotel.metrics.exporter=otlp -Dotel.logs.exporter=otlp -Dotel.resource.attributes=service.namespace=production,service.version=1.2.3 -jar order-service.jar
Go手动接入:
// main.go
package main
import (
"context"
"log"
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func initTracer() (*sdktrace.TracerProvider, error) {
ctx := context.Background()
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("inventory-service"),
semconv.ServiceVersion("1.0.0"),
attribute.String("deployment.environment", "production"),
),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithBatchTimeout(5*time.Second),
sdktrace.WithMaxExportBatchSize(512),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.AlwaysSample())),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return tp, nil
}
func main() {
tp, err := initTracer()
if err != nil {
log.Fatal(err)
}
defer tp.Shutdown(context.Background())
// 使用otelhttp自动注入HTTP处理器
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer("inventory-service")
// 手动创建子Span
ctx, span := tracer.Start(ctx, "check-inventory")
defer span.End()
span.SetAttributes(
attribute.String("product.id", r.URL.Query().Get("product_id")),
attribute.Int("inventory.count", 42),
)
w.Write([]byte("OK"))
})
wrappedHandler := otelhttp.NewHandler(handler, "inventory-check")
log.Fatal(http.ListenAndServe(":8080", wrappedHandler))
}
Python自动注入:
# 安装OTel Python包 pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-bootstrap -a install # 启动Python应用(自动注入) opentelemetry-instrument --service_name user-service --exporter_otlp_endpoint http://localhost:4317 --exporter_otlp_protocol grpc --traces_exporter otlp --metrics_exporter otlp python app.py
三、示例代码和配置
3.1 完整配置示例
3.1.1 Collector完整Pipeline配置(生产级)
这是我们线上47个微服务环境跑了10个月的Gateway配置,日均处理Span量约2亿条:
# /etc/otelcol/gateway-production.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 max_recv_msg_size_mib: 32 keepalive: server_parameters: max_connection_idle: 60s max_connection_age: 300s time: 30s timeout: 10s http: endpoint: 0.0.0.0:4318 # 接收Prometheus格式的指标 prometheus: config: scrape_configs: - job_name: 'otel-collector' scrape_interval: 30s static_configs: - targets: ['localhost:8888'] processors: batch/traces: send_batch_size: 2048 send_batch_max_size: 4096 timeout: 10s batch/metrics: send_batch_size: 4096 timeout: 15s batch/logs: send_batch_size: 2048 timeout: 10s memory_limiter: check_interval: 5s limit_mib: 6144 spike_limit_mib: 1024 tail_sampling: decision_wait: 30s num_traces: 200000 expected_new_traces_per_sec: 2000 policies: - name: error-traces type: status_code status_code: status_codes: [ERROR] - name: slow-traces type: latency latency: threshold_ms: 1000 - name: payment-traces type: string_attribute string_attribute: key: service.name values: [payment-service, refund-service] - name: health-check-drop type: string_attribute string_attribute: key: http.target values: [/health, /ready, /metrics] invert_match: true - name: default-sampling type: probabilistic probabilistic: sampling_percentage: 5 # 过滤掉健康检查的Span,减少存储 filter/traces: error_mode: ignore traces: span: - 'attributes["http.target"] == "/health"' - 'attributes["http.target"] == "/ready"' - 'attributes["http.target"] == "/metrics"' - 'attributes["http.target"] == "/favicon.ico"' # 敏感信息脱敏 attributes/sanitize: actions: - key: http.request.header.authorization action: delete - key: db.statement action: hash # 对SQL语句做hash,保留可关联性但不暴露具体内容 spanmetrics: metrics_exporter: prometheusremotewrite dimensions: - name: service.name - name: http.method - name: http.status_code - name: rpc.method histogram: explicit: buckets: [2ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s] dimensions_cache_size: 5000 aggregation_temporality: AGGREGATION_TEMPORALITY_CUMULATIVE exporters: otlp/jaeger: endpoint: jaeger-collector.monitoring:4317 tls: insecure: true retry_on_failure: enabled: true initial_interval: 5s max_interval: 60s sending_queue: enabled: true num_consumers: 20 queue_size: 10000 prometheusremotewrite: endpoint: http://prometheus.monitoring:9090/api/v1/write tls: insecure: true resource_to_telemetry_conversion: enabled: true loki: endpoint: http://loki-gateway.monitoring:3100/loki/api/v1/push extensions: health_check: endpoint: 0.0.0.0:13133 zpages: endpoint: 0.0.0.0:55679 pprof: endpoint: 0.0.0.0:1777 service: extensions: [health_check, zpages, pprof] pipelines: traces: receivers: [otlp] processors: [memory_limiter, filter/traces, attributes/sanitize, tail_sampling, spanmetrics, batch/traces] exporters: [otlp/jaeger] metrics: receivers: [otlp, prometheus] processors: [memory_limiter, batch/metrics] exporters: [prometheusremotewrite] logs: receivers: [otlp] processors: [memory_limiter, batch/logs] exporters: [loki] telemetry: logs: level: warn metrics: address: 0.0.0.0:8888
3.1.2 Java自动注入高级配置
通过配置文件精细控制Java Agent的行为:
# /opt/otel/otel-agent.properties # 启动时通过 -Dotel.javaagent.configuration-file 指定 otel.service.name=order-service otel.resource.attributes=service.namespace=production,service.version=2.1.0 otel.exporter.otlp.endpoint=http://otel-agent:4317 otel.exporter.otlp.protocol=grpc otel.exporter.otlp.timeout=10000 otel.exporter.otlp.compression=gzip otel.traces.sampler=parentbased_traceidratio otel.traces.sampler.arg=1.0 otel.bsp.schedule.delay=5000 otel.bsp.max.queue.size=2048 otel.bsp.max.export.batch.size=512 # 禁用对性能影响大但价值不高的instrumentation otel.instrumentation.runtime-metrics.enabled=false otel.instrumentation.log4j-appender.enabled=false otel.propagators=tracecontext,baggage otel.span.attribute.value.length.limit=1024 otel.span.attribute.count.limit=64
java -javaagent:/opt/otel/opentelemetry-javaagent.jar -Dotel.javaagent.configuration-file=/opt/otel/otel-agent.properties -jar order-service.jar
3.1.3 Kubernetes DaemonSet部署
# otel-collector-agent-daemonset.yaml apiVersion: apps/v1 kind: DaemonSet metadata: name: otel-collector-agent namespace: monitoring spec: selector: matchLabels: app: otel-collector-agent template: metadata: labels: app: otel-collector-agent spec: serviceAccountName: otel-collector containers: - name: otel-collector image: otel/opentelemetry-collector-contrib:0.96.0 args: ["--config=/etc/otelcol/config.yaml"] ports: - containerPort: 4317 hostPort: 4317 - containerPort: 4318 hostPort: 4318 resources: requests: cpu: 200m memory: 512Mi limits: cpu: 1000m memory: 2Gi livenessProbe: httpGet: path: / port: 13133 initialDelaySeconds: 15 volumeMounts: - name: config mountPath: /etc/otelcol volumes: - name: config configMap: name: otel-agent-config
3.2 实际应用案例
案例一:慢请求根因定位
场景描述:线上下单接口P99延迟从200ms突然飙到3秒,需要快速定位是哪个环节变慢了。
排查流程:
# 第一步:在Jaeger UI中搜索慢Trace
# Service: api-gateway
# Operation: POST /api/v1/orders
# Min Duration: 2s
# 找到一条耗时3.2秒的Trace
# 第二步:查看Trace瀑布图,发现调用链:
# api-gateway (3.2s)
# └── order-service (3.1s)
# ├── user-service (50ms) 正常
# ├── product-service (80ms) 正常
# ├── inventory-service (2.8s) <-- 这里有问题
# │ └── Redis GET (2.7s) <-- 根因
# └── payment-service (未执行,被inventory阻塞)
# 第三步:看inventory-service的Redis Span属性
# db.system: redis
# db.operation: GET
# net.peer.name: redis-cluster-03
# 发现是redis-cluster-03节点响应慢
# 第四步:用spanmetrics生成的指标确认影响范围
# PromQL: histogram_quantile(0.99,
# rate(traces_spanmetrics_latency_bucket{
# service_name="inventory-service",
# span_name="Redis GET"
# }[5m]))
# 确认P99从5ms飙到了2.7s
从发现问题到定位根因,整个过程不到5分钟。如果没有链路追踪,光看各服务自己的日志,至少要1-2小时。
案例二:跨服务上下文传播验证
场景描述:新接入OTel的服务和老的Zipkin服务之间Trace断裂,需要排查上下文传播问题。
排查步骤:
# OTel默认使用W3C TraceContext格式: # traceparent: 00-- - # 老服务用的是B3格式: # X-B3-TraceId / X-B3-SpanId / X-B3-Sampled # 解决方案:Java Agent配置多传播器 # otel.propagators=tracecontext,baggage,b3multi # 验证方法:用curl手动发请求检查响应头 curl -v -H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" http://new-service:8080/api/test 2>&1 | grep -i "traceparent|b3"
迁移期间两种格式并存,等所有服务都迁移到OTel后再去掉B3支持。
四、最佳实践和注意事项
4.1 最佳实践
4.1.1 采样策略选择
采样策略直接决定了存储成本和排障能力之间的平衡。我们踩过的坑:一开始全量采集,47个服务每天产生20亿条Span,Jaeger后端的Elasticsearch撑了两周就扛不住了。
头部采样(Head-based Sampling):在Trace的第一个Span就决定是否采样。优点是实现简单、资源消耗低;缺点是决策时不知道这个Trace后续会不会出错,可能漏掉错误Trace。
# Java Agent头部采样配置 # 按比例采样:只采集10%的Trace otel.traces.sampler=parentbased_traceidratio otel.traces.sampler.arg=0.1
尾部采样(Tail-based Sampling):等Trace的所有Span都到达Collector后再决定是否采样。优点是可以根据完整信息决策(比如只保留错误的和慢的Trace);缺点是Collector需要在内存中缓存所有Trace,资源消耗大。
# Collector尾部采样配置(推荐放在Gateway层) tail_sampling: decision_wait: 30s num_traces: 200000 policies: - name: errors-always type: status_code status_code: status_codes: [ERROR] - name: slow-always type: latency latency: threshold_ms: 1000 - name: normal-sample-5pct type: probabilistic probabilistic: sampling_percentage: 5
我们的做法:Gateway层用尾部采样,错误Trace和慢Trace(>1s)100%保留,正常Trace只保留5%。这样存储量从20亿条/天降到了约3亿条/天,但所有有问题的Trace都不会丢。
4.1.2 Span属性规范
Span属性(Attributes)是排障时的关键信息,但不是越多越好。属性太多会增加网络传输和存储开销,太少又查不到有用信息。
我们团队制定的Span属性规范:
# 必须包含的属性(所有服务统一) service.name: order-service # 服务名 service.version: 1.2.3 # 服务版本 deployment.environment: production # 环境 service.namespace: ecommerce # 业务域 # HTTP请求Span必须包含 http.method: GET http.url: /api/v1/orders/12345 http.status_code: 200 http.request_content_length: 1024 http.response_content_length: 2048 # 数据库调用Span必须包含 db.system: mysql db.name: order_db db.operation: SELECT db.statement: "SELECT * FROM orders WHERE id = ?" # 注意:db.statement只记录参数化的SQL,不记录实际参数值,防止泄露敏感数据 # RPC调用Span必须包含 rpc.system: grpc rpc.service: inventory.InventoryService rpc.method: CheckStock # 禁止包含的属性(敏感信息) # - 用户密码、Token # - 信用卡号、身份证号 # - 请求体中的完整JSON(太大)
4.1.3 与Prometheus/Loki关联
OTel的三信号关联是排障效率提升的关键。在Grafana中配置数据源关联:
# Grafana数据源配置 - Jaeger # Settings -> Trace to logs: # Data source: Loki # Tags: service.name -> job # Span start time shift: -5m # Span end time shift: 5m # Filter by trace ID: true # Settings -> Trace to metrics: # Data source: Prometheus # Tags: service.name -> service_name # Span start time shift: -5m # Span end time shift: 5m
这样配置后,在Jaeger的Trace详情页可以一键跳转到:
对应时间段的Loki日志(按Trace ID过滤)
对应服务的Prometheus指标图表
4.1.4 安全加固
敏感信息脱敏:在Collector的attributes处理器中删除Authorization头、对SQL语句做hash处理。别把用户Token写到Span里,Jaeger的存储是明文的。
传输加密:生产环境Collector之间的通信用mTLS加密,特别是跨网络的Agent到Gateway链路。
访问控制:Jaeger UI加Nginx反向代理做IP白名单或SSO认证,不要裸露在公网。
数据保留策略:Trace数据保留7-14天就够了,超过这个时间的Trace基本不会再查。Jaeger的Elasticsearch后端配置ILM策略自动清理。
4.2 注意事项
4.2.1 配置注意事项
memory_limiter必须放在processors列表的第一个位置。如果放在batch后面,batch已经消耗了大量内存,memory_limiter来不及限流,Collector直接OOM。我们线上出过一次这个问题,Collector反复重启。
tail_sampling的decision_wait不能设太短。设10秒的话,如果某个服务的Span因为网络延迟晚到了,这个Trace就不完整,采样决策可能不准确。我们设30秒,覆盖了99%的场景。
Java Agent的otel.bsp.max.queue.size默认2048,在高并发场景下可能不够。队列满了新的Span会被丢弃,日志里会出现"Dropping span"警告。我们调到了4096。
别在生产环境开Collector的logging exporter的info级别,每条Span都会打一行日志,IO会被打满。只在debug时临时开启。
4.2.2 常见错误
| 错误现象 | 原因分析 | 解决方案 |
|---|---|---|
| Trace断裂,父子Span不关联 | 上下文传播失败,propagator配置不匹配 | 检查所有服务的propagator配置是否一致 |
| Collector报"dropping spans" | 发送队列满或后端写入慢 | 增大sending_queue.queue_size,检查后端性能 |
| Java Agent启动后应用变慢 | 自动注入的instrumentation太多 | 禁用不需要的instrumentation |
| Span中缺少预期的属性 | SDK版本不匹配或属性名不符合语义约定 | 检查SDK版本,参考OTel Semantic Conventions |
| 尾部采样不生效 | decision_wait太短或Span分散在多个Collector | 增大decision_wait,确保同一Trace的Span路由到同一Collector |
| Collector OOM | memory_limiter未配置或位置不对 | 确保memory_limiter在processors列表第一位 |
4.2.3 兼容性问题
OTel SDK版本:Java Agent、Go SDK、Python SDK的版本尽量保持一致,至少大版本一致。混用不同大版本可能出现协议不兼容。
Collector版本:Contrib版本的Collector包含社区贡献的组件,更新频率快,偶尔会有breaking change。升级前先看Release Notes。
Jaeger兼容:Jaeger 1.35+原生支持OTLP协议,不需要再用jaeger exporter,直接用otlp exporter发送到Jaeger的4317端口。
Prometheus兼容:OTel的Metric和Prometheus的Metric在命名规范上有差异(OTel用点号分隔,Prometheus用下划线),Collector的prometheusremotewrite exporter会自动转换。
五、故障排查和监控
5.1 故障排查
5.1.1 日志查看
# Collector日志 sudo journalctl -u otelcol -f --no-pager # 只看错误 sudo journalctl -u otelcol -p err --since "1 hour ago" # Collector自身指标(通过/metrics端点) curl -s http://localhost:8888/metrics | grep otelcol # zpages调试页面(需要开启zpages扩展) # 浏览器访问 http://collector-host:55679/debug/tracez # 可以看到Collector内部处理的Trace样本
5.1.2 常见问题排查
问题一:Trace数据丢失,Jaeger中查不到预期的Trace
# 第一步:检查Collector是否收到了数据 curl -s http://localhost:8888/metrics | grep otelcol_receiver_accepted_spans # 如果这个值在增长,说明Collector收到了Span # 第二步:检查Collector是否成功发送 curl -s http://localhost:8888/metrics | grep otelcol_exporter_sent_spans # 对比accepted和sent的差值,差值大说明有丢弃 # 第三步:检查发送失败数 curl -s http://localhost:8888/metrics | grep otelcol_exporter_send_failed_spans # 不为0说明发送到后端失败了 # 第四步:检查采样策略是否过滤掉了 curl -s http://localhost:8888/metrics | grep otelcol_processor_tail_sampling
解决方案:
发送失败:检查后端(Jaeger/Tempo)是否正常运行,网络是否通
被采样过滤:检查tail_sampling策略,确认目标Trace符合保留条件
队列溢出:增大sending_queue.queue_size
问题二:Collector OOM反复重启
# 检查内存使用 curl -s http://localhost:8888/metrics | grep go_memstats_heap_inuse_bytes # 检查是否配置了memory_limiter grep -A5 "memory_limiter" /etc/otelcol/config.yaml # 检查tail_sampling的num_traces是否设太大 # 每个Trace在内存中大约占用2-5KB # num_traces=200000 大约需要400MB-1GB内存
解决方案:
确保memory_limiter在processors列表第一位
减小tail_sampling的num_traces
增大Collector的内存限制
如果是Agent模式,不要在Agent层做tail_sampling
问题三:SDK初始化失败,应用启动报错
# Java Agent常见错误 # "Failed to export spans. The request could not be executed." # 原因:Collector地址不可达 # 解决:检查otel.exporter.otlp.endpoint配置 # "java.lang.NoClassDefFoundError: io/opentelemetry/..." # 原因:Agent版本和应用依赖的OTel库版本冲突 # 解决:用Agent自带的instrumentation,移除应用pom.xml中的OTel依赖 # Go SDK常见错误 # "context deadline exceeded" # 原因:Collector连接超时 # 解决:检查endpoint配置,确认Collector在运行
问题四:跨服务上下文传播断裂
# 排查步骤: # 1. 确认所有服务使用相同的propagator # 2. 检查中间件(Nginx/Kong/Envoy)是否透传了traceparent头 # 3. 检查消息队列消费者是否正确提取了上下文 # Nginx需要配置透传trace头 # proxy_set_header traceparent $http_traceparent; # proxy_set_header tracestate $http_tracestate; # 验证Nginx是否透传 curl -v -H "traceparent: 00-aaaabbbbccccddddeeeeffffgggghhhh-1111222233334444-01" http://nginx-proxy/api/test 2>&1 | grep traceparent
5.1.3 调试模式
# 临时开启Collector debug日志 # 修改配置文件中的telemetry.logs.level为debug # 或者用环境变量 OTEL_LOG_LEVEL=debug otelcol --config=/etc/otelcol/config.yaml # 使用logging exporter查看处理的数据 # 在配置中临时添加logging exporter # exporters: # logging: # verbosity: detailed # sampling_initial: 5 # sampling_thereafter: 200 # Java Agent debug模式 java -javaagent:opentelemetry-javaagent.jar -Dotel.javaagent.debug=true -jar app.jar # 会输出所有instrumentation的加载信息和Span详情
5.2 性能监控
5.2.1 关键指标监控
# Collector接收的Span数量 curl -s http://localhost:8888/metrics | grep otelcol_receiver_accepted_spans # Collector发送成功的Span数量 curl -s http://localhost:8888/metrics | grep otelcol_exporter_sent_spans # Collector发送失败的Span数量 curl -s http://localhost:8888/metrics | grep otelcol_exporter_send_failed_spans # Collector发送队列大小 curl -s http://localhost:8888/metrics | grep otelcol_exporter_queue_size # Collector内存使用 curl -s http://localhost:8888/metrics | grep go_memstats_heap_inuse_bytes # 处理器丢弃的Span数量 curl -s http://localhost:8888/metrics | grep otelcol_processor_dropped_spans
5.2.2 监控指标说明
| 指标名称 | 正常范围 | 告警阈值 | 说明 |
|---|---|---|---|
| otelcol_receiver_accepted_spans rate | 根据业务量 | 突降50%以上 | 接收速率突降说明有服务停止上报 |
| otelcol_exporter_send_failed_spans rate | 0 | > 0持续5分钟 | 任何发送失败都需要关注 |
| otelcol_exporter_queue_size | < queue_size的70% | > queue_size的85% | 队列接近满说明后端写入跟不上 |
| go_memstats_heap_inuse_bytes | < limit_mib的80% | > limit_mib的85% | 内存接近限制需要扩容 |
| otelcol_processor_dropped_spans rate | 0 | > 0 | 有Span被丢弃 |
| traces_spanmetrics_latency P99 | 根据SLA | 超过SLA阈值 | 从Trace生成的延迟指标 |
5.2.3 Prometheus监控规则
# prometheus-rules/otel-collector-alerts.yaml
groups:
- name: otel-collector-alerts
rules:
- alert: OtelCollectorDown
expr: up{job="otel-collector"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "OTel Collector实例 {{ $labels.instance }} 不可用"
- alert: OtelCollectorSpanExportFailure
expr: rate(otelcol_exporter_send_failed_spans_total[5m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "OTel Collector Span发送失败"
description: "失败速率: {{ $value }}/s,检查后端存储是否正常"
- alert: OtelCollectorQueueNearFull
expr: otelcol_exporter_queue_size / otelcol_exporter_queue_capacity > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "OTel Collector发送队列接近满"
- alert: OtelCollectorMemoryHigh
expr: go_memstats_heap_inuse_bytes{job="otel-collector"} / 1024 / 1024 / 1024 > 5
for: 5m
labels:
severity: warning
annotations:
summary: "OTel Collector堆内存超过5GB"
- alert: OtelCollectorSpanDropped
expr: rate(otelcol_processor_dropped_spans_total[5m]) > 0
for: 3m
labels:
severity: warning
annotations:
summary: "OTel Collector有Span被丢弃"
5.3 备份与恢复
5.3.1 备份策略
OTel Collector本身是无状态的,不需要备份数据,只需要备份配置文件。Trace数据的备份由后端存储(Jaeger/Tempo)负责。
#!/bin/bash
# otel-backup.sh
set -euo pipefail
BACKUP_DIR="/backup/otel/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
# 备份Collector配置
tar czf "$BACKUP_DIR/otel-config.tar.gz" /etc/otelcol/
# 备份Java Agent配置
tar czf "$BACKUP_DIR/otel-agent-config.tar.gz" /opt/otel/ 2>/dev/null || true
# 备份Jaeger配置(如果有)
tar czf "$BACKUP_DIR/jaeger-config.tar.gz" /etc/jaeger/ 2>/dev/null || true
# 清理7天前的备份
find /backup/otel -maxdepth 1 -type d -mtime +7 -exec rm -rf {} ;
echo "[$(date)] OTel配置备份完成: $BACKUP_DIR"
5.3.2 恢复流程
停止Collector:sudo systemctl stop otelcol
恢复配置:tar xzf /backup/otel/20250101/otel-config.tar.gz -C /
验证配置:otelcol validate --config=/etc/otelcol/agent-config.yaml
重启服务:sudo systemctl start otelcol && curl -s http://localhost:13133/
全部0条评论
快来发表一下你的评论吧 !