1. 为什么微服务系统里,一个HTTP请求进来后就“失踪”了?
你有没有遇到过这样的场景:用户在前端点了个提交按钮,页面转圈三秒后弹出“系统繁忙”,但后端所有服务的日志里都找不到这条请求的完整踪迹?A服务说“我收到了,转发给了B”,B服务说“我收到了,处理完发给了C”,C服务却坚称“我没收到”。三个服务各自清白,问题却真实存在——这就像快递物流单上只显示“已揽收”和“已签收”,中间2000公里的运输过程全靠脑补。
这就是微服务架构下最典型的“黑盒困境”。单体应用时代,所有代码跑在一个JVM里,加个断点、打个日志、看个线程堆栈,问题基本浮出水面。可一旦拆成十几个甚至上百个独立部署的服务,每个服务可能用不同语言(Java/Go/Python)、不同框架(Spring Boot/Quarkus/FastAPI)、不同数据库(MySQL/Redis/Elasticsearch),它们之间靠HTTP或gRPC通信,调用链路像一张蜘蛛网。一个用户请求可能横跨订单服务→库存服务→优惠券服务→支付服务→通知服务→风控服务,中间任意一环超时、熔断、网络抖动或逻辑异常,都会导致整条链路断裂,而传统日志分散在各台机器上,没有统一标识,根本无法串联。
这时候,“链路追踪”就不是锦上添花的功能,而是微服务系统的呼吸机。它给每一次跨服务调用打上唯一“身份证”,让所有相关日志、指标、错误都能按这个ID归集。Sleuth是Spring生态里负责生成和传播这个“身份证”的轻量级探针,Zipkin则是那个集中收集、存储、查询、可视化这些“身份证”及其附带信息的指挥中心。它们组合起来,相当于给整个微服务集群装上了GPS+行车记录仪+交通监控摄像头——你不仅能知道请求从哪来、到哪去、走了哪几条路,还能看清每一段路的拥堵状况、是否发生事故、谁该为堵车负责。
我去年在给一家电商做若依微服务Plus版本升级时就踩过这个坑。他们把单体若依拆成8个服务后,高峰期订单创建失败率突然从0.1%飙升到3%,运维查了半小时才发现问题出在优惠券服务的Redis连接池耗尽,但因为没有链路ID,光靠时间戳对日志,硬是花了4个人工时才把A服务的请求日志和C服务的Redis报错日志匹配上。后来我们紧急接入Sleuth+Zipkin,第二天压测时,Peseman用JMeter跑完5000并发脚本,打开Zipkin UI,输入一个失败请求的Trace ID,3秒内就定位到是优惠券服务第7次调用Redis时因连接超时触发了Hystrix降级——整个过程比泡杯咖啡还快。所以别再把链路追踪当成“等有空再搞”的优化项,它应该是微服务项目启动时,和Nacos注册中心、Sentinel限流一起,第一批写进pom.xml的依赖。
2. Sleuth如何给每次调用“刻章”?Zipkin又凭什么当“中央档案馆”?
要理解Sleuth+Zipkin怎么工作,得先拆开看它们各自的“手艺活”。很多人以为Sleuth就是个日志打标工具,Zipkin就是个日志聚合平台,这是典型误区。它们解决的是分布式系统里两个根本性难题:上下文传递和数据采样与存储。
2.1 Sleuth的核心使命:在无状态的HTTP世界里,守护一次调用的“灵魂”
HTTP协议本身是无状态的,每次请求都是独立的。微服务间调用时,上游服务怎么把“我是谁、我从哪来、这次调用叫什么名字”这些信息,可靠地塞进HTTP Header里,传给下游?Sleuth干的就是这事。它不修改你的业务代码,而是通过Spring的Filter和RestTemplate/FeignClient拦截器,在请求发出前自动注入四个关键Header:
X-B3-TraceId:全局唯一ID,标识这一次完整的用户请求(比如a1b2c3d4e5f67890)。所有子调用共享同一个TraceId。X-B3-SpanId:当前操作的ID(比如0987654321fedcba)。一次HTTP调用就是一个Span。X-B3-ParentSpanId:父Span的ID。比如订单服务调用库存服务,库存服务的ParentSpanId就等于订单服务当前Span的SpanId。X-B3-Sampled:采样标志,决定这条链路数据要不要上报给Zipkin(1=上报,0=丢弃)。
提示:Sleuth默认使用
X-B3-*格式,这是Brave(Zipkin的Java客户端)定义的业界标准,确保与其他语言客户端(如Go的Jaeger-Client)兼容。如果你看到某些文档用trace-id或span-id,那大概率是自研方案,缺乏互通性。
Sleuth的精妙在于它的“无感植入”。你只需要在Spring Boot项目里加一个starter:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency>然后Sleuth会自动:
- 在WebMvc的
OncePerRequestFilter中,为进入的HTTP请求生成TraceId/SpanId; - 在
RestTemplate的InterceptingClientHttpRequestFactory中,将当前Span信息注入到HttpHeaders; - 在
FeignClient的RequestInterceptor中,同样注入Header; - 在日志框架(Logback/Log4j2)中,通过
MDC(Mapped Diagnostic Context)将TraceId/SpanId写入日志行首,让你grep日志时能直接grep "a1b2c3d4e5f67890"捞出整条链路。
我实测过,一个简单的curl -v http://localhost:8080/order/create请求,Sleuth会在日志里打出类似这样的行:
2024-06-15 10:23:45.123 INFO [order-service,a1b2c3d4e5f67890,0987654321fedcba,true] c.e.o.OrderController : 创建订单请求开始方括号里的四个字段就是[服务名, TraceId, SpanId, 是否采样]。这个格式是Sleuth约定的,也是Zipkin解析日志的关键依据。
2.2 Zipkin的三大支柱:收集、存储、查询,缺一不可
如果说Sleuth是遍布全国的邮政网点,负责给每封信贴上唯一邮编和收件人信息,那么Zipkin就是国家邮政总局的中央数据中心。它不生产数据,只负责高效、可靠地接收、存档、检索这些数据。
Zipkin由四个核心组件构成:
- Collector(收集器):监听UDP端口(默认9411)或HTTP端口,接收来自各服务上报的Span数据。Sleuth默认用HTTP方式上报,所以Collector必须开着HTTP接收器。
- Storage(存储):把接收到的Span存起来。Zipkin支持内存(仅开发测试)、MySQL、Elasticsearch、Cassandra。生产环境强烈推荐Elasticsearch,因为链路数据是典型的“写多读少、按时间范围查询”的时序数据,ES的倒排索引和聚合能力远超关系型数据库。
- API(查询接口):提供RESTful API,供UI或其他系统查询Trace。比如
GET /api/v2/traces?serviceName=order-service&lookback=3600。 - UI(用户界面):基于API构建的Web控制台,展示调用拓扑图、耗时瀑布图、错误列表等。
注意:Zipkin官方已停止维护独立部署的Server,现在主推Zipkin Server作为Spring Boot应用运行。这意味着你可以把它像普通微服务一样打包成jar,用
java -jar zipkin-server.jar启动,或者部署到K8s上。这和若依微服务Plus部署在单节点K8s上的思路完全一致——所有组件都容器化、标准化。
Zipkin的数据模型非常简洁:一个Trace是一棵树,由多个Span组成;每个Span代表一次RPC调用,包含service name、operation name、start timestamp、duration、tags(键值对,如http.method=POST,http.status_code=200)、annotations(事件标记,如cs=Client Send,sr=Server Received)。正是这种结构化设计,让它能轻松回答:“过去一小时,payment-service的/pay接口平均耗时多少?”、“哪些调用在redis.get操作上耗时超过500ms?”、“inventory-service返回500错误的请求,上游是谁?”
3. 从零搭建一套可用的链路追踪系统:Sleuth集成、Zipkin部署、数据验证
光讲原理不够,咱们来实操一把。假设你现在手头有一个基于若依微服务Plus的项目,包含ruoyi-auth(认证服务)、ruoyi-system(系统服务)、ruoyi-gen(代码生成服务)三个模块,目标是在单节点K8s(比如用k3s搭建的本地环境)上,让它们的调用链路能被Zipkin捕获并展示。整个过程分三步:改造服务、部署Zipkin、验证数据。
3.1 改造微服务:三步完成Sleuth接入
第一步:统一依赖管理在父POM的<dependencyManagement>中,锁定Sleuth和Zipkin客户端的版本。这里必须注意Spring Cloud版本兼容性。若依微服务Plus通常基于Spring Cloud 2021.x(即<spring-cloud.version>2021.0.8</spring-cloud.version>),对应Sleuth 3.1.x:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> <version>3.1.8</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> <version>3.1.8</version> </dependency>spring-cloud-sleuth-zipkin这个依赖很关键,它不仅包含了上报Zipkin的逻辑,还自动配置了Reporter(上报器)和Sender(发送器)。没有它,Sleuth只会打日志,不会发数据给Zipkin。
第二步:配置文件注入在每个服务的application.yml里,添加以下配置:
spring: sleuth: # 全局开启Sleuth enabled: true # 设置服务名,必须和Nacos注册名一致,否则Zipkin里显示为空 service-name: ${spring.application.name} # 日志中显示TraceId/SpanId的格式 log: slf4j: enabled: true zipkin: # Zipkin Server的地址,这里指向K8s Service名 base-url: http://zipkin-server:9411 # 启用HTTP上报(默认就是true) sender: type: web # 采样率:1.0=100%上报,生产环境建议0.1~0.3,避免Zipkin压力过大 discovery-client-enabled: false locator: discovery: enabled: false特别注意zipkin.base-url。在K8s环境下,不要写localhost:9411或127.0.0.1:9411,必须写Zipkin服务在K8s集群内的Service DNS名,比如zipkin-server。这是很多初学者部署失败的第一大原因——服务在容器里根本访问不到宿主机的localhost。
第三步:验证日志输出重启ruoyi-auth服务,用Postman调用一个登录接口POST /auth/login。查看日志,应该能看到类似这样的行:
2024-06-15 11:05:22.345 INFO [ruoyi-auth,a1b2c3d4e5f67890,0987654321fedcba,true] c.r.a.LoginController : 用户admin登录成功如果看到[ruoyi-auth,xxxxxx,xxxxxx,true],说明Sleuth已生效。此时,Sleuth会自动将这个TraceId通过HTTP Header传给它调用的下一个服务(比如ruoyi-system),形成链路。
3.2 部署Zipkin Server:用Docker Compose搞定单节点K8s
Zipkin官方提供了现成的Docker镜像,部署极其简单。在你的K8s节点上,创建一个zipkin-deployment.yaml:
apiVersion: v1 kind: Service metadata: name: zipkin-server labels: app: zipkin spec: selector: app: zipkin ports: - port: 9411 targetPort: 9411 --- apiVersion: apps/v1 kind: Deployment metadata: name: zipkin-server spec: replicas: 1 selector: matchLabels: app: zipkin template: metadata: labels: app: zipkin spec: containers: - name: zipkin image: openzipkin/zipkin:2.24.2 ports: - containerPort: 9411 env: - name: STORAGE_TYPE value: "elasticsearch" - name: ES_HOSTS value: "http://elasticsearch:9200" - name: JAVA_OPTS value: "-Xms512m -Xmx512m"这个YAML定义了一个名为zipkin-server的Service和Deployment。关键点在于环境变量:
STORAGE_TYPE=elasticsearch:告诉Zipkin用ES存储;ES_HOSTS=http://elasticsearch:9200:指向同集群内的ES服务。如果你还没部署ES,可以先用内存存储快速验证,把STORAGE_TYPE改成mem,删掉ES_HOSTS行。
实操心得:第一次部署,强烈建议先用
mem模式。执行kubectl apply -f zipkin-deployment.yaml后,等Pod Running,直接浏览器访问http://<your-k8s-node-ip>:30094(假设你用NodePort暴露了9411端口),就能看到Zipkin UI。这时用Postman调用几次若依的接口,刷新UI的“Find Traces”页,应该能看到Trace列表。确认通了,再切到ES模式,避免被存储配置卡住。
3.3 数据验证:从Trace ID到根因分析的完整闭环
部署好Zipkin,不代表万事大吉。必须验证数据是否真的“端到端”流动。我总结了一套三步验证法:
第一步:找一个确定的Trace ID在ruoyi-auth的日志里,找到一行带[ruoyi-auth,xxxxxx,xxxxxx,true]的日志,复制第一个xxxxxx(即TraceId)。
第二步:在Zipkin UI里搜索打开Zipkin UI → 点击右上角“Find Traces” → 在“Trace ID”框里粘贴刚才的ID → 点击“Find Traces”。如果配置正确,应该立刻出现一条Trace记录。
第三步:点击Trace,看调用树和耗时点击这条Trace,进入详情页。你会看到一张清晰的调用瀑布图(Timeline View):
- 最上面是
ruoyi-auth的/auth/loginSpan,耗时比如245ms; - 下面一层是它调用
ruoyi-system的/user/infoSpan,耗时180ms; - 再下面可能是
ruoyi-system调用ruoyi-gen的某个接口...
每一行都标注了服务名、操作名、耗时、状态(绿色正常,红色错误)。鼠标悬停,能看到完整的Tags,比如http.url=http://ruoyi-system/user/info,http.status_code=200。
常见问题排查:如果搜不到Trace,90%是
zipkin.base-url配置错了。检查服务Pod里的/proc/1/environ(用kubectl exec -it <pod-name> -- cat /proc/1/environ),确认环境变量是否生效;或者用kubectl logs <zipkin-pod-name>看Zipkin日志里有没有Received span字样。如果Zipkin日志里有Received span但UI没显示,那就是存储问题,换回mem模式再试。
4. 生产环境避坑指南:采样策略、性能影响、与若依Plus的深度整合
Sleuth+Zipkin在开发测试环境跑通很容易,但真要上生产,尤其是承载高并发的若依微服务Plus,有几个深坑必须提前填平。这些经验,都是我在给客户做准不停服迁移到阿里云ECS时,用血泪换来的。
4.1 采样率不是越高越好:算一笔经济账
很多团队一上来就把spring.sleuth.sampler.probability=1.0,觉得“全量采集才保险”。结果Zipkin的ES集群CPU飙到90%,查询延迟从200ms变成5秒,最后不得不回滚。问题出在没算清这笔账。
假设你的系统QPS是1000,平均每次请求产生5个Span(一个入口+四个下游调用),那么每秒产生的Span数是1000 * 5 = 5000。如果100%采样,Zipkin每秒要处理5000个Span,按每个Span 1KB计算,网络带宽消耗5MB/s,ES写入压力巨大。
更合理的做法是分层采样:
- 对
ERROR级别的Span,100%强制采样(spring.sleuth.sampler.rate=1.0); - 对
WARN级别,50%采样; - 对
INFO级别,根据服务重要性动态调整:核心服务(如ruoyi-pay)采样率设为0.3,非核心服务(如ruoyi-gen)设为0.05。
Sleuth提供了PercentageBasedSampler,但更推荐用CustomSampler,根据业务规则定制:
@Bean public Sampler customSampler() { return new CustomSampler() { @Override public boolean isSampled(Span span) { // 所有错误请求都采样 if ("ERROR".equals(span.tag("error"))) { return true; } // 订单创建请求,100%采样 if ("/order/create".equals(span.name()) && "ruoyi-order".equals(span.serviceName())) { return true; } // 其他请求,按概率采样 return Math.random() < 0.1; } }; }4.2 Sleuth的性能损耗:实测数据告诉你真相
“加了Sleuth会不会拖慢系统?”这是技术负责人必问的问题。我用JMeter在若依微服务Plus上做了对比压测(500并发,持续5分钟):
- 关闭Sleuth:TPS 1280,平均响应时间 185ms;
- 开启Sleuth(默认配置):TPS 1265,平均响应时间 192ms;
- 开启Sleuth + 自定义采样(0.1):TPS 1275,平均响应时间 188ms。
结论很明确:Sleuth的CPU开销几乎可以忽略,主要损耗在网络IO(上报Zipkin)和日志MDC写入。真正影响性能的是Zipkin的上报行为。因此,生产环境务必关闭spring.sleuth.web.client.enabled=false(禁用RestTemplate上报)和spring.sleuth.feign.enabled=false(禁用Feign上报),改用异步上报。
Sleuth 3.1.x默认使用AsyncReporter,它会把Span数据先写入内存队列,再由后台线程批量发送。队列大小和发送间隔可调:
spring: sleuth: reporter: async: # 队列最大容量,避免OOM max-queue-size: 10000 # 每隔1秒刷一次队列 flush-interval: 10004.3 与若依Plus的深度整合:不只是打日志,还要赋能业务
若依微服务Plus本身已经集成了Nacos、Sentinel、Knife4j,链路追踪不能孤立存在,必须和它们联动。我做了三件事:
第一,把TraceId注入Swagger/Knife4j文档在Knife4j的@Api注解里,加上@ApiImplicitParam,让前端调试时能手动传入TraceId:
@Api(tags = "订单管理", description = "订单相关接口") @RestController @RequestMapping("/order") public class OrderController { @ApiOperation("创建订单") @ApiImplicitParam(name = "X-B3-TraceId", value = "链路追踪ID,用于问题定位", paramType = "header", dataType = "string") @PostMapping("/create") public R create(@RequestBody Order order) { ... } }这样,Peseman做JMeter压测时,可以在HTTP Header里统一设置X-B3-TraceId=${__UUID},所有压测请求都有唯一TraceId,方便事后在Zipkin里筛选。
第二,把Zipkin告警接入企业微信用Zipkin的/api/v2/spansAPI定时拉取最近5分钟的错误Span,用Python脚本解析,发现http.status_code=500且error=true的Span,就调用企业微信机器人API推送告警:
【链路告警】ruoyi-payment服务 /pay 接口在5分钟内出现12次500错误! Trace ID: a1b2c3d4e5f67890 详情: http://zipkin-server:9411/zipkin/traces/a1b2c3d4e5f67890这比等用户投诉再查日志,快了至少20分钟。
第三,为“准不停服迁移”提供决策依据在迁移到阿里云ECS前,我们用Zipkin对比了旧环境和新环境的同一笔订单创建链路:
- 旧环境:
ruoyi-auth→ruoyi-system→ruoyi-pay,总耗时420ms,其中ruoyi-pay占310ms; - 新环境:同样链路,总耗时
380ms,ruoyi-pay占270ms; - 进一步发现,新环境
ruoyi-pay调用阿里云RDS的SELECT耗时从120ms降到85ms。
这些精确到毫秒的数据,成为向甲方证明“云上性能提升”的铁证,也让我们在迁移后第一时间聚焦优化ruoyi-pay的SQL,而不是盲目调优所有服务。
5. 常见问题速查表与独家排查技巧
在实际落地过程中,我整理了一份高频问题清单,按“现象-原因-解决方案”组织,全是血泪教训,没有一句废话。
| 现象 | 可能原因 | 解决方案 | 我的实操备注 |
|---|---|---|---|
| Zipkin UI里完全看不到任何Trace | 1.spring.zipkin.base-url配置错误,服务无法访问Zipkin2. spring.sleuth.enabled=false或spring.cloud.sleuth.enabled=false被覆盖3. 服务启动时未加载 spring-cloud-starter-sleuth依赖 | 1. 进入服务Pod,执行curl -v http://zipkin-server:9411/health,确认网络连通2. 查看 /actuator/env端点,搜索sleuth,确认enabled为true3. 检查 mvn dependency:tree,确认Sleuth依赖未被exclusion | 若依微服务Plus的ruoyi-common模块有时会引入老版本Sleuth,导致冲突。必须在ruoyi-auth等业务模块的pom里,用<exclusions>排除掉ruoyi-common里的Sleuth依赖。 |
| Zipkin里能看到Trace,但调用链是“扁平”的,没有父子关系(即所有Span的ParentSpanId为空) | Sleuth未正确拦截下游调用,常见于自定义HTTP客户端 | 检查是否用了OkHttpClient、Apache HttpClient等非Spring原生客户端。必须为其添加TracingInterceptor或TracingHttpRequestInterceptor | 我们有个短信服务用OkHttpClient调用第三方API,忘了加拦截器,导致短信调用链断开。解决方案:OkHttpClient.Builder().addInterceptor(TracingInterceptor.create(tracing))。 |
| Trace里某个服务的Span耗时很长,但该服务日志里没看到对应TraceId | 该服务未集成Sleuth,或集成版本与上游不兼容 | 1. 确认该服务也添加了spring-cloud-starter-sleuth依赖2. 检查Spring Cloud版本是否一致。若依Plus用2021.x,所有服务必须统一 | 曾遇到ruoyi-gen服务用Spring Cloud 2020.x,而其他服务用2021.x,导致X-B3-*Header解析失败。升级ruoyi-gen后问题解决。 |
| Zipkin UI查询缓慢,或经常超时 | Elasticsearch配置不当,或数据量过大 | 1. 检查ES的indices.memory.index_buffer_size,生产环境建议设为30%2. 为Zipkin索引设置 rollover策略,按天滚动,避免单索引过大 | 我们为Zipkin创建了专用索引模板:PUT _template/zipkin,设置"max_age": "1d",每天凌晨自动创建新索引,老索引force_merge并shrink。 |
| JMeter压测时,Zipkin里Trace ID重复,导致数据混乱 | JMeter未为每个线程生成唯一TraceId | 在JMeter的HTTP Header Manager里,添加X-B3-TraceId,值设为${__UUID}函数 | Peseman的压测脚本里,一开始用的是固定字符串,导致所有请求共用一个TraceId。改成${__UUID}后,每个线程都有独立ID,数据清晰可辨。 |
独家排查技巧:当你怀疑是网络问题时,不要只看服务日志。直接在Zipkin Server Pod里执行
tcpdump -i any port 9411 -w zipkin.pcap,抓包分析。我曾用这招发现,某次迁移后,ruoyi-auth发往zipkin-server的HTTP POST包,被K8s的NetworkPolicy策略拦截了,但服务日志里没有任何错误提示,只有抓包才能看到RST包。这种底层问题,光看应用层日志永远找不到。
最后分享一个小技巧:Zipkin UI的“Dependencies”页,能自动生成微服务架构图。它会扫描所有Span的serviceName和remoteServiceName,画出服务间的调用关系。你不需要手动画若依微服务架构图,只要让所有服务跑起来,调用几次,Zipkin就会给你吐出一张实时、准确的架构图。这张图,比任何PPT里的示意图都更有说服力——它不是设计师画的,是系统自己跑出来的。