4. 采样策略:头采样、尾采样、速率限制

说到调用链的存储开销,我见过太多团队被吓到——每秒百万级的调用,每个请求几十个span,全量存?一个月硬盘就炸了。但你又不能完全丢数据,不然线上出了故障抓瞎。所以必须引入采样。

说白了,采样就是要在数据完整性和存储开销之间找个平衡点。今天聊三种主流策略:头采样、尾采样、速率限制。我自己在实际项目里都折腾过,各有各的坑。

头采样:先下手为强

头采样的逻辑很简单:请求进来的时候,立马决定是否采样。记录这个请求的开始上下文、Trace ID,后续的span都带上标记。如果没被采样,那后面的span直接扔掉。

优点很明显——延迟低。你想想,数据不需要等请求结束就能决定存不存,适合高吞吐场景。但问题也大:它提前把宝压在“头部”,如果请求中间变慢或报错,你根本没机会改变主意。

我在一个电商API网关项目里就用过头采样。线上有个问题:某些请求头采样没选上,结果这些请求超时了,我们根本查不到数据。后来我才意识到,头采样会漏掉尾部异常。

尾采样:等结果出来再说话

尾采样是反过来的:让请求先跑完,根据最终的结果(比如耗时、错误码)来决定要不要采样。如果请求返回了500或者延迟超过P99,那必须存。

这样能精准捕获异常,对诊断问题很有用。但代价呢?你得把整个请求期间的span缓存起来,等结果出来才能决定。这个缓存窗口如果设短了,请求还没结束就被清掉了,数据就丢了。我曾经踩过这个坑——

避坑提醒: 我曾经把尾采样的缓存窗口设成10秒,结果有个慢查询跑了15秒,中间所有span都被缓存淘汰掉了。上线后一脸懵,怎么这个慢请求中间是空的?后来改成30秒,内存压力又上来了。你需要根据业务最慢请求的耗时来调整。

尾采样适合对准确性要求高的场景,但内存开销大,不能无脑用。

速率限制:简单粗暴,但有效

速率限制不讲什么头尾,直接限定每秒最多采样多少个请求。比如1000 TPS,你只采100个,剩下的全丢。实现简单,资源开销稳定。

问题是,它完全不管请求的好坏。如果每秒1000个请求里全是异常,但你的采样率只有10%,那90%的异常就丢了。如果全是正常请求,你又浪费了采样额度。说白了,这只适合流量特别稳定、异常比例低的系统。我在压测环境里会用速率限制来兜底,防止采样系统本身被打爆。

三种策略对比

策略存储开销数据完整性延迟影响适用场景
头采样低(固定比例)差(漏尾部异常)低高吞吐、异常可提前感知
尾采样中高(需缓存)高(精准捕获异常)中(等待结果)对异常诊断要求高的场景
速率限制可控(固定上限)差(完全随机)低高并发、简单兜底

组合策略才是王道

别想着只用一种。我的习惯是做分级:头采样先快速削减流量,比如所有请求先经过“速率限制”把最大TPS锁死,然后头采样按比例选一批。选中的请求持续跟踪,请求结束后再触发“尾采样”逻辑,根据结果决定要不要补充存储。

举个例子:

// 伪代码示意
if (rateLimiter.TryAcquire()) {
    // 头采样:只记录开始时间
    context = InitSampling(1/100);
    result = ExecuteRequest(context);
    if (result.IsError || result.Latency > threshold) {
        // 尾采样:补充存储完整span
        context.FinalizeSampling(true);
    } else {
        context.FinalizeSampling(false);
    }
}

这样既控制了整体存储量,又能保证异常和慢请求不被漏掉。我建议你实际用的时候,把采样率设成动态的——比如根据当前磁盘水位自动调整,高峰期降采样,低峰期提采样。

核心思路: 头采样保吞吐,尾采样保质量,速率限制保安全。三种策略搭配,你才能既省硬盘又不丢关键数据。
实用建议: 刚开始上线时把采样率设高一点(比如20%),跑一周观察实际数据量和异常命中率,再逐步降低。不要一次就压到1%,不然出了问题你都不知道是不是采样丢了。
组合采样策略流程 全量请求 速率限制器 头采样决策 执行请求(跟踪span) 结果判断(错误/慢?) 是 尾采样:存储完整span 否 丢弃数据 说明:速率限制和头采样可在运行中动态调整采样率,实现自适应。

你看这个流程,其实就是把三道关卡串起来。实际部署时,你还需要考虑分布式环境下的全局一致性——比如同一个Trace的不同span可能落在不同机器上,采样决策要能同步。这个后面章节会聊,这里先不展开。

再次提醒: 任何采样策略都是一种妥协。不要追求100%的数据完整,那是不可能的。你的目标是“诊断常见问题”和“节省90%以上的存储”。如果你真想全量,请先算算硬盘预算。