4. 采样策略:头采样、尾采样、速率限制
说到调用链的存储开销,我见过太多团队被吓到——每秒百万级的调用,每个请求几十个span,全量存?一个月硬盘就炸了。但你又不能完全丢数据,不然线上出了故障抓瞎。所以必须引入采样。
说白了,采样就是要在数据完整性和存储开销之间找个平衡点。今天聊三种主流策略:头采样、尾采样、速率限制。我自己在实际项目里都折腾过,各有各的坑。
头采样:先下手为强
头采样的逻辑很简单:请求进来的时候,立马决定是否采样。记录这个请求的开始上下文、Trace ID,后续的span都带上标记。如果没被采样,那后面的span直接扔掉。
优点很明显——延迟低。你想想,数据不需要等请求结束就能决定存不存,适合高吞吐场景。但问题也大:它提前把宝压在“头部”,如果请求中间变慢或报错,你根本没机会改变主意。
我在一个电商API网关项目里就用过头采样。线上有个问题:某些请求头采样没选上,结果这些请求超时了,我们根本查不到数据。后来我才意识到,头采样会漏掉尾部异常。
尾采样:等结果出来再说话
尾采样是反过来的:让请求先跑完,根据最终的结果(比如耗时、错误码)来决定要不要采样。如果请求返回了500或者延迟超过P99,那必须存。
这样能精准捕获异常,对诊断问题很有用。但代价呢?你得把整个请求期间的span缓存起来,等结果出来才能决定。这个缓存窗口如果设短了,请求还没结束就被清掉了,数据就丢了。我曾经踩过这个坑——
尾采样适合对准确性要求高的场景,但内存开销大,不能无脑用。
速率限制:简单粗暴,但有效
速率限制不讲什么头尾,直接限定每秒最多采样多少个请求。比如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);
}
}
这样既控制了整体存储量,又能保证异常和慢请求不被漏掉。我建议你实际用的时候,把采样率设成动态的——比如根据当前磁盘水位自动调整,高峰期降采样,低峰期提采样。
你看这个流程,其实就是把三道关卡串起来。实际部署时,你还需要考虑分布式环境下的全局一致性——比如同一个Trace的不同span可能落在不同机器上,采样决策要能同步。这个后面章节会聊,这里先不展开。