采样
采样是分布式追踪里少数"纯工程取舍"的话题:追踪数据的价值在排障那一刻兑现,而成本在每一秒都在发生。全量采集一条 trace 的 span 往往只要几 KB,乘上业务的 QPS 和留存天数就是一笔谁也签不下字的账单;采样率压得太狠,故障现场又什么都查不到。Jaeger 官方对这件事的定义很朴素(sampling 页):采样是为了控制"产生 trace 的应用的开销"和"trace 数据的存储与处理成本",理想状态下丢掉不 Interesting 的数据、保留诊断问题需要的数据。
这一章要建立的空间心智只有一句话:采样是一个"在哪一层、拿什么信息、做决定"的问题。决定做得越早,能利用的信息越少,但省的钱越多;决定做得越晚,判断越准,但被丢弃的数据已经付过一遍传输和处理成本。Jaeger v2 把这三层都配齐了:SDK 侧的 head 采样(OTel SDK 内建)、中央策略下发(remote_sampling 扩展)、后端的事后筛选(tail_sampling 处理器),外加一个介于两者之间的自适应闭环(adaptive sampling)。搞不清"谁在哪里丢的数据",就既省钱省不明白,也排障排不明白。
先画一张光谱:谁在哪一层做决定
先把最容易混淆的一个概念钉住:remote_sampling 不是决策点,它是"策略的分发器"。它自己不丢任何 span——它把"该用什么概率/速率采样"这条策略,回答给前来拉取策略的 SDK;真正执行丢数据动作的仍然是 SDK 里的 head sampler。这个定位差异后面还会反复用到,先放这儿。
把 v2 世界里与"丢不丢一条 trace"相关的参与者排成一条光谱:
| 决策层 | 何时决定 | 决定时能看到什么 | 数据丢在哪里 | 成本特征 |
|---|---|---|---|---|
| SDK head 采样 | trace 开始、span 创建的那一刻 | 只有 traceID(及其哈希)和父 span 的采样标志 | 应用进程内,未记录、未导出 | 最省:下游全链路按比例一起减 |
| 边缘 collector head 采样 | span 到达中转 collector 时 | 单个 trace 的碎片(到达顺序不可控) | 中转层出口 | 省掉后端存储,但应用和传输已花全量成本 |
| 后端 tail 采样 | trace 全部 span 到齐,或超过 decision_wait 窗口 |
接近完整的 trace(时长、状态码、属性都有) | 存储写入之前 | 最贵:全量数据先收进来再丢 |
| remote_sampling 策略服务 | 不丢数据,只下发策略 | 什么都不看(file 模式读静态文件,adaptive 模式看流量统计) | —— | 近乎为零(SDK 定期 HTTP/gRPC 拉取) |
两个"决定时机"带来的本质差别,值得用具体事故场景来体会:
- head 采样看不见"这条 trace 后来出错了没有"。概率 1% 的意思是:错误 trace 也只留 1%。深夜告警时想找出那百分之一的失败链路,多半查无此 trace——这是 head 采样最经典的痛点,也是 tail 存在的理由。
- tail 采样要付"信息差"的钱。为了看到完整 trace 再做决定,全量 span 必须先到 collector;带宽、接收、内存都按全量支付,最后只有被选中的部分落盘。官方 sampling 页对此写得非常直白:tail 的代价(a)是应用必须记录并导出所有 trace 的运行时开销,(b)是 Jaeger 后端更高的内存与处理成本。
工程上三层经常叠着用:SDK 用 parentbased 的确定性比例采样保住链路一致,策略统一由 remote_sampling 下发,重要的错误/慢流量在后端由 tail 兜底捞回。每往光谱右边挪一层,就多花一层成本、多留一层精准。
SDK 侧 head 采样:先看默认值,再谈采样率
Jaeger 官方对 head 采样的表述只有一句,但信息量很大:head based sampling 的支持内建在 OpenTelemetry SDK 里(sampling 页原文),Jaeger v2 自己不重复造轮子——这符合 v2 “OTel Collector 定制发行版"的整体定位(见 02-v2架构与组件.md)。所以这一节的内容本质是 OTel SDK 的通用知识,只是排障时要在 Jaeger 语境下用。
默认值:parentbased_always_on
官方 troubleshooting 页明确写着:OpenTelemetry SDK 默认使用 parentbased_always_on 采样器,等效于 100% 采样。它做两件事:
- 没有父上下文(自己是链路入口):总是采样;
- 有父上下文(被上游调用):跟随父 span 的采样决定。
“跟随父"是 distributed tracing 的采样正确性基石:一条 trace 的所有进程必须做出同一个决定,否则 UI 里会出现半棵树。这个规则由 SDK 自动处理——上游在 W3C traceparent 的 flags 里带上 sampled 位,下游照办即可,不需要任何配置。
换句话说,一个什么都不配、跑在默认值上的 OTel SDK,是全量上报的。如果后端只收到一小部分 trace,在 SDK→collector 这一段的原因通常是两种:有人显式改了采样器,或远端策略接管;数据明明到齐却被筛掉(tail 策略)则是另一回事,见下文。
常用采样器与环境变量
OTel SDK 统一用环境变量控制采样器(troubleshooting 页点名了 OTEL_TRACES_SAMPLER,“typically, the sampling strategy can be set via environment variables”)。常用取值(这是 OTel 各语言 SDK 的通用约定):
# 全采(默认)
OTEL_TRACES_SAMPLER=parentbased_always_on
# 入口按 5% 概率采,下游跟随
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.05
# 全不采(灾难演练/极端降级时用)
OTEL_TRACES_SAMPLER=always_off选概率采样器时优先用 parentbased_traceidratio 而不是裸 traceidratio,理由就是上一节的一致性:traceidratio 对无父上下文的新 trace 按 traceID 哈希做确定性决定(同一个 traceID 在哪个进程摇签都是同一结果),parentbased_* 在这个基础上再加"有父跟父”。两者搭配起来,采样决定在整个调用图上天然自洽,不存在"上游采了下游没采”。Go SDK 的代码级配置方式见 05-Go客户端埋点.md。
概率采样的盲区
概率采样对"错误信息"是盲目的,前面已经点过,这里补一个量化的直觉:假设错误率 0.5%、采样率 1%,那么平均每 200 条错误链路才能留下 1 条;一次只影响某个新发布服务、错误率 10% 但 QPS 很低的故障,留下的样本可能为零。越是低流量、越需要留证据的服务,越不能用一刀切的比例。补救的三条路——按服务下发不同策略(remote_sampling)、按流量自动算率(adaptive)、按内容事后捞回(tail)——正是后面三节。
remote_sampling:把策略收拢到一处下发
官方定义(sampling 页):remote sampling 是 head 采样的一种形态,SDK 从 Jaeger 后端拉取采样策略配置,价值在于"把采样策略集中到一处管理,简化大规模生产系统里 SDK 的治理"。v1 时代的独立采样服务在 v2 里对应 remote_sampling 扩展(extension),声明在 service.extensions 里;它同时暴露两种协议端点:
| 端点 | 默认地址(all-in-one) | 协议与路径 |
|---|---|---|
| HTTP | localhost:5778 |
GET /sampling?service=<名>,sampling.proto 的 Protobuf-to-JSON 映射 |
| gRPC | localhost:5779 |
jaeger.api_v2.SamplingManager |
| Legacy HTTP | localhost:14268 |
同一个 Thrift-over-HTTP 端口也提供 GET /api/sampling?service=<名> |
注意:all-in-one 出厂把 5778/5779 绑在
localhost(${env:JAEGER_LISTEN_HOST:-localhost})。SDK 在别的机器/容器里拉不到策略,十有八九是这个原因,而不是配置本身——要么设JAEGER_LISTEN_HOST=0.0.0.0,要么显式改 extension 的 endpoint。troubleshooting 页"Network connectivity"一节讲的就是这类绑定点名。
策略来源二选一:**file(静态策略文件)**或 adaptive(按流量动态计算)。
file 策略:一份 JSON 描述全公司的采样率
remote_sampling:
file:
path: ./cmd/jaeger/sampling-strategies.json策略 JSON 的结构延续 v1 格式(官方 sampling 页完整示例,逐字核对):
{
"service_strategies": [
{
"service": "foo",
"type": "probabilistic",
"param": 0.8,
"operation_strategies": [
{ "operation": "op1", "type": "probabilistic", "param": 0.2 },
{ "operation": "op2", "type": "probabilistic", "param": 0.4 }
]
},
{
"service": "bar",
"type": "ratelimiting",
"param": 5
}
],
"default_strategy": {
"type": "probabilistic",
"param": 0.5,
"operation_strategies": [
{ "operation": "/health", "type": "probabilistic", "param": 0.0 },
{ "operation": "/metrics", "type": "probabilistic", "param": 0.0 }
]
}
}读法(全部是官方页面写明的语义):
service_strategies按服务名匹配:foo整体按 0.8 概率采样,但其中op1、op2两个操作分别按 0.2、0.4;bar用ratelimiting,参数 5 表示恒定每秒最多采 5 条 trace——底层是漏桶(leaky bucket)限速器;probabilistic则按配置概率随机决定,0.1 即约十分之一;default_strategy是未在服务列表里出现的其他所有服务的兜底(示例里是 0.5);default_strategy里也可以带operation_strategies:这些"共享的按操作策略"对所有未列出的服务生效,对foo、bar也生效——除非它们自己定义了同名操作的策略。示例把/health、/metrics的概率设为 0,等于全公司关掉健康检查接口的追踪,这是最典型的用法;- 官方原文的括号注记要记住:
ratelimiting不支持operation_strategies级别,只能整服务配。想"某个接口单独限流"只能走 probabilistic 或 tail 侧的rate_limiting策略。
热重载:全手册唯一的例外
注意:Jaeger v2 的主配置 YAML 没有整体热加载,改任何 receiver/processor/exporter 配置都要重启进程(v1 那套
COLLECTOR_*/QUERY_*环境变量旗标在 v2 已全部失效,见02-v2架构与组件.md)。唯一的例外就是这里:官方 sampling 页原文——“The file will be automatically reloaded if its contents change”。all-in-one 出厂配置里给 file 来源显式写了reload_interval: 1s:
remote_sampling:
file:
path:
default_sampling_probability: 1
reload_interval: 1s
http:
endpoint: "${env:JAEGER_LISTEN_HOST:-localhost}:5778"
grpc:
endpoint: "${env:JAEGER_LISTEN_HOST:-localhost}:5779"实际运维里这条通道很好用:K8s ConfigMap 挂进容器、kubectl edit configmap 更新,策略在一秒级(取决于 reload_interval)到达 SDK,不用重启任何 Jaeger 进程。前提是 SDK 端本来就在定期拉策略(拉取间隔由 SDK 的 remote sampler 配置决定)。
两个"默认值",别混
关于"不配策略会发生什么",官方文档里有两个层面的默认,方向完全不同,这是本章最容易写反因果的地方:
| 场景 | 事实来源 | 默认行为 |
|---|---|---|
remote_sampling 启用、但没给任何策略来源 |
sampling 页原文:“If no configuration is provided, Jaeger will return the default probabilistic sampling policy with probability 0.001 (0.1%) for all services” | 策略服务对所有服务返回 probabilistic 0.001(千分之一) |
| 跑官方 all-in-one 配置 | v2.22.0 仓库 cmd/jaeger/internal/all-in-one.yaml |
file 来源 path 为空 + default_sampling_probability: 1,即全采(开发场景该留证据) |
troubleshooting 页那个 curl 示例的响应体正好是第一种:
curl -s "http://localhost:14268/api/sampling?service=foobar"
# {"strategyType":"PROBABILISTIC","probabilisticSampling":{"samplingRate":0.001}}看到 0.001,读法是"服务端此刻对外宣告的策略是千分之一(说明没人给它配过显式策略)",而不是"Jaeger 正在把数据丢成千分之一"。
因果边界:0.001 到底能影响谁
把三层关系捋直:
- 这个策略只影响支持 Jaeger remote sampling 协议、并且真的配置了指向本策略服务的 SDK。官方 troubleshooting 页原话:“Some, but not all, OpenTelemetry SDKs support remote sampling, often via extensions”——支持与否、经哪个扩展支持,以你所用 SDK 生态为准(Go/Java 生态有相应的 remote sampler 实现,多数 SDK 的出厂默认并不启用远端策略)。
- 对没启用远端策略的 SDK,它返回 0.001 还是 1,对上报流量零影响——SDK 用的是自己本地的
OTEL_TRACES_SAMPLER。 - 对已经到达 collector 的 span,策略服务更管不着:collector 的接收与处理流水线不做 head 采样(想按内容丢只能上 tail_sampling 处理器)。
所以"客户端明明全采、怎么 trace 还是没了"这种问题,第一步永远是查 SDK 本地采样器,第二步查"是不是某个服务悄悄接了远端策略、而策略服务没配置默认回了千分之一"。反过来"改了策略文件怎么没生效",先确认 SDK 端本来就在用它。
补充一个容易忽略的同款能力:标准 OpenTelemetry Collector 本身也说 Jaeger Remote Sampling 协议(architecture 页原文)——既可以静态文件方式直接供策略,也可以把请求代理给 Jaeger 后端(比如 Jaeger 侧跑 adaptive 的情况)。在大集群里用边缘 collector 代理集中策略、而不是让几千个 SDK 直捣 Jaeger,是常见拓扑。
tail 采样:trace 到齐之后再做决定
tail 采样的思想是"让数据先飞一会儿":collector 把收到的 span 按 trace 攒起来,等一个 decision_wait 窗口,用完整 trace 的信息(总时长、有没有错误、命中了什么属性)决定去留。v2 的实现方式是 OTel Collector Contrib 的 tailsamplingprocessor(组件名沿用官方示例里的实例名 tail_sampling),Jaeger 二进制已注册该组件,官方 sampling 页直接链接它并给了两份现成配置。
最小可用:先全采、开 debug 观察
排障阶段最常见的需求是"我要亲眼看看到达了什么",官方给的起步配置就是 always_sample(config-tail-sampling-always-sample.yaml,pipeline 骨架逐字摘自仓库):
service:
extensions: [jaeger_storage, jaeger_query, healthcheckv2]
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling]
exporters: [jaeger_storage_exporter]
processors:
tail_sampling:
decision_wait: 5s
policies: [{ name: test-policy-1, type: always_sample }]
exporters:
jaeger_storage_exporter:
trace_storage: some_storealways_sample 全留,配合 logs: level: debug,用来对照"进来到达的 trace"和"最终落库的 trace"。确认链路通后,再换筛选策略。
按服务筛:官方 service-name 策略示例
config-tail-sampling-service-name-policy.yaml 的处理器部分逐字如下——用 string_attribute 匹配 service.name 属性,只保留指定两个服务的 trace:
processors:
tail_sampling:
decision_wait: 5s
policies:
[
{
name: filter-by-attribute,
type: string_attribute,
string_attribute:
{ key: service.name, values: [tracegen-00, tracegen-03] },
},
]这里 service.name 是 resource 属性,string_attribute 策略匹配它等价于"按服务名留流量"。
策略清单(速查)
以下是 tailsamplingprocessor README(contrib 主分支,2026-10 核对)给出的全部策略类型与关键参数。每个策略对一条 trace 投出"sample / not sample / drop"三种票之一,汇总规则:任一策略投 sample 即采样;任一投 drop 即丢弃;其余情况一律不采样:
| 策略类型 | 判断依据 | 关键参数 |
|---|---|---|
always_sample |
无条件采 | —— |
probabilistic |
按 traceID 哈希决定 | sampling_percentage(另有 hash_salt) |
latency |
trace 时长区间 | threshold_ms、upper_threshold_ms |
status_code |
span 状态码集合 | status_codes: [ERROR, UNSET] |
string_attribute |
字符串属性等值/正则 | key、values、enabled_regex_matching、invert_match |
numeric_attribute |
数值属性区间 | key、min_value、max_value |
boolean_attribute |
布尔属性 | key、value |
span_count |
trace 的 span 总数区间 | min_spans、max_spans |
trace_state |
W3C tracestate 键值 | key、values |
rate_limiting |
恒定速率采样 | spans_per_second、burst_capacity |
bytes_limiting |
恒定字节速率 | bytes_per_second、burst_capacity |
ottl_condition |
OTTL 表达式(面向 span / spanevent 各写一组条件语句) | span: [...]、spanevent: [...]、error_mode |
and |
子策略全部 sample 才 sample | and_sub_policy |
not |
对单个子策略取反 | not_sub_policy |
drop |
子策略命中即显式丢弃(一票否决) | drop_sub_policy |
composite |
多子策略按配额分配总速率 | max_total_spans_per_second、policy_order、rate_allocation |
日常最值钱的是这四种组合拳:status_code(错误全留)、latency(慢请求全留)、probabilistic/rate_limiting(正常流量抽稀)、drop(健康检查之类的噪音显式踢掉)。ottl_condition 最灵活——能用 OTTL 表达式直接写 attributes["http.status_code"] >= 500 这类条件,代价是每 span 求值的开销。
一个典型的生产策略形态(按上面的清单组合出来的示意,非官方文件):
processors:
tail_sampling:
decision_wait: 10s
num_traces: 200000
policies:
[
# 显式丢弃健康检查(示例:路径匹配)
{ name: drop-health, type: drop,
drop: { drop_sub_policy: [
{ name: dh, type: string_attribute,
string_attribute: { key: http.route, values: ["/healthz", "/metrics"] } } ] } },
# 错误全留
{ name: keep-errors, type: status_code,
status_code: { status_codes: [ERROR] } },
# 慢请求全留:2s 以上
{ name: keep-slow, type: latency,
latency: { threshold_ms: 2000 } },
# 其余流量按 1% 抽稀
{ name: baseline, type: probabilistic,
probabilistic: { sampling_percentage: 1 } },
]语义对照汇总规则读:先 drop 掉 /healthz、/metrics;剩下的里面,出错或超过 2 秒的一律留;都不沾的,按 traceID 确定性留 1%。
工程参数与内存账
decision_wait:攒 trace 的窗口。窗口越长,trace 到齐率越高、内存占用越大;官方示例都是 5s/10s 量级。另有decision_wait_after_root_received(从"收到根 span"起算窗口)与sampling_strategy: trace-complete | span-ingest两种评估模式(默认trace-complete等齐再决;span-ingest边进边决、对有状态策略不友好),新手不必碰,知道存在即可。num_traces:内存里同时保留的 trace 条数上限,环形缓冲。满了会把最老的挤出去——挤出去时尚未过窗的 trace 会被"还没来得及决定就丢了",对应指标otelcol_processor_tail_sampling_sampling_trace_dropped_too_early。README 的 FAQ 说得很直白:这个计数器持续上涨基本就是负载问题,加大num_traces或调小decision_wait(前者费内存、后者丢完整度)换。expected_new_traces_per_sec用于预分配。- 迟到 span:决定做出后才到的 span,若 trace 的结论还在内存缓存里则继承原决定;缓存过期后(默认无决策缓存)会被当成新 trace 重新攒窗、重新决策。配
decision_cache.sampled_cache_size/non_sampled_cache_size可以让"已采样"决定记住更久,避免同一条 trace 被切成两半、两半各自重新决定。观测迟到程度的指标是sampling_late_span_age。 - 采样观测:
global_count_traces_sampled(实际采样比例 = 比值)、count_traces_sampled{policy=...}(各策略投票频次,用来验证"错误全留"这类策略是否真的在工作)、sampling_decision_timer_latency(决策定时器延迟,超过 1 秒就有丢 trace 风险,所以下游 pipeline 要快)。
扩容约束:同一条 trace 必须进同一个实例
tailsamplingprocessor 的决策依赖"全量 span 聚齐",因此多实例横向扩展时,同一 trace 的所有 span 必须路由到同一个实例,否则每个实例都只看见半条 trace。README 给出的标准解法是两级 collector:第一级用 loadbalancing exporter 按 traceID 路由,第二级跑 tail 采样。用 Kafka 做中间缓冲时同理——分区键必须是 traceID。这一层的部署细节见 03-部署与Kubernetes.md,Kafka 链路见 04-存储后端.md。
什么时候值得用 tail
把账摊开:tail 的受益是"错误/慢/关键流量一条不漏",代价是"SDK 必须全采全报 + collector 全量接收 + 窗口内全量驻留内存"。所以它适合的场景很典型:
- 错误与慢请求的取证是刚需(SRE 团队排障优先于成本);
- 业务量级在"全量到 collector"可承受的范围内(内存账按
num_traces × 平均 trace 大小估算,先按 QPS 粗算); - 拓扑允许两级 collector 或 Kafka 分区(解决同 trace 同实例)。
量级再大、或者对"全量传输"本身就受不了时,往下看 adaptive——它的哲学是"不追求留下所有错误 trace,但让留下的样本总量恒定可控"。
adaptive sampling:闭环自动算率
官方 sampling 页对 adaptive 的定义:remote_sampling 扩展观察收到的 span 流量,为每个"服务 × 操作"组合重算采样概率,使采集到的 trace 总量向 target_samples_per_second 收敛;新出现的服务/操作先用 initial_sampling_probability 兜住,攒够数据再换成算出来的率。
它的思想可以概括为反馈环:策略服务自己不丢数据,但它数得清"每个服务每秒到了多少 trace"。流量翻倍 → 下发概率减半;流量腰斩 → 概率翻倍。结果是你花一份固定的预算(比如全公司每秒 10 条 trace),买到了各服务均匀的、随流量自动稀释的样本。它保留的是"统计意义上的样本",不承诺"错误全留"——这点和 tail 是本质分野。
启用要三处配合(官方 cmd/jaeger/config.yaml 实测结构,注释逐字):
service:
extensions: [jaeger_storage, jaeger_query, remote_sampling]
pipelines:
traces:
receivers: [otlp]
processors: [batch, adaptive_sampling] # 处理器必须加
exporters: [jaeger_storage_exporter]
extensions:
remote_sampling:
adaptive:
sampling_store: some_store # 流量观测数据存哪
initial_sampling_probability: 0.1 # 新服务/操作的起步概率
# target_samples_per_second: 1.0 # 收敛目标(官方 sampling 页命名)
http:
grpc:
processors:
batch:
# Adaptive Sampling Processor is required to support adaptive sampling.
# It expects remote_sampling extension with `adaptive:` config to be enabled.
adaptive_sampling:sampling_store 的支持列表(官方 sampling 页原文):memory(全合一)、cassandra、badger、elasticsearch、opensearch。选型常识:多副本策略服务共享同一 store(否则各算各的,下发互相打架);生产上一般跟随主存储走 ES/OpenSearch/Cassandra,badger 适合单机自建。
与 tail 的取舍一句话:adaptive 是"总量的自动旋钮",tail 是"内容的精准筛子"。前者成本只到策略服务(看的是到达流量),后者要为决策付全量传输与内存。两套都嫌重的时候,回到最朴素的 remote_sampling file 模式手工分层定率。
采样率与成本:会算这笔账
不引任何单价,只给可复用的估算方法。三个量先定义清楚:Q(入口 QPS)、S(平均每条 trace 的 span 数)、r(有效采样率)。
每日落盘 span 数 ≈ Q × S × 86400 × r
每日存储量 ≈ 每日落盘 span 数 × 单 span 平均字节数(压缩前)单 span 字节数不要拍脑袋:它随标签/日志数量变化很大,几百字节到 1KB+ 都有可能。最准的测法是用已有集群反推——ES 后端看 jaeger-span* 索引的 size 除以文档数(_cat/indices、_count),badger/cassandra 看总盘量除以估算 span 数。
有了这三个量,两种典型用法:
- 正算成本:业务方问"1% 采样要多少盘"——Q=500、S=20、r=0.01、单 span 取 0.5KB,日增约 0.4GB(
500×20×86400×0.01×0.5KB)。换 100% 就是 40GB/日,数字一铺开,谈判立刻清楚。 - 反算采样率:预算给定存储上限,
r = 预算日增字节 / (Q × S × 86400 × 单span字节)。
注意这套账对 tail 要改算法:tail 的 SDK/传输/接收侧按 r=1 支付,只有存储侧按决策后的比例省——所以 tail 省的是存储与查询的钱,不省带宽和 collector 的钱。而 head 采样(SDK 侧)省的才是全链路。这就是"在哪一层决定"在成本维度的镜像。
组合套路:不是单选,是分层
成熟组合通常长这样(按团队规模从小到大演进):
小团队起步:SDK parentbased_traceidratio 一个值打天下,10%~100% 按预算。什么都不用配服务端。
中等规模,服务数上来后:加 remote_sampling file 模式,核心服务高采样、边角服务低采样、健康检查全关;策略文件挂 ConfigMap 享受唯一的热重载通道。仍接受"错误样本稀少"。
要错误/慢请求全取证:上 tail——前提是 SDK 侧改回全采或高采样(tail 的窗口只能看见到达的 span,SDK 已经丢掉的那部分 tail 救不回来)。链路量大时两级 collector + loadbalancing exporter 或 Kafka 按 traceID 分区(部署见 03-部署与Kubernetes.md,Kafka 链路见 04-存储后端.md)。
预算恒定、不想人肉调率:adaptive 替换 file,自动向 target_samples_per_second 收敛。
还有一条贯穿始终的纪律——指标口径要跟着采样走。这是最反直觉的一点:SPM 的 RED 指标(Monitor 页)看起来"没有采样",但它是由实际到达的 span 聚合出来的(prometheus 路线由 spanmetrics connector 在 pipeline 内算、ES 直查路线由查询侧从存储里的 span 现算——见 07-查询UI与API.md)。SDK 侧 1% 概率采样,calls_total 就只剩真实调用量的约 1%;tail 保留错误全留时,错误率曲线会人为升高(分母抽稀、分子全留)。两个实操结论:
- 用 RED 指标做告警/容量时,要么按有效采样率还原分母,要么把 tail 的口径写进仪表盘说明;
- tail 管线里 spanmetrics connector 挂在哪个阶段决定它统计到的是谁——官方
config-spm.yaml把它挂在 traces pipeline 的exporters里,而 processor 在所有 exporter 之前执行:若同一条管线里 processors 含 tail_sampling,指标就只统计被 tail 保留的 trace。想要全量口径的指标,就另开一条 processors 不含 tail_sampling 的 traces 管线(OTel Collector 允许一个 receiver 挂多条管线),把 connector 挂在那条上——这个取舍要在设计时想明白。
排障:查不到 trace,按官方顺序先过采样关
troubleshooting 页把"Verify the sampling strategy"列为第一条(“Before everything else”),顺序照抄官方即可:
-
调试期先全采样。官方建议:development/低流量场景就采所有 trace。SDK 默认
parentbased_always_on本就是 100%——如果查不到,多半有人改过;用环境变量显式钉死OTEL_TRACES_SAMPLER=parentbased_always_on再试。 -
确认 SDK 是否在用远端策略、拉到的策略是什么。SDK 侧检查 remote sampler 的地址可达(就是本章 5778/5779/14268 三个端点之一);服务端直接 curl:
curl -s "http://localhost:14268/api/sampling?service=checkout" # 或(remote_sampling 扩展的 HTTP 端点) curl -s "http://localhost:5778/sampling?service=checkout"响应里
samplingRate是服务端宣告的策略。读到0.001而策略文件里明明写了别的值——十有八九是服务没起来、没配 file/adaptive、或者改的path指错了文件。 -
用 stdout exporter 验证"应用确实产生了 span"。OTel SDK 支持的 stdout exporter 会把记录的 span 原样打印(官方 troubleshooting 页点名此法),它打不出来,问题在埋点;打得出来而后端没有,问题在传输/接收。
-
Bypass intermediate collectors:中间还挂了自己的 OTel Collector/agent 的,先让 SDK 直连 Jaeger 缩小问题域。
-
查网络与端口绑定:容器里监听写
localhost不写0.0.0.0是经典事故(troubleshooting 页专门点了这条);端口没 expose 同理。 -
看后端计数器:
curl -s http://<collector>:8888/metrics里otelcol_receiver_accepted_spans在涨说明收到,otelcol_exporter_sent_spans同步在涨说明写存储成功——两个涨速对不上,问题在 Jaeger 内部;都不涨,问题在上游。
如果 trace 能查到但"少了一截/断链",那通常不是采样问题,回头查传播(见 01-核心概念.md 与 05-Go客户端埋点.md)。
症状速查
| 症状 | 最可能的原因 | 第一动作 |
|---|---|---|
| 生产的 trace 突然只剩一小部分 | SDK 采样器被改/被远端策略接管 | 显式钉 OTEL_TRACES_SAMPLER,再 curl 策略端点看下发了什么 |
策略端点回 samplingRate 0.001 |
remote_sampling 没配 file/adaptive 来源 |
补 file.path 或 adaptive,别把"默认千分之一"当故障 |
| 改了策略文件不生效 | SDK 根本没用远端采样器;path 指错;5778 绑在 localhost 拉不通 |
先确认 SDK 侧接线,再查文件路径与监听地址 |
| tail 说好的"错误全留"却查不到错误 trace | SDK head 采样把错误丢在了前面,tail 看不见 | head 改回全采/高采样(tail 的前提是数据到齐) |
| 同一条 trace 被 tail 切成两段、两半去留不一致 | 迟到 span 掉出决策缓存 | 配 decision_cache.sampled_cache_size |
tail collector 内存高、dropped_too_early 上涨 |
num_traces 或 decision_wait 与流量不匹配 |
调大 num_traces 或缩窗,同时看 trace_removal_age 分位 |
| 多副本 tail 采样率忽高忽低 | 同一 trace 的 span 散落到不同实例 | 前置按 traceID 路由(loadbalancing exporter / Kafka 分区键) |
实战建议
按规模给架构结论,其余写成通用纪律:
- 小规模/起步:只做 SDK head 采样,
parentbased_traceidratio+ 一个团队公认的比例;不引入任何服务端采样组件。采样率宁高勿低,先保证"出了事查得到"。 - 中等规模/多服务:
remote_samplingfile 模式集中定率(核心服务高、噪音接口归零),策略文件进 ConfigMap 吃热重载;总量难拍时用 adaptive 替换 file,设target_samples_per_second。Go 客户端侧的配置方式见05-Go客户端埋点.md。 - 大规模/强取证需求:SDK 全采或高采样 → Kafka 削峰(
04-存储后端.md)→ 两级 collector,第一级按 traceID 路由、第二级 tail_sampling(错误/慢全留 + 基线抽稀 + drop 噪音)。上线前把sampling_trace_dropped_too_early加进告警(09-性能与运维.md)。 - 通用纪律:
- 任何采样架构都要留一条"调试逃生门"——一个知道 traceID 就能强制捞全量的手段(调高采样率、重放请求、或临时全采的 tail always_sample 配置),别等到故障时现搭。
- 采样决定要写进团队文档:有效采样率是多少、从哪个环节起生效(SDK/远端策略/tail),以及它对 SPM 指标口径的影响(对照
07-查询UI与API.md的命名与基数小节读)。 - 概率采样对低频错误不友好是数学事实,不是配置能修的——要"低频错误也不漏"就必须上按内容决策的 tail,接受它的带宽与内存价。
- v2 里除 remote sampling 策略文件外没有任何热加载,把"调采样率"设计成可热更的(file + ConfigMap),能省掉大量半夜重启(
10-常见陷阱与最佳实践.md有汇总)。