尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

分布式系统Stack Trace失效?TraceId链路追踪重建全链路排查实战

分布式系统Stack Trace失效?TraceId链路追踪重建全链路排查实战 1. 从一次线上故障看分布式 Stack Trace 的痛点1.1 单机 Stack Trace 为什么好用在单体应用时代排查问题是一件相对简单的事。应用内某个方法抛出异常后日志会记录完整的Stack Trace堆栈信息java.lang.NullPointerException: null at com.example.user.service.UserServiceImpl.getUser(UserServiceImpl.java:42) at com.example.user.controller.UserController.detail(UserController.java:18) at com.example.user.middleware.GlobalExceptionHandler.resolve(GlobalExceptionHandler.java:31)这段堆栈能告诉我们三件事异常类型是什么。异常发生在哪个类的哪个方法。异常从入口到出口经过了哪些调用层级每一层位于哪个文件、哪一行。因为在单机环境下整个调用链发生在同一个进程内JVM 的线程栈天然就是调用链。我们只需要打开日志文件CtrlF 搜到异常类名基本就能定位问题。这也是为什么很多从单体转微服务的开发者刚开始会非常不适应——明明日志里也有异常栈却总是定位不到根因。1.2 分布式环境下“有堆栈但定位不到根因”到了分布式系统情况完全不同。一个典型的用户请求会经过客户端请求 - API 网关 - 订单服务 - 用户服务 - 库存服务 - 数据库 - 缓存假设库存服务中发生了一个 SQL 异常订单服务捕获后抛出了业务异常网关再把它包装成 HTTP 500 返回给前端。最终我们在网关日志里看到的异常栈可能只有com.example.common.BusinessException: 商品库存扣减失败 at com.example.order.service.OrderServiceImpl.createOrder(OrderServiceImpl.java:88) at com.example.order.controller.OrderController.create(OrderController.java:25)这段堆栈并没有错但它缺少最关键的根因信息——库存服务中 SQL 执行失败的具体异常栈。根本异常发生在另一个进程被包装后原始异常链断裂了。分布式环境下Stack Trace 并不是“一个完整的调用栈”而是被切割成了多个片段散落在不同服务的日志文件里。如果没有合适的串联手段看到异常栈就跟盲人摸象一样能看见局部拼不出全貌。1.3 为什么“no stack trace available”会频繁出现排查分布式问题时还有一个经常碰到的情况日志里只有一行异常信息没有堆栈。2025-04-10 14:22:31.456 ERROR [order-service] [traceIdabc123] - 扣减库存失败: no stack trace available“no stack trace available” 直译是“没有可用的堆栈信息”。它并不是一种具体的异常类型而是一个日志输出提示含义是日志框架在输出异常的堆栈时拿不到完整的堆栈内容。这种情况在分布式系统里尤其常见原因包括异常对象被重新包装后丢失了原始堆栈。日志配置中关闭了堆栈输出。异步日志丢弃或截断了异常堆栈。异常在线程池提交时被吞掉。后面第 6 节会专门展开分析这里先记住一个结论在分布式系统中Stack Trace 必须通过链路追踪技术重新“拼接”起来否则多个服务的异常栈只是孤立碎片。2. 分布式 Stack Trace 失效的三个边界2.1 进程边界只有局部栈没有全局栈JVM 的线程栈是进程级别的。同一个进程内方法调用天然形成嵌套结构一旦跨进程比如使用 HTTP、RPC、MQ 调用另一个服务调用栈在进程边界就断了。例如订单服务调用库存服务时本质上只做了一件事发起一次 HTTP 请求。订单服务的线程栈里只记录了它调用了RestTemplate.exchange()至于库存服务内部经历了什么订单服务的 JVM 根本不知道。这种进程边界决定了如果只是简单地打印日志再多的异常信息也无法自动关联到同一个请求。2.2 异步边界线程池切断了上下文即使不跨服务只要使用了异步也会遇到问题。一个典型的场景是Async public void sendOrderMessage(Order order) { // 这里发生异常堆栈信息可能和主调用链对不上 }Spring 的Async会把任务提交给线程池执行。主线程的任务上下文、MDC 信息、TraceId都不会自动传递到子线程。异常发生时你看到的堆栈是“线程池线程”的完整堆栈但看不到它是从哪个订单、哪个用户请求、哪个 traceId 发起的。2.3 数据边界异常变量与上下文分离分布式环境中即使你拿到了完整堆栈也未必能定位问题因为堆栈只包含了“代码执行路径”不包含关键业务上下文。例如java.sql.SQLException: Deadlock found when trying to get lock at com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException...这段堆栈只能告诉你“MySQL 发生了死锁”但看不出是哪个订单号触发的涉及哪些商品事务中的 SQL 是什么锁竞争发生在哪两个事务之间这时候即使有 Stack Trace意义也非常有限。堆栈负责回答“代码怎么走”日志和上下文负责回答“业务发生了什么”两者必须结合。3. 分布式全链路 Stack Trace 的基础TraceId 链路标识3.1 TraceId 与 SpanId 基本模型要解决 Stack Trace 碎片化问题目前业界最通用的方案就是引入链路追踪Distributed Tracing。它的核心模型是TraceId一次完整请求链路的全局唯一 ID。从入口服务生成向后传递到所有被调用的服务。SpanId链路中的某一层调用单元 ID。每个服务处理一次调用就创建一个 Span。Parent SpanId指向调用方的 SpanId用父子关系串起整条链路。通过这三个字段就能把分散在各个服务日志中的异常栈重新组织成一条完整的调用链路。目前落地方案很多Zipkin、SkyWalking、Jaeger、OpenTelemetry以及 Spring Cloud 体系中的 Sleuth 和 Micrometer Tracing。无论底层实现是什么核心思想都是先有 TraceId然后日志必须带上 TraceId。3.2 基于 MDC 的统一日志上下文在 Java 中最常用的方式是使用MDCMapped Diagnostic Context。MDC 是 SLF4J 提供的线程本地上下文容器可以把 TraceId 放进 MDC日志输出时通过 pattern 自动打印。// 文件路径src/main/java/com/example/common/TraceIdUtil.java import org.slf4j.MDC; import java.util.UUID; public class TraceIdUtil { private static final String TRACE_ID traceId; public static String getTraceId() { return MDC.get(TRACE_ID); } public static void setTraceId(String traceId) { MDC.put(TRACE_ID, traceId); } public static void clear() { MDC.remove(TRACE_ID); } public static String initTraceId() { String traceId MDC.get(TRACE_ID); if (traceId null || traceId.isEmpty()) { traceId UUID.randomUUID().toString().replace(-, ); MDC.put(TRACE_ID, traceId); } return traceId; } }在入口处设置一个拦截器统一生成或接收 TraceId// 文件路径src/main/java/com/example/common/TraceIdFilter.java import org.slf4j.MDC; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; Component Order(1) public class TraceIdFilter implements Filter { private static final String TRACE_ID_HEADER X-Trace-Id; Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 优先接收上游传入的 TraceId否则新建 HttpServletRequest httpRequest (HttpServletRequest) request; String traceId httpRequest.getHeader(TRACE_ID_HEADER); if (traceId null || traceId.isEmpty()) { traceId TraceIdUtil.initTraceId(); } else { MDC.put(traceId, traceId); } chain.doFilter(request, response); } finally { // 请求结束必须清理防止线程池线程上下文污染 TraceIdUtil.clear(); } } }然后在 logback 配置中把%X{traceId}加进输出 pattern!-- 文件路径src/main/resources/logback-spring.xml -- property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40} [%X{traceId}] - %msg%n/ appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender这样配置之后所有日志都会携带 TraceId只要按 TraceId 过滤日志就能还原一次请求的全过程。3.3 跨线程传递 TraceId异步日志容易丢 TraceId根本原因是 MDC 存在 ThreadLocal 中子线程拿不到父线程的上下文。解决办法是在提交任务时手动把父线程的 MDC 上下文复制到子线程。Spring Boot 的线程池可以通过TaskDecorator实现统一处理// 文件路径src/main/java/com/example/common/MdcTaskDecorator.java import org.slf4j.MDC; import org.springframework.core.task.TaskDecorator; import java.util.Map; public class MdcTaskDecorator implements TaskDecorator { Override public Runnable decorate(Runnable runnable) { MapString, String contextMap MDC.getCopyOfContextMap(); return () - { try { MDC.setContextMap(contextMap); runnable.run(); } finally { MDC.clear(); } }; } }配置线程池时应用该装饰器// 文件路径src/main/java/com/example/common/ThreadPoolConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; Configuration public class ThreadPoolConfig { Bean(asyncExecutor) public Executor asyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(100); executor.setThreadNamePrefix(async-); executor.setTaskDecorator(new MdcTaskDecorator()); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }这种做法的好处是不会改变业务代码结构只需要在创建线程池时统一配置一次。3.4 跨服务传递 TraceIdTraceId 必须跟随 HTTP/RPC 请求一路传递。以 Spring Cloud OpenFeign 为例// 文件路径src/main/java/com/example/common/FeignTraceIdInterceptor.java import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; Configuration public class FeignTraceIdInterceptor { Bean public RequestInterceptor traceIdInterceptor() { return template - { String traceId TraceIdUtil.getTraceId(); if (StringUtils.hasText(traceId)) { template.header(X-Trace-Id, traceId); } }; } }如果使用的是RestTemplate则可以通过ClientHttpRequestInterceptor实现// 文件路径src/main/java/com/example/common/TraceIdClientInterceptor.java import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.StringUtils; import java.io.IOException; public class TraceIdClientInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String traceId TraceIdUtil.getTraceId(); if (StringUtils.hasText(traceId)) { request.getHeaders().add(X-Trace-Id, traceId); } return execution.execute(request, body); } }注册方式Configuration public class RestTemplateConfig { Bean public RestTemplate restTemplate() { RestTemplate restTemplate new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(new TraceIdClientInterceptor())); return restTemplate; } }下游服务只需要像刚才的TraceIdFilter一样优先从请求头读取 TraceId就能保证全链路日志中的traceId是同一个值。4. 完整实战基于 Spring Boot 搭建分布式链路追踪4.1 技术选型与版本说明下面用一个包含两个微服务的示例演示完整的分布式链路追踪搭建过程。order-service订单服务端口 8081模拟创建订单并调用库存服务。inventory-service库存服务端口 8082模拟扣减库存。链路追踪组件Zipkin Server端口 9411。基础框架Spring Boot 2.7.x Spring Cloud 2021.x Spring Cloud Sleuth 3.x。注意Spring Boot 3.x 之后Sleuth 已经迁移到io.micrometer.tracing配置方式有变化但 TraceId 的传递思想完全一致。本文示例以 Spring Boot 2.x 常见稳定组合演示重点理解链路传递机制。4.2 搭建 Zipkin Server如果本地有 Docker 环境可以直接一行命令启动 Zipkindocker run -d -p 9411:9411 --name zipkin openzipkin/zipkin没有 Docker 也可以从 Zipkin 官网下载对应的可执行 jarjava -jar zipkin-server-2.24.3-exec.jar启动后访问http://localhost:9411如果能打开 Zipkin 控制台说明服务端就绪。4.3 订单服务核心实现order-service的 pom.xml 需要引入以下依赖!-- 文件路径order-service/pom.xml -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring Cloud 统一版本管理 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version2021.0.8/version typepom/type scopeimport/scope /dependency !-- 链路追踪 Starter -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-sleuth/artifactId /dependency !-- Zipkin 客户端上报 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-sleuth-zipkin/artifactId /dependency /dependencies配置文件# 文件路径order-service/src/main/resources/application.yml server: port: 8081 spring: application: name: order-service zipkin: base-url: http://localhost:9411 discovery-client-enabled: false sleuth: sampler: # 生产环境一般不推荐 1.0这里演示时全量采样 probability: 1.0核心 Controller// 文件路径order-service/src/main/java/com/example/order/OrderController.java import com.example.order.OrderService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/order) public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService orderService; } GetMapping(/create) public String create(RequestParam(orderId) String orderId) { return orderService.createOrder(orderId); } }// 文件路径order-service/src/main/java/com/example/order/OrderService.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; Service public class OrderService { private static final Logger log LoggerFactory.getLogger(OrderService.class); private final RestTemplate restTemplate; public OrderService(RestTemplate restTemplate) { this.restTemplate restTemplate; } public String createOrder(String orderId) { log.info(创建订单开始, orderId{}, orderId); String inventoryUrl http://localhost:8082/inventory/deduct?orderId orderId; String result restTemplate.getForObject(inventoryUrl, String.class); log.info(库存服务返回结果: {}, result); return 订单创建成功, orderId orderId , 库存结果 result; } }RestTemplate建议显式配置并调整超时时间// 文件路径order-service/src/main/java/com/example/order/RestTemplateConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; Configuration public class RestTemplateConfig { Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(5000); return new RestTemplate(factory); } }因为spring-cloud-starter-sleuth会自动增强 RestTemplate、Feign 等组件所以这里不用手动写拦截器也能完成 TraceId 的跨服务传递。4.4 库存服务核心实现inventory-service的依赖与订单服务相同只需要修改配置和服务代码。# 文件路径inventory-service/src/main/resources/application.yml server: port: 8082 spring: application: name: inventory-service zipkin: base-url: http://localhost:9411 discovery-client-enabled: false sleuth: sampler: probability: 1.0// 文件路径inventory-service/src/main/java/com/example/inventory/InventoryController.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/inventory) public class InventoryController { private static final Logger log LoggerFactory.getLogger(InventoryController.class); GetMapping(/deduct) public String deduct(RequestParam(orderId) String orderId) { log.info(扣减库存开始, orderId{}, orderId); // 模拟异常场景orderId 以 error 开头时抛出异常 if (orderId ! null orderId.startsWith(error)) { log.error(扣减库存失败, 模拟运行时异常, orderId{}, orderId); throw new RuntimeException(inventory deduct failed, simulate error); } log.info(扣减库存成功, orderId{}, orderId); return 库存扣减成功; } }4.5 运行与验证分别启动两个服务后在浏览器访问http://localhost:8081/order/create?orderId10001返回结果中出现“订单创建成功”即链路正常。再访问http://localhost:8081/order/create?orderIderror-1024此时订单服务拿到的其实是库存服务返回的 HTTP 500 错误信息。打开 Zipkin 控制台http://localhost:9411搜索order-service可以看到一条完整的调用链路包含总调用耗时。调用了哪些服务。每个服务内部的 Span 和耗时。请求是否成功以及异常状态。如果此时在订单服务日志中看到类似堆栈说明虽然跨服务调用发生异常但通过链路追踪已经准确定位到“问题发生在 inventory-service”而不是在 order-service 里盲目排查。5. 日志聚合与堆栈重建5.1 按 TraceId 聚合日志有了 TraceId接下来最关键的一步是日志聚合。生产环境通常把日志收集到一起再按 TraceId 或业务单号搜索。常见的日志聚合方案有组件作用适用场景ELK (Elasticsearch Logstash Kibana)日志收集、索引、检索适合大团队、复杂查询Loki Grafana轻量级日志聚合与 Prometheus 技术栈集成适合中小团队部署成本低SkyWalking 日志插件自动关联 TraceId 与日志配合 SkyWalking APM 使用无论用哪种方案日志采集端最关键的是把 TraceId 作为结构化字段输出不能只写在日志正文里。推荐使用 JSON 格式输出日志!-- logback-spring.xml JSON 示例需要引入 logstash-logback-encoder 依赖 -- appender nameJSON classch.qos.logback.core.ConsoleAppender encoder classnet.logstash.logback.encoder.LogstashEncoder includeMdctrue/includeMdc customFields{appName:order-service}/customFields /encoder /appender这样在 Kibana 或 Grafana 中可以直接按traceId字段过滤效率远高于搜索日志文本。5.2 异常堆栈的完整记录策略分布式环境下的异常堆栈应该有意识地“留全”而不是只记e.getMessage()。推荐的异常记录方式// 推荐打印完整堆栈 log.error(扣减库存失败, orderId{}, traceId{}, orderId, TraceIdUtil.getTraceId(), e); // 不推荐只记录消息丢失堆栈 log.error(扣减库存失败: {}, e.getMessage());同时要注意跨服务包装异常时要保留根因异常// 不推荐把原始异常丢弃只抛业务异常 catch (Exception e) { throw new BusinessException(库存扣减失败); } // 推荐把原始异常传入保留根因 catch (Exception e) { log.error(库存扣减失败, orderId{}, orderId, e); throw new BusinessException(库存扣减失败, e); }这一步非常重要。很多“no stack trace available”都是因为代码中catch后没有把堆栈写进日志或者写日志时只用了e.getMessage()。5.3 跨服务重构完整堆栈当链路追踪体系搭建完成后虽然无法让 A 服务直接看到 B 服务的 Java 堆栈但可以通过“日志 TraceId”实现等价效果在入口服务拿到 TraceId。下游服务日志中输出相同 TraceId。通过日志聚合平台按 TraceId 搜索。把散落在多个服务的异常栈片段合并成一个完整的排查视图。这就相当于用“日志链路”重建了分布式环境下的全局 Stack Trace。6. 高频问题排查no stack trace available6.1 问题现象日志中只输出一行异常描述没有堆栈ERROR [order-service] [traceId8f3a9c2b] - biz error: no stack trace available有些时候异常类名还在但堆栈行数为空。6.2 常见原因分析问题现象常见原因解决思路异常日志只有一行无堆栈日志配置中使用了%ex{0}或%nopex关闭堆栈检查日志 pattern去掉堆栈裁剪选项堆栈只有第一行%throwable{short}截断了堆栈使用完整%throwable或%ex有异常栈但调用方看不到原始堆栈只打印在下游服务调用方日志只有错误状态配合 TraceId 查下游日志异步线程中无堆栈MDC 没有传递日志上下文丢失使用TaskDecorator传递 MDCJSON 日志无stack_trace字段日志 encoder 未配置异常字段输出检查 logstash-encoder 或日志结构6.3 典型错误配置示例logback 中把异常堆栈长度设置为 0是最常见的配置问题!-- 错误示例禁止输出异常堆栈 -- pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%ex{0}%n/pattern上面%ex{0}表示输出 0 行堆栈运行后日志里只会显示-后面的异常信息然后就是“no stack trace available”。正确配置应该是!-- 正确示例完整输出异常堆栈 -- pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} [%X{traceId}] - %msg%n%ex{full, 50}/pattern%ex{full, 50}表示输出最多 50 行完整堆栈。如果不希望限制行数直接写%ex或%throwable即可。6.4 排查步骤排查 “no stack trace available” 问题时建议按以下顺序检查检查日志配置文件全局搜索%ex{0}、%nopex、%throwable{short}等关键字确认是否裁剪了堆栈。检查代码日志语句确认打印异常时是否传入了异常对象e例如log.error(xxx, e)而不是log.error(xxx, e.getMessage())。检查是否有异常包装catch后是否重新抛出了新异常且没有把原始异常传入新异常。检查异步场景确认 MDC 上下文是否有传递异步线程池是否配置了TaskDecorator。检查日志采集端如果使用 JSON 日志确认日志采集器拿到的是完整的stack_trace字段。检查日志存储某些日志系统对超大字段有截断策略需要查看采集端是否截断了堆栈内容。最后在本地复现问题后优先用Thread.currentThread().getStackTrace()验证堆栈完整性区分是“日志配置问题”还是“代码逻辑问题”。7. 分布式 Stack Trace 的最佳实践与工程建议7.1 日志规范所有日志必须输出traceId且建议放到MDC中而不是手动拼接到消息里。统一日志 pattern禁止各服务自行定义不同格式。推荐使用 JSON 日志格式便于日志平台结构化检索。异常日志必须输出完整堆栈不要使用e.getMessage()替代e。关键业务日志要带上业务单号orderId、userId与traceId共同组成排查维度。7.2 异常处理规范在分布式系统中不要轻易吞掉根因异常。即使要做降级或补偿也要先记录完整的异常堆栈。抛出业务异常时保留 cause例如new BusinessException(扣减库存失败, e)。区分“可预期业务异常”和“系统异常”。业务异常可以不打印完整堆栈但系统异常网络、数据库、外部服务必须打完整堆栈。在网关层不要过度包装异常。如果包装一定要把下游的 HTTP 状态码、服务名、异常体透传给监控系统。7.3 链路追踪选型建议需求推荐方案说明已有 Spring Cloud 生态Sleuth Zipkin集成成本低适合 Java 技术栈需要 Java/.NET/Go 多语言支持OpenTelemetry Jaeger标准开放社区活跃需要字节码增强、无侵入上报SkyWalking对业务代码侵入小已有 Prometheus Grafana 体系Tempo 或 Loki 的 trace 集成可观测性栈统一选定方案后务必统一TraceId的 header 名称。企业内建议使用标准头例如X-Trace-Id或遵循 W3C 的traceparent避免各团队自行命名导致串联不上。7.4 关于采样率的取舍全量采样probability1.0在低流量环境没问题但在高并发环境下会带来额外存储和网络开销。推荐策略核心交易链路全量采样或 100% 采集错误链路。非核心链路按比例采样如 10%。无论采样率多低错误链路必须全采即Sampler要针对error状态特殊处理。Zipkin 等组件都支持基于标签或错误状态决定是否采样生产环境一定要配置好。7.5 生产环境排障工具箱一个成熟的分布式排障体系至少包含以下能力日志中能按traceId查询完整调用链。调用链中能看到各服务耗时和状态。异常信息能和告警事件关联。关键业务指标订单成功率、库存扣减耗时能落到具体 Trace 上。有了这些能力Stack Trace 才能真正成为分布式系统的“指南针”而不只是异常对象的附庸。8. 总结从单机到分布式Stack Trace 的含义和用法发生了本质变化。单机环境里异常栈就是完整的调用链分布式环境里Stack Trace 只是调用链的一个切片必须靠 TraceId 和管理 Span 才能重新拼接。回到主题本身解决分布式 Stack Trace 问题核心思路可以概括为三点生成在入口为每个请求生成全局唯一的 TraceId。传递通过 HTTP 头、RPC 元数据、MQ 消息头把 TraceId 传播到所有下游服务。聚合日志统一携带 TraceId并通过日志平台按 TraceId 聚合、检索、重建全链路栈。同时不要忽略日志配置和异常处理等细节。一个%ex{0}就能让堆栈“消失”一个catch块也能让根因“失踪”。在分布式系统里Stack Trace 从来不是凭空出现的它靠的是每个服务、每行日志、每个异常处理点的共同维护。如果本文对你有帮助建议先用两个本地服务把 Zipkin 链路跑通再结合生产环境的日志聚合平台做验证。只有亲自动手把 traceId 串起来才能真正理解分布式全链路排查的每个环节。
返回列表