重点一:APM 链路追踪 —— 先确定慢在哪一跳
1.1 排查思路
一个请求的典型链路是:
网关 → 服务A(业务逻辑) → 服务B(RPC调用) → MySQL → Redis
慢接口排查的第一步不是看日志,而是把整条链路的耗时拆解出来。链路追踪(Distributed Tracing)就是为这件事而生的:每个请求携带一个全局 TraceId,每经过一个节点产生一个 Span,记录该节点的开始时间和耗时。
1.2 接入 SkyWalking(开箱即用方案)
SkyWalking 是 Apache 开源的 APM 工具,Java 服务接入只需加一个 Agent 参数,零代码侵入:
启动命令中挂载 agent
java -javaagent:/path/to/skywalking-agent/skywalking-agent.jar \
-Dskywalking.agent.service_name=order-service \
-Dskywalking.collector.backend_service=127.0.0.1:11800 \
-jar order-service.jar
接入后在 UI 的 Trace 页面可以看到这样的耗时瀑布图:
/order/detail 3200ms ├──────────────────────────────┤
├─ OrderService.detail 180ms ├──┤
├─ UserRPC.getUser 2400ms ├───────────────────┤
│ └─ MySQL.select 2350ms ├────────────────┤ ← 慢在这里
└─ Redis.get 12ms ├┤
一眼就能看出:总耗时 3.2 秒,其中 2.4 秒花在一次 RPC 调用上,而 RPC 内部又有 2.35 秒是一条 SQL。排查范围瞬间从"整个系统"缩小到"一条 SQL"。
1.3 没有 APM 时的自建轻量方案
如果团队暂时没有条件部署 SkyWalking,可以用 MDC + AOP 自建一个简化版,把每个环节的耗时打到日志里:
/**
- 基于 AOP 的链路耗时埋点:在关键方法上标注 @TraceSpan 即可记录耗时
*/
@Aspect
@Component
@Slf4j
public class TraceSpanAspect {
@Around("@annotation(traceSpan)")
public Object around(ProceedingJoinPoint pjp, TraceSpan traceSpan) throws Throwable {
String spanName = traceSpan.value();
String traceId = MDC.get("traceId"); // 由网关过滤器统一塞入
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
// 统一格式输出,方便用 ELK 做聚合分析
log.info("SPAN|{}|{}|{}ms", traceId, spanName, cost);
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceSpan {
String value();
}
在业务方法上使用:
@Service
public class OrderService {
@TraceSpan("OrderService.detail")
public OrderDetailVO detail(Long orderId) {
UserDTO user = userRpcClient.getUser(orderId); // RPC 调用
OrderDO order = orderMapper.selectById(orderId); // DB 查询
return assemble(user, order);
}
}
同时在网关层加一个过滤器生成 TraceId:
@Component
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
String traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
MDC.put("traceId", traceId);
try {
chain.doFilter(req, res);
} finally {
MDC.remove("traceId"); // 防止线程复用导致 traceId 串号
}
}
}
这样用 grep "SPAN|abc123" app.log 就能还原一次请求各环节的耗时分布。
本阶段结论:链路追踪解决的是"定位"问题。找到耗时占比最高的那一跳之后,才进入下一阶段。