花了近一周时间 来优化traceid传递的代码,先说下之前的思路:
方案一 :
创建一个trace 基类,设置traceid属性,用来被供业务请求vo实体来继承传参traceid,这样可以简单粗暴的解决traceid传递的问题。
缺点:显而易见,这种方式代码侵入性问题太严重,如果我有几千个vo,那岂不是要继承几千次,不现实。
方案二:
也是dubbo官网提供的一个方案,dubbo过滤器。附上官网地址:http://dubbo.apache.org/en-us/docs/dev/impls/filter.html
过滤器代码开发,官网教程中有这里不再赘述,简单附上几张效果截图:
过滤器代码如下:
package com.yuance.common.filter; import com.yuance.common.constant.TraceContant; import com.yuance.common.util.TraceCommonUtil; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; import org.springframework.util.StringUtils; /** * Describe:traceId dubbo 过滤器desc<br> * Company:xixi<br> * Author:wdm<br> * Version:1.0<br> * Date:2020/9/21<br> */ @Activate(group ={CommonConstants.CONSUMER,CommonConstants.PROVIDER}) public class TraceIdFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { String traceId = RpcContext.getContext().getAttachment(TraceContant.HTTP_HEADER_TRACE_ID); if ( !StringUtils.isEmpty(traceId) ) { // *) 从RpcContext里获取traceId并保存 TraceCommonUtil.setTraceId(traceId); } else { // *) 交互前重新设置traceId, 避免信息丢失 RpcContext.getContext().setAttachment(TraceContant.HTTP_HEADER_TRACE_ID, TraceCommonUtil.getTraceId()); } // *) 实际的rpc调用 return invoker.invoke(invocation); } } TraceCommonUtil类的作用是保存当前traceid,代码提供如下: package com.yuance.common.util; /** * Describe:duboo traceId副本 desc<br> * Company:xixi<br> * Author:wdm<br> * Version:1.0<br> * Date:2020/10/14<br> */ public class TraceCommonUtil { private static final ThreadLocal<String> traceIdCache = new ThreadLocal<String>(); public static String getTraceId() { return traceIdCache.get(); } public static void setTraceId(String traceId) { traceIdCache.set(traceId); } public static void clear() { traceIdCache.remove(); } }使用过滤器需要在@Service(注意:是dubbo的service注解)@Refrence 传入配置文件中的key.见下图:
接下来就是开始模拟调用场景了:
首先获取zipkin系统服务自动生成的traceid,然后再调用dubbo服务前需要将这个traceid传递给dubbo的api,这里我是写的一个工具类。如下图:
截图中工具类代码提供如下:
package com.yuance.checkorder.util; import brave.Tracer; import com.yuance.common.constant.TraceContant; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.common.utils.StringUtils; import org.apache.dubbo.rpc.RpcContext; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.UUID; /** * Describe:traceId 维护工具类desc<br> * Company:元测<br> * Author:王德铭<br> * Version:1.0<br> * Date:2020/10/19<br> */ @Component @Slf4j public class TraceIdUtils { private static Tracer tracer; @Autowired public TraceIdUtils(Tracer _tracer) { tracer = _tracer; } //前端 header 请求头传traceId 如果没有传 后端自动生成 traceId public static String getTraceId() { String ctxTraceId = "traceId"; //针对服务多次交互进一步定位trace String ctxOpId = UUID.randomUUID().toString(); try { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); ctxTraceId = request.getHeader(TraceContant.HTTP_HEADER_TRACE_ID); MDC.put(TraceContant.HTTP_HEADER_TRACE_ID, ctxTraceId + "," + ctxOpId); } catch (Exception e) { log.error("获取traceID 失败",e); } if (StringUtils.isBlank(ctxTraceId)) { try { ctxTraceId = tracer.currentSpan().context().traceIdString(); MDC.put(TraceContant.LOG_TRACE_ID, ctxTraceId); // log.info("重新生成 traceId:{}",ctxTraceId); } catch (Exception e) { log.error("",e); } } return ctxTraceId; } /** * dubbo rpc traceId 传递 */ public static void passTraceId(){ RpcContext.getContext().setAttachment(TraceContant.HTTP_HEADER_TRACE_ID, TraceIdUtils.getTraceId()); log.info("app_trace_id 传递:{}", TraceIdUtils.getTraceId()); } }接下来就是dubbo服务提供方来接收traceid的处理步骤了,我是设计一个切面,通过MDC来保存上游传递的traceid。
切面实现如下:
package com.yuance.common.dubbo.aop; import com.yuance.common.dubbo.mdc.DubboMDC; import com.yuance.common.util.TraceCommonUtil; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.common.utils.StringUtils; import org.apache.dubbo.rpc.RpcContext; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.MDC; /** * Describe:dubbo traceid 切面拦截 desc<br> * Company:xixi<br> * Author:wdm<br> * Version:1.0<br> * Date:2020/10/20<br> */ @Slf4j public class DubboServiceAop { /** * 定义拦截规则:拦截com.xxx.api.controller包下面的所有类中,有@RequestMapping注解的方法。 * * @param */ @Pointcut("execution(* com.yuance.demo.controller..*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)") public void apiPointCut() { } /** * 拦截器执行前操作 * * @param point * @return */ @Before("apiPointCut()") public void excuteBefore(JoinPoint point) { try { //保存上游服务传递来的traceid MDC.put(DubboMDC.EXCUTE_ID, TraceCommonUtil.getTraceId()); Object rpcExcuteId = RpcContext.getContext().getAttachments().get(DubboMDC.EXCUTE_ID); if (rpcExcuteId != null) { String sRpcExcuteId = (String) rpcExcuteId; DubboMDC.put(DubboMDC.EXCUTE_ID, sRpcExcuteId); } String mdcExcuteId = MDC.get(DubboMDC.EXCUTE_ID); if (StringUtils.isNotEmpty(mdcExcuteId)) { RpcContext.getContext().setAttachment(DubboMDC.EXCUTE_ID, mdcExcuteId); } } catch (Exception e) { log.error("[Dubbo拦截器traceId传递出现异常]"); } } /** * 拦截器执行后操作 * * @param point * @return */ @AfterReturning(value = "apiPointCut()", returning = "value") public void excuteAfter(JoinPoint point, Object value) { //todo } }这里拦截器切面写的比较糙,原因是这个是一个可扩展的切面类,可以考虑在公共模块中开发这个切面类,供其他模块中的切面来继承扩展功能使用。
示例:
重写了公用模块中的注解,只要是dubbo注解就会被扫描到,执行切面中的逻辑。
之后就是在logback-config.xml配置文件中添加traceid 展示了,附上我的配置文件信息:
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" debug="false"> <property name="application.name" value="checkorder-server" /> <property name="log.path" value="/app/full-cycle-chain2.0/logs" /> <!--输出到控制台 --> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%X{traceid}] [%X{app_trace_id}] [%p] [%t] %c - %m%n</pattern> </encoder> </appender> <!-- info级别日志控制 --> <appender name="info_file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 文件路径 --> <file>${log.path}/${application.name}/info.log</file> <!-- 是否追加 默认为true --> <append>true</append> <!-- 滚动策略 日期+大小 策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${log.path}/${application.name}/%d{yyyy-MM-dd}/info/info-%i.log.gz</fileNamePattern> <!-- 单个日志大小 --> <maxFileSize>50MB</maxFileSize> <!-- 日志保存周期 --> <maxHistory>7</maxHistory> <!-- 总大小 --> <totalSizeCap>2GB</totalSizeCap> </rollingPolicy> <!-- 格式化 --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%X{traceid}] [%X{app_trace_id}] [%p] [%t] %c - %m%n</pattern> </encoder> <!-- 级别过滤 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- warn级别日志控制 --> <appender name="warn_file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 文件路径 --> <file>${log.path}/${application.name}/warn.log</file> <!-- 是否追加 默认为true --> <append>true</append> <!-- 滚动策略 日期+大小 策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${log.path}/${application.name}/%d{yyyy-MM-dd}/warn/warn-%i.log.gz</fileNamePattern> <!-- 单个日志大小 --> <maxFileSize>50MB</maxFileSize> <!-- 日志保存周期 --> <maxHistory>15</maxHistory> <!-- 总大小 --> <totalSizeCap>2GB</totalSizeCap> </rollingPolicy> <!-- 格式化 --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%X{traceid}] [%X{app_trace_id}] [%p] [%t] %c - %m%n</pattern> </encoder> <!-- 级别过滤 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>WARN</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- error级别日志控制 --> <appender name="error_file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 文件路径 --> <file>${log.path}/${application.name}/error.log</file> <!-- 是否追加 默认为true --> <append>true</append> <!-- 滚动策略 日期+大小 策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${log.path}/${application.name}/%d{yyyy-MM-dd}/error/error-%i.log.gz</fileNamePattern> <!-- 单个日志大小 --> <maxFileSize>50MB</maxFileSize> <!-- 日志保存周期 --> <maxHistory>15</maxHistory> <!-- 总大小 --> <totalSizeCap>2GB</totalSizeCap> </rollingPolicy> <!-- 格式化 --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%X{traceid}] [%X{app_trace_id}] [%p] [%t] %c - %m%n</pattern> </encoder> <!-- 级别过滤 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- 特殊处理 --> <logger name="org.springframework" level="warn" /> <logger name="com.netflix.discovery" level="warn" /> <logger name="com.alibaba.nacos.client.naming" level="warn" /> <!-- 开发、默认环境 只输出到控制台 --> <springProfile name="default,dev"> <root level="info"> <appender-ref ref="console" /> </root> </springProfile> <!-- 测试环境 输出info及以上日志 --> <springProfile name="test"> <root level="info"> <appender-ref ref="info_file" /> <appender-ref ref="warn_file" /> <appender-ref ref="error_file" /> </root> </springProfile> <!-- 正式环境 输出warn及以上日志 --> <springProfile name="prod"> <root level="info"> <appender-ref ref="info_file" /> <appender-ref ref="warn_file" /> <appender-ref ref="error_file" /> </root> </springProfile> </configuration>红色字体就是传递的traceid参数,为啥是两个,因为起初是按照前端来传 请求头 参数traceid来设计的,把app_trace_id当做key来传递,后来考虑到如果前端不传traceid的话系统也要自动生成一个唯一的traceid,traceid作用也是为了后端微服务之间请求的唯一标识,traceid的生成逻辑是zipkin框架中实现的,这里不做详细说明。
至此,方案二完成了。
大概思想是这样的,表达的比较啰嗦,以后我组织下语言可以再优化下,目前这种设计可以实现多服务间的traceid的传递,如果朋友们在使用中发现有何缺陷问题,欢迎在评论区讨论。欢迎转载提供出处,原创纯码字。