摘要:当前线程的子线程会继承其父线程中的的内容。若希望在线程池与主线程间传递,需配合和使用。
一、背景
开发排查系统问题用得最多的手段就是查看系统日志,在分布式环境中一般使用ELK来统一收集日志,但是在并发大时使用日志定位问题还是比较麻烦,由于大量的其他用户/其他线程的日志也一起输出穿行其中导致很难筛选出指定请求的全部相关日志,以及下游线程/服务对应的日志。
二、解决思路
每个请求都使用一个唯一标识来追踪全部的链路显示在日志中,并且不修改原有的打印方式(代码无入侵)
使用Logback的MDC机制日志模板中加入traceId标识,取值方式为%X{traceId}
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
三、方案实现
由于MDC内部使用的是ThreadLocal所以只有本线程才有效,子线程和下游的服务MDC里的值会丢失;所以方案主要的难点是解决值的传递问题,主要包括以几下部分:
API网关中的MDC数据如何传递给下游服务
服务如何接收数据,并且调用其他远程服务时如何继续传递
异步的情况下(线程池)如何传给子线程
3.1. 修改日志模板logback配置文件模板格式添加标识%X{traceId}
3.2. 网关添加过滤器
生成traceId并通过header传递给下游服务
@Component public class TraceFilter extends ZuulFilter { @Autowired private TraceProperties traceProperties; @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return FORM_BODY_WRAPPER_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { //根据配置控制是否开启过滤器 return traceProperties.getEnable(); } @Override public Object run() { //链路追踪id String traceId = IdUtil.fastSimpleUUID(); MDC.put(CommonConstant.LOG_TRACE_ID, traceId); RequestContext ctx = RequestContext.getCurrentContext(); ctx.addZuulRequestHeader(CommonConstant.TRACE_ID_HEADER, traceId); return null; } }
3.3. 下游服务增加spring拦截器
接收并保存traceId的值
拦截器
public class TraceInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String traceId = request.getHeader(CommonConstant.TRACE_ID_HEADER); if (StrUtil.isNotEmpty(traceId)) { MDC.put(CommonConstant.LOG_TRACE_ID, traceId); } return true; } }
注册拦截器
public class DefaultWebMvcConfig extends WebMvcConfigurationSupport { @Override protected void addInterceptors(InterceptorRegistry registry) { //日志链路追踪拦截器 registry.addInterceptor(new TraceInterceptor()).addPathPatterns("/**"); super.addInterceptors(registry); } }
3.4. 下游服务增加feign拦截器
继续把当前服务的traceId值传递给下游服务
public class FeignInterceptorConfig { @Bean public RequestInterceptor requestInterceptor() { RequestInterceptor requestInterceptor = template -> { //传递日志traceId String traceId = MDC.get(CommonConstant.LOG_TRACE_ID); if (StrUtil.isNotEmpty(traceId)) { template.header(CommonConstant.TRACE_ID_HEADER, traceId); } }; return requestInterceptor; } }
3.5. 解决父子线程传递问题
主要针对业务会使用线程池(异步、并行处理),并且spring自己也有@Async注解来使用线程池,要解决这个问题需要以下两个步骤
3.5.1. 重写logback的LogbackMDCAdapter由于logback的MDC实现内部使用的是ThreadLocal不能传递子线程,所以需要重写替换为阿里的TransmittableThreadLocal
TransmittableThreadLocal 是Alibaba开源的、用于解决 “在使用线程池等会缓存线程的组件情况下传递ThreadLocal” 问题的 InheritableThreadLocal 扩展。若希望 TransmittableThreadLocal 在线程池与主线程间传递,需配合 TtlRunnable 和 TtlCallable 使用。
TtlMDCAdapter类
package org.slf4j; import com.alibaba.ttl.TransmittableThreadLocal; import org.slf4j.spi.MDCAdapter; public class TtlMDCAdapter implements MDCAdapter { /** * 此处是关键 */ private final ThreadLocal
其他代码与ch.qos.logback.classic.util.LogbackMDCAdapter一样,只需改为调用copyOnInheritThreadLocal变量
TtlMDCAdapterInitializer类用于程序启动时加载自己的mdcAdapter实现
public class TtlMDCAdapterInitializer implements ApplicationContextInitializer{ @Override public void initialize(ConfigurableApplicationContext applicationContext) { //加载TtlMDCAdapter实例 TtlMDCAdapter.getInstance(); } }
3.5.2. 扩展线程池实现
增加TtlRunnable和TtlCallable扩展实现TTL
public class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { @Override public void execute(Runnable runnable) { Runnable ttlRunnable = TtlRunnable.get(runnable); super.execute(ttlRunnable); } @Override publicFuture submit(Callable task) { Callable ttlCallable = TtlCallable.get(task); return super.submit(ttlCallable); } @Override public Future> submit(Runnable task) { Runnable ttlRunnable = TtlRunnable.get(task); return super.submit(ttlRunnable); } @Override public ListenableFuture> submitListenable(Runnable task) { Runnable ttlRunnable = TtlRunnable.get(task); return super.submitListenable(ttlRunnable); } @Override public ListenableFuture submitListenable(Callable task) { Callable ttlCallable = TtlCallable.get(task); return super.submitListenable(ttlCallable); } }
四、场景测试 4.1. 测试代码如下
网关生成traceId值为13d9800c8c7944c78a06ce28c36de670
显示的traceId与网关相同,这里特意模拟发生异常的场景
当系统出现异常时,可直接通过该异常日志的traceId的值,在日志中心中询该请求的所有日志信息
五、源码下载
附上我的开源微服务框架(包含本文中的代码),欢迎 star 关注
https://gitee.com/zlt2000/mic...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/76187.html
摘要:今年的无论是常态全链路压测或者是双十一当天,面临的主要问题是如何保障自身系统在海量数据冲击下的稳定性,以及如何更快的展现各个系统的状态及更好的帮助开发同学发现及定位问题。在整个双十一备战过程中,遇到并解决了很多疑难杂症。 摘要: EagleEye作为阿里集团老牌的链路跟踪系统,其自身业务虽不在交易链路上,但却监控着全集团的链路状态,特别是在中间件的远程调用上,覆盖了集团绝大部分的场景,...
摘要:接下来我们以余额宝为例,重点剖析天弘基金在日志数据分析领域是如何突破的此前,天弘基金一直使用开源的日志方案,研发和运维人员通过对日志数据进行处理,使用日志文件进行查询检索。 双十一刚刚结束,其实最紧张的不是商铺理货,也不是网友紧盯大促商品准备秒杀,而是网购幕后的运维人员,他们最担心:什么网络中断、应用卡顿、响应速度慢,服务器宕机……双十一作为电商 IT 部门的头等大事,大促前,运维人员就需要...
摘要:在软件世界里,观察意味着设置断点添加调试语句监视程序值以及检查内存在医学领域,需要测试血样和进行光透视。福尔摩斯,最后一案如果你不修复,它不会自动消失。修复解决问题的能力,是软件工程师的核心竞争力之一。 这篇文章是《调试九法:软硬件错误的排查之道》的阅读笔记。这本书的主旨,是介绍如何修复bug:找出bug发生的原因、并给出修复方案。 调试bug的九个规则列举如下,建议将这个清单打印出来...
摘要:阿里云上领域各个产品最终目标是为了对以上各个组件进行有效监控。阿里云的解决方案地图基于今天的云上的应用架构,阿里云的解决方案地图如下所示。其他阿里云服务包括缓存,等。阿里云解决方案地图以下表格对阿里云解决方案进行总结。 摘要: PM是近5年来伴随着云技术、微服务架构发展起来的一个新兴监控领域。在国内外,无论是云厂商(如AWS, Azure,等)还是独立的公司(Dynatrace, Ap...
阅读 2753·2023-04-25 14:41
阅读 2340·2021-11-23 09:51
阅读 3635·2021-11-17 17:08
阅读 1616·2021-10-18 13:31
阅读 5478·2021-09-22 15:27
阅读 883·2019-08-30 15:54
阅读 2186·2019-08-30 13:16
阅读 705·2019-08-29 17:04