Spring日志扩展

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

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

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

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

1、生成、传递和输出flowId

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

这样所有的请求都可以自带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的封装,参考下面配置文件:

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

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

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

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

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

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

相关类关系可参考:

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

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

3、flowId的多线程问题

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

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

写在最后

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

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

参考文献:

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,768评论 19 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 32,084评论 18 399
  • 一. Java基础部分.................................................
    wy_sure阅读 9,279评论 0 11
  • 青春,在电影里、故事中都充满了浪漫色彩……事实中,更多人的青春是枯燥的,烦闷的,索然无味的…… 所以...
    冰茜阅读 780评论 0 0
  • 01 去年的平安夜,是和JJ一起度过,我记得我们下班之后,一起去吃了炒菜,有没有喝酒忘记了,吃完饭之后,我们各种拍...
    田小辣阅读 3,838评论 0 2