分布式日志中添加traceid(分布式存储)
在上一篇文章
如何设计一个单点登录系统(3)?
中给大家介绍了一下可跨域单点登录系统代码层的实现逻辑,今天想跟大家分享一下分布式日志跟踪的问题。
为什么要有分布式日志跟踪?
作为研发的同学,你是否也遇到过这样的问题? 线上有个订单有问题,赶紧去查查日志,线上服务器那么多,怎么查呢,一台台来吧,第一台,第二台,第三台…终于查到下单时请求的机器了,赶紧定位问题吧,结果发现日志那么多,到底哪些日志是本次请求的啊,根据订单号grep一下吧, 哎,才这么点,而且看不到详细点的日志,异常堆栈也看不到,grep -n试试吧,天啦,那么多无用的日志也显示出来了, 想要的数据还是那么难找,入库的日志在哪里?操作缓存的日志在哪里啊?调用渠道的入参,返回值日志在哪里啊?这次请求的入参, 返回值日志在哪里啊?这次请求总响应时间到底是多少啊?这几乎是大家会遇到的共同问题,对于这些问题,笔者自己基于slf4j的MDC提供了一套开源工具,目前已经托管到 github上,而且已经应用于生产环境,github地址
首先讲一下实现思路,我们在输出日志的时候,一般都是这样
我们一般就是在这里拼接我们想要的字符串,这样一来的话我在一次请求中,如果希望能够通过某个字符串标示的话是不是就能解决这个问题呢,比如订单号,笔者之前确实见过类似这样的代码:
这种方式确实能解决问题,我通过一个orderId就能查到所有相关的日志,但是问题来了,我想要输出日志的时候拼接字符串需要orderId,那我所有的方法调用里岂不是都得传递订单号?太麻烦啦,为了输出日志还得我所有的方法都传递订单号,那我们能不能把这个订单号放到一个地方,而且只有这次请求中的代码能够拿到此参数?此时很多读者可能会想到可以用ThreadLocal,的确,我们可以使用ThreadLocal来解决此问题,于是代码变成了这样
这种方式虽然解决了需要传递参数的问题,但是代码不优雅,主要在于一下方面:
-
需要开发者自己去new一个ThreadLocal的对象,而且往里面set值
-
开发者输出日志时还需要去取ThreadLocal里的值,我只是做业务开发的,我的代码中根本不需要关注订单号,为啥还要去关注从哪里取订单号
看到这里大家不要灰心丧气,要相信你所遇到的问题是大家都会遇到的问题,我们的前辈们自然也会遇到,而且那些大神们肯定也会去想办法解决这个问题。
既然我们不想显示的去用代码获取orderId,甚至不想输出日志时用代码显示去获取,了解过log4j,logback的人都应该很熟悉PatternLayout这个组件,这个组件用于格式化最终的日志内容,那么我们能不能考虑在这个上面做文章呢?通过某种表达式的方式指定格式化时需要渲染的变量,然后以某种方式去约定变量的值如何获取,这样的话我们就可以保证最终的内容在格式化的时候动态生成,而不需要程序显示去拼装,答案是肯定的,那就是我前面提到的MDC,说到这里可能您已经想跃跃欲试了,那就先满足一下您的好奇心,接入方式如下:
-
HTTP接口接入方式
在web.xml中配置MDC过滤器
-
Dubbo接口接入方式
在resources目录下添加纯文本文件META-INF/dubbo/com.alibaba.dubbo.rpc.Filter,内容如下
然后修改dubbo的provider或者consumer配置文件,添加filter,如下:
以上是HTTP接口和Dubbo接口需要单独做的配置,接下来是logback.xml需要做的配置:
在logback.xml配置文件中Appender的layout添加 [TRACE_ID:%X{TRACE_ID}],例如
这里[TRACE_ID:%X{TRACE_ID}]就是MDC和PatternLayout约定的表达式,%X{TRACE_ID}表示这里只是一个占位符,且最终需要根据MDC中的TRACE_ID的值来填充,所以不管是我的ServletLogFilter还是DubboLogFilter的作用都是在请求进入系统都时候去初始化TRACE_ID,然后绑定到MDC中,大家如果有使用其他的RPC框架也可以使用类似的方案去实现。
这样配置以后,开发者就可以心无旁骛的去写代码,输出日志的时候只需要输出自己想输出的内容即可,不用再关注如何获取orderId之类的事情,这种事情就留给Filter和MDC去做吧,然后我们就可以先根据订单号或者其他参数先查到那条日志的traceId,然后根据traceId就能查到此次请求所有的日志
这种方式在没有切换进程的时候是OK的,但是由于MDC底层是基于InheritableThreadLocal实现的,当我们直接new一个Thread的子类或者Runnable的实现类并直接调用其start()方法时, MDC里的值是能成功继承给子线程的,但是如果任务是提交给线程池执行的话,就需要我们做一些额外的工作,这里我提供了一个工具类和一个包装类, 直接这样使用就OK
核心代码如下
做到这一步,本机内的日志通过一个TRACE_ID就可以关联起来了,但是目前大部分系统都不是一个只对外提供接口的服务,难免会涉及到调用其他服务提供的接口,这样的话就需要能够将TRACE_ID在调用过程中传递,而且不影响正常参数的签名等逻辑,对于Dubbo接口的调用我已经在DubboLogFilter中封装了,对于Http请求我也提供了对应的工具类HttpClientUtil,在发送Http请求的时候将当前MDC中的TRACE_ID放到Http请求头,同时在ServletLogFilter里会去解析请求头里的TRACE_ID然后Set到MDC,这样便可以实现一次调用链中所有的日志都拥有相同的TRACE_ID。
虽然我现在保证了一次请求中所有的日志都拥有相同的TRACE_ID,但是依然没有解决需要一台台机器去查的问题,依然很费时间,所以现在业界相当成熟的ELK解决方案应运而生,不过ELK解决方案不是本文的重点,本文的重点在于如何让一次分布式调用链中所有的日志都拥有相同的TRACE_ID,大家在使用MDC的过程不仅仅可以加TRACE_ID,我们完全可以根据自身业务把用户id,orderId,流量染色等信息都加入到MDC中,然后接入ELK平台,为不同的字段建索引,最后对外开放一个 简单易用的Web页面,供开发人员和技术支持的同学使用。
最后非常感谢大家的阅读,您的每一次阅读,每一次转发,每一次评论,每一个赞都是我继续写文章的动力,我也会继续坚持原创,继续努力,争取为大家带来更好的文章!
如发现本站有涉嫌抄袭侵权/违法违规等内容,请联系我们举报!一经查实,本站将立刻删除。