1. 前言
在 OpenTelemetry 的语境中,采样(Sampling)指的是决定是否收集并上报包含 Span 的追踪信息。
对于订单类等核心且QPS不高的服务,进行全量采样是可行的。然而,对于微博、抖音这类读多写少的内容型服务,全量采样的成本极高,既不现实也无必要。此时,除了按百分比采样外,一种更保守的策略是:仅对异常的请求(例如通过客户端埋点、指标告警或客服反馈识别)进行流量重放,并在此过程中采集完整的追踪信息。
为了实现上述策略,我们需要一个明确的标识来指示某个请求必须被采样。以HTTP请求为例,可以约定一个特殊的请求头。
X-Force-Trace: 1
2.1 上游服务实现
OpenTelemetry SDK 预留了 Sampler 接口,允许开发者自定义采样逻辑。
// Sampler decides whether a trace should be sampled and exported.
type Sampler interface {
// ShouldSample returns a SamplingResult based on a decision made from the passed parameters.
ShouldSample(parameters SamplingParameters) SamplingResult
// Description returns information describing the Sampler.
Description() string
}
以下是一个在 Gin 框架中实现的完整示例。完整代码可参考项目:vearne/otel-test。
主程序 (main.go):
func main() {
tp := InitTracerProvider()
defer func() { _ = tp.Shutdown(context.Background()) }()
r := gin.New()
r.Use(headerToCtxMiddleware("X-Force-Trace")) // ① 将Header值注入Context
r.Use(otelgin.Middleware("gin-header-sampler")) // ② 使用官方OpenTelemetry Gin中间件
r.GET("/ping", func(c *gin.Context) {
_, span := otel.Tracer("").Start(c.Request.Context(), "ping")
defer span.End()
c.String(200, "pong")
})
_ = r.Run(":8080")
}
追踪提供者初始化 (tracer.go):
func InitTracerProvider() *sdktrace.TracerProvider {
ctx := context.Background()
exporter, err := otlptracehttp.New(ctx)
if err != nil {
log.Fatalf("new otlp trace http exporter failed: %v", err)
}
tp := sdktrace.NewTracerProvider(
// 关键:使用自定义的基于Header的采样器
sdktrace.WithSampler(NewHeaderSampler("X-Force-Trace", "1")),
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(initResource()),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return tp
}
Gin中间件 (middleware.go): 用于将Header值传递到Context中。
func headerToCtxMiddleware(headerKey string) gin.HandlerFunc {
return func(c *gin.Context) {
v := c.GetHeader(headerKey)
// 将值写入Context,后续采样器可以读取
ctx := context.WithValue(c.Request.Context(), "http.header."+headerKey, v)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
自定义采样器 (sampler.go):
type headerSampler struct {
key, value string
}
// NewHeaderSampler 返回一个自定义的 trace.Sampler
func NewHeaderSampler(headerKey, headerValue string) trace.Sampler {
return &headerSampler{key: headerKey, value: headerValue}
}
func (s *headerSampler) ShouldSample(p trace.SamplingParameters) trace.SamplingResult {
// 从父级Context中读取Gin中间件预先设置的值
v := p.ParentContext.Value("http.header." + s.key)
if str, ok := v.(string); ok && strings.TrimSpace(str) == s.value {
return trace.SamplingResult{Decision: trace.RecordAndSample} // 记录并采样
}
return trace.SamplingResult{Decision: trace.Drop} // 丢弃
}
func (s *headerSampler) Description() string {
return "HeaderSampler"
}
值得注意的是,无论是否采样,Span信息都会随着 context.Context 向下传递,这是 云原生 可观测性中上下文传播的基础。
2.2 下游服务
在微服务调用链中,若服务A调用服务B,则A为上游服务,B为下游服务。上游服务通过W3C TraceContext规范将采样决策传递给下游服务。
traceparent Header的格式如下,其最后一个字节(flags)的最低位即为采样标志位:
traceparent: 00-6ef809a63797e155758fc91d5cec9cae-f85da44ea72a809d-01
Q: 如果上游服务没有传递TraceContext,下游服务如何行为?
A: 下游服务需要在配置中显式定义此场景下的行为。例如,可以配置为:当存在父级追踪上下文时遵从上游决策,否则永不采样。
func InitTracerProvider() *sdktrace.TracerProvider {
...
tp := sdktrace.NewTracerProvider(
// 使用ParentBased采样器:有父上下文则遵从,否则使用NeverSample
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.NeverSample())),
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(initResource()),
)
...
return tp
}
3. 总结
OpenTelemetry的设计非常完备,赋予了使用者高度的灵活性来实现个性化采样策略。开发者可以轻松地基于用户ID、入口IP、特定应用、调用链入口或关键业务标识等维度来动态开启采样,从而在观测成本与问题排查效率间取得最佳平衡。