一、为什么Zipkin的采样率会成为工程师的“甜蜜的烦恼”
我之前在负责电商平台的订单服务时,就踩过这个坑:为了排查晚上高峰期偶尔出现的支付状态异常问题,一开始把Zipkin的采样率设成了全量采集——结果才跑了半天,服务端的存储就涨了几十G,告警消息刷满了飞书群,不得不临时降采样。后来把采样率调成了10%,结果隔了一周,核心支付接口又出现了两次用户支付失败的情况,因为那两次请求刚好没被采样到,排查了整整三天才找到是回调接口的超时时间设置太短,核心链路的异常根本抓不到。这就是每个用Zipkin做链路追踪的开发者都会遇到的问题:采样率调太低,存不下;调太高,查不到核心问题,到底怎么权衡?
二、两种采样方式的性格对比
2.1 全量采集:不留死角的死心眼
全量采集的意思就是,不管请求是什么样的,所有链路数据都要存下来,相当于给每一个用户的请求都拍了“全程vlog”。它的优点很明显:只要请求被处理,链路数据就会被留存,不管是核心支付还是边缘注册,出问题都能查到对应的调用链,不会有遗漏。比如刚才的支付异常,如果用全量采集,那两次异常请求的链路肯定能抓到,不用花几天排查。但缺点也很扎心:存储成本会跟着请求量线性涨。举个例子,每秒1万次请求,每次链路数据约1KB,一天就是864GB,相当于普通云服务器的半个盘,小公司根本扛不住。 这里用Go语言配置全量采集的示例:
package main
import (
"github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/reporter/http"
)
func main() {
// 初始化Reporter,把链路数据发送到Zipkin服务端
reporter := http.NewReporter("http://zipkin-server:9411/api/v2/spans")
defer reporter.Close()
// 配置全量采样器:所有发起的请求都采集链路,无遗漏
// 这个采样器会对每一个新请求生成Trace ID,留存完整链路
sampler := zipkin.AlwaysSample()
// 初始化服务端Endpoint,标记当前是订单服务
endpoint, _ := zipkin.NewEndpoint("order-service", "localhost:8080")
// 初始化Tracer,绑定采样器和Reporter
tracer, _ := zipkin.NewTracer(reporter, zipkin.WithEndpoint(endpoint), zipkin.WithSampler(sampler))
_ = tracer
// 后续业务代码(比如处理下单、支付逻辑)...
}
2.2 概率采样:按比例选代表的精明
概率采样是按固定比例,比如10%、1%,随机选一部分请求留存链路,相当于从所有用户里抽“幸运观众”留数据。它的核心优点是存储成本暴降,比如设10%的采样率,刚才的例子一天只需要86GB存储,小公司也能轻松扛。但缺点也突出:如果问题是偶发的,比如千分之一概率的超时,刚好没被抽中,就会出现“问题存在但查不到”的尴尬情况。 用Go语言配置10%概率采样的示例:
package main
import (
"github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/reporter/http"
)
func main() {
reporter := http.NewReporter("http://zipkin-server:9411/api/v2/spans")
defer reporter.Close()
// 配置概率采样器:0.1表示只采集10%的请求,随机均匀分布
// 参数范围是0到1,0表示不采集,1表示全量,0.1就是10%
sampler, err := zipkin.NewCountingSampler(0.1)
if err != nil {
panic("初始化概率采样器失败")
}
endpoint, _ := zipkin.NewEndpoint("order-service", "localhost:8080")
tracer, _ := zipkin.NewTracer(reporter, zipkin.WithEndpoint(endpoint), zipkin.WithSampler(sampler))
_ = tracer
// 后续业务代码...
}
三、实际场景的权衡技巧
3.1 核心场景vs边缘场景:差异化采样
不同接口的重要程度不一样,不能用同一个采样率。比如支付、下单这类核心接口,用户感知强,出问题影响直接,应该用高采样率甚至全量;而用户注册、商品列表这类边缘接口,出问题影响小,用低概率采样就行。比如我后来的电商平台,把核心支付接口设成全量,边缘商品列表设成1%,既省下了存储,又没漏核心问题。
3.2 平峰vs高峰:动态调整采样率
一天里的请求量是波动的,晚上8点是高峰期,凌晨是低峰期。平峰期请求少,可以把核心接口的采样率提到50%,高峰期间请求多,把核心接口降到20%,边缘接口降到0.5%,这样既保证核心问题能查到,又不会在高峰期爆存储。
3.3 常见坑点:别踩一刀切的坑
最容易犯的错就是直接把采样率改成1%或者10%,不区分场景——我之前踩的坑就是这个,把整个服务的采样率都设成1%,结果核心支付的偶发超时刚好没被采到,排查了三天。另外,采样率也不能太低,低于0.1的话,连千分之一概率的偶发问题都很难抓到,相当于白采。
3.4 混合采样:兼顾成本与效率
最好的方式是用自定义采样器,核心路径全量采,边缘路径概率采。比如Go语言里可以写一个自定义采样器,只要Span的名字带“core”(比如“order-core-pay”)就全量采,其他的按10%采,示例:
package main
import (
"strings"
"github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/reporter/http"
)
// 自定义采样器:核心路径全采,边缘路径采10%
type CustomSampler struct {
coreSampler zipkin.Sampler // 全量采样器,用于核心接口
edgeSampler zipkin.Sampler // 概率采样器,用于边缘接口
}
// 实现Zipkin的Sampler接口,判断每个请求是否要采集
func (s *CustomSampler) ShouldSample(traceID zipkin.TraceID, spanName string) bool {
// 只要Span名字包含"core",就是核心路径,全量采集
if strings.Contains(spanName, "core") {
return s.coreSampler.ShouldSample(traceID, spanName)
}
// 边缘路径按10%概率采集
return s.edgeSampler.ShouldSample(traceID, spanName)
}
func main() {
reporter := http.NewReporter("http://zipkin-server:9411/api/v2/spans")
defer reporter.Close()
// 初始化自定义采样器的两个部分
sampler := &CustomSampler{
coreSampler: zipkin.AlwaysSample(), // 核心全量
edgeSampler: zipkin.NewCountingSampler(0.1), // 边缘10%
}
endpoint, _ := zipkin.NewEndpoint("order-service", "localhost:8080")
tracer, _ := zipkin.NewTracer(reporter, zipkin.WithEndpoint(endpoint), zipkin.WithSampler(sampler))
_ = tracer
// 业务代码...
}
四、总结
Zipkin的采样率调优从来不是选“全量还是概率”,而是在存储成本和问题定位能力之间找一个平衡点。小流量的核心场景,用全量采集确保无遗漏;大流量的非核心场景,用低概率采样省存储;混合采样、动态调整采样率,才是更实用的方案——毕竟我们要的不是绝对的“全”或“省”,而是在需要排查问题的时候,能快速找到链路数据,同时不会被高昂的存储成本拖垮。
评论
围绕“Zipkin采样率调优之争:全量采集与概率采样之间如何权衡存储成本同问题定位能力的实际收益关系”参与讨论