微服务拆分后,一个请求可能经过五六个服务,出问题时排查像破案。之前我们用 ELK 做日志、Prometheus 做指标、Jaeger 做追踪,三套系统数据不互通,排查时要来回切换。2024年初引入 OpenTelemetry,把追踪、指标、日志统一起来,今天把落地实践分享出来。
一、为什么选 OpenTelemetry# OpenTelemetry(简称 OTel)是 CNCF 的可观测性标准,核心优势:
统一标准 :追踪、指标、日志三套信号统一规范,不用各搞一套厂商中立 :SDK 开源,后端可以换 Jaeger、Tempo、Zipkin、Datadog,不用改代码自动埋点 :Go/Java/Python 都有自动埋点库,常用框架(Gin、gRPC、GORM、Redis)自动生成 span上下文传播 :trace_id 跨服务自动传播,不用手动传参生态成熟 :CNCF 毕业项目,社区活跃,K8s 生态一等公民二、架构# 应 用 服 务 ( O T e l S D K ) → O J ( T a 追 e e 踪 l g ) ┌ ↓ e C ─ r o ─ ( l ─ 指 l ─ P 标 e ─ r ) c ─ o t ─ m o ┼ ↓ e r ─ t ↓ ─ h ( → ─ e 日 ─ u 志 后 ─ s ) 端 ─ 存 ─ 储 ┐ ↓ E l a s t i c s e a r c h
OTel Collector 是核心,负责接收、处理、导出可观测数据,应用只需要把数据发给 Collector,后端换什么都不用改应用。
三、Go 服务接入# 安装依赖# 1
2
3
4
5
6
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin
go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
go get go.opentelemetry.io/contrib/instrumentation/gorm.io/gorm/otelgorm
初始化 TracerProvider# 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
package main
import (
"context"
"go.opentelemetry.io/otel"
"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.21.0"
)
func initTracer ( ctx context . Context , serviceName string ) ( * sdktrace . TracerProvider , error ) {
// 1. 创建 OTLP gRPC exporter,发给 Collector
exporter , err := otlptracegrpc . New ( ctx ,
otlptracegrpc . WithEndpoint ( "otel-collector:4317" ),
otlptracegrpc . WithInsecure (),
)
if err != nil {
return nil , err
}
// 2. 资源信息:服务名、版本、环境
res , err := resource . New ( ctx ,
resource . WithAttributes (
semconv . ServiceName ( serviceName ),
semconv . ServiceVersion ( "v1.0.0" ),
semconv . DeploymentEnvironment ( "production" ),
),
)
if err != nil {
return nil , err
}
// 3. 创建 TracerProvider
tp := sdktrace . NewTracerProvider (
sdktrace . WithBatcher ( exporter ),
sdktrace . WithResource ( res ),
sdktrace . WithSampler ( sdktrace . TraceIDRatioBased ( 0.1 )), // 10% 采样
)
otel . SetTracerProvider ( tp )
// 4. 设置上下文传播器(W3C TraceContext)
otel . SetTextMapPropagator ( propagation . NewCompositeTextMapPropagator (
propagation . TraceContext {},
propagation . Baggage {},
))
return tp , nil
}
Gin 中间件自动埋点# 1
2
3
4
import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
r := gin . Default ()
r . Use ( otelgin . Middleware ( "resume-service" )) // 服务名
加一行中间件,Gin 的每个请求自动生成 span,包含 HTTP 方法、路径、状态码、延迟。
gRPC 拦截器# 1
2
3
4
5
6
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
server := grpc . NewServer (
grpc . UnaryInterceptor ( otelgrpc . UnaryServerInterceptor ()),
grpc . StreamInterceptor ( otelgrpc . StreamServerInterceptor ()),
)
GORM 自动埋点# 1
2
3
4
import "go.opentelemetry.io/contrib/instrumentation/gorm.io/gorm/otelgorm"
db , err := gorm . Open ( mysql . Open ( dsn ), & gorm . Config {})
db . Use ( otelgorm . NewPlugin ())
加完之后,每个 SQL 自动生成 span,包含 SQL 语句、执行时间、影响行数。
自定义 span# 业务逻辑里手动加 span,追踪关键步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func ( s * ResumeService ) ParseResume ( ctx context . Context , file [] byte ) ( * Resume , error ) {
// 创建自定义 span
ctx , span := otel . Tracer ( "resume-service" ). Start ( ctx , "ParseResume" )
defer span . End ()
// 1. 解析文件
_ , span1 := otel . Tracer ( "resume-service" ). Start ( ctx , "ParseFile" )
text , err := parseFile ( file )
span1 . End ()
if err != nil {
span . RecordError ( err ) // 记录错误
return nil , err
}
// 2. AI 提取信息
_ , span2 := otel . Tracer ( "resume-service" ). Start ( ctx , "AIExtract" )
resume , err := s . aiClient . Extract ( text )
span2 . End ()
// 3. 存数据库(GORM 自动生成 span)
s . db . Create ( resume )
return resume , nil
}
四、上下文传播# 跨服务调用时,trace_id 自动传播:
1
2
3
4
5
// HTTP 客户端调用其他服务
req , _ := http . NewRequest ( "GET" , "http://other-service/api/data" , nil )
// OTel 自动把 traceparent 注入请求头
otel . GetTextMapPropagator (). Inject ( ctx , propagation . HeaderCarrier ( req . Header ))
resp , err := http . DefaultClient . Do ( req )
gRPC 客户端用拦截器自动传播:
1
2
3
conn , _ := grpc . Dial ( "other-service:50051" ,
grpc . WithUnaryInterceptor ( otelgrpc . UnaryClientInterceptor ()),
)
这样一个请求从网关到 A 服务到 B 服务到数据库,整条链路的 trace_id 是同一个,在 Jaeger 里能看到完整的调用链和每个环节的耗时。
五、采样策略# 全量采样数据量太大,存储成本高。用按比例采样 + 错误全采:
1
2
3
4
5
// 10% 正常请求采样,但错误请求 100% 采样
sampler := sdktrace . ParentBased (
sdktrace . TraceIDRatioBased ( 0.1 ),
sdktrace . WithRemoteParentSampled ( sdktrace . AlwaysSample ()),
)
还可以按接口设置不同采样率:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 自定义采样器:关键接口全采,普通接口10%
type customSampler struct {}
func ( s customSampler ) ShouldSample ( p sdktrace . SamplingParameters ) sdktrace . SamplingResult {
// 从参数里取接口路径
for _ , attr := range p . Attributes {
if attr . Key == "http.route" {
if strings . Contains ( attr . Value . AsString (), "/api/resume/parse" ) {
return sdktrace . SamplingResult { Decision : sdktrace . RecordAndSample }
}
}
}
// 默认10%
if rand . Float64 () < 0.1 {
return sdktrace . SamplingResult { Decision : sdktrace . RecordAndSample }
}
return sdktrace . SamplingResult { Decision : sdktrace . Drop }
}
六、OTel Collector 配置# 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
# otel-collector-config.yaml
receivers :
otlp :
protocols :
grpc :
endpoint : 0.0.0.0 : 4317
http :
endpoint : 0.0.0.0 : 4318
processors :
batch :
timeout : 5s
send_batch_size : 1000
memory_limiter :
check_interval : 1s
limit_mib : 512
attributes :
actions :
- key : env
value : production
action : insert
exporters :
otlp/jaeger :
endpoint : jaeger:4317
tls :
insecure : true
prometheus :
endpoint : 0.0.0.0 : 8889
elasticsearch :
endpoints : [ "http://elasticsearch:9200" ]
index : otel-logs
service :
pipelines :
traces :
receivers : [ otlp]
processors : [ batch, memory_limiter, attributes]
exporters : [ otlp/jaeger]
metrics :
receivers : [ otlp]
processors : [ batch]
exporters : [ prometheus]
logs :
receivers : [ otlp]
processors : [ batch]
exporters : [ elasticsearch]
Collector 做了数据接收、批量处理、内存限制、属性增强,然后分发给不同后端。应用只需要连 Collector,后端怎么换都不用改应用。
七、踩坑经验# trace_id 不连续 :早期有些服务没接 OTel,调用链断了。必须所有服务都接,哪怕只加 HTTP 中间件也行,保证链路完整采样导致错误看不到 :10% 采样率下,错误请求可能被丢掉。改成"错误全采",在 span 里检测到 status=error 就强制采样Collector 内存溢出 :流量大时 Collector 处理不过来,内存暴涨。加了 memory_limiter 处理器 + 批量发送,限制内存使用GORM 埋点性能损耗 :每个 SQL 都生成 span,高并发下有性能损耗。压测发现增加约 3% 延迟,可接受。对非核心接口可以关闭 GORM 埋点日志和追踪关联 :日志里要带 trace_id,才能在 ELK 里按 trace_id 搜整条链路的日志。在日志库的 hook 里从 context 取 trace_id 注入日志八、效果# 指标 接入前 接入后 故障排查时间 平均 30 分钟 平均 5 分钟 慢接口定位 靠猜,看日志 直接看 span 火焰图 跨服务调用链 看不到 完整可视化 错误根因定位 多系统切换查 一个 trace_id 串联所有
九、总结# OpenTelemetry 落地核心:
统一标准 :追踪、指标、日志一套 SDK,不用各搞各的自动埋点优先 :Gin/gRPC/GORM/Redis 都有自动埋点,先接上覆盖 80% 场景自定义 span 补全 :关键业务逻辑手动加 span,补全调用链细节采样策略 :按比例采样 + 错误全采 + 关键接口全采,平衡成本和可见性Collector 中间层 :应用只连 Collector,后端可插拔,架构解耦日志关联 :日志里注入 trace_id,实现追踪和日志联动可观测性是微服务架构的"眼睛",没有它出问题就是瞎摸。OpenTelemetry 把可观测性的标准统一了,不用再在多个系统之间来回切换。建议从自动埋点开始,先把追踪链路跑通,再逐步加指标和日志,最后做告警和 dashboard,一步一步来。