Spring日志扩展

分布式的流行导致程序的调用关系越来越复杂,一次调用可能会涉及到十几个微服务的运转和几十个代码块的执行,特别是与订单和支付相关的业务。此外还会涉及到各个部门的沟通和协作。

当线上项目出现问题,如何在海量日志中快速有效地定位到故障点,就显得至关重要。

通过调用链,利用一个自上而下全局的调用id,把每一次请求调用过程完整的串联起来,将日志以调用id为维度进行汇总和分类,这样就可以实现对调用链的跟踪和监控。目前有许多比较成的分布式跟踪系统,如Google的Dapper,Twitter的zipkin,淘宝的鹰眼,京东的Hydra等。

今天笔者就来为大家分享一下,如何一步步通过扩展现有的日志组件,实现一个简单的调用跟踪功能,从而能够使用flowId(即调用id)来对日志进行归类,达到快速定位bug的效果。

1、生成、传递和输出flowId

如何生成flowId?这里以rest服务为例。可以让所有的Request参数就都继承一个BaseRequest,BaseRequest中包括一个flowId,

Spring日志扩展_第1张图片

这样所有的请求都可以自带flowId,也可以随着方法地调用一层一层地传递下去,办法和思路都很简洁,但实施起来却有诸多隐患:

会对项目之前所有的Request类进行修改。

按照当前思路,日志输出的时候还需要在原有的log代码上添加相应的log输出操作。

若是无参方法,那岂不是要专门添加参数来传递这个flowId?

总的来说,代码修改量太大,回归测试点较多,容易影响现有的程序。

如何才能最方便地生成、传递和输出flowId呢? slf4j的MDC可以满足我们的要求。MDC(Mapped Diagnostic Context)为每个线程建立一个独立的存储空间,MDC 中包含的内容可以被同一线程中执行的代码所访问。开发人员可以根据需要把信息以 key/value 对的形式存储在 Map 中,当需要记录日志时,只需要从 MDC 中获取所需的信息即可,

下面介绍一下使用方法:

// 清空map所有的条目。

public static void clear();

// 根据key值返回相应的对象

public static Object get(String key);

//返回所有的key值.

public static Enumeration getKeys();

//把key值和关联的对象,插入map中

public static void put(String key, Object val),

//删除key对应的对象

public static  remove(String key)

通过MDC.put(“flow-id”, flowId),将flowId放入上下文,key为flow-id,然后指定log输出格式为:

其中%X{flow-id}就是用来读取MDC里面的key为flow-id的值,即可在log中输出flowId。

2、flowId在服务之间传递

上面说到,MDC是基于上下文传递的,所以原生的MDC信息不能跨服务调用,但是flowId的使用都遵守统一的原则:在使用前赋值,在使用后销毁。这一原则可以帮助我们找到切入点,在这里,笔者就以RabbitMQ RPC为例,来说一下我的解决方案。

Spring AMQP提供了对rabbitMQ的封装,参考下面配置文件:

Spring日志扩展_第2张图片

其中有两个关键类RabbitTemplate和AmqpInvokerServiceExporter,RabbitTemplate是一个消息模板类,可以通过调用RabbitTemplate 的convertSendAndReceive方法发送和接收消息消息,在发送之前会通过Message convertMessageIfNecessary(final Object object)方法来对消息进行预处理

Spring日志扩展_第3张图片

封装成用于传递消息的Message对象,message中包括了远程接口的参数和message的一些属性MessageProperties,而MessageProperties会有一个叫做header的HashMap对象,如下图:

Spring日志扩展_第4张图片

类之间的关系可参考下图:

Spring日志扩展_第5张图片

参考源码截图和类关系可以得出结论:消息经过组装,处理之后发送给远程服务,并携带了headers,如果我们在消息处理的时候,覆写原来的消息处理方法convertMessageIfNecessary,把需要传递的flowId封装在header中,即可将它发送给远程服务,实现方法参考下图:

Spring日志扩展_第6张图片

flowId已经被发送给了服务方,接下来我们研究一下服务方如何接收和输出flowId信息。

AmqpInvokerServiceExporter可以发布注册服务和监听客户端的消息,并通过AMQP传递,onMessage方法可在接收到消息之后读消息进行提取,处理和回送,所以我们可以覆写onMessage方法,在onMessage方法中取出headers中的flowId,并put到MDC中,即可成功接收flowId:

Spring日志扩展_第7张图片

相关类关系可参考:

Spring日志扩展_第8张图片

定义好了两个子类,再通过配置文件注入到上下文中,就可以生效了。

Spring日志扩展_第9张图片

以上为大家分享了通过微调MQ相关的类,将flowID从客户端传递到服务端。Rest服务也可以通过Filter和request header实现这一功能,这里就不赘述了。

3、flowId的多线程问题

当我们在多线程场景下是,会遇到flowId为空的情况,如ThreadLocal一样,MDC信息也无法被子线程获取。 MDC提供了getCopyOfContextMap()方法来从父线程赋值MDC信息,并通过setContextMap(mdcContext)方法来赋值到子线程,这样子线程就继承了父线程的flowId。

Spring日志扩展_第10张图片

在spring中还有一种特殊的线程启动方式,如quarz和Scheduled,没有按照我们平时的Thread.start()方式启动,但跟踪Scheduled的运行可以发现,这类方法是通过反射的方式调用的,所以笔者考虑避开线程,从job启动时执行的方法入手:自定义一个注解@FlowIdAnnotation来标记需要切入并生成flowId的方法,在Aspect中定义一个@Around方法,加入flowId生成和销毁的逻辑,即可达到需求。

Spring日志扩展_第11张图片

写在最后

以上就是笔者本次分享的全部内容,Spring生态系统为开发者提供了一个很优秀的平台,但在我们的使用过程中难免遇到一些有特殊需求的场景,这时需要我们在原来的基础上做一些扩展。

本文以一次日志扩展抛砖引玉,期待大家分享更多、更好的DIY。

参考文献:

https://yq.aliyun.com/articles/58408

http://www.cnblogs.com/LBSer/p/3390852.html

本文作者:谭雷(点融黑帮),现就职于点融成都Data部门,毕业于四川大学,热爱排球运动,励志做一个瘦一点的程序员。

你可能感兴趣的:(Spring日志扩展)