# 采样 · 头部还是尾部

> 追踪的成本随流量线性增长，所以必须采样。这一章讲清头部采样与尾部采样的根本取舍——前者便宜但会漏掉出问题的那条，后者能「只留有意思的」但代价高昂，以及为什么尾采样必须在 gateway 且必然有状态。

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-07-15
- 原文：https://daiw.org/manual/observability/sampling
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 采样 · 头部还是尾部

[第 0 部分讲成本模型时说过](https://daiw.org/manual/observability/three-pillars)：**追踪的数据量随流量线性增长**。10 万 QPS 全量采，一天几百亿个 span——**没人付得起**。

所以必须**采样**。而“怎么采”是追踪工程里**最重要的决策**。

## 两种时机，两个世界

<Callout type="info">
  **区别只有一个：**在哪个时刻决定“这条 trace 要不要留”**。**

  | | **头部采样（head）** | **尾部采样（tail）** |
  | --- | --- | --- |
  | **决定时刻** | **trace 一开始**（第一个 span） | **trace 结束后**（所有 span 都到齐） |
  | **依据** | 只有随机数 / trace_id | **整条 trace 的全貌**（慢不慢？错没错？） |
  | **成本** | **极低**（没被采的根本不产生） | **高**（所有 span 都要先收上来、缓存住） |
  | **能“只留有意思的”吗** | **❌ 不能** | **✅ 能** |
  | **在哪做** | SDK（应用里） | **[必须在 gateway](https://daiw.org/manual/observability/collector-config)** |
  | **有状态吗** | 无状态 | **有状态**（要缓存 span 等齐） |

  **这张表的每一行都是同一个根本矛盾的投影：你要在“**决定得早（便宜）**”和“**决定得对（信息全）**”之间选。**
</Callout>

## 头部采样：便宜，但会漏掉重要的

```
网关收到请求 → 掷骰子:1% 概率 → 决定"采"
             → 把决定写进 traceparent 的[采样位](https://daiw.org/manual/observability/traces-basics)
             → 下游所有服务照做(不采的连 span 都不产生)
```

<Callout type="warn">
  **头部采样最大的优点是**便宜**：没被采中的 trace，**span 压根不会被创建**——不占 CPU、不占内存、不占网络。这是“[零成本](https://daiw.org/manual/observability/three-pillars)”级别的便宜。**

  **但它有个致命缺陷：决定的时候，你还什么都不知道。**

  ```
  1% 采样率下:
  - 100 个正常请求 → 采到 1 个 → 挺好
  - 1 个超慢的请求(5 秒!)→ 99% 概率不采 → 你永远不知道它发生过
  ```

  **而“那个出问题的请求”恰恰是你唯一想看的。** 你付钱存下了 99 个“一切正常”的 trace，却漏掉了那个唯一有价值的。

  **这就是头部采样的根本问题：它对“有趣”和“无聊”一视同仁**——因为在决定的那一刻，**它还不知道这条 trace 会不会出问题**。这和“[未知的未知](https://daiw.org/manual/observability/why-observability)”是同一类困境：**你没法预先知道该留什么。**

  **缓解手段（都不完美）：**
  - **对错误请求提高采样率** —— 但**错误是在最后才知道的**，而决定在最开始。有些 SDK 支持“事后补救”（先记录、延迟决定），但那已经在往尾采样靠了。
  - **按路径分层采样** —— 重要接口 100%、健康检查 0.01%。**这是最实用的头部采样优化**，而且不需要任何额外基础设施。
</Callout>

## 尾部采样：能只留有意思的，但很贵

```
所有 span 都发到 Collector gateway
  → gateway 把同一 trace_id 的 span 缓存起来(等!)
  → 等到 trace "看起来完整了"(或超时)
  → 现在看全貌:有错误吗?超过 1s 吗?
  → 决定留还是丢
```

<Callout type="info">
  **尾部采样的价值是**你可以按结果决定**——这是质的飞跃：**

  ```yaml
  # tail_sampling processor 的典型策略
  policies:
    - name: errors                  # 所有错误的 trace,100% 留
      type: status_code
      status_code: { status_codes: [ERROR] }
    - name: slow                    # 所有慢的,100% 留
      type: latency
      latency: { threshold_ms: 1000 }
    - name: normal                  # 正常的,只留 1%
      type: probabilistic
      probabilistic: { sampling_percentage: 1 }
  ```

  **“错的全留、慢的全留、正常的采样”——这几乎就是你想要的一切。** 存储成本大降，而**信息价值几乎没损失**（那 99% 被丢掉的正常 trace，本来也没人看）。
</Callout>

<Callout type="warn">
  **代价极其现实，三条都很痛：**

  **① 所有 span 都必须先发出来** —— 尾采样是在 gateway 决定的，所以**应用必须先把 100% 的 span 发给它**。**你省的是“[后端存储](https://daiw.org/manual/observability/three-pillars)”，没省“[应用的埋点开销 + 网络传输](https://daiw.org/manual/observability/three-pillars)”。** 这和头部采样“压根不产生”的便宜完全不是一回事。

  **② 必须缓存等齐，于是有状态** —— gateway 要**把一条 trace 的所有 span 攒在内存里**，直到它“完整”。这带来：
  - **内存开销** —— 攒的是所有 trace（不只是最后会留下的那 1%）。
  - **`decision_wait` 的两难** —— 等多久算“完整”？**等太短会把慢 trace 拦腰截断**（讽刺的是：**慢 trace 恰恰是你最想留的，却最可能因为超时被截断**）；等太长内存吃不消。
  - **[有状态就不能随便扩副本](https://daiw.org/manual/observability/pdata)** —— **这是最大的坑**，见下。

  **③ 必须保证“同一 trace 的 span 落到同一个 gateway 实例”**

  这是尾采样部署时**最容易搞砸**的一点。如果 gateway 有 3 个副本、前面挂个普通负载均衡：
  ```
  span A(trace=abc)→ 副本 1
  span B(trace=abc)→ 副本 2     ← 灾难!
  span C(trace=abc)→ 副本 3
  ```
  **每个副本都只看到 trace 的一部分，谁也没法做出正确决定。** 结果是：要么各自瞎决定（trace 被撕碎），要么全都留（等于没采样）。

  **解法：必须按 `trace_id` 做一致性路由**——用 `loadbalancing` exporter（它按 trace_id 哈希路由到固定副本）组成**两层 gateway**：第一层只负责按 trace_id 路由，第二层做尾采样。

  **这就是[上一部分说“尾采样必须在 gateway”](https://daiw.org/manual/observability/collector-config)的完整含义**——不只是“要集中”，而是“要**按 trace_id 一致性地**集中”。**很多团队第一次上尾采样都栽在这里：配好了策略，发现 trace 全是碎的。**
</Callout>

## 怎么选

```mermaid
flowchart TD
    Q["要上采样"] --> A{"流量多大?"}
    A -->|"小(全量存得起)"| A1["✅ 不采样<br/>(最好的采样是不采样)"]
    A -->|"大"| B{"有 Collector gateway 吗?"}
    B -->|"没有"| B1["头部采样<br/>+ 按路径分层<br/>(重要接口高采样率)"]
    B -->|"有"| C{"愿意承担有状态的复杂度?"}
    C -->|"不愿意"| B1
    C -->|"愿意"| C1["✅ 尾部采样<br/>错的/慢的全留 + 正常的 1%<br/>(记得 loadbalancing 按 trace_id 路由!)"]
    style A1 fill:#22c55e,color:#fff
    style C1 fill:#22c55e,color:#fff
```

<Callout type="info">
  **几条实战经验：**

  - **最好的采样是不采样** —— 如果你的流量不大，**全量存**。别为了“最佳实践”给自己加复杂度。**采样是被成本逼出来的，不是美德。**
  - **头部采样 + 按路径分层，能解决 80% 的问题** —— 而且**零基础设施成本**。先做这个，不够了再上尾采样。
  - **上尾采样前，先确认你真的需要它** —— 它带来一整套[有状态服务的运维负担](https://daiw.org/manual/observability/sampling)（扩缩容、一致性路由、内存调优）。**如果你只是想“别漏掉错误的 trace”，很多 SDK 支持“错误请求提高采样率”，可能就够了。**
  - **[exemplar 是个被低估的中间道路](https://daiw.org/manual/observability/exemplars)** —— [下一章讲](https://daiw.org/manual/observability/exemplars)：它让你从**指标**曲线直接跳到**那一刻的某条真实 trace**，而不需要“存下所有 trace”。**用聚合的便宜，换到了个案的入口。**
</Callout>

## 小结与下一章

追踪[成本随流量线性增长](https://daiw.org/manual/observability/three-pillars)，必须采样。**根本矛盾：决定得早（便宜） vs 决定得对（信息全）。** **头部采样**（[trace 一开始就掷骰子，靠 `traceparent` 的采样位传播](https://daiw.org/manual/observability/traces-basics)）：**极便宜**（没采中的 span 压根不产生），但**致命缺陷是决定时什么都不知道**——[1% 采样下，那个 5 秒的慢请求 99% 会被漏掉，而它恰恰是你唯一想看的](https://daiw.org/manual/observability/why-observability)；最实用的优化是**按路径分层**（重要接口 100%、健康检查 0.01%），零基础设施成本。**尾部采样**（等 trace 齐了看全貌再决定）：能做到“**错的全留、慢的全留、正常的 1%**”——信息价值几乎无损。但三条代价：①[**所有 span 仍要先发出来**（省的是存储，不是埋点和网络）](https://daiw.org/manual/observability/three-pillars)；②[**必须缓存等齐 → 有状态**；`decision_wait` 两难（**等太短会截断慢 trace，而慢 trace 恰恰最该留**）](https://daiw.org/manual/observability/pdata)；③[**必须按 `trace_id` 一致性路由**（否则副本各看一半，trace 全碎）——要用 `loadbalancing` exporter 做两层 gateway](https://daiw.org/manual/observability/collector-config)，**很多团队第一次上尾采样都栽在这**。选型：[**最好的采样是不采样**（采样是被成本逼的，不是美德）→ 不够就头部+分层 → 再不够才上尾采样](https://daiw.org/manual/observability/sampling)。

有没有办法“用聚合的便宜，换个案的入口”？有——下一章讲那座最巧妙的桥。👉 [exemplar](https://daiw.org/manual/observability/exemplars)。

## 参考资料

- [1] OTel Collector contrib：`tailsamplingprocessor`、`loadbalancingexporter` 文档。
- [2] OpenTelemetry 官方文档：Sampling（head vs tail）。
- [3] 关联[三大支柱的成本模型](https://daiw.org/manual/observability/three-pillars)、[agent/gateway 部署](https://daiw.org/manual/observability/collector-config)、[上下文传播](https://daiw.org/manual/observability/traces-basics)。
