前言:
排查问题是程序员的基本能力也是必须要会的,在开发环境,我们可以debug,但是一旦到了服务器上,就很难debug了,最有效的方式就是通过日志揪出bug,而一次请求的日志如果没有一个唯一的链路标识(我们下边称他为traceId),单靠程序员人工分析的话,费时费力,尤其是请求量高频的接口,更是雪上加霜,排查问题效率大打折扣,作为程序员,低效的方式是忍不了的!!! 本文我将用一次实战演练,来演示常用框架/中间件/多服务 之间如何传递traceId
本文大概有如下内容:
-
- 链路追踪简述和自实现思路
-
- 单服务内如何实现链路id的输出
-
- 垮服务调用时,实现链路id传递的各种方式
( 包含http(openFeign,httpClient restTemplate)、rpc(motan、 dubbo)、mq(RocketMq)
)
-
- 异步调用时,如何解决log4j2自带的ThreadLocal丢失链路id问题
-
- 起4个服务,进行调用,观察链路追踪的效果
1、链路追踪实现简述
所谓链路追踪,就是为了 把整个请求链路从头到尾串起来,不管调用链路有多深,多复杂,只要将一次链路完整无误的串联起来,就是合格的链路追踪功能。
业界不乏skywalking zipkin
等等链路追踪方面牛逼的框架,但是我们为了更轻量更灵活可控同时也是抱着学习心态,所以自己来实现链路追踪。
首先想实现链路追踪,有两点是核心,实现了这两点,问题也就不大了
- traceId 如何在
本地
(或者说单服务内
)传递
? - 在
分布式环境
中,traceId如何跨服务/中间件 传递
?
2、单体服务 的链路追踪
首先我们先讲下单服务内的链路传递
作为java开发,最常用的就是slf4j来实现打印日志的功能(但是slf4j并不没有实现逻辑,因为 slf4j整个的定义是一个日志门面,该包中并无具体的实现,实现都是在 比如:logback log4j2
等等日志实现框架中)
slf4j的门面
不仅给我们提供了打印日志的功能,还提供了 org.slf4j.MDC
类, 该类的作用大概如下:
映射诊断上下文(Mapped Diagnostic Context,简称MDC)是一种工具,用于区分不同来源的交错日志输出。当服务器几乎同时处理多个客户机时,日志输出通常是交错的。 MDC是基于每个线程进行管理的 。
上边这个官方解释,最重要的一句话就是 MDC是基于每个线程进行管理的
上边这个太官方,说下我个人对MDC的理解:
他是一个日志的扩展,
扩展的目的就是给 每个线程 输出的日志打上一个标记
(一个线程只有一个标记且不能重复一般使用uuid即可),这样我们在查看日志时候,就可以根据这个标记来区分调用链路
了ps: 当然了, 光往MDC中设置当前线程的链路id也是不行的你还得在log4j2.xml文件中,设置占位符,这样最终输出的日志才会带链路信息。如何设置会在 2.1节 有讲。
从代码层面看下 MDC做了啥:
- MDC类中通过一个 MDCAdapter实例
- 调用MDCAdapter的put get remove clear等方法。
- 而put get remove clear 具体的实现是不同厂商来做的 比如我常用的log4j2包中就实现了 MDCAdapter 接口,实现在 org.apache.logging.slf4j.Log4jMDCAdapter类中
- Log4jMDCAdapter类中使用了一个 ThreadContext来执行put get remove clear逻辑,
- 而ThreadContext 中又是一个 ThreadContextMap ThreadContextMap是一个接口 ,有不同的实现
- 其中默认的是 DefaultThreadContextMap 该类中维护了一个 ThreadLocal<Map<String, String>> localMap 类型的成员变量 其中map中的k,v 就是你调用MDC.put(k,v); 时传入的k v
- 最终你调用MDC.put(k,v);时候传入的k和v会被放倒 localMap这个ThreadLocal中去。
- 在你给某个线程设置了key,value后 ,log4j2在打印日志时候,将会去log4j2.xml文件中找 占位符等于 key的,