线上突发:调用链路中途"消失"了
背景是这样的: 随着技术架构的升级,我们正在将早期的 SkyWalking 替换为自研 Trace 组件。但现实总是骨感的,旧项目暂时没空下线,新项目已经上线,这就形成了"新老混部"的局面。
暂时不讨论早期技术架构设计的合理性,先讨论如何解决遗留问题
问题复现: 假设有一个调用链路:A服务 ➔ B服务 ➔ C服务 ➔ D服务
A、B 服务: 也就是"老前辈",依然使用 SkyWalking 进行 Trace 传递。
C、D 服务: 是"新秀",接入了自研 Trace。
当 B 调用 C 时,事故发生了: C 服务接收到了请求,但是它读不懂 B 服务传过来的"暗号",于是 C 服务自作主张重新生成了一个 TraceId。
结果: 整个链路图在 B 到 C 之间断开了。运维同学在查日志时,发现 A-B 是一个链路,C-D 是另一个链路,中间完全由于"失忆"导致无法串联。
根因拆解:不同Trace组件的"语言不通"
Trace 能够串联的核心在于透传。
SkyWalking 的方言: 使用 sw8 这个 Header 来传递 Trace 信息。
自研 Trace 的方言: 使用自定义的 Header(例如 X-TRACE-ID)来传递。
当 B 服务(SkyWalking)请求 C 服务(自研)时,HTTP 请求头里带的是 sw8。 C 服务的 Trace 过滤器按照自研规则去拿 X-TRACE-ID,结果当然是拿不到。
简单说:请求头变了,接收方不认识
破局方案:兼容适配 + 反射黑科技
既然 C 服务无法识别 sw8,那我们就教它识别。但是,我们不希望强依赖 SkyWalking 的全套 Jar 包,以免造成代码冗余或版本冲突。
解决方案思路: 在 C 服务获取 Trace 时,做一个降级策略(Fallback)------如果自研 Header 里没有,就尝试去问问 SkyWalking 的上下文。
为了保持优雅,我们采用**反射(Reflection)**的方式来调用 SkyWalking 的 API。
🛠️ 第一步:打造适配器工具类
我们编写一个 SkyWalkingAccessor,利用反射动态判断当前环境是否有 SkyWalking,并提取 TraceId。
java
@Slf4j
public class SkyWalkingAccessor {
private static boolean skyWalkingPresent;
private static Method traceIdMethod;
private static Method spanIdMethod;
// 静态代码块:初始化反射方法,避免运行时重复消耗性能
static {
try {
// 尝试加载 SkyWalking 的核心类
Class<?> clazz = Class.forName("org.apache.skywalking.apm.toolkit.trace.TraceContext");
traceIdMethod = clazz.getMethod("traceId");
spanIdMethod = clazz.getMethod("spanId");
skyWalkingPresent = true;
log.info("SkyWalking Toolkit found. 兼容模式已开启。");
} catch (Throwable e) {
log.debug("SkyWalking Toolkit not found. 仅使用内部 Trace。");
skyWalkingPresent = false;
}
}
/**
* 优雅获取 SkyWalking TraceId
*/
public static String getTraceId() {
if (!skyWalkingPresent) {
return null;
}
try {
String tid = (String) traceIdMethod.invoke(null);
if (isValid(tid)) {
return tid;
}
} catch (Exception e) {
// 忽略异常,降级处理,不影响主流程
}
return null;
}
private static boolean isValid(String tid) {
return StrUtil.isNotBlank(tid)
&& !"Ignored_Trace".equals(tid)
&& !"N/A".equals(tid);
}
}
🛠️ 第二步:改造获取 Trace 的入口
在原本的 HttpTracer 中,加入兼容逻辑:
java
// 1. 优先尝试:从自研的标准请求头获取
String traceId = request.getHeader(ContextContainer.X_TRACE_ID);
// 2. 降级兼容:如果为空,尝试从 SkyWalking 上下文"窃取"
if (StrUtil.isBlank(traceId)) {
traceId = SkyWalkingAccessor.getTraceId();
}
// 3. 如果还是没有,说明是链路起点,生成新的 TraceId
if (StrUtil.isBlank(traceId)) {
traceId = IdGenerator.generate();
}
业务侧如何使用
使用方式非常简单,无需大量改造:
- 引入SkyWalking的依赖(如果项目已接入SkyWalking则无需重复引入):
xml
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>你的SkyWalking版本</version>
</dependency>
- 升级基础组件jar版本
进阶优化:封装独立模块,降低业务接入成本
上面的方案虽然能解决问题,但如果每个业务都需要手动引入apm-toolkit-trace依赖,还是会增加接入成本,而且容易出现版本不一致的问题。
我们的优化思路是:封装一个独立的适配模块,比如叫xxx-skywalking-adapt。
具体做法:
-
将
apm-toolkit-trace依赖都集成到这个独立模块中; -
业务侧无需手动引入
apm-toolkit-trace,直接将原来的基础组件依赖替换成这个适配模块即可。
这里需要注意一个责任边界问题:
-
如果业务自己单独引入了
apm-toolkit-trace依赖,后续的版本维护、问题排查由业务侧自己负责; -
如果通过基础组件的适配模块引入依赖,則由基础组件团队负责版本管理和问题兜底
可能会存在一个责任划分的问题
总结
新旧Trace组件共存导致链路断开的问题,核心原因是"透传协议不兼容"(请求头不一致)。
解决这类问题的核心思路是"兼容适配"------让新组件能识别老组件的协议,实现上下文的平滑传递