资讯动态

微服务链路追踪实战:Sleuth与Zipkin从原理到应用

发布时间:2026/10/13 8:23:54 来源:尧图企业网站定制
排查分布式系统的慢请求往往是最让人头疼的事情之一。一个下单接口背后可能串了七八个微服务中间还有缓存、消息队列、定时任务一旦响应变慢或数据对不上你连问题出在哪个环节都说不清楚。Spring Cloud Sleuth 配合 Zipkin 就是解决这个问题的标准组合Sleuth 负责在每次请求进入时生成全局唯一的 TraceId并在服务间调用时自动传递Zipkin 则负责把这些分散的链路数据收集、存储、展示出来。本篇文章我会从零开始搭一套完整的示例把一个三个服务互相调用的链路在 Zipkin 界面上跑通再聊聊采样率、异步线程、消息队列这些实战中绕不开的细节适合刚接触链路追踪的开发者也适合已经用过但一直没把原理吃透的人。1. 为什么需要链路追踪1.1 一次请求背后的调用迷宫单体应用时代排查问题很简单一个请求进来Controller 调 ServiceService 调 Mapper日志都在同一个进程里按时间戳排序就能把调用过程捋清楚。微服务拆分之后情况完全变了。举个最容易理解的例子。用户在前端点了一下“提交订单”这个操作可能先经过网关然后订单服务调用库存服务库存服务发现缓存没有命中又去查数据库同时订单服务还通过消息队列通知了积分服务。如果整个流程耗时 3 秒你只知道入口服务花了 3 秒但不知道这 3 秒到底消耗在哪个服务上是库存服务查询慢还是消息队列积压还是数据库连接池不够用。更麻烦的是三个服务各自的日志里记录的只是局部信息你没法把它们串成一条完整的调用链。这时候就需要一个机制一次完整的业务请求无论内部调用了多少个服务都能拥有一个统一的标识并且在所有服务的日志、调用记录中都带上这个标识。有了这个全局标识你就能顺藤摸瓜把请求经过的每一个节点、每一次调用耗时、每一段异常信息完整还原出来。1.2 Sleuth 与 Zipkin 各自扮演的角色很多人刚开始接触时会搞混Sleuth 和 Zipkin 到底谁是谁是不是二选一就行实际上它们是分工协作的关系。Spring Cloud Sleuth 是客户端组件嵌入在业务服务里负责做三件事第一在请求进入时生成 TraceId 和 SpanId第二在服务间调用时通过 HTTP Header 或消息队列的 Header 把这两个 ID 传递到下游服务第三把链路的 Span 信息调用开始时间、结束时间、服务名、标签等记录下来通过 HTTP 或 Kafka 上报给 Zipkin。Zipkin 则是服务端它本身是一个独立的部署组件负责接收客户端上报的链路数据存储起来并提供一套 Web UI 供你查询和分析。打个比方Sleuth 是每个快递网点在包裹上贴面单、登记信息Zipkin 就是全网包裹追踪系统把所有网点的记录汇总起来输入一个单号就能看到包裹从发货到签收的完整轨迹。实际项目中你也可以只用 Sleuth 不接 Zipkin这样日志里会多出 TraceId方便在日志平台里按 ID 串联检索。但只看日志不够直观接上 Zipkin 之后链路调用过程变成可视化视图每段调用的耗时一目了然定位问题效率会高很多。1.3 核心概念TraceId、SpanId、Annotation想要用好这套工具几个基础概念必须搞清楚。TraceId 是一条完整调用链的唯一标识贯穿整个请求链路。一次用户请求从网关进来无论后续调用多少次服务TraceId 始终不变。这就好比快递单号凭借它你能查到整个运输全流程。Span 是链路中的一次具体调用操作可以理解为链路中的一个环节。比如订单服务调用库存服务这个远程调用行为就是一个 Span。一次完整的请求链路由多个 Span 组成这些 Span 通过 SpanId 和 ParentSpanId 关联起来形成一棵调用树。Span 之间有父子关系调用方的 Span 是父 Span被调用方处理逻辑的 Span 是子 Span。如果某个服务内部又调用了另一个服务就会继续生成下一级子 Span整个链路就像一棵向下生长的树。Annotation 是 Span 中的时间标记点。标准的有四个Client Send 表示客户端发起请求Server Receive 表示服务端收到请求Server Send 表示服务端返回响应Client Receive 表示客户端收到响应。这四个时间点之间的差值就是网络传输耗时和服务端处理耗时的关键依据。Zipkin 界面上的耗时统计本质就是把这几个时间点拉出来做减法。理解了这些再看 Sleuth 自动生成的日志就不会懵了。Sleuth 默认会在日志中注入[appname,traceId,spanId,exportable]格式的元数据比如[order-service,7f2a1c...,7f2a1c...,true]其中第四位表示这条 Span 是否被导出给 Zipkin。2. 环境准备与选型思路2.1 版本搭配Spring Boot、Spring Cloud、Zipkin 的兼容关系选版本这件事看起来不起眼却是新手踩坑的重灾区。Spring Cloud 的组件版本是和 Spring Boot 版本强绑定的Sleuth 作为 Spring Cloud 家族的一员这个铁律同样适用。我用的是 Spring Boot 2.3.12.RELEASE 配 Spring Cloud Hoxton.SR12这是一套非常经典且资料丰富的组合。对应的 Sleuth 依赖坐标是spring-cloud-starter-sleuth和spring-cloud-sleuth-zipkin。这套组合下配置项是传统的spring.zipkin.base-url和spring.sleuth.sampler.probability网上绝大多数教程写的都是这套照着做基本不会出幺蛾子。如果你用的是 Spring Boot 2.4 以上的版本搭配的是 Spring Cloud 2020.0.xSleuth 3.x 的配置项已经变了Zipkin 地址不再用spring.zipkin.base-url而是改成management.zipkin.tracing.endpoint采样配置也从spring.sleuth.sampler.probability迁移到了management.tracing.sampling.probability。很多人在网上搜到旧教程套在自己的新版本项目上结果 Zipkin 界面死活看不到数据多半就是这个原因。更激进的如果直接上了 Spring Boot 3Spring Cloud Sleuth 已经被移除官方推荐的替代是 Micrometer Tracing 配合 Zipkin依赖坐标和配置方式又是另一套东西。我个人的建议是如果你只是学习链路追踪首选 Boot 2.3.x Hoxton 的组合跑通再说如果是新项目从零搭建直接研究 Micrometer Tracing 也没问题但别把四五个版本的资料混着看极易精神分裂。2.2 Zipkin 服务端三种启动方式Zipkin 服务端的部署方式非常灵活我这里列三种最常见的按需选择。第一种是 Docker 方式也是最省事的。一条命令起一个 Zipkin 容器服务器如果没有装 Docker这条路就断了docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin容器启动之后浏览器访问http://localhost:9411就能看到 Zipkin 的查询界面。默认数据存在内存里重启即丢学习环境无所谓。第二种是 jar 包方式适合不想装 Docker 或者要自定义启动参数的场景。用官方提供的 quickstart 脚本直接拉 jarcurl -sSL https://zipkin.io/quickstart.sh | bash -s java -jar zipkin.jar启动完成后端口同样是 9411。这种方式的好处是能很方便地加启动参数比如指定存储类型、打开某个采样器等我后面的持久化部分会提到。第三种是源码编译运行适合要改 Zipkin 源码做二次开发的场景。从 GitHub 把 zipkin 仓库克隆下来执行mvn -DskipTests install然后到zipkin-server目录下启动。如果不是有定制需求我不建议这么干没必要把自己困在编译上。2.3 存储方案怎么选Zipkin 默认把数据存在内存里好处是零配置、部署完就能用坏处是重启就丢、数据量大了查询也扛不住。所以生产环境必须考虑持久化。Zipkin 支持的主流存储有 MySQL、Elasticsearch、Cassandra 三种。MySQL 适合中小规模、团队已经有 MySQL 运维经验、数据量不大的场景配置相对简单执行官方提供的 schema 建表脚本再设置几个环境变量就能用。Elasticsearch 适合数据量大、需要支持全文检索和聚合分析的场景也是大厂用得比较多的方案但运维成本高。Cassandra 是分布式存储适合超大规模链路数据但如果不是日均千万级 Span完全没有必要上。学习阶段直接用内存存储就够了先把链路跑通。我见过不少人在学习阶段就纠结存 MySQL 还是 ES其实链路都还没显示出来这纯属本末倒置。等你在 Zipkin 界面上看到一条完整链路之后再考虑持久化的事。3. 从零搭建完整的链路追踪示例3.1 三个服务的基础工程与依赖引入这一节我们搭建一个最经典的调用场景网关服务 - 订单服务 - 库存服务。因为重点是链路追踪我直接用 RestTemplate 和 Feign 来模拟远程调用不引入真实的注册中心和网关组件避免无关依赖干扰主线。先创建三个服务的父工程统一管理依赖版本。Maven 的dependencyManagement里引入dependencyManagement dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId versionHoxton.SR12/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version2.3.12.RELEASE/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement三个服务各自的 pom 里核心依赖是这样的dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-sleuth/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-sleuth-zipkin/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency注意如果你的订单服务要用 RestTemplate 调库存服务不需要额外引入 Feign 的依赖但为了演示不同调用方式都能被 Sleuth 自动增强建议订单服务里两个都用RestTemplate 调一个接口Feign 调另一个接口这样能看到两种技术栈在 Zipkin 里的表现差异。3.2 配置文件中只改几个关键参数三个服务的 application.yml 配置逻辑一致核心参数就几个。以订单服务为例spring: application: name: order-service zipkin: base-url: http://localhost:9411 sleuth: sampler: probability: 1.0 server: port: 8081服务名spring.application.name必须配置Zipkin 界面上显示的服务节点名称就是它。如果忘了配界面里会显示一堆无意义的默认名链路视图没法看。spring.zipkin.base-url指向 Zipkin 服务端的地址这里我用的是本地默认端口 9411。如果你的 Zipkin 部署在别处改成对应 IP 或域名即可。spring.sleuth.sampler.probability: 1.0表示采样率 100%也就是所有请求都上报。生产环境通常不会配这么高会改成 0.110%甚至更低节省存储和带宽。学习阶段为了能稳定看到链路必须配成 1.0否则很可能出现刷新好几次界面才偶尔抓到一条链路的情况。网关服务端口配 8080库存服务配 8082。三个服务都是独立启动的 Spring Boot 应用不需要额外的注册中心。这里不要引入 Eureka 或 Nacos加了反而多一层配置干扰。Sleuth 的链路传递靠的是 HTTP Header跟服务发现机制没有直接关系。3.3 打通 Feign 调用链链路传递的关键在于Sleuth 会把 TraceId 放在 HTTP 请求的 Header 里往下游传下游服务接收到请求时先从 Header 里提取 TraceId如果取到了就沿用取不到就生成新的。这个逻辑是 Sleuth 自动完成的不需要你手写任何传递代码前提是你的 HTTP 客户端是 Spring Cloud 生态支持的组件。先看 Feign 这边。在订单服务里定义一个 Feign 接口指向库存服务FeignClient(name inventory-service, url http://localhost:8082) public interface InventoryClient { GetMapping(/inventory/deduct) String deduct(RequestParam(productId) String productId, RequestParam(count) Integer count); }订单服务的 Controller 里调用这个 Feign 接口RestController RequestMapping(/order) public class OrderController { Autowired private InventoryClient inventoryClient; Autowired private RestTemplate restTemplate; GetMapping(/create) public String create(RequestParam(productId) String productId, RequestParam(count) Integer count) { // 记录业务开始时间 long start System.currentTimeMillis(); // 方式一通过 Feign 调用库存服务 String feignResult inventoryClient.deduct(productId, count); // 方式二通过 RestTemplate 调用库存服务另一个接口 String url http://localhost:8082/inventory/query?productId productId; String restResult restTemplate.getForObject(url, String.class); long cost System.currentTimeMillis() - start; return order created, cost cost ms, feign result feignResult , rest result restResult; } }RestTemplate 需要在配置类里显式声明 BeanConfiguration public class WebConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } }这里有个容易踩的坑自定义的 RestTemplate Bean 一定要是 Spring 容器托管的Sleuth 会通过 BeanPostProcessor 给 RestTemplate 注入拦截器如果你在自己的代码里手动new RestTemplate()那这个实例不会被增强TraceId 也就不会自动传递。我以前排查过一个诡异问题某条链路在 Zipkin 界面上从某个节点开始就断了排查到最后发现是有人在工具类里直接 new 了一个 RestTemplate 发 HTTP 请求绕过了 Sleuth 的拦截增强。库存服务的 Controller 就很简单了模拟一点处理逻辑RestController RequestMapping(/inventory) public class InventoryController { GetMapping(/deduct) public String deduct(RequestParam(productId) String productId, RequestParam(count) Integer count) { // 模拟库存扣减耗时 try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return inventory deducted for product productId , count count; } GetMapping(/query) public String query(RequestParam(productId) String productId) { try { Thread.sleep(80); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return inventory for product productId is 100; } }三个服务依次启动然后用浏览器或 Postman 调用/order/create?productIdP001count1。这个过程会触发订单服务调用库存服务的两个接口。此时去 Zipkin 界面刷新正常情况下就能看到一条完整的链路了。3.4 手动埋点给 Span 加上业务标签自动生成的 Span 只包含服务名、耗时这类基础信息。但很多时候我们需要在链路里看到业务维度信息比如当前操作的用户 ID、订单编号、商品 ID、异常码等。这时候就需要手动埋点给 Span 打上自定义标签。Sleuth 提供了一套 API可以从容器里注入 Tracer 来操作当前 SpanComponent public class OrderTraceService { Autowired private Tracer tracer; public void addTraceTags(String userId, String orderId) { Span currentSpan tracer.currentSpan(); if (currentSpan null) { return; } currentSpan.tag(userId, userId); currentSpan.tag(orderId, orderId); currentSpan.tag(channel, web); currentSpan.event(order-create-started); } }span.tag(name, value)会把键值对挂到当前 Span 上之后在 Zipkin 界面的 Span 详情里能看到这些标签。span.event()可以打一个自定义的时间标记点用于记录某个业务动作发生的时刻。这里有个使用技巧打标签前先判断currentSpan是否为空。因为采样率不是 100% 的时候有些请求本来就不建 Span执意打标签会出现空指针。即便采样率是 100%也建议保持这个习惯谁也不敢保证未来别人会不会把采样率调低。除了打标签你还可以主动创建子 Span。比如在订单服务里有一段比较重的本地计算逻辑你想单独统计它的耗时可以这样做Span computeSpan tracer.nextSpan().name(local-compute-order-price).start(); try (Tracer.SpanInScope ws tracer.withSpanInScope(computeSpan)) { // 模拟复杂计算 Thread.sleep(200); } finally { computeSpan.finish(); }tracer.nextSpan()创建的子 Span 会自动挂在当前 TraceId 下形成链路中的一个独立节点。这种手法在定位本地逻辑性能问题时特别有用你可以在 Zipkin 界面上清晰看到这一段到底耗时多少。3.5 启动后在 Zipkin 界面看懂一条完整链路上述服务都启动完成后调用一次下单接口打开http://localhost:9411点击界面右上角的 Find Traces 按钮。如果一切正常会看到一条最新的链路记录点进去就能看到一张调用瀑布图。瀑布图从上到下依次是网关如果有的话本示例没有- 订单服务 - 库存服务。每一行是一个 Span横轴代表时间跨度越长说明这段调用耗时越久。订单服务这一行会显示总耗时下面挂着两段子 Span分别是 Feign 调用和 RestTemplate 调用引起的库存服务处理。每段 Span 展开后可以看到 Service Name、耗时、开始时间、标签信息等详细数据。新手第一次看到这个界面容易有一个误解Zipkin 瀑布图里每个服务显示的行数并不等于服务数而是等于 Span 数。订单服务本身可能因为内部逻辑复杂产生多个 Span库存服务被调用了两次也会产生两个 Span这些都会在瀑布图上分别展示。如果界面上没有数据优先检查三件事采样率是否配成 1.0、Zipkin 地址是否可达、服务之间是否真的发生了远程调用。这三个问题包揽了九成查不到数据的场景。4. 进阶玩法与实战中的细节4.1 采样率到底怎么配采样率是链路追踪系统里最需要权衡的参数直接决定了存储成本和数据完整度之间的取舍。probability: 1.0代表 100% 采样每个请求都会生成 Span 并上报。优点是链路完整排查问题最轻松缺点是流量一大Zipkin 的存储压力成倍增加。假设一个系统的 QPS 是 500每个请求平均产生 15 个 Span那么每秒会产生 7500 条 Span 记录光一天的存储量就很可观了。probability: 0.1是常见的生产配置代表 10% 采样也就是每 10 个请求里随机挑 1 个生成链路。这样存储成本降为十分之一但仍然能通过一定概率捕捉到慢请求和异常请求。缺点是复现问题时可能需要多次触发请求才能抓到一条链路。如果觉得固定采样率太死板Spring Cloud Sleuth 还提供了自定义采样器的方式。比如只对包含指定 Header 的请求进行 100% 采样或者根据请求路径匹配只采某个危险接口的链路。这需要实现Sampler接口Component public class CustomSampler implements Sampler { Override public boolean isSampled(Span span) { // 可以按 span 名称或者其他维度做判断 return span.getName() ! null !span.getName().contains(health); } }自定义采样器的灵活度很高比如可以做渐进式调整平时采 10%遇到大促压测临时调高。不过对于大多数项目来说固定采样率已经足够不建议在一开始就为了采样器过度设计。4.2 异步线程池和 MQ 场景下如何传递 TraceId这是被问得最多、也是踩坑率最高的一个点。默认情况下Sleuth 对同步调用Feign、RestTemplate的链路传递是自动的但一旦你开启了异步线程池事情就变了。原因在于 TraceId 的传递依赖 ThreadLocal而线程池里的线程跟请求线程不是同一个。请求线程里的 TraceId 存在它的 ThreadLocal 里异步任务跑到另一个线程去执行那边根本读不到这个值。结果就是异步任务内部产生的 Span 没有父 Span在 Zipkin 界面上变成了孤立的链路。解决办法有两种。第一种是用 Sleuth 自带的TraceableExecutorService包装线程池。给线程池加一个包装层提交任务时捕获当前 TraceContext执行任务时把这个上下文设置到执行线程中Configuration public class AsyncConfig { Autowired private BeanFactory beanFactory; Bean public ExecutorService traceExecutorService() { ExecutorService delegate Executors.newFixedThreadPool(4); return new TraceableExecutorService(beanFactory, delegate); } }第二种是针对消息队列场景。Kafka 或 RocketMQ 的生产者在发送消息时需要手动把 TraceId 和 SpanId 塞进消息 Header消费方消费消息时再提取 Header 并设置回上下文。Sleuth 3.x 对主流消息中间件有自动注入的适配但如果你用的客户端版本比较老就得自己动手。核心思路是// 发送消息前 TraceContext context tracer.currentSpan().context(); RecordHeaders headers new RecordHeaders(); headers.add(new RecordHeader(X-B3-TraceId, context.traceIdString().getBytes())); headers.add(new RecordHeader(X-B3-SpanId, context.spanIdString().getBytes()));// 消费消息时 String traceId new String(record.headers().lastHeader(X-B3-TraceId).value()); String spanId new String(record.headers().lastHeader(X-B3-SpanId).value());这里的自定义 Header 名称对应 Sleuth 的 B3 协议。当然更省事的方案是直接用 Sleuth 自带的注解ContinueSpan和NewSpan修饰异步任务方法但前提是你把线程池包装好了。4.3 把自定义指标写进 Span链路追踪不只是用来查问题的用好自定义标签和计时事件它还能变成一套简易的可观测平台。比如你的订单服务每天要被多个渠道调用你想知道每个渠道的调用量、平均耗时、失败率。如果能往 Span 里写入channel标签Zipkin 界面就可以按标签筛选。在查询输入框里输入channelweb配合时间范围选择所有来自 Web 渠道的链路就都过滤出来了非常直观。另一个实用场景是把错误信息写进 Span。当方法抛出异常时给当前 Span 打上error标签并记录异常信息try { // 业务逻辑 } catch (Exception e) { Span currentSpan tracer.currentSpan(); if (currentSpan ! null) { currentSpan.tag(error, true); currentSpan.tag(error.message, e.getMessage()); } throw e; }这样在 Zipkin 界面上正常链路显示为绿色标注了 error 标签的链路会显示为红色一眼就能揪出所有异常请求。我们团队以前排查线上问题时就是靠这个把错误链路筛出来再对比正常链路找差异的效率比翻日志高很多。4.4 持久化把链路数据存进 MySQL/ES上了生产环境Zipkin 内存存储是不行的必须做持久化。这里说下最简单的 MySQL 方案。先用 Zipkin 官方 GitHub 仓库里的zipkin-storage-mysql目录下的 schema.sql 建表主要就是zipkin_spans、zipkin_annotations、zipkin_dependencies这几张核心表。然后启动 Zipkin 时通过环境变量指定存储类型STORAGE_TYPEmysql \ MYSQL_HOSTlocalhost \ MYSQL_PORT3306 \ MYSQL_USERzipkin \ MYSQL_PASSzipkin \ MYSQL_DBzipkin \ java -jar zipkin.jar配置完成后重启 Zipkin链路数据就会落到 MySQL 里。之后配合定时任务执行依赖分析表的数据生成逻辑就能看到服务间依赖关系的可视化视图了。我个人的经验是链路数据量超过千万级别的系统MySQL 就不太行了建议尽快切换到 Elasticsearch。但如果你只是中小规模系统一天几百万 Span 以内MySQL 完全够用一个 8G 内存的实例就能扛住。没必要为了上 ES 而上 ESES 集群的维护成本可不是开玩笑的。5. 常见问题与排查技巧实录5.1 常见问题速查表实践了这么多次我把踩过的坑和身边同事遇到过的问题整理成了一张速查表按出现频率排序问题现象根本原因解决方案Zipkin 界面上查不到任何链路采样率配置为 0 或未配置把spring.sleuth.sampler.probability设为 1.0Zipkin 界面上查不到任何链路Zipkin 地址配错或未配置检查spring.zipkin.base-urlSleuth 2.x或management.zipkin.tracing.endpointSleuth 3.x链路中间某一段丢失手动new RestTemplate没走容器管理的 Bean改为注入容器中已有的 RestTemplate链路中间某一段丢失服务间调用用了自研 HttpClient换成 Sleuth 支持的 Feign 或 RestTemplate或手动传递 Header日志中有 TraceId但 Zipkin 中没有对应链路客户端没有上报给 Zipkin 或上报失败检查客户端与 Zipkin 的网络连通性查看客户端启动日志中有无上报报错异步线程中出现孤儿链路ThreadLocal 不能跨线程传递用TraceableExecutorService包装线程池服务显示名称不对spring.application.name未配置或拼错正确配置服务名并重启应用界面数据显示有延迟客户端默认抽样上报数据上报存在积压等待几秒再刷新或调整上报队列大小5.2 排查链路丢失的几个突破口当链路数据不完整时我建议按照“入口 - 中间传递 - 上报”的顺序排查。先在入口服务确认请求是否生成了 TraceId。打开日志看输出的记录里有没有[order-service,xxxx,xxxx,true]这串标记。如果日志里连 TraceId 都没有说明 Sleuth 的自动装配就没生效优先检查依赖是否引入、启动类有没有被 Spring Boot 扫描到。再看服务间传递是否成功。调用下游服务前可以在下游服务日志里搜索上一个服务日志里的 TraceId如果搜到了说明传递链路没断如果搜不到说明上游请求到达下游时TraceId 没有被正确传递。这时候打开下游服务的入口日志看它是不是生成了一个全新的 TraceId如果是说明上游的 Header 没带过来或者被覆盖了。最后查上报环节。Sleuth 默认通过 HTTP 上报到 Zipkin如果上报失败服务端是收不到数据的。在客户端配置里调低上报队列的容量或者修改上报方式配合日志观察上报是否报错一般能定位到问题。我遇到过最诡异的情况是链路上报到了 Zipkin但界面上某些 Span 显示的时间明显不正常后来发现是服务所在服务器的系统时间没同步导致 Span 的时间戳对不上局部耗时出现负数。链路追踪对跨机器的时间同步要求极高生产环境的服务器必须配置 NTP 时间同步否则即使链路数据齐全时间维度上的分析也全是错的。这一点很容易被忽略排查的时候可以留意一下。5.3 日志集成的小技巧链路追踪的价值有一半体现在日志关联上。默认情况下Sleuth 已经把 TraceId 放进了日志的 MDC 中key 是traceIdSpanId 的 key 是spanId。只要你的日志配置文件里把这些字段加进输出模式就能实现日志按链路维度检索。在 logback-spring.xml 里加上这样的 patternpattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{spanId:-}] %logger{36} - %msg%n/pattern%X{traceId:-}表示从 MDC 中取 traceId取不到就显示为空。加完之后同一个请求经过所有服务产生的日志都带着同一个 traceId在日志平台上按这个 ID 一搜整个请求的服务端处理过程就全出来了。配合 Zipkin 的链路视图一个看全局一个看细节排查效率直接上一个台阶。我在实际项目里的体会是链路追踪这套东西搭建难度不高真正拉开差距的往往是对细节的敏感度。同一个功能有人只搭了个能用就跑有人会把采样率、自定义标签、异步上下文传递全都设计进去。后者的排查效率在线上问题爆发时会高出不止一个数量级。建议你先照着本文把三个服务的链路跑通然后再逐步加上自定义埋点和持久化把这些细节一个一个补上这套技能就成了你自己的工具箱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑