分布式链路追踪:SkyWalking的字节码注入原理与自定义Span
分布式链路追踪:SkyWalking的字节码注入原理与自定义Span
适读人群:中高级Java工程师 | 阅读时长:约18分钟 | 技术栈:Spring Boot 3.x、SkyWalking 9.x、Java Agent
开篇故事
2021年,我们有一个接口的 P99 从 200ms 突然飙到了 4000ms,但不是每次都触发,大概每小时有几次,完全随机,难以复现。
没有链路追踪之前,这种问题简直是噩梦:看日志没有异常,看 CPU/内存监控没有异常,看数据库慢查询日志也没有。只有用户反馈说偶尔会很慢。
我们当时刚接入了 SkyWalking,终于有了排查工具。用 SkyWalking 的 Trace 检索功能,按 P99 排序找到那几次慢请求,点开链路图一看:整个调用链路里,有一个叫"内容安全审核"的 RPC 调用,偶发性超时 3 秒以上。而这个 RPC 是我们接入的一个第三方内容审核服务,它的超时很不稳定。
链路追踪把问题的根因定位到了具体的服务调用,从"接口慢"精确到了"第三方服务超时",排查时间从几天缩短到了 20 分钟。
今天把 SkyWalking 的原理和自定义 Span 的实现说清楚。
一、核心原理:字节码注入(Java Agent)
Java Agent 是什么
Java Agent 是 JVM 提供的一种机制,允许在 JVM 启动时(或运行时)动态修改字节码。SkyWalking 的探针(skywalking-agent.jar)就是一个 Java Agent。
启动 Java 应用时添加 -javaagent:/path/to/skywalking-agent.jar,JVM 在加载每个类时,都会先调用 Agent 的 premain 方法,Agent 可以检查并修改类的字节码。
SkyWalking 的插件机制
SkyWalking 为各种主流框架(Spring MVC、MyBatis、Redis、Feign、RocketMQ 等)提供了插件,每个插件对应一个"拦截点":
以 Spring MVC 插件为例,它拦截了 DispatcherServlet.doDispatch 方法,在方法执行前创建一个 Span(记录请求开始),在方法执行后结束 Span(记录请求结束和状态码)。
TraceContext 的传播
在分布式调用中,TraceId 需要从上游服务传递到下游服务。HTTP 调用时,TraceContext 通过 HTTP Header 传播:
sw8:SkyWalking 的追踪 Header,包含 TraceId、SpanId、ServiceName 等信息
SkyWalking 的 HTTP 插件在发送请求时自动注入这个 Header,在接收请求时自动解析并关联到当前 Trace。
二、核心概念
Trace、Segment、Span
Trace:一次完整的请求链路,从用户发起请求到返回响应的全过程。一个 Trace 由唯一的 TraceId 标识。
Segment:一个服务实例内的追踪片段,一个服务处理请求的完整过程是一个 Segment。一个 Trace 由多个 Segment 组成(每个服务一个 Segment)。
Span:Segment 内的最小追踪单元,代表一个具体的操作(如一次 HTTP 请求、一次数据库查询、一次 RPC 调用)。Span 有父子关系,构成树形结构。
三、完整代码实现
启动配置
# JVM 启动参数
JAVA_OPTS="-javaagent:/opt/skywalking/agent/skywalking-agent.jar \
-Dskywalking.agent.service_name=order-service \
-Dskywalking.collector.backend_service=oap-server:11800 \
-Dskywalking.agent.sample_n_per_3_secs=-1" # -1 表示全量采集# agent.config(放在 skywalking-agent.jar 同目录的 config/ 下)
agent.service_name=order-service
agent.instance_name=${HOSTNAME} # 用容器 hostname 区分实例
collector.backend_service=oap-server:11800
# 采样率(生产建议1000~5000,不要全量)
agent.sample_n_per_3_secs=1000
# 忽略的端点(健康检查等不需要追踪)
agent.ignore_suffix=.jpg,.png,.css,.js
trace.ignore_path=/actuator/**,/favicon.ico自定义 Span(业务关键节点追踪)
SkyWalking 的自动插桩覆盖了大多数框架,但业务逻辑中的关键步骤(如风控校验、优惠券计算)需要手动创建 Span:
@Service
@Slf4j
public class OrderCreateService {
@Autowired
private InventoryFeignClient inventoryFeignClient;
@Autowired
private CouponService couponService;
@Autowired
private RiskControlService riskControlService;
/**
* 创建订单(带自定义追踪)
*/
public OrderResult createOrder(CreateOrderRequest request) {
AbstractSpan parentSpan = ContextManager.activeSpan();
// 1. 风控校验(业务关键步骤,手动 Span)
boolean riskPassed = doWithSpan("risk-control-check", () -> {
return riskControlService.check(request.getUserId(), request.getAmount());
});
if (!riskPassed) {
parentSpan.tag("risk.blocked", "true");
return OrderResult.blocked();
}
// 2. 优惠券计算
BigDecimal discount = doWithSpan("coupon-calculate", () -> {
return couponService.calculateDiscount(request);
});
// 3. 库存扣减(走 Feign,SkyWalking 自动插桩)
boolean deducted = inventoryFeignClient.deductStock(
request.getProductId(), request.getQuantity());
// 4. 创建订单记录
Order order = buildAndSaveOrder(request, discount);
// 在当前 Span 上打标签(会展示在追踪详情里)
if (ContextManager.isActive()) {
ContextManager.activeSpan()
.tag("order.id", order.getId().toString())
.tag("order.amount", order.getTotalAmount().toString());
}
return OrderResult.success(order.getId());
}
/**
* 工具方法:在 Local Span 内执行逻辑
*/
private <T> T doWithSpan(String operationName, Supplier<T> supplier) {
AbstractSpan span = ContextManager.createLocalSpan(operationName);
try {
T result = supplier.get();
span.tag("result", "success");
return result;
} catch (Exception e) {
span.log(e);
span.errorOccurred();
throw e;
} finally {
ContextManager.stopSpan();
}
}
}自定义注解式追踪
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Trace {
/** Span 的操作名,支持 SpEL 表达式 */
String operationName() default "";
/** 要添加的标签(key=value 格式) */
String[] tags() default {};
}@Aspect
@Component
@Slf4j
public class TraceAspect {
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(trace)")
public Object around(ProceedingJoinPoint joinPoint, Trace trace) throws Throwable {
String operationName = resolveOperationName(joinPoint, trace);
AbstractSpan span = ContextManager.createLocalSpan(operationName);
// 添加自定义标签
for (String tagStr : trace.tags()) {
String[] parts = tagStr.split("=", 2);
if (parts.length == 2) {
span.tag(parts[0], resolveSpEL(joinPoint, parts[1]));
}
}
try {
Object result = joinPoint.proceed();
return result;
} catch (Throwable e) {
span.log(e);
span.errorOccurred();
throw e;
} finally {
ContextManager.stopSpan();
}
}
private String resolveOperationName(ProceedingJoinPoint joinPoint, Trace trace) {
if (!trace.operationName().isEmpty()) {
return resolveSpEL(joinPoint, trace.operationName());
}
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
return sig.getDeclaringType().getSimpleName() + "." + sig.getName();
}
private String resolveSpEL(ProceedingJoinPoint joinPoint, String expression) {
if (!expression.contains("#")) {
return expression;
}
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
String[] paramNames = nameDiscoverer.getParameterNames(sig.getMethod());
EvaluationContext context = new StandardEvaluationContext();
if (paramNames != null) {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
return parser.parseExpression(expression).getValue(context, String.class);
}
}使用:
@Service
public class CouponService {
@Trace(operationName = "coupon.validate", tags = {"coupon.id=#couponId", "user.id=#userId"})
public boolean validateCoupon(Long couponId, Long userId) {
// 业务逻辑
return true;
}
}异步场景的 Trace 传递
@Configuration
public class AsyncConfig {
/**
* 自定义线程池,支持 SkyWalking Trace 上下文传递
*/
@Bean
public Executor traceAwareExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("async-trace-");
// 使用 SkyWalking 提供的 RunnableWrapper 传递 Trace 上下文
executor.setTaskDecorator(runnable -> {
// 捕获当前线程的 Trace 上下文
AbstractSpan activeSpan = ContextManager.isActive()
? ContextManager.activeSpan() : null;
ContextSnapshot snapshot = activeSpan != null
? ContextManager.capture() : null;
return () -> {
if (snapshot != null) {
ContextManager.continued(snapshot);
}
try {
runnable.run();
} finally {
if (snapshot != null) {
ContextManager.stopSpan();
}
}
};
});
executor.initialize();
return executor;
}
}四、生产调优
采样策略
全量采样(sample_n_per_3_secs=-1)在高并发下会产生大量追踪数据,消耗存储和网络带宽。生产建议:
# 每3秒采集1000条(约333 TPS的追踪,够用了)
agent.sample_n_per_3_secs=1000
# 慢请求强制采样(无论采样率,超过阈值的请求一定采样)
agent.force_sampled_for_slow_request=3000 # 超过3秒强制采样OAP Server 调优
# application.yml (oap server)
storage:
elasticsearch:
clusterName: your-es-cluster
clusterNodes: es1:9200,es2:9200
indexShardsNumber: 3
indexReplicasNumber: 1
# 数据保留时间
recordDataTTL: 7 # 追踪数据保留7天
metricsDataTTL: 90 # 指标数据保留90天五、踩坑实录
坑一:Agent 与某些类库冲突
SkyWalking Agent 的字节码注入偶尔会与某些类库发生冲突,导致启动报错(StackOverflowError 或 ClassCircularityError)。这类问题通常是 Agent 插件对某个类的拦截与该类的依赖关系形成了循环引用。
解决方案:确定是哪个插件冲突后,在 Agent 配置中排除该插件:
# 排除特定插件(文件名,不含 .jar)
agent.exclude_plugins=apm-spring-annotation-plugin,apm-xxx-plugin坑二:追踪数据丢失(Span 未正确结束)
手动创建 Span 时,如果业务代码抛出异常,且没有在 finally 中 stopSpan(),这个 Span 会在内存中积压,不会被上报。SkyWalking 有默认的 Segment 大小限制(300个 Span),超过后整个 Trace 数据丢弃。
必须用 try-finally 确保 Span 一定被结束:
AbstractSpan span = ContextManager.createLocalSpan("myOp");
try {
doWork();
} catch (Exception e) {
span.log(e);
span.errorOccurred();
throw e;
} finally {
ContextManager.stopSpan(); // 无论如何都要停止
}坑三:Spring Boot 3 兼容性
SkyWalking 8.x 对 Spring Boot 3 / Java 17 的支持不完整,某些插件在 Java 17 上因为模块化系统(JPMS)的限制无法正常工作。需要升级到 SkyWalking 9.x 并配置 JVM 开放模块:
JAVA_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
-javaagent:/opt/skywalking/agent/skywalking-agent.jar"六、总结
SkyWalking 的核心价值:
一、零代码侵入,Java Agent 自动插桩,应用代码无需任何改动。 二、丰富的生态,内置 Spring MVC、MyBatis、Redis、Feign、RocketMQ 等 100+ 插件。 三、根因定位,链路追踪把"慢在哪里"从模糊的感知变成精确到具体调用的数据。 四、自定义 Span,业务关键路径(风控、优惠计算)可以手动打 Span,追踪细节。
分布式系统里,链路追踪是不可缺少的基础设施。没有它,微服务调用链路的问题排查就像在黑暗中摸索。
