从零打造一个分布式调用链追踪系统
前言
分布式链路追踪是微服务架构中不可或缺的基础设施。市面上的 APM 产品如 SkyWalking、Jaeger、Zipkin 功能强大,但其架构往往比较厚重,依赖组件多,定制化成本高。
CallRope 是一个自研的轻量级分布式调用链追踪系统,核心代码仅 2000+ 行,通过 Java Agent 字节码增强 + SPI 插件化架构,实现了对 Spring MVC、Dubbo、异步线程的无侵入式监控。本文将深入解析 CallRope 的核心设计理念与技术实现。
一、项目架构总览
CallRope 采用三模块分层架构,职责清晰,耦合极低:
┌─────────────────────────────────────────────────────────┐
│ CallRope-agent │
│ Java Agent 入口,类加载管理,字节码加载器 │
└────────────────────────┬────────────────────────────────┘
│ Boot-ClassLoader 注入
┌────────────────────────▼────────────────────────────────┐
│ CallRope-spy(API 层) │
│ SpyAPI / SpySPI / 数据模型 / TTL / 日志框架 │
└────────────────────────┬────────────────────────────────┘
│ 自定义 ClassLoader 加载
┌────────────────────────▼────────────────────────────────┐
│ CallRope-core(核心层) │
│ 切面实现 / 字节码增强 / Disruptor 队列 / ES 存储 │
└─────────────────────────────────────────────────────────┘
| 模块 | 职责 | 加载方式 |
|---|---|---|
| CallRope-agent | Java Agent 入口,控制模块隔离 | AppClassLoader |
| CallRope-spy | 对外 API + 数据模型,业务代码可见 | BootstrapClassLoader |
| CallRope-core | 核心逻辑,不与业务代码冲突 | 自定义 ClassLoader |
设计亮点:spy 层通过 BootstrapClassLoader 加载,确保所有类都能访问;core 层通过自定义 ClassLoader 加载,实现完全的类隔离,避免 Jar 包冲突。
二、自研 SPI API 机制
2.1 为什么不用原生 SPI
Java 原生 ServiceLoader 虽然解决了服务发现,但存在明显不足:
- 每次遍历都创建新实例,无法复用
- 没有统一的生命周期管理
- 调用入口分散,难以统一拦截
CallRope 对 SPI 机制进行了三层增强封装:
┌──────────────────────────────────────────────────┐
│ SpyAPI(静态门面层) │
│ atEnter() / atExit() / atFrameworkEnter() │
└──────────────────────┬───────────────────────────┘
│ 委托
┌──────────────────────▼───────────────────────────┐
│ SpySPI(接口契约层) │
│ 定义 7 个 Hook 方法 │
└──────────────────────┬───────────────────────────┘
│ 实现
┌──────────────────────▼───────────────────────────┐
│ SpyImpl(实现管理层) │
│ 单例缓存 + aspectManager 统一注册 │
└──────────────────────────────────────────────────┘
2.2 核心实现
SpyAPI - 静态门面
public class SpyAPI {
private static volatile SpySPI spyInstance = doNothingSpy;
public static void setSpy(SpySPI spy) {
spyInstance = spy;
}
public static void atEnter(Class<?> clazz, String methodInfo,
Object target, Map<String, Object> infos) {
spyInstance.atEnter(clazz, methodInfo, target, infos);
}
// atExit、atExceptionExit、atFrameworkEnter、atFrameworkExit...
}
SpyImpl - 初始化与注册
public class SpyImpl implements SpySPI {
private volatile static Map<String, MethodAspect> methodAspects;
private volatile static Map<String, FrameworkAspect> frameworkAspects;
private volatile static Map<String, AsyncThreadAspect> asyncThreadAspects;
public static void init() {
methodAspects = createMethodAspectsMap(methodAspects);
frameworkAspects = createFrameworkAspectsMap(frameworkAspects);
asyncThreadAspects = createAsyncThreadAspectsMap(asyncThreadAspects);
// 加载并注册所有切面
aspectManager.init(methodAspects, frameworkAspects, asyncThreadAspects);
// 设为全局单例
SpyAPI.setSpy(new SpyImpl());
}
}
2.3 与原生 SPI 的对比
| 维度 | 原生 Java SPI | CallRope SPI |
|---|---|---|
| 实例管理 | 每次遍历新建实例 | Map 缓存单例复用 |
| 生命周期 | 无统一管理 | init() 全局初始化 |
| 调用方式 | 直接遍历调用 | SpyAPI 静态方法统一入口 |
| 扩展性 | 仅服务发现 | 支持三类切面分类注册 |
| 默认行为 | 无默认实现 | DoNothingSpy 空实现兜底 |
三、字节码增强机制
CallRope 基于 Javassist 实现无侵入式字节码增强,采用责任链模式灵活扩展增强点。
3.1 核心转换器
public class CallRopeClassfileTransformer implements ClassFileTransformer {
private static final List<transformer> transformerList = new ArrayList<>();
static {
transformerList.add(new SpringBootHandlerInteceptorTransformer());
transformerList.add(new SpringBootControllerTransformer());
transformerList.add(new JdkExecutorTtlTransformlet());
transformerList.add(new DubboProducerFilterTransformer());
transformerList.add(new DubboConsumerFilterTransformer());
}
@Override
public byte[] transform(ClassLoader loader, String className, ...) {
ClassInfo classInfo = new ClassInfo(className, classfileBuffer, ...);
// 跳过 java.lang 包
if (isClassUnderPackage(classInfo.getClassName(), "java.lang")) return null;
// 责任链遍历
for (transformer t : transformerList) {
t.doTransform(classInfo);
if (classInfo.isModified()) {
return classInfo.getCtClass().toBytecode();
}
}
return classfileBuffer;
}
}
3.2 Controller 增强实例
以 Spring MVC Controller 为例,展示如何无侵入注入调用链代码:
public class SpringBootControllerTransformer implements transformer {
@Override
public void doTransform(ClassInfo classInfo) {
CtClass ctClass = classInfo.getCtClass();
// 只处理 @Controller / @RestController
if (!ctClass.hasAnnotation(RESTCONTROLLER_ANNOTATION)) return;
for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
if (ctMethod.hasAnnotation(REQUESTMAPPING_ANNOTATION)) {
// 插入 before 代码
ctMethod.insertBefore(
"zql.CallRope.point.SpyAPI.atFrameworkEnter(span, null, ...);"
);
// 插入 after 代码
ctMethod.insertAfter(
"zql.CallRope.point.SpyAPI.atFrameworkExit(span, null, ...);"
);
}
}
}
}
增强后的代码等价于:
@RestController
public class UserController {
@RequestMapping("/user")
public User getUser(HttpServletRequest request) {
// === 自动插入 ===
SpyAPI.atFrameworkEnter(span, null, enhanceClassnames);
Trace.spanTtl.set(span);
// === 原始业务逻辑 ===
User user = userService.findById(id);
// === 自动插入 ===
SpyAPI.atFrameworkExit(span, null, enhanceClassnames);
return user;
}
}
四、线程上下文传递机制
4.1 为什么需要 TTL
在分布式追踪中,traceId 需要通过 ThreadLocal 在线程间传递。但当任务提交到线程池后,父线程的 ThreadLocal 值会丢失,导致调用链断裂:
[父线程 traceId=abc] → 提交任务 → [子线程 traceId=null] ❌ 链路断裂
4.2 自研 TransmittableThreadLocal
CallRope 自研了 TransmittableThreadLocal(精简版),核心原理:
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> {
// 全局注册中心,跟踪所有 TTL 实例
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal, ?>> holder = ...;
// 快照机制:capture → replay → restore
public static class Transmitter {
// 1. 捕获父线程所有 TTL 值
public static Object capture() {
WeakHashMap ttl2Value = captureTtlValues();
return new Snapshot(ttl2Value);
}
// 2. 将父线程值回放到子线程
public static Object replay(Object captured) {
replatTtlValues(capturedSnapshot.ttl2Value);
return new Snapshot(backup);
}
// 3. 任务执行完成后恢复子线程原有状态
public static void restore(Object backup) {
restoreTtlValues(backupSnapshot.ttl2Value);
}
}
}
4.3 TtlRunnable 装饰器
public class TtlRunnable implements Runnable {
private Object captured; // 父线程快照
private Runnable runnable; // 原始任务
@Override
public void run() {
Object backup = Transmitter.replay(captured); // 回放父线程值
try {
runnable.run(); // 执行任务
} finally {
Transmitter.restore(backup); // 恢复子线程原状
}
}
}
流程示意:
父线程 线程池
┌─────────────────┐ ┌─────────────────┐
│ ttl.set("abc") │ │ │
│ │ submit(TtlRunnable) │ │
│ capture() ──────┼────────────────────────►│ replay(captured)│
│ 快照: ttl=abc │ │ ttl = "abc" ✅│
│ │ │ task.run() │
│ │ │ restore(backup) │
│ 继续执行 │ │ 回收线程 │
└─────────────────┘ └─────────────────┘
五、切面体系设计
CallRope 设计了三类切面接口,覆盖不同的监控场景:
┌─────────────────┐
│ aspectManager │
│ ServiceLoader │
│ 加载 + 注册 │
└────────┬────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ MethodAspect │ │ FrameworkAspect │ │ AsyncThreadAspect│
│ 普通方法监控 │ │ 框架入口/出口 │ │ 异步线程监控 │
├───────────────┤ ├──────────────────┤ ├──────────────────┤
│ · before() │ │ · entry() │ │ · enter() │
│ · after() │ │ · exit() │ │ · exit() │
│ · error() │ │ │ │ │
└───────────────┘ └──────────────────┘ └──────────────────┘
5.1 框架切面示例
public class SpringFrameworkAspectImpl implements FrameworkAspect {
private DisruptorProducer<Span> producer = DisruptorConfig.createProducer(...);
@Override
public Object entry(Span span, Map<String, Object> infos) {
span.start = System.currentTimeMillis(); // 记录开始时间
span.setEnv(SpanEnvironment.TEST_ENVIRONMENT);
SpanBuilder.fix(span); // 修正缺失字段
TraceInfos.spanTtl.set(span); // 存入 TTL 上下文
return span;
}
@Override
public Object exit(Span span, Map<String, Object> infos) {
span.end = System.currentTimeMillis(); // 记录结束时间
span.duration = span.end - span.start; // 计算耗时
producer.onData(span); // 投递到 Disruptor
TraceInfos.spanTtl.remove(); // 清理 TTL
return span;
}
}
六、高性能事件队列 —— Disruptor
CallRope 使用 Disruptor 环形缓冲区处理 Span 事件,避免监控对业务线程的阻塞:
┌──────────┐ ┌───────────────────────────────────┐ ┌──────────┐
│ 业务线程 │───►│ RingBuffer (环形缓冲区) │───►│ 消费线程1│
│ │ │ ┌───┬───┬───┬───┬───┬───┬───┐ │ │ 消费线程2│
│ │ │ │ S │ S │ S │ S │ │ │ │ │ │ 消费线程3│
│ │ │ └───┴───┴───┴───┴───┴───┴───┘ │ │ 消费线程4│
│ │ │ SequenceBarrier │ │ │
└──────────┘ └───────────────────────────────────┘ └────┬─────┘
│
┌──────▼──────┐
│ Elasticsearch│
│ 持久化 │
└─────────────┘
核心实现:
public class DisruptorManager<T> {
public void start() {
Disruptor<DataEvent<T>> disruptor = new Disruptor<>(
eventFactory,
ringBufferSize, // 默认 16384
threadFactory,
ProducerType.MULTI, // 多生产者
new BlockingWaitStrategy()
);
DisruptorConsumer<T>[] consumers = new DisruptorConsumer[consumerSize];
for (int i = 0; i < consumerSize; i++) {
consumers[i] = new DisruptorConsumer<>(dataEventListener);
}
disruptor.handleEventsWithWorkerPool(consumers); // Worker Pool 模式
disruptor.start();
}
}
设计要点:
- RingBuffer 大小
16384,2 的幂次方保证取模用位运算 - BlockingWaitStrategy 减少 CPU 空转
- WorkerPool 模式,多个消费者并行处理
- 数据最终落入 Elasticsearch,方便查询和分析
七、数据模型
7.1 Span — 调用单元
public class Span {
public String traceId; // 全局追踪ID
public String spanId; // 当前跨度ID
public String pspanId; // 父跨度ID
public String serviceName; // 服务名
public String methodName; // 方法名
public long start; // 开始时间
public long end; // 结束时间
public long duration; // 耗时
public Map<String, Object> logInfos; // 附加信息
public Boolean isAsyncThread; // 是否异步线程
}
7.2 调用链结构
Trace (traceId: abc123)
├── Segment (segmentId: service-A)
│ ├── Span (spanId: 1) Spring MVC /user/get
│ ├── Span (spanId: 2) Dubbo consumer 调用
│ └── Span (spanId: 3) 异步线程任务
└── Segment (segmentId: service-B)
└── Span (spanId: 1) Dubbo provider 处理
7.3 SpanBuilder — 建造者模式
Span span = new SpanBuilder(traceId, spanId, pspanId, "user-service", "getUser")
.withEnv(SpanEnvironment.PRODUCTION_ENVIRONMENT)
.withStart(System.currentTimeMillis())
.withIsAsyncThread(false)
.build();
八、模块隔离与类加载设计
CallRope 的类加载策略是其核心设计之一:
public class CallRopeClassLoader extends URLClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) {
// JDK 核心类、spy API 类 → 委托给 SystemClassLoader
if (name.startsWith("java.") ||
name.contains("zql.CallRope.point")) {
return getSystemClassLoader().loadClass(name);
}
// Core 自身类 → 自己加载
try {
return findClass(name);
} catch (ClassNotFoundException ignored) {}
// 其他 → 委托给 SystemClassLoader
return getSystemClassLoader().loadClass(name);
}
}
加载策略:
┌─────────────────────────────────────────────┐
│ Bootstrap ClassLoader │
│ CallRope-spy.jar (所有类可见) │
└────────────────────┬────────────────────────┘
│
┌────────────────────▼────────────────────────┐
│ CallRopeClassLoader(自定义) │
│ CallRope-core.jar (隔离,不污染业务) │
└─────────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────┐
│ App ClassLoader │
│ 业务代码 + 第三方依赖 │
└─────────────────────────────────────────────┘
九、CallRope 对比主流方案
| 特性 | CallRope | SkyWalking | Jaeger |
|---|---|---|---|
| 核心代码量 | ~2000 行 | 数十万行 | 数万行 |
| 部署依赖 | 仅需 ES + Agent | OAP + UI + ES | Collector + ES |
| 定制化成本 | 极低(源码级) | 中等(插件开发) | 中等 |
| 类隔离 | 自定义 ClassLoader | Plugin 体系 | 依赖 Sidecar |
| 线程池追踪 | 自研 TTL | bootstrap 增强 | 需手动集成 |
| SPI 机制 | 自研增强 SPI | 原生 SPI | 无 SPI 概念 |
| 适用场景 | 中小型项目 | 大型微服务体系 | 通用场景 |
十、总结与感悟
核心设计思想
- 分而治之:spy/code/agent 三模块分离,各司其职
- 无侵入性:Java Agent + Javassist 字节码增强,业务代码零感知
- 高性能:Disruptor 异步解耦,监控不影响业务性能
- 可扩展:SPI 插件化架构,新增监控场景只需实现接口
- 类隔离:自定义 ClassLoader,杜绝包冲突
不足与改进方向
- 目前仅支持 ES 存储,可扩展支持 MySQL、InfluxDB 等
- 缺少可视化 UI 界面
- 链式过滤匹配可优化为 Trie 树或前缀树
为什么选择自研
"不重复造轮子" 是好的工程原则,但 "理解轮子原理并改进它" 是更好的成长方式。
CallRope 的价值不仅仅在于它实现了什么功能,更在于它展示了如何从零设计一个可插拔、可扩展的 APM 框架。每一个模块都可以替换,每一个切面都可以自定义,这正是自研的魅力所在。
附:完整模块目录
CallRope/
├── CallRope-agent/ # Java Agent 入口
│ └── RopeAgent.java # premain 启动 + 类加载管理
├── CallRope-spy/ # API 层(Bootstrap 加载)
│ ├── SpyAPI.java # 对外 API 门面
│ ├── SpySPI.java # SPI 接口
│ ├── threadpool/ # TTL 线程上下文传递
│ │ ├── TransmittableThreadLocal.java # 自研 TTL
│ │ ├── TtlRunnable.java
│ │ └── TtlCallable.java
│ ├── model/ # 数据模型
│ │ ├── Span.java # 调用单元
│ │ ├── SpanBuilder.java # 建造者
│ │ └── Trace.java # 完整调用链
│ └── log/ # 自研日志框架
├── CallRope-core/ # 核心层(自定义 CL 加载)
│ ├── aspect/ # 切面体系 + SPI 管理
│ ├── instrumentation/ # 字节码增强
│ ├── distruptor/ # 高性能事件队列
│ └── es/ # ES 存储客户端
└── Example Modules/ # 使用示例
├── CallRope-http-example/
└── CallRope-dubbo-producer-example/
如果这篇文章对你有所启发,欢迎点赞收藏,也欢迎在评论区交流你的自研思考!