Skip to content
采样

采样

采样是分布式追踪里少数"纯工程取舍"的话题:追踪数据的价值在排障那一刻兑现,而成本在每一秒都在发生。全量采集一条 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 到底能影响谁

把三层关系捋直:

  1. 这个策略只影响支持 Jaeger remote sampling 协议、并且真的配置了指向本策略服务的 SDK。官方 troubleshooting 页原话:“Some, but not all, OpenTelemetry SDKs support remote sampling, often via extensions”——支持与否、经哪个扩展支持,以你所用 SDK 生态为准(Go/Java 生态有相应的 remote sampler 实现,多数 SDK 的出厂默认并不启用远端策略)。
  2. 对没启用远端策略的 SDK,它返回 0.001 还是 1,对上报流量零影响——SDK 用的是自己本地的 OTEL_TRACES_SAMPLER。
  3. 对已经到达 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_store

always_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 保留错误全留时,错误率曲线会人为升高(分母抽稀、分子全留)。两个实操结论:

  1. 用 RED 指标做告警/容量时,要么按有效采样率还原分母,要么把 tail 的口径写进仪表盘说明;
  2. 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”),顺序照抄官方即可:

  1. 调试期先全采样。官方建议:development/低流量场景就采所有 trace。SDK 默认 parentbased_always_on 本就是 100%——如果查不到,多半有人改过;用环境变量显式钉死 OTEL_TRACES_SAMPLER=parentbased_always_on 再试。

  2. 确认 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 指错了文件。

  3. 用 stdout exporter 验证"应用确实产生了 span"。OTel SDK 支持的 stdout exporter 会把记录的 span 原样打印(官方 troubleshooting 页点名此法),它打不出来,问题在埋点;打得出来而后端没有,问题在传输/接收。

  4. Bypass intermediate collectors:中间还挂了自己的 OTel Collector/agent 的,先让 SDK 直连 Jaeger 缩小问题域。

  5. 查网络与端口绑定:容器里监听写 localhost 不写 0.0.0.0 是经典事故(troubleshooting 页专门点了这条);端口没 expose 同理。

  6. 看后端计数器: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_sampling file 模式集中定率(核心服务高、噪音接口归零),策略文件进 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)。
  • 通用纪律:
    1. 任何采样架构都要留一条"调试逃生门"——一个知道 traceID 就能强制捞全量的手段(调高采样率、重放请求、或临时全采的 tail always_sample 配置),别等到故障时现搭。
    2. 采样决定要写进团队文档:有效采样率是多少、从哪个环节起生效(SDK/远端策略/tail),以及它对 SPM 指标口径的影响(对照 07-查询UI与API.md 的命名与基数小节读)。
    3. 概率采样对低频错误不友好是数学事实,不是配置能修的——要"低频错误也不漏"就必须上按内容决策的 tail,接受它的带宽与内存价。
    4. v2 里除 remote sampling 策略文件外没有任何热加载,把"调采样率"设计成可热更的(file + ConfigMap),能省掉大量半夜重启(10-常见陷阱与最佳实践.md 有汇总)。