在单体应用中,日志追踪通常的解决方案是给日志添加 tranID(追踪ID),生成规则因系统而异,大致效果如下:
查询时只要使用 grep 命令进行追踪id筛选即可查到此次调用链中所有日志,但是在 dubbo 分布式系统中,由于 tranID 底层存储在 ThreadLocal 中,由于应用分布在不同的机器中,无法跨应用共享,同一链路多个应用之间的 tranID 不一致,出现线上问题时,日志的排查就尤为棘手。
先说结论:dubbo 分布式应用也可使用MDC进行分布式日志追踪,但是需要配合dubbo提供的filter技术来实现,更细化的,如果想对系统无侵入加入追踪日志,则可再配合拦截器对请求进行统一拦截,实现追踪id自动生成,链路id自动传递,最终实现效果为任意系统日志的追踪id即可获取全系统全链路日志。
实现步骤:
定义 TraceUtil,该工具类主要作用为将操作MDC的步骤部分封装,提供初始化链路、传递链路id、销毁链路操作,即链路生命周期管理类。
import org.apache.commons.lang3.StringUtils; import org.apache.dubbo.rpc.RpcContext; import org.slf4j.MDC; import java.util.UUID; public class TraceUtil { public final static String TRACE_ID = "trace_id"; public final static String TRACE_EXTENDED_INFO = "extended_info"; public static void initTrace(String extendedInfo) { if (StringUtils.isBlank(MDC.get(TRACE_ID))) { String traceId = generateTraceId(); setTraceId(traceId); MDC.put(TRACE_EXTENDED_INFO, extendedInfo); } } public static void getTraceFrom(RpcContext context) { String traceId = context.getAttachment(TRACE_ID); if (StringUtils.isNotBlank(traceId)) { setTraceId(traceId); } String uri = context.getAttachment(TRACE_EXTENDED_INFO); if (StringUtils.isNotEmpty(uri)) { MDC.put(TRACE_EXTENDED_INFO, uri); } } public static void putTraceInto(RpcContext context) { String traceId = MDC.get(TRACE_ID); if (StringUtils.isNotBlank(traceId)) { context.setAttachment(TRACE_ID, traceId); } String uri = MDC.get(TRACE_EXTENDED_INFO); if (StringUtils.isNotBlank(uri)) { context.setAttachment(TRACE_EXTENDED_INFO, uri); } } public static void clearTrace() { MDC.clear(); } private static void setTraceId(String traceId) { traceId = StringUtils.left(traceId, 36); MDC.put(TRACE_ID, traceId); } private static String generateTraceId() { return UUID.randomUUID().toString(); } }
DubboRequestDTO 和 DubboResponseDTO 定义如下:
@Data public class DubboRequestDTO implements Serializable { private String interfaceClass; private String methodName; private Object[] args; } @Data public class DubboResponseDTO implements Serializable { private String interfaceClassName; private String methodName; private String result; private long spendTime; }
定义Dubbo Filter,如下,该类为 Dubbo Filter 接口实现类,主要作用为传递追踪ID,打印追踪日志
package com.serviceshare.component.tracking; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.registry.Constants; import org.apache.dubbo.rpc.*; import java.io.File; import java.io.InputStream; import java.util.Arrays; @Slf4j @Activate(order = 999, group = {Constants.PROVIDER_PROTOCOL, Constants.CONSUMER_PROTOCOL}) public class DubboTraceFilter implements Filter { private static final int SLOW_METHOD_THRESHOLD = 3000; @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // 处理 Trace 信息 printRequest(invocation); // 执行前 handleTraceId(); long start = System.currentTimeMillis(); Result result = invoker.invoke(invocation); long end = System.currentTimeMillis(); // 执行后 final long executionTime = end - start; printResponse(invocation, result, executionTime); return result; } private void printRequest(Invocation invocation) { DubboRequestDTO requestDTO = new DubboRequestDTO(); requestDTO.setInterfaceClass(invocation.getInvoker().getInterface().getName()); requestDTO.setMethodName(invocation.getMethodName()); requestDTO.setArgs(getArgs(invocation)); log.info("RPC请求开始 , {}", requestDTO); } private void printResponse(Invocation invocation, Result result, long spendTime) { DubboResponseDTO responseDTO = new DubboResponseDTO(); responseDTO.setInterfaceClassName(invocation.getInvoker().getInterface().getName()); responseDTO.setMethodName(invocation.getMethodName()); responseDTO.setResult(JSON.toJSONString(result.getValue())); responseDTO.setSpendTime(spendTime); } private Object[] getArgs(Invocation invocation) { Object[] args = invocation.getArguments(); args = Arrays.stream(args).filter(arg -> { // 过滤大参 if (arg instanceof byte[] || arg instanceof Byte[] || arg instanceof InputStream || arg instanceof File) { return false; } return true; }).toArray(); return args; } private void handleTraceId() { RpcContext context = RpcContext.getContext(); if (context.isConsumerSide()) { TraceUtil.putTraceInto(context); } else if (context.isProviderSide()) { TraceUtil.getTraceFrom(context); } } }
启用Filter,Dubbo 采用 SPI 扩展方式,启用我们自己编写的 Filter ,非常方便,只需要在 classpath 路径下 META-INF 文件夹下新建 com.alibaba.dubbo.rpc.Filter 文件引入以下内容即可
(注意,包名替换为自己定义的 Filter 类包名)
com.serviceshare.component.tracking.DubboTraceFilter
修改 logback 配置文件 pattern,添加 extended_info 和 trace_id
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}[%X{extended_info}][%X{trace_id}][%thread] %-5level %logger{50}:%L - %msg%n</pattern>
尝试在启动类插桩测试
public static void main(String[] args) throws UnknownHostException { TraceUtil.initTrace("START-APP"); SpringApplication.run(SysApplication.class, args); }
启动项目,查看效果
多个系统都按照上述配置配置后,即可实现日志链路追踪,这样属于半侵入式集成,因为每个“入口”都需要手动插桩初始化链路,对系统所有已有接口造成了代码侵入,肯定有更完美的方案,那就是最终实现方案,无侵入式集成,怎么做?
首先,入口,不一定是 web 接口等一系列由内外部触发程序代码运行的点,都可以称为入口。还可能是定时任务,所以如果要保证每条日志都有迹可循的话,需要保证外部触发和内部触发的操作都要初始化MDC,Spring 提供的AOP+拦截器刚好可以实现这一点,借助拦截器我们拦截所有系统请求,在系统进入系统入口时,初始化链路,后续一系列调用自然而然都记录了追踪ID。
定时任务属于系统调度内部触发,同一定时任务如果不分片不会跨系统,但是也需要追踪日志,可以使用 Spring 提供的 AOP 进行定时任务注解定点拦截,实现定时任务执行前插桩,执行完毕后拆桩。
web请求自动插桩拦截器实现:
import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * web请求链路追踪拦截器 * * @author shaozhengmao * @create 2021-05-26 1:10 下午 * @desc */ @Configuration public class TraceWebFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { TraceUtil.initTrace(request.getRequestURI()); filterChain.doFilter(request, response); TraceUtil.clearTrace(); } }
该类实现了 JavaEE 提供的 Filter 接口,即过滤器,对所有web请求进行拦截,前置设置追踪元信息,后置清空追踪元信息,实现了 web 请求全链路追踪,为什么不使用 Spring 提供的 WebMvcConfigurer 接口?如果使用 WebMvcConfigurer 接口,请求失败的情况下,追踪ID会丢失,可自行测试。
定时任务自动插桩实现:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 定时任务链路追踪拦截器 * @author shaozhengmao * @create 2021-05-26 1:32 下午 * @desc */ @Aspect @Component @Order(1) public class TraceTaskConfigurer { @Around("@annotation(org.springframework.scheduling.annotation.Scheduled)") public Object verifyRoleExecuteCommand(ProceedingJoinPoint pjp) throws Throwable { // 获取当前拦截方法的对象 MethodSignature msig = (MethodSignature) pjp.getSignature(); Method targetMethod = pjp.getTarget().getClass().getDeclaredMethod(msig.getName(), msig.getMethod().getParameterTypes()); try { TraceUtil.initTrace(targetMethod.getName()); return pjp.proceed(); } finally { TraceUtil.clearTrace(); } } }
和链路追踪系统集成,SkyWlaking 提供了分布式链路追踪技术,当集成 SkyWlaking 后,可使用 SkyWlaking 的追踪id来初始化日志追踪id,这样既可无缝和 SkyWlaking 集成,从而实现,分布式调用链路追踪+分布式链路调用日志深度集成。
分布式追踪日志有什么好处,对于生产环境开发or运维人员排查错误有极大的帮助,可以很方便的找到一个请求完整的请求链路日志,当然,对于日志的探索,远不止此,随着系统的庞大,肯定会选择更好的处理方式,比如ELK,而分布式追踪ID对于集成 ELK 也提供了非常好的帮助。
集成步骤总结:
1.classpath 新增 /META-INF/dubbo/com.alibaba.dubbo.rpc.Filter 内容 com.emax.zhenghe.common.tracking.DubboTraceFilter
2.Spring扫描包新增 "com.emax.zhenghe.common.tracking"
3.logback 打印模板新增 [%X{extended_info}][%X{trace_id}]
最终效果
系统1:
系统2:
千里之行,始于足下