分布式中采用 Logback 的 MDC 机制与 AOP 切面结合串联日志 - 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

导读:在实际开发中,打印日志是十分重要的。在生产环境中,如果日志打得好可以快速地排查问题,而在分布式的场景下,一个请求会跨越多个节点,既一个业务

导读: 在实际开发中,打印日志是十分重要的。在生产环境中,如果日志打得好可以快速地排查问题,而在分布式的场景下,一个请求会跨越多个节点,既一个业务可能需要多个节点协调配合处理。那么日志将会分散,而为了更好的查看日志,我们需要将它们串联起来,这样便会使排查问题变得更佳轻松。

https://p26.toutiaoimg.com/origin/pgc-image/f66645395e144a9e9780412e57dccbf3?from=pc

举例一个简单分布式场景下使用串联日志的例子。场景如下:一笔支付请求从产品系统发起,期间经历了核心系统和网关系统最后调用银行系统实现资金划转,并逐步响应结果直到回到产品系统。这里暂且把整个支付流程看作是同步的,当这笔交易在生产环境中因其中某一环境出现异常时,我们需查看日志进行排查,而这笔交易因为流经多个系统,所以日志是分散的。这时候如果有一个唯一标识且能把所有日志串联起来那么将会方便和提高问题排查的效率。

https://p26.toutiaoimg.com/origin/pgc-image/3d60164e1ab0410790487f69df597ecf?from=pc

串联的核心要点是把 ID 做为一个请求必传参数。常见如采用手动打印日志,既在各个接口服务内多处加上 logger 打印,打印内容里加上串联 ID,如:

https://p26.toutiaoimg.com/origin/pgc-image/2db7daa8381f456bad858d4ad281a237?from=pc

但是,还有另一种更简便的打印方式,既是 MDC (Mapped Diagnostic Contexts) + AOP 切面结合。MDC 它是一个线程安全的存放诊断日志的容器。在处理请求前将请求的唯一标示放到 MDC 容器中,这个唯一标示会随着日志一起输出,以此来区分该条日志是属于那个请求的。并在请求处理完成之后清除 MDC 容器。

https://p26.toutiaoimg.com/origin/pgc-image/b707f7efea994f888ac7c41b8b397148?from=pc

使用 Logback 的 MDC 机制,需要在 logback.xml 日志模板中进行一些设置,在 logback.xml 中,通过使用 %X{ } 来占位,替换到对应的 MDC 中 key 的值。MDC 容器的 key 可以多次赋值,每一次赋值会覆盖上一次的值。

https://p26.toutiaoimg.com/origin/pgc-image/76fdb609b96b43edbf95ce00f762ccc7?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/b51af014988c42fc84d2f69eb90156d7?from=pc

往 MDC 容器中 put 入键值对,在日志打印时,日志会按照我们预先在 logback.xml 中的格式输出,而其中占位符会替换上 MDC 中对应 key 的 value 值。打印结果如下:

https://p26.toutiaoimg.com/origin/pgc-image/a0518c90bc9b48f1a7afc860caf1e829?from=pc

如编写一个切面,被调用的服务在执行操作前,切面会将关键信息输出日志并同时将串联 ID put 到容器中,使其能在接下来同一线程内输出的日志中都包含该串联 ID 信息,在将日志串联起来的同时也方便了日志的打印。

https://p26.toutiaoimg.com/origin/pgc-image/61f6010301d240a6b8d5141c5553833e?from=pc

除了自定义切面外,Logback 也提供了一个过滤器 MDCInsertingServletFilter,感兴趣的朋友可以去详细了解下。

https://p26.toutiaoimg.com/origin/dfic-imagehandler/1b1e46fb-a9ec-497a-813c-442d0d5bfbfc?from=pc

这里要特别要注意一点的是在主线程上,新起一个子线程,并由 java.util.concurrent.Executors 来执行它时,在早期的版本中子线程可以直接自动继承父线程的 MDC 容器中的内容,因为 MDC 在早期版本中使用的是 InheritableThreadLocal 来作为底层实现。但是由于性能问题被取消了,最后还是使用的是 ThreadLocal 来作为底层实现。这样子线程就不能直接继承父线程的 MDC 容器。

举个例子:

https://p26.toutiaoimg.com/origin/pgc-image/026f7a8379d14b059966e242c468b3a8?from=pc

例如:支付操作为异步时,网关接收了核心的支付请求后会新开一个线程去处理支付请求,并响应回核心受理成功(注意:这里受理代表接收到支付的请求,而不代表处理成功)。那这样做就会导致新的子线程 MDC 并没有继承父线程中的东西,导致响应结果时缺失串联 ID 信息,不能与支付请求关联起来。

根据以上问题,Logback 官方建议父线程新建子线程之前调用 MDC.getCopyOfContextMap() 方法将 MDC 内容取出来传给子线程,子线程在执行操作前先调用 MDC.setContextMap() 方法将父线程的 MDC 内容设置到子线程。

https://p26.toutiaoimg.com/origin/dfic-imagehandler/c087972a-57e8-47bc-b868-57d85839d79f?from=pc

以上就是分布式场景下一种较为不错的日志打印方式,通过结合 AOP 切面与 Logback 的 MDC 机制将多个系统间有关联的日志串联起来,有助于问题的排查及信息的查看。如果有其他不错的日志打印方式也欢迎提出,共同讨论学习。

感谢您的阅读,如果喜欢本文欢迎关注和转发,本头条号将坚持原创,持续分享 IT 技术知识。对于文章内容有其他想法或意见建议等,欢迎提出共同讨论共同进步