无侵入式日志链路追踪

51 阅读8分钟

从零打造一个分布式调用链追踪系统

前言

分布式链路追踪是微服务架构中不可或缺的基础设施。市面上的 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-agentJava 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 SPICallRope 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 对比主流方案

特性CallRopeSkyWalkingJaeger
核心代码量~2000 行数十万行数万行
部署依赖仅需 ES + AgentOAP + UI + ESCollector + ES
定制化成本极低(源码级)中等(插件开发)中等
类隔离自定义 ClassLoaderPlugin 体系依赖 Sidecar
线程池追踪自研 TTLbootstrap 增强需手动集成
SPI 机制自研增强 SPI原生 SPI无 SPI 概念
适用场景中小型项目大型微服务体系通用场景

十、总结与感悟

核心设计思想

  1. 分而治之:spy/code/agent 三模块分离,各司其职
  2. 无侵入性:Java Agent + Javassist 字节码增强,业务代码零感知
  3. 高性能:Disruptor 异步解耦,监控不影响业务性能
  4. 可扩展:SPI 插件化架构,新增监控场景只需实现接口
  5. 类隔离:自定义 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/

如果这篇文章对你有所启发,欢迎点赞收藏,也欢迎在评论区交流你的自研思考!