【Java项目技术亮点】CAT分布式监控链路追踪

写在前面:微服务拆完之后,线上出问题排查起来简直是噩梦。我之前经历过一个订单超时的bug,从网关到订单到库存到支付到通知,五个服务串起来,光看日志就花了半天,最后发现是库存服务一个慢SQL导致的。后来上了CAT链路追踪,一条TraceID就能把整条链路串起来,排查时间从小时级降到了分钟级。今天把CAT从原理到实战完整讲一遍,这个技术亮点写在简历上绝对加分。

文章目录


一、为什么需要分布式链路追踪?

1.1 微服务排查的噩梦

先看一个真实场景:用户下单请求经过五个服务。

复制代码
用户点击"下单"
    |
    ├──> API网关:鉴权、限流、路由
    |
    ├──> 订单服务:创建订单
    |
    ├──> 库存服务:扣减库存
    |
    ├──> 支付服务:调用第三方支付
    |
    └──> 通知服务:发送短信/推送

用户反馈:下单花了8秒,体验极差!
问题来了:到底是哪个服务慢了?

在单体应用时代,一个请求就是一个线程,打个断点就能看到全链路。但微服务拆完之后,一个请求会跨越多个进程、多台机器,每个服务都有自己的日志系统。

你去看订单服务日志,它说"我200ms就返回了"。去看库存服务日志,它也说"我300ms就返回了"。那剩下的7.5秒去哪了?

没有链路追踪,你只能靠时间戳去对日志,运气好能找到,运气不好就只能干瞪眼。

1.2 生活类比:快递追踪

你寄一个包裹,经过揽收站→分拣中心→中转站→派送站→收件人,每个环节都有一个扫描记录。

你打开快递APP,能看到包裹在每个节点的状态和时间,哪个环节卡住了一目了然。

分布式链路追踪就是给系统请求做"快递追踪",每个服务就是一个中转站,每次调用就是一次扫描。

1.3 单体 vs 微服务的调试痛苦对比

对比维度 单体应用 微服务架构
请求链路 一个线程内完成 跨多个进程/机器
日志查看 一份日志搞定 N份日志分散在不同机器
问题定位 断点调试 需要链路追踪工具
性能瓶颈 Profiler直接看 需要分布式监控
故障影响面 单个服务 可能级联影响多个服务

二、链路追踪核心概念

2.1 Trace:一次完整的请求链路

Trace代表一次完整的请求从入口到返回的全过程。用户点一次下单,从网关进来到最终返回结果,这就是一条Trace。

每条Trace都有一个全局唯一的TraceID,用来标识这一次请求。

2.2 Span:链路中的一个调用单元

Span是Trace中的一个调用单元,代表一次方法调用、一次HTTP请求或一次RPC调用。

每个Span有:

  • SpanID:当前调用的唯一标识
  • ParentSpanID:父调用的SpanID(谁调了我)
  • ServiceName:服务名称
  • OperationName:操作名称
  • StartTime / EndTime:开始和结束时间

2.3 SpanContext:Span的上下文信息

SpanContext携带了Span的上下文,核心信息包括:

字段 说明 示例
TraceID 全链路唯一标识 a1b2c3d4e5f6
SpanID 当前Span标识 1001
ParentSpanID 父Span标识 1000(根Span为null)
SamplingFlag 采样标记 1=采样,0=不采样

这个上下文信息需要在服务间传递,通常通过HTTP Header或RPC的Attachment来透传。

2.4 Annotation:事件标注

CAT借鉴了Google Dapper的设计,用Annotation来标注Span中的关键事件:

Annotation 全称 含义
cs Client Send 客户端发起调用
sr Server Receive 服务端收到请求
ss Server Send 服务端处理完成,返回响应
sa Client Answer 客户端收到响应

一次完整的调用链:cs → sr → ss → sa

  • cs - sr 之间的时间 = 网络耗时
  • sr - ss 之间的时间 = 服务端处理耗时
  • ss - sa 之间的时间 = 网络返回耗时

2.5 五个服务的调用链示例

假设一个下单请求经过5个服务,Trace和Span的关系如下:

复制代码
TraceID: T-20240101-0001(整条链路唯一)

Span-1(网关): SpanID=1, ParentSpanID=null
    |
    ├── Span-2(订单服务): SpanID=2, ParentSpanID=1
    |       |
    |       ├── Span-3(库存服务): SpanID=3, ParentSpanID=2
    |       |
    |       └── Span-4(支付服务): SpanID=4, ParentSpanID=2
    |               |
    |               └── Span-5(通知服务): SpanID=5, ParentSpanID=4

整条链路耗时 = Span-1的endTime - Span-1的startTime
库存服务耗时 = Span-3的endTime - Span-3的startTime

通过TraceID,你可以在CAT的UI上看到整条链路的调用树,哪个节点慢了一目了然。


三、主流链路追踪工具对比

3.1 四大主流工具对比

对比维度 Zipkin Jaeger SkyWalking CAT
开源方 Twitter/Netflix Uber Apache 美团
性能 中等 较好 较好 优秀
侵入性 低(Agent) 低(Agent) 低(Agent) 中等(需埋点)
功能丰富度 基础链路追踪 基础链路追踪 链路+性能监控 链路+监控+告警+业务
社区活跃度 高(国际) 高(国际) 高(国际) 中等(国内)
UI可视化 一般 较好 优秀 良好
告警能力 中等 较好 强大
业务监控 不支持 不支持 部分 原生支持
国内大厂使用 很多

3.2 为什么国内大厂偏爱CAT和SkyWalking?

说实话,选型这事儿没有绝对的好坏,主要看场景:

  • CAT:美团开源,功能最全面,不仅能做链路追踪,还能做业务监控、性能监控、告警。侵入性虽然高一点,但换来的是更细粒度的监控能力。适合对监控要求极高的业务场景。

  • SkyWalking:Apache顶级项目,无侵入Agent模式,上手简单,UI漂亮。适合快速接入、对侵入性敏感的项目。

我个人建议:如果项目对监控要求高(比如交易链路),用CAT;如果只是想快速接入链路追踪,用SkyWalking。


四、CAT架构与核心组件

4.1 CAT整体架构

CAT的架构可以分四层来看:

复制代码
┌─────────────────────────────────────────────────┐
│                   应用服务层                      │
│  订单服务 / 库存服务 / 支付服务 / 通知服务 ...    │
│  (每个服务内嵌CAT Client Agent)                │
└──────────────────┬──────────────────────────────┘
                   │ 上报监控数据(Netty HTTP)
                   ▼
┌─────────────────────────────────────────────────┐
│                CAT服务端集群                      │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐         │
│  │  控制节点  │  │  报告节点  │  │  存储节点  │         │
│  └─────────┘  └─────────┘  └─────────┘         │
└──────────────────┬──────────────────────────────┘
                   │
        ┌──────────┼──────────┐
        ▼          ▼          ▼
   ┌────────┐ ┌────────┐ ┌────────┐
   │ MySQL  │ │  HDFS  │  │  HBase │
   │(报表数据)│ │(原始数据)│ │(可选)   │
   └────────┘ └────────┘ └────────┘
        │
        ▼
┌─────────────────────────────────────────────────┐
│                CAT Web控制台                      │
│  服务健康度 / 调用链路 / 异常报警 / 业务监控       │
└─────────────────────────────────────────────────┘

4.2 客户端Agent

CAT客户端以Java Agent或SDK方式嵌入应用,核心能力:

  • AOP拦截:通过Spring AOP拦截Controller、Service方法调用
  • MV拦截:拦截MyBatis的SQL执行
  • HTTP拦截:拦截HTTP/RPC调用,自动透传TraceID
  • 消息模型上报:将采集到的数据通过Netty异步上报到服务端

CAT客户端的设计理念是"高吞吐低延迟",数据先写到内存队列,然后批量异步上报,对业务逻辑的影响降到最低。

4.3 服务端

CAT服务端负责接收、聚合、分析客户端上报的数据:

  • 控制节点:负责集群管理、配置下发
  • 报告节点:负责数据聚合、报表生成
  • 存储节点:负责数据持久化

4.4 存储层

CAT采用分层存储策略:

存储层 存储内容 存储介质 保留时间
报表数据 聚合后的统计数据 MySQL 长期
原始数据 单次调用的详细数据 HDFS 短期(如7天)
实时数据 当前小时的监控数据 内存 1小时

4.5 CAT消息模型

CAT定义了四种消息类型:

消息类型 说明 使用场景
Transaction 有开始和结束的调用 HTTP请求、方法调用、SQL执行
Event 离散事件 错误日志、业务事件
Heartbeat 心跳信息 系统状态上报(CPU、内存等)
Metric 指标数据 QPS、响应时间、错误率

Transaction就像一个计时器,有start和end。Event就像一个日志条目,记录一个瞬间发生的事情。Metric就像一个计数器,用来统计某个指标。


五、CAT埋点实战

5.1 @CatAnnotation注解埋点

CAT提供了注解方式,最简单的埋点方式:

java 复制代码
import com.dianping.cat.annotation.CatAnnotation;

@Service
public class OrderService {

    // CatAnnotation会自动创建一个Transaction,记录方法调用的耗时
    @CatAnnotation(type = "Order", name = "createOrder")
    public Order createOrder(OrderRequest request) {
        // 业务逻辑
        Order order = new Order();
        order.setOrderId(IdGenerator.nextId());
        order.setUserId(request.getUserId());
        order.setAmount(request.getAmount());
        return order;
    }
}

5.2 手动埋点API

注解方式适合简单场景,复杂场景需要手动埋点:

java 复制代码
import com.dianping.cat.Cat;
import com.dianping.cat.message.Transaction;
import com.dianping.cat.message.Event;
import com.dianping.cat.message.Metric;

@Service
public class InventoryService {

    /**
     * 手动创建Transaction记录方法调用
     */
    public boolean deductStock(Long skuId, int quantity) {
        // 创建一个Transaction,type和name自定义
        Transaction t = Cat.newTransaction("Inventory", "deductStock");

        try {
            // 记录业务事件
            Cat.logEvent("Inventory", "deductStock.request",
                Message.SUCCESS, "skuId=" + skuId + ",quantity=" + quantity);

            // 执行扣减库存逻辑
            int rows = stockMapper.deductStock(skuId, quantity);

            if (rows > 0) {
                // 记录成功指标
                Cat.logMetricForCount("inventory.deduct.success");
                t.setStatus(Transaction.SUCCESS);
                return true;
            } else {
                // 库存不足,记录失败
                Cat.logEvent("Inventory", "deductStock.fail",
                    "ERROR", "库存不足,skuId=" + skuId);
                t.setStatus("库存不足");
                return false;
            }
        } catch (Exception e) {
            // 记录异常
            Cat.logError(e);
            t.setStatus(e);
            throw e;
        } finally {
            // 必须调用complete()结束Transaction
            t.complete();
        }
    }

    /**
     * 记录自定义监控指标
     */
    public void recordMetrics() {
        // 记录QPS计数
        Cat.logMetricForCount("order.create.count");

        // 记录响应时间(单位毫秒)
        Cat.logMetricForDuration("order.create.duration", 156);

        // 记录求和指标(如订单金额)
        Cat.logMetricForSum("order.create.amount", 9980);
    }
}

5.3 跨服务TraceID透传

链路追踪的核心是TraceID在服务间透传。CAT通过HTTP Header传递:

java 复制代码
import com.dianping.cat.Cat;
import com.dianping.cat.message.Message;
import com.dianping.cat.message.Transaction;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

/**
 * RestTemplate拦截器,自动透传CAT上下文
 * 在发起HTTP调用前,将当前线程的CAT上下文注入到Header中
 */
public class CatHttpInterceptor implements ClientHttpRequestInterceptor {

    private static final String CAT_TRACE_ID = "X-CAT-TraceID";
    private static final String CAT_PARENT_SPAN_ID = "X-CAT-ParentSpanID";
    private static final String CAT_ROOT_SPAN_ID = "X-CAT-RootSpanID";

    @Override
    public ClientHttpResponse intercept(
            HttpRequest request,
            byte[] body,
            ClientHttpRequestExecution execution) throws Exception {

        // 创建一个Transaction记录这次HTTP调用
        Transaction t = Cat.newTransaction("Call", request.getURI().toString());

        try {
            // 将CAT上下文信息写入HTTP Header
            HttpHeaders headers = request.getHeaders();
            String context = Cat.getManager().getCatContext().toString();
            headers.add("X-CAT-Context", context);

            // 执行HTTP调用
            ClientHttpResponse response = execution.execute(request, body);

            t.setStatus(Message.SUCCESS);
            return response;
        } catch (Exception e) {
            Cat.logError(e);
            t.setStatus(e);
            throw e;
        } finally {
            t.complete();
        }
    }
}

接收端需要从Header中恢复CAT上下文:

java 复制代码
import com.dianping.cat.Cat;
import com.dianping.cat.CatConstants;
import com.dianping.cat.servlet.CatHttpConstants;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * Servlet Filter,从HTTP Header中恢复CAT上下文
 * 确保下游服务能和上游服务在同一个Trace链路中
 */
public class CatContextFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        try {
            // 从Header中获取上游传过来的CAT上下文
            String catContext = httpRequest.getHeader("X-CAT-Context");

            if (catContext != null && !catContext.isEmpty()) {
                // 解析并设置CAT上下文,这样当前线程的后续操作
                // 会自动归属到同一个TraceID下
                Cat.getManager().getCatContext().parse(catContext);
            }

            chain.doFilter(request, response);
        } finally {
            // 清理线程上下文,防止线程池复用时上下文串了
            Cat.getManager().getCatContext().reset();
        }
    }
}

5.4 完整下单链路埋点示例

java 复制代码
@Service
public class OrderFacadeService {

    @Autowired
    private OrderService orderService;
    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private PaymentService paymentService;
    @Autowired
    private NotifyService notifyService;
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 完整下单流程埋点
     * 每个服务调用都是一个嵌套的Span
     */
    @CatAnnotation(type = "Order", name = "placeOrder")
    public OrderResult placeOrder(OrderRequest request) {
        Transaction orderT = Cat.newTransaction("Order", "placeOrder");

        try {
            // 1. 创建订单
            Cat.logEvent("Order", "createOrder.start", Message.SUCCESS,
                "userId=" + request.getUserId());
            Order order = orderService.createOrder(request);
            Cat.logMetricForCount("order.create.success");

            // 2. 扣减库存(调用库存服务)
            Transaction stockT = Cat.newTransaction("Inventory", "deductStock.remote");
            try {
                boolean stockResult = inventoryService.deductStock(
                    order.getSkuId(), order.getQuantity());
                if (!stockResult) {
                    stockT.setStatus("库存不足");
                    Cat.logEvent("Order", "placeOrder.fail", "ERROR", "库存不足");
                    orderT.setStatus("库存不足");
                    return OrderResult.fail("库存不足");
                }
                stockT.setStatus(Transaction.SUCCESS);
            } finally {
                stockT.complete();
            }

            // 3. 发起支付(调用支付服务)
            Transaction payT = Cat.newTransaction("Payment", "pay.remote");
            try {
                PaymentResult payResult = paymentService.pay(
                    order.getOrderId(), order.getAmount());
                if (!payResult.isSuccess()) {
                    payT.setStatus("支付失败");
                    orderT.setStatus("支付失败");
                    return OrderResult.fail("支付失败");
                }
                payT.setStatus(Transaction.SUCCESS);
                Cat.logMetricForSum("payment.amount", order.getAmount());
            } finally {
                payT.complete();
            }

            // 4. 发送通知
            Transaction notifyT = Cat.newTransaction("Notify", "sendNotify.remote");
            try {
                notifyService.sendOrderNotify(order);
                notifyT.setStatus(Transaction.SUCCESS);
            } catch (Exception e) {
                // 通知失败不影响主流程
                Cat.logError("通知发送失败", e);
                notifyT.setStatus(e);
            } finally {
                notifyT.complete();
            }

            Cat.logMetricForCount("order.placeOrder.success");
            orderT.setStatus(Transaction.SUCCESS);
            return OrderResult.success(order);

        } catch (Exception e) {
            Cat.logError(e);
            orderT.setStatus(e);
            return OrderResult.fail("系统异常");
        } finally {
            orderT.complete();
        }
    }
}

六、CAT监控大盘

6.1 服务健康度看板

CAT的Web控制台提供了服务健康度看板,你可以看到:

  • 各服务的QPS趋势:实时QPS曲线,一眼看出流量高峰
  • 响应时间分布:P50/P90/P99的响应时间变化
  • 错误率监控:5xx错误率的实时趋势
  • 依赖关系:服务间的调用依赖关系

6.2 调用链路拓扑图

CAT能自动生成服务间的调用拓扑图:

复制代码
    ┌───────┐
    │  网关   │
    └───┬───┘
        │ 1000 QPS
    ┌───┴───┐
    │ 订单服务 │
    └───┬───┘
    ┌───┼───┐
    │       │
┌───┴──┐ ┌┴────┐
│库存服务│ │支付服务│
└──────┘ └──┬──┘
            │
        ┌───┴───┐
        │ 通知服务 │
        └───────┘

每个节点上显示QPS、平均响应时间、错误率。如果某个节点变红,说明出问题了。

6.3 慢接口TOP10

CAT会自动统计每个服务的慢接口排行:

排名 接口 平均耗时 P99耗时 调用量 错误率
1 /api/order/create 856ms 3200ms 12000 0.5%
2 /api/inventory/deduct 320ms 1500ms 15000 0.1%
3 /api/payment/pay 280ms 800ms 10000 1.2%
... ... ... ... ... ...

这个功能特别好用,我之前就是通过慢接口TOP10发现了一个N+1查询的问题,优化之后接口耗时从800ms降到了50ms。

6.4 异常报警配置

CAT支持配置报警规则,当指标超过阈值时自动告警:

java 复制代码
// CAT服务端配置报警规则(在CAT管理后台配置)
// 报警维度:
// 1. 错误率超过阈值(如 > 1%)
// 2. 响应时间超过阈值(如 P99 > 3000ms)
// 3. QPS突降(如下降超过50%)
// 4. 自定义业务指标异常

// 报警方式:
// - 邮件通知
// - 钉钉/企业微信机器人
// - 短信通知(严重级别)

七、踩坑指南

7.1 采样率设置

踩坑提醒:100%采样在生产环境是大忌! CAT默认采样率是100%,如果QPS很高,会产生大量监控数据,不仅影响应用性能,还会把CAT服务端打挂。

建议的采样率设置:

环境 建议采样率 说明
开发环境 100% 需要完整的链路信息来调试
测试环境 100% 需要完整的链路信息来验证
预发环境 50% 平衡性能和可观测性
生产环境 10%~30% 根据QPS调整,QPS越高采样率越低

7.2 TraceID透传的线程上下文问题

踩坑提醒:异步线程中TraceID会丢失! CAT的上下文信息存储在ThreadLocal中,如果你用了@AsyncCompletableFuture或线程池,新线程里拿不到TraceID。

解决方案:

java 复制代码
import com.dianping.cat.Cat;
import java.util.concurrent.*;

/**
 * CAT上下文传递的线程池包装器
 * 在提交任务时,将当前线程的CAT上下文传递到子线程
 */
public class CatContextThreadPoolExecutor extends ThreadPoolExecutor {

    public CatContextThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
            long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    public void execute(Runnable command) {
        // 在提交任务前,捕获当前线程的CAT上下文
        String catContext = Cat.getManager().getCatContext().toString();
        super.execute(() -> {
            // 在子线程中恢复CAT上下文
            try {
                Cat.getManager().getCatContext().parse(catContext);
                command.run();
            } finally {
                Cat.getManager().getCatContext().reset();
            }
        });
    }
}

7.3 CAT服务端性能瓶颈

踩坑提醒:CAT服务端本身也可能成为瓶颈。 如果接入的服务太多、QPS太高,CAT服务端的聚合和存储压力会非常大。

应对策略:

  • CAT服务端集群部署,水平扩展
  • 合理设置采样率,减少数据量
  • MySQL报表数据定期归档
  • HDFS原始数据设置合理的保留周期

7.4 日志量过大导致存储压力

踩坑提醒:CAT的Event日志如果滥用,数据量会爆炸式增长。 我见过一个项目在每个方法里都Cat.logEvent,结果一天产生了几百GB的监控数据。

建议:

  • Event只记录关键业务事件和异常
  • Metric用于高频统计,而不是Event
  • 定期检查监控数据的增长趋势

八、问题与解答

Q1:CAT和SkyWalking该选哪个?

A: 看你的需求。如果你只需要链路追踪,SkyWalking更合适,接入简单,无侵入。如果你需要全面的监控(链路追踪+业务监控+告警),CAT更合适。CAT的侵入性虽然高一点,但换来的是更细粒度的监控能力。国内大厂很多选CAT就是因为它的业务监控能力很强。

Q2:CAT的Transaction不调用complete()会怎样?

A: 会导致内存泄漏。CAT的Transaction在创建时会在内部维护一个栈结构,complete()是出栈操作。如果不调用complete(),这个Transaction会一直留在栈中,导致内存泄漏,最终可能OOM。所以一定要在finally块中调用complete(),这是血泪教训。

Q3:CAT的采样率怎么设置比较合理?

A: 生产环境建议10%~30%。具体要看你的QPS和CAT服务端的承受能力。一个简单的估算公式:每天的数据量 = QPS × 采样率 × 86400 × 单条数据大小。如果QPS是10000,采样率10%,单条数据1KB,一天大约8.6GB。根据你的存储能力来反推采样率。


九、面试高频考点汇总

考点1:什么是分布式链路追踪?为什么要用它?

答: 分布式链路追踪是一种用于跟踪请求在分布式系统中各个服务间调用路径的技术。在微服务架构中,一个请求可能经过多个服务,链路追踪通过给每个请求分配一个唯一的TraceID,并在服务间透传这个ID,从而将整个调用链串联起来。它的核心价值在于:快速定位性能瓶颈、排查故障、分析服务依赖关系。

考点2:Trace、Span、SpanContext的关系是什么?

答: Trace是一次完整的请求链路,由多个Span组成。Span是链路中的一个调用单元,记录了单次调用的详细信息(服务名、操作名、耗时等)。SpanContext是Span的上下文,包含TraceID、SpanID、ParentSpanID等信息,用于在服务间传递链路关系。多个Span通过ParentSpanID形成树状结构,构成一条完整的Trace。

考点3:CAT的消息模型有哪些?分别什么场景使用?

答: CAT有四种消息模型:Transaction用于记录有开始和结束的调用(如HTTP请求、方法调用);Event用于记录离散事件(如错误日志、业务事件);Heartbeat用于上报系统状态(如CPU、内存使用率);Metric用于记录指标数据(如QPS、响应时间、错误率)。Transaction是最核心的模型,用于构建调用链路。

考点4:CAT的TraceID是怎么在服务间透传的?

答: CAT通过HTTP Header来透传TraceID。在发起HTTP调用前,客户端拦截器会将当前线程的CAT上下文(包含TraceID、SpanID等信息)写入HTTP Header。接收端通过Servlet Filter从Header中取出上下文信息并恢复到当前线程。这样上下游服务就归属到同一个Trace链路中。对于异步场景,需要手动传递ThreadLocal中的上下文。

考点5:CAT在生产环境的采样率应该怎么设置?

答: 生产环境建议10%~30%的采样率。100%采样会产生大量数据,影响应用性能和CAT服务端的存储压力。具体设置需要根据QPS和存储能力来估算。开发环境和测试环境可以100%采样,因为需要完整的链路信息来调试和验证。采样率过低可能导致慢请求和异常请求被漏掉,需要在性能和可观测性之间找平衡。


十、模拟面试官提问

场景题1:线上用户反馈下单超时,你怎么排查?

参考答案:

第一步,拿到用户的请求时间,在CAT控制台通过时间范围搜索该用户的请求链路。第二步,找到对应的TraceID,查看完整的调用链路拓扑。第三步,分析每个Span的耗时,定位到耗时最长的服务。第四步,进入该服务的详细监控,查看是SQL慢、外部调用慢还是业务逻辑慢。第五步,如果是SQL慢,通过CAT的SQL监控找到慢SQL,优化索引或SQL语句。整个排查过程通常5-10分钟就能定位问题。

场景题2:你们CAT接入后,对系统性能有多大影响?

参考答案:

CAT对性能的影响主要在三个方面:一是埋点代码本身的开销,CAT的API调用非常轻量,通常在微秒级别;二是数据上报的开销,CAT采用异步批量上报,Netty客户端将数据先写到内存队列,然后批量发送,对业务线程几乎无阻塞;三是采样率的影响,10%采样率下QPS为10000的系统,CAT的额外CPU开销通常在1%-3%以内。我们在压测环境中做过对比,接入CAT后接口P99响应时间增加了不到5ms。

场景题3:异步场景下CAT的TraceID丢失了,你怎么解决?

参考答案:

TraceID丢失的根本原因是CAT的上下文存储在ThreadLocal中,异步线程无法访问。解决方案有三种:一是自定义线程池包装器,在提交任务时捕获当前线程的CAT上下文,在子线程执行时恢复;二是使用CAT提供的Cat.logRemoteCall方法,手动传递上下文;三是使用InheritableThreadLocal,但这只适用于创建子线程的场景,线程池复用时会有问题。我们项目用的是第一种方案,封装了一个CatContextThreadPoolExecutor,所有异步任务都通过它提交。

场景题4:CAT服务端挂了会影响业务吗?

参考答案:

不会。CAT客户端的设计原则是"业务优先"。客户端的数据上报是异步的,数据先写到内存队列,然后批量发送。如果CAT服务端不可用,客户端会将数据丢弃(或缓存到本地文件),不会阻塞业务线程。CAT客户端有本地消息队列的容量限制,超过容量后新数据会被丢弃,但不会影响业务逻辑的执行。所以CAT服务端挂了,业务系统不受影响,只是这段时间的监控数据会丢失。

场景题5:如果让你从零搭建一个链路追踪系统,你会怎么设计?

参考答案:

我会分四个层面来设计:一是埋点层,通过Java Agent字节码增强实现无侵入埋点,拦截HTTP、RPC、SQL等调用,生成Span数据;二是传输层,Span数据先写到内存队列,然后通过Kafka异步传输到服务端,保证高吞吐和低延迟;三是存储层,热数据存Elasticsearch(支持全文检索),冷数据存HDFS(低成本存储),聚合报表存MySQL;四是展示层,提供调用链路查询、服务拓扑图、性能大盘、告警配置等功能。核心难点在于TraceID的透传和异步场景的上下文传递,需要考虑ThreadLocal、线程池、消息队列等各种场景。


互动话题

你在实际项目中用过CAT或者SkyWalking吗?遇到过什么坑?欢迎在评论区分享你的经验。比如:

  • 你们公司的链路追踪用的什么工具?选型的理由是什么?
  • 接入CAT后有没有发现过什么意想不到的问题?
  • 异步场景下的TraceID传递你们是怎么解决的?

参考资料