花了近一周时间 来优化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的传递,如果朋友们在使用中发现有何缺陷问题,欢迎在评论区讨论。欢迎转载提供出处,原创纯码字。