一、为什么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的采样率调优从来不是选“全量还是概率”,而是在存储成本和问题定位能力之间找一个平衡点。小流量的核心场景,用全量采集确保无遗漏;大流量的非核心场景,用低概率采样省存储;混合采样、动态调整采样率,才是更实用的方案——毕竟我们要的不是绝对的“全”或“省”,而是在需要排查问题的时候,能快速找到链路数据,同时不会被高昂的存储成本拖垮。