分布式系统中如何优雅地追踪日志
最后更新于
需要一个全服务唯一的id,即traceId,如何保证?
traceId如何在服务间传递?
traceId如何在服务内部传递?
traceId如何在多线程中传递?
我们一一来解答:
全服务唯一的traceId,可以使用uuid生成,正常来说不会出现重复的;
关于服务间传递,对于调用者,在协议头加上traceId,对于被调用者,通过前置拦截器或者过滤器统一拦截;
关于服务内部传递,可以使用ThreadLocal传递traceId,一处放置,随处可用;
关于多线程传递,分为两种情况:
子线程,可以使用InheritableThreadLocal。
线程池,需要改造线程池对提交的任务进行包装,把提交者的traceId包装到任务中。
比如,上面这个系统,系统入口在A处,A调用B的服务,B里面又起了一个线程B1去访问D的服务,B本身又去访问C服务。
我们就可以这么来跟踪日志:
所有服务都需要一个全局的InheritableThreadLocal保存服务内部traceId的传递;
所有服务都需要一个前置拦截器或者过滤器,检测如果请求头没有traceId就生成一个,如果有就取出来,并把traceId放到全局的InheritableThreadLocal里面;
一个服务调用另一个服务的时候把traceId塞到请求头里,比如http header;
改造线程池,在提交的时候包装任务,这个工作量比较大,因为服务内部可能依赖其它框架,这些框架的线程池有可能也需要修改;
我们模拟A到B这两个服务来实现一个日志跟踪系统。
为了简单起见,我们使用SpringBoot,它默认使用的日志框架是logback,而且Slf4j提供了一个包装了InheritableThreadLocal
的类叫MDC,我们只要把traceId放在MDC中,打印日志的时候统一打印就可以了,不用显式地打印traceId。
我们分成三个模块:
公共包:封装拦截器,traceId的生成,服务内传递,请求头的传递等;
A服务:只依赖于公共包,并提供一个接口接收外部请求;
B服务:依赖于公共包,并内部起一个线程池,用于发送B1->D的请求,当然我们这里不发送请求,只在线程池中简单地打印一条日志;
TraceFilter.java
前置过滤器,用拦截器实现也是一样的。
从请求头中获取traceId,如果不存在就生成一个,并放入MDC中。
TraceThreadPoolExecutor.java
改造线程池,提交任务的时候进行包装。
TraceAsyncConfigurer.java
改造Spring的异步线程池,包装提交的任务。
HttpUtils.java
封装Http工具类,把traceId加入头中,带到下一个服务。
A服务通过Http调用B服务。
A服务的日志输出格式:
中间加了[%X{traceId}]
一串表示输出traceId。
B服务内部有两种跨线程调用:
利用Spring的异步线程池。
使用自己的线程池。
BController.java
BService.java
B服务的日志输出格式:
中间加了[%X{traceId}]
一串表示输出traceId。
打开浏览器,输入http://localhost:8001/a?name=andy
。
A服务输出日志:
B服务输出日志:
可以看到,A服务成功生成了traceId,并且传递给了B服务,且B服务线程间可以保证同一个请求的traceId是可以传递的。