Loki 链路可观测性基础:使用 Jaeger Remote Sampler 实现 OpenTelemetry 动态采样策略
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
导读
在 Loki 这类大规模分布式系统中,追踪(Tracing)数据的采集成本会随着服务规模线性增长,而采样(Sampling)正是控制链路数据量与存储开销的核心手段。本文基于 Loki 仓库中 vendor 目录携带的 jaegerremote 采样器文档,讲解如何在 OpenTelemetry Go SDK 中接入 Jaeger Remote Sampling 协议:由后端按「服务 + 接口」粒度下发采样策略、支持热更新与自适应采样,并深入源码解释其轮询、解析与内嵌采样器的实现原理。读完本文,你将能够独立完成 Jaeger Remote Sampler 的接入配置、采样服务端选型,以及常见问题的排查。
背景:为什么需要远程采样
在分布式追踪系统中,无差别地记录每一条 trace 会产生巨大的存储与网络开销。Loki 仓库将 OpenTelemetry 的jaegerremote包以 vendor 形式引入(见 go.mod 中的依赖声明与 vendor/go.opentelemetry.io/contrib/samplers/jaegerremote 目录),其核心价值在于:
- 集中管控:采样配置由后端统一维护,服务无需重新发布即可调整采样率;
- 细粒度控制:采样策略可精确到「服务 + 接口(endpoint/operation)」维度,而不是整个服务一刀切;
- 动态更新:配置支持热加载,修改后端配置后采样器会自动感知并生效。
该包实现的正是 Jaeger 官方的 Remote Sampling 协议。当后端为 Jaeger 时,采样配置可以来自两个数据源:
- 静态配置文件:支持热重载(hot-reload),修改文件即生效;
- 自适应采样(Adaptive Sampling):Jaeger 后端根据每个服务的目标 trace 数据量,自动计算期望的采样概率。
快速上手:在代码中接入 Jaeger Remote Sampler
基础配置示例
文档给出的最简用法是在代码中显式构造采样器并挂载到 TracerProvider 上:
import ( "time" "go.opentelemetry.io/contrib/samplers/jaegerremote" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/trace" ) jaegerRemoteSampler := jaegerremote.New( "your-service-name", jaegerremote.WithSamplingServerURL("http://{sampling_service_host_name}:5778/sampling"), jaegerremote.WithSamplingRefreshInterval(10*time.Second), jaegerremote.WithInitialSampler(trace.TraceIDRatioBased(0.5)), ) tp := trace.NewTracerProvider( trace.WithSampler(jaegerRemoteSampler), // ... 其他 TracerProvider 配置 ) otel.SetTracerProvider(tp)要点说明:
- 服务名必须传入构造函数:
New的第一个参数是服务名,采样器会用它去后端查询该服务的采样策略(对应源码中 New 函数 将serviceName存入采样器并启动后台轮询协程); - 初始采样器:
WithInitialSampler(trace.TraceIDRatioBased(0.5))指定在首次从远端拉到策略之前,本地先用 50% 的 TraceID 比例采样器兜底; - 刷新间隔:
WithSamplingRefreshInterval控制轮询后端策略的周期,示例中为 10 秒。
采样服务端(Sampling Server)的选择
采样策略由 HTTP 服务端对外提供,文档明确列出了三种可用实现:
| 提供方 | 默认端点 | 说明 |
|---|---|---|
| Jaeger Agent | http://{agent_host}:5778/sampling | 历史最悠久的方式,Agent 直接托管采样策略 |
| Jaeger Collector | http://collector_host:14268/api/sampling | 不运行 Agent 时的替代方案,端点路径略有不同 |
| OpenTelemetry Collector | http://{otel_collector_host}:5778/sampling | 通过配置jaegerremotesampling扩展提供采样端点 |
其中 OpenTelemetry Collector 的方案最有意思:运行 Jaeger Remote 采样协议并不需要真的部署 Jaeger,只要某个服务实现了协议端点即可;只有当你想利用 Jaeger 的自适应采样引擎(自动计算远程采样策略)时,才必须依赖 Jaeger 后端。
环境变量注意点
文档特别强调了一个限制:当前 Jaeger Remote Sampler 只能在代码中配置,尚不支持通过OTEL_TRACES_SAMPLER=jaeger_sampler环境变量直接启用。但从源码看,getEnvOptions 函数 已经解析了OTEL_TRACES_SAMPLER_ARG中的endpoint、pollingIntervalMs、initialSamplingRate三个键值对,可以推断该环境变量驱动的能力正处于逐步演进中,接入时应以代码配置为主。
端到端示例:用 OpenTelemetry Collector 托管采样策略
仓库内随包的 example 目录 演示了完整的「Collector 托管策略 + Go 程序动态采样」链路(README 已注明该目录存在,属于示例代码位置;由于 vendored 依赖目录可能不含完整示例文件,实践时可按官方示例结构自行组织)。整体流程如下:
- 启动采样服务端(OpenTelemetry Collector),通过 docker-compose 一键拉起:
docker-compose up -dCollector 使用 Jaeger receiver 托管策略文件(也就是jaegerremotesampling扩展的职责)。
- 验证策略可被拉取:
curl 'localhost:5778/sampling?service=foo' curl 'localhost:5778/sampling?service=myService'注意这里的 URL 形态{host}:5778/sampling?service={serviceName}与源码中 httpSamplingStrategyFetcher.Fetch 的实现完全一致:它把服务名放进查询参数service,发起一次 GET 请求,并对 4xx/5xx 状态码返回错误。
- 运行 Go 示例程序:
go run .程序以 50% 的初始采样率启动,随后不断从 Collector 拉取最新策略,并每 10 秒打印一次整个 Jaeger Remote Sampler 的内部结构,便于观察采样器随策略更新的动态变化。
源码剖析:Jaeger Remote Sampler 的工作原理
委派式采样器与后台轮询控制器
从源码结构看,jaegerremote包的核心是一个委派(delegating)采样器:Sampler 结构体 本身不直接做采样决策,而是内部持有一个sampler字段(真正的决策者),通过sync.RWMutex保护读写。New之后,pollController协程会按samplingRefreshInterval周期性地执行UpdateSampler():
- 启动时立即拉取一次策略(
s.UpdateSampler()); - 之后每次 tick 再拉取,同时监听
doneChan以响应Close()的优雅关闭。
UpdateSampler的完整链路是:HTTP 拉取(Fetcher)→ JSON 解析(Parser)→ 策略分发(Updaters)。其中解析器使用 gogo protobuf 的jsonpb.Unmarshal解析jaeger_api_v2.SamplingStrategyResponse,并且官方注释特别说明:新版 Remote Sampling 协议将枚举编码为字符串,旧版协议编码为数字,jsonpb两种格式都能解析(见 samplingStrategyParserImpl.Parse)。
三类策略更新器(Updaters)
解析出的策略对象会依次交给三个samplerUpdater(顺序见 newConfig):
- perOperationSamplerUpdater:处理
GetOperationSampling()(按接口细分的策略); - probabilisticSamplerUpdater:处理
GetProbabilisticSampling()(全局概率采样); - rateLimitingSamplerUpdater:处理
GetRateLimitingSampling()(每秒固定条数额度)。
某个 updater 处理成功并返回新的trace.Sampler后立即生效;如果都不匹配则报错unsupported sampling strategy(见 updateSamplerViaUpdaters)。这解释了远端策略的优先级关系:按接口的 per-operation 策略 > 概率采样 > 限流采样。
底层采样器的四种形态
对应三种更新器,sampler.go 中实现了四种采样器:
- probabilisticSampler:基于 SDK 的
trace.TraceIDRatioBased,采样率被钳制在[0.0, 1.0]区间; - rateLimitingSampler:每秒最多采样
maxTracesPerSecond条,其额度算法来自内部包 ratelimiter 实现的漏桶(leaky bucket)式信用余额模型:每次CheckCredit按流逝时间补充信用额度,余额足够则扣减放行,Update时按比例保留既有余额以避免策略切换瞬间的“惊群效应”; - guaranteedThroughputProbabilisticSampler:组合上述两者,限流器作为保底下限(例如
lowerBound = 1.0/(60*10)表示每 10 分钟至少采样一次),概率采样器优先级更高; - perOperationSampler:以 operation 名为 key 维护
map[string]*guaranteedThroughputProbabilisticSampler,最多跟踪MaxOperations(远端策略未指定时的包内默认值为 2000)个接口,超出部分回落到默认采样器。
这些采样器在决策通过后还会给 span 附加jaeger.sampler.type与jaeger.sampler.param两个属性,便于下游分析;可通过WithAttributesDisabled()关闭。
配置选项速查
文档示例用到的选项及源码中定义的全部选项整理如下(实现见 sampler_remote_options.go):
| Option | 作用 | 默认值 |
|---|---|---|
WithSamplingServerURL(url) | 设置采样策略服务地址 | http://127.0.0.1:5778/sampling(见 constants.go) |
WithSamplingRefreshInterval(d) | 轮询策略的刷新周期 | 1 分钟 |
WithInitialSampler(s) | 首次拉取前使用的兜底采样器 | 0.001(0.1%)概率采样 |
WithMaxOperations(n) | per-operation 模式跟踪的最大接口数 | 256 |
WithOperationNameLateBinding(b) | 是否允许在 span 创建后延迟绑定名称再定案采样 | true |
WithLogger(l) | 注入 logr 兼容的日志器 | 静默丢弃 |
WithSamplingStrategyFetcher(f) | 自定义策略拉取器(可加自定义 header、超时或改从文件读取) | HTTP fetcher |
WithAttributesDisabled() | 关闭jaeger.sampler.type/jaeger.sampler.param属性 | 关闭属性输出 |
两个与网络相关的内置默认值同样值得注意:HTTP 拉取超时为 10 秒(defaultRemoteSamplingTimeout),刷新间隔默认 1 分钟(defaultSamplingRefreshInterval),均定义在 sampler_remote.go。当前 vendored 版本的包版本号为0.37.3(见 version.go),说明这是经过依赖锁定、可直接复用的成熟实现。
实践建议与常见问题
1. 初始采样率如何设置?服务刚启动、远端策略尚未返回时,WithInitialSampler决定这段时间的采样行为。文档示例用 50%(TraceIDRatioBased(0.5)),生产环境建议与后端最终策略保持量级接近,避免冷启动窗口期采集量激增。
2. 三种采样服务的端口怎么选?若已部署 Jaeger Agent,直接用:5778/sampling;若只部署 Jaeger Collector,改用:14268/api/sampling;若基于 OpenTelemetry Collector 扩展,保持:5778/sampling并与 Jaeger receiver 配合即可。只要协议兼容,服务端形态对客户端是透明的。
3. 为什么改了后端策略没生效?先确认刷新间隔(默认 1 分钟)是否已过,再检查UpdateSampler拉取与解析是否报错——WithLogger注入日志器后,failed to fetch sampling strategy、failed to parse sampling strategy response等错误会直接可见(见 UpdateSampler 实现)。
4. 需要自定义鉴权或从文件加载策略?实现SamplingStrategyFetcher接口(仅一个Fetch(service string) ([]byte, error)方法),再通过WithSamplingStrategyFetcher注入即可,官方注释明确说明该接口就是为自定义 header、超时以及从文件等来源获取策略而设计的。
小结
Jaeger Remote Sampler 把「采样策略」从服务本地配置提升到了后端集中管控的层面:静态配置热更新、按服务+接口粒度下发、以及与 Jaeger 自适应采样引擎的对接,都通过一套简单的 HTTP 协议完成。在 Loki 仓库中,它作为 vendored 依赖为整个链路的追踪数据采集提供了可动态调整的采样底座;理解其「轮询拉取 + 策略解析 + 委派更新」的实现骨架,也能帮助你在实际接入 OpenTelemetry 时快速定位问题、合理设置各项选项。
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考