点击上方“方志朋”,选择“设为星标”

做积极的人,而不是积极废人

作者:朱乐陶,软件架构师,具备多年Java开发及架构设计经验,擅长微服务领域

作者博客:https://blog.csdn.net/zlt2000

背景

开发排查系统问题用得最多的手段就是查看系统日志,在分布式环境中一般使用ELK来统一收集日志,但是在并发大时使用日志定位问题还是比较麻烦,由于大量的其他用户/其他线程的日志也一起输出穿行其中导致很难筛选出指定请求的全部相关日志,以及下游线程/服务对应的日志。

解决思路

每个请求都使用一个唯一标识来追踪全部的链路显示在日志中,并且不修改原有的打印方式(代码无入侵) 使用Logback的MDC机制日志模板中加入traceId标识,取值方式为%X{traceId}

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

方案实现

由于MDC内部使用的是ThreadLocal所以只有本线程才有效,子线程和下游的服务MDC里的值会丢失;所以方案主要的难点是解决值的传递问题,主要包括以几下部分:

  • API网关中的MDC数据如何传递给下游服务

  • 服务如何接收数据,并且调用其他远程服务时如何继续传递

  • 异步的情况下(线程池)如何传给子线程

修改日志模板

logback配置文件模板格式添加标识%X{traceId}

网关添加过滤器

生成traceId并通过header传递给下游服务

@Component
public class TraceFilter extends ZuulFilter {@Autowiredprivate TraceProperties traceProperties;@Overridepublic String filterType() {return FilterConstants.PRE_TYPE;}@Overridepublic int filterOrder() {return FORM_BODY_WRAPPER_FILTER_ORDER - 1;}@Overridepublic boolean shouldFilter() {//根据配置控制是否开启过滤器return traceProperties.getEnable();}@Overridepublic Object run() {//链路追踪idString traceId = IdUtil.fastSimpleUUID();MDC.put(CommonConstant.LOG_TRACE_ID, traceId);RequestContext ctx = RequestContext.getCurrentContext();ctx.addZuulRequestHeader(CommonConstant.TRACE_ID_HEADER, traceId);return null;}
}

下游服务增加spring拦截器

接收并保存traceId的值拦截器

public class TraceInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String traceId = request.getHeader(CommonConstant.TRACE_ID_HEADER);if (StrUtil.isNotEmpty(traceId)) {MDC.put(CommonConstant.LOG_TRACE_ID, traceId);}return true;}
}

注册拦截器

public class DefaultWebMvcConfig extends WebMvcConfigurationSupport {@Overrideprotected void addInterceptors(InterceptorRegistry registry) {//日志链路追踪拦截器registry.addInterceptor(new TraceInterceptor()).addPathPatterns("/**");super.addInterceptors(registry);}
}

下游服务增加feign拦截器

继续把当前服务的traceId值传递给下游服务

public class FeignInterceptorConfig {@Beanpublic RequestInterceptor requestInterceptor() {RequestInterceptor requestInterceptor = template -> {//传递日志traceIdString traceId = MDC.get(CommonConstant.LOG_TRACE_ID);if (StrUtil.isNotEmpty(traceId)) {template.header(CommonConstant.TRACE_ID_HEADER, traceId);}};return requestInterceptor;}
}

解决父子线程传递问题

主要针对业务会使用线程池(异步、并行处理),并且spring自己也有@Async注解来使用线程池,要解决这个问题需要以下两个步骤

重写logback的LogbackMDCAdapter

由于logback的MDC实现内部使用的是ThreadLocal不能传递子线程,所以需要重写替换为阿里的TransmittableThreadLocal

TransmittableThreadLocal 是Alibaba开源的、用于解决 “在使用线程池等会缓存线程的组件情况下传递ThreadLocal” 问题的 InheritableThreadLocal 扩展。若希望 TransmittableThreadLocal 在线程池与主线程间传递,需配合 TtlRunnable 和 TtlCallable 使用。

TtlMDCAdapter类

package org.slf4j;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.spi.MDCAdapter;
public class TtlMDCAdapter implements MDCAdapter {/*** 此处是关键*/private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();private static TtlMDCAdapter mtcMDCAdapter;static {mtcMDCAdapter = new TtlMDCAdapter();MDC.mdcAdapter = mtcMDCAdapter;}public static MDCAdapter getInstance() {return mtcMDCAdapter;}

其他代码与ch.qos.logback.classic.util.LogbackMDCAdapter一样,只需改为调用copyOnInheritThreadLocal变量

TtlMDCAdapterInitializer类用于程序启动时加载自己的mdcAdapter实现

public class TtlMDCAdapterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {//加载TtlMDCAdapter实例TtlMDCAdapter.getInstance();}
}

扩展线程池实现

增加TtlRunnable和TtlCallable扩展实现TTL

public class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {@Overridepublic void execute(Runnable runnable) {Runnable ttlRunnable = TtlRunnable.get(runnable);super.execute(ttlRunnable);}@Overridepublic <T> Future<T> submit(Callable<T> task) {Callable ttlCallable = TtlCallable.get(task);return super.submit(ttlCallable);}@Overridepublic Future<?> submit(Runnable task) {Runnable ttlRunnable = TtlRunnable.get(task);return super.submit(ttlRunnable);}@Overridepublic ListenableFuture<?> submitListenable(Runnable task) {Runnable ttlRunnable = TtlRunnable.get(task);return super.submitListenable(ttlRunnable);}@Overridepublic <T> ListenableFuture<T> submitListenable(Callable<T> task) {Callable ttlCallable = TtlCallable.get(task);return super.submitListenable(ttlCallable);}
}

场景测试

测试代码如下

api网关打印的日志

网关生成traceId值为13d9800c8c7944c78a06ce28c36de670

请求跳转到文件服务时打印的日志

显示的traceId与网关相同,这里特意模拟发生异常的场景

ELK聚合日志通过traceId查询整条链路日志

当系统出现异常时,可直接通过该异常日志的traceId的值,在日志中心中询该请求的所有日志信息

源码下载

附上我的开源微服务框架(包含本文中的代码),欢迎 star 关注

https://gitee.com/zlt2000/microservices-platform

热门内容:    

  • RateLimiter 源码分析(Guava 和 Sentinel 实现)

  • 干货 | 阿里巴巴混沌测试工具ChaosBlade两万字解读

  • API 网关从入门到放弃

  • 面试必备:Java线程池解析

  • 挑战10个最难回答的Java面试题(附答案)

  • 跳来跳去,到底去大公司还是小公司?

  • 史上最全的 Linux Shell 文本处理工具集锦,快收藏!

  • 为什么推荐 Java 程序员使用 Google Guava 编程

喜欢就点个"在看"呗^_^

日志排查问题困难?分布式日志链路跟踪来帮你相关推荐

  1. 五分钟学会 Spring Cloud Sleuth:分布式请求链路跟踪(小白必看,一看就会教程)

    Spring Cloud Sleuth:分布式请求链路跟踪 Spring Cloud Sleuth 简介 给服务添加请求链路跟踪 整合Zipkin获取及分析日志 使用Elasticsearch存储跟踪 ...

  2. java https请求_Spring Cloud Sleuth:分布式请求链路跟踪

    Spring Cloud Sleuth 是分布式系统中跟踪服务间调用的工具,它可以直观地展示出一次请求的调用过程,本文将对其用法进行详细介绍. Spring Cloud Sleuth 简介 随着我们的 ...

  3. python分布式日志收集系统_分布式日志收集系统Scribe原理

    1.分布式日志收集系统:背景介绍 许多公司的平台每天会产生大量的日志(一般为流式数据,如,搜索引擎的pv,查询等),处理这些日志需要特定的日志系统,一般而言,这些系统需要具有以下特征: (1) 构建应 ...

  4. (七)日志采集工具sleuth--分布式链路跟踪(zipkin)

    微服务架构上通过业务来划分服务的,通过REST调用,对外暴露的一个接口,可能需要很多个服务协同才能完成这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败.随着业务的不断 ...

  5. 配置中心、消息队列、分布式服务链路跟踪

    配置中心 配置中心定义 配置中心目的 配置中心选型 开源配置中心 选型对比 配置中心是AP模型? 比如将超时时间从100毫米修改为200毫米1秒后生效还是1分钟生效都没有关系无非是用户体验的问题 1秒 ...

  6. 分布式全链路跟踪系统-skywalking

    什么是链路追踪 本节视频 [视频]Spring Cloud Alibaba-SkyWalking-分布式链路追踪 微服务架构是通过业务来划分服务的,使用 REST 调用.对外暴露的一个接口,可能需要很 ...

  7. SpringCloud学习笔记(十二)Sleuth 分布式请求链路跟踪

    目录 一.Sleuth介绍 1.为什么会出现这个技术?需要解决哪些问题? 2.是什么 二.搭建链路监控步骤 1.zipkin搭建安装 1)下载 2)运行jar 3)运行控制台 4)术语 2.服务提供者 ...

  8. SpringCloud 分布式链路追踪、分布式日志系统

    目录 概述 Zipkin的使用 搭建zipkin server zipkin dependencies 在服务中添加zipkin的相关配置 SkyWalking的使用 部署skywalking web ...

  9. 各大厂分布式链路跟踪系统架构对比

    随着互联网架构的扩张,分布式系统变得日趋复杂,越来越多的组件开始走向分布式化,如微服务.消息收发.分布式数据库.分布式缓存.分布式对象存储.跨域调用,这些组件共同构成了繁杂的分布式网络,那现在的问题是 ...

最新文章

  1. 一文读懂PID控制算法
  2. yarn RM crash问题一例
  3. CCNA学习心得(一)
  4. 浅析Condition与等待通知机制
  5. 对比图像分类五大方法:KNN、SVM、BPNN、CNN和迁移学习
  6. 家居海报PSD模板,是否也是你理想的生活!
  7. SAP License:SAP MM物料管理
  8. 输入一个日期,计算这个日期与 2018-03-12差多少天;
  9. 寻找相同的乱序字符串
  10. cyyz: Day 4 网络流整理
  11. javascript弹窗和基本数据类型
  12. 如何编写Word文档 多级编号
  13. 照片如何换背景?分享两个快速换背景的方法
  14. ubuntu 键盘delete删除文件失效
  15. SO逆向入门实战教程四:mfw
  16. 计算机windows无响应,win10电脑开软件自动无响应怎么办_win10打开软件没响应的解决方法...
  17. HTML文字阴影火焰,本教程火焰字效果非常逼真
  18. 计算机二级没考过考计算机三级,怎么样才能学好计算机二级,考了两次还没有过啊...
  19. sqlserver触发器Tigger使用及示例
  20. 爱奇艺攻防演练平台的探索实践(文末有直播福利)

热门文章

  1. 8.29 对象?数组?
  2. Jzzhu and Chocolate
  3. Task04:青少年软件编程(Scratch)等级考试模拟卷(一级)
  4. LeetCode实战:删除排序数组中的重复项
  5. LeetCode实战:x 的平方根
  6. Matlab数据的可视化 -- 视角与色彩控制
  7. 【算法导论】冒泡排序 选择排序
  8. centos 默认mysql_centos改变mysql默认目录
  9. 基于 Python 环境搭建 - YOLO 实现吸烟行为监测
  10. Python爬虫并自制新闻网站,太好玩了