Administrator
发布于 2021-04-03 / 3325 阅读
36

SkyWalking 链路追踪接入实践

排查一次下单失败,登了 7 台机器

4 月初有用户反馈下单失败,报错是"系统繁忙"。这个接口要经过 7 个服务:网关 → 订单 → 库存 → 价格 → 风控 → 优惠券 → 支付。

我们的排查方式是:先猜可能是哪个服务,登录它的机器 grep 日志,找不到就换下一个。那天从晚上八点查到十点半,最后在风控服务里找到一句 RiskRejectException,根因是风控规则表的一条配置被改错了。

2.5 小时。这个效率不能再忍了,那一周我开始做链路追踪。

选型:为什么是 SkyWalking

方案接入方式我们的顾虑
SkyWalking 8.4.0Java Agent,字节码增强,零代码OAP 要单独部署,存储用 ES
Zipkin + SleuthSpring Cloud 集成,需引依赖功能偏简单,只有调用链没有指标
CAT(大众点评)需代码埋点侵入性太强,7 个服务都要改

决定因素是零侵入。我们这 7 个服务有几个是老服务,改代码要过测试,成本很高。SkyWalking 只要加一个 -javaagent 启动参数。

还有一个加分项:SkyWalking 不只是链路追踪,它同时有服务拓扑、JVM 指标、实例告警、数据库慢查询。我们当时正好也在评估 APM,一套东西解决两件事。

架构和部署

三个部分:

应用进程 (skywalking-agent.jar)
    ↓ gRPC 11800
OAP Server (接收、分析、聚合)
    ↓ 写入
Elasticsearch 7.10 集群
    ↑ 查询
SkyWalking UI (8080)

OAP 用 Docker 起,ES 复用已有的日志集群(单独建了 sw_* 索引)。

version: '3'
services:
  oap:
    image: apache/skywalking-oap-server:8.4.0-es7
    ports:
      - "11800:11800"
      - "12800:12800"
    environment:
      SW_STORAGE: elasticsearch7
      SW_STORAGE_ES_CLUSTER_NODES: 10.0.3.11:9200,10.0.3.12:9200
      SW_STORAGE_ES_INDEX_SHARDS_NUMBER: 2
      SW_STORAGE_ES_INDEX_REPLICAS_NUMBER: 1
      SW_STORAGE_ES_RECORD_DATA_TTL: 7        # 链路明细保留 7 天
      SW_STORAGE_ES_OTHER_METRIC_DATA_TTL: 45 # 指标保留 45 天
      SW_STORAGE_ES_MONTH_METRIC_DATA_TTL: 18 # 保留 18 个月
      SW_CORE_RECORD_DATA_TTL: 7
  ui:
    image: apache/skywalking-ui:8.4.0
    ports:
      - "8080:8080"
    environment:
      SW_OAP_ADDRESS: oap:12800

接入:只改一行启动参数

Dockerfile 加两行:

FROM openjdk:11-jre-slim
COPY skywalking-agent/ /opt/skywalking-agent/
COPY target/order-service.jar /app.jar

ENTRYPOINT ["java", \
    "-javaagent:/opt/skywalking-agent/skywalking-agent.jar", \
    "-Dskywalking.agent.service_name=order-service", \
    "-Dskywalking.collector.backend_service=10.0.3.20:11800", \
    "-jar", "/app.jar"]

也可以不用 Dockerfile,直接在 K8s 的 deployment 里加环境变量:

env:
  - name: JAVA_TOOL_OPTIONS
    value: "-javaagent:/opt/skywalking-agent/skywalking-agent.jar"
  - name: SW_AGENT_NAME
    value: order-service
  - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES
    value: 10.0.3.20:11800

agent 的配置在 config/agent.config 里,但用环境变量或系统属性覆盖更方便,不用每次改镜像里的文件。

七分钟之后,UI 上就出现了拓扑图。那一刻挺震撼的,我从没见过我们系统的真实调用关系长什么样。

把 traceId 打进业务日志

光有 UI 还不够。问题发生时,我要能从业务日志反查到这条链路,也能从链路跳到日志。两边必须对得上。

加一个依赖:

<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-logback-1.x</artifactId>
    <version>8.4.0</version>
</dependency>

改 logback 的 pattern,用 %tid 占位符:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%tid] %logger{36} - %msg%n</pattern>
        </layout>
    </encoder>
</appender>

注意 layout 必须换成 TraceIdPatternLogbackLayout,用普通的 PatternLayout 的话 %tid 不生效。

日志变成这样:

2021-04-08 14:22:41.318 [http-nio-8080-exec-18] INFO  [TID:order-service.8823412031.41.162] c.x.OrderService - create order, orderId=...

TID 拿到手,就能在 SkyWalking UI 的搜索框里直接粘贴查到整条链路。

没有链路时(比如定时任务触发的),%tid 会输出成 TID:N/A,不会报错。

异步和 MQ 场景:traceId 会断

这是接入后遇到的第一个真问题。Agent 用 ThreadLocal 传递链路上下文,一旦换了线程,上下文就丢了。我们有三处:

  1. @Async 异步方法
  2. 自己创建的线程池
  3. RocketMQ / Kafka 的消费端

表现是:链路图里主流程断了,后面那段变成一条独立的、没有父节点的链路。

SkyWalking 提供了包装类来解决:

<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-trace</artifactId>
    <version>8.4.0</version>
</dependency>
@Service
public class AsyncOrderTask {

    @Autowired private ThreadPoolExecutor taskPool;

    public void submitAfterCreate(Order order) {
        String traceId = TraceContext.traceId();      // 主线程里取出来

        taskPool.submit(RunnableWrapper.of(() -> {
            // 子线程里链路上下文已恢复
            log.info("async notify, traceId={}", TraceContext.traceId());
            notifyService.send(order);
        }));
    }
}

三个包装类对应三种场景:RunnableWrapperCallableWrapperSupplierWrapper

对于 MQ,我们做的更彻底:把 traceId 塞进消息头,消费端取出来放到自己的日志 MDC 里。这样即使不依赖 agent 的跨进程传播,也能人工串起来:

// 生产端
Message msg = new Message("ORDER_PAID", JSON.toJSONBytes(event));
msg.putUserProperty("traceId", TraceContext.traceId());

// 消费端
String traceId = msg.getUserProperty("traceId");
MDC.put("traceId", traceId);
try {
    handle(msg);
} finally {
    MDC.remove("traceId");
}

logback 里加 %X{traceId} 就能输出。这个方案不优雅,但对 MQ 这种"天然异步、可能延迟几小时才消费"的场景,比依赖链路传播更实用——因为链路早就结束了,你只能靠日志。

性能损耗:实测

这是接入前大家最担心的。我在压测环境做了完整对比,QPS 1200,持续 30 分钟。

指标无 Agent有 Agent(采样率 100%)有 Agent(采样率 30%)
平均响应时间142 ms159 ms (+12%)147 ms (+3.5%)
P99341 ms402 ms (+18%)352 ms (+3.2%)
CPU44%58%49%
堆内存2.1 GB2.4 GB2.2 GB
Full GC 次数374
Young GC 平均耗时12 ms18 ms13 ms

100% 采样率下 P99 涨 18%,这个代价我们接受不了。降到 30% 采样后只涨 3.2%,且拓扑图和指标统计不受影响(指标是全量聚合的,采样的只是链路明细)。

配置:

# agent.config
agent.sample_n_per_3_secs=-1        # 不用这个限流方式
agent.trace_segment_ref_limit_per_span=500

采样率在 OAP 端配(8.4 支持通过 SW_RECEIVER_ZIPKIN_... 不对,采样率是在 agent 端通过后端动态下发,或者在 agent.config 里配 agent.sample_n_per_3_secs)。我们最后用的是 OAP 端配置文件里的 sampleRate

# config/application.yml receiver-trace 部分
receiver-trace:
  default:
    sampleRate: 10000       # 10000 / 10000 = 100%,设成 3000 即 30%

这个值改完后要重启 OAP。我们设成 3000(30%)。

另外,agent 自己也在向 OAP 发数据,走 gRPC。如果 OAP 挂了,agent 会缓存数据并重试,不会阻塞业务线程,这点我们验证过——kill 掉 OAP 十分钟,业务完全正常,恢复后数据补传上来了。

告警接钉钉

SkyWalking 8.x 的告警规则写在 config/alarm-settings.yml

rules:
  service_resp_time_rule:
    metrics-name: service_resp_time
    op: ">"
    threshold: 1000
    period: 10
    count: 3
    silence-period: 5
    message: 服务 {name} 响应时间超过 1000ms,最近 10 分钟内 3 次

  service_sla_rule:
    metrics-name: service_sla
    op: "<"
    threshold: 9900
    period: 10
    count: 2
    message: 服务 {name} 成功率低于 99%

webhooks:
  - http://10.0.3.30:8080/alert/dingtalk

webhook 收到的是 JSON,我们自己写了个小服务转发到钉钉机器人。

接入两周后的一次实战

接入后第二周,商品服务 P99 突然升高。我打开 SkyWalking,三分钟定位到:

  • 拓扑图上看,是 item-service → price-service 这一段变红
  • 点进去看链路,最慢的那个 span 是 GET /price/batch,耗时 812 ms
  • SkyWalking 自动采集了 SQL:SELECT * FROM t_price WHERE item_id IN (...),显示执行时间 780 ms
  • 拿这条 SQL 去 EXPLAIN,发现走了全表扫描,索引失效了

根因是前一天上线的一段代码把 item_id 的类型从 Long 改成了 String,MySQL 隐式类型转换导致索引失效。

从发现到定位,3 分钟。对比之前的 2.5 小时。

小结

  • SkyWalking Agent 是字节码增强,零代码侵入,加一个 -javaagent 参数就行。K8s 里用 JAVA_TOOL_OPTIONS 环境变量更方便。
  • 必须把 traceId 打进业务日志:引 apm-toolkit-logback-1.x,layout 换成 TraceIdPatternLogbackLayout,pattern 里用 %tid
  • 异步线程会丢链路上下文,用 RunnableWrapper / CallableWrapper 包装。MQ 场景建议直接把 traceId 塞消息头,靠日志串联更可靠。
  • 性能损耗实测:100% 采样 P99 +18%,30% 采样 P99 +3.2%。一定要降采样
  • ES 的 TTL 要提前配(recordDataTTL 7 天,指标 45 天),否则磁盘很快满。我们第一周没配,三天涨了 240 GB。
  • 告警规则写在 alarm-settings.yml,webhook 转发到钉钉。

还有个副作用我没预料到:接入链路追踪之后,跨团队扯皮变少了。以前订单慢,订单组说是库存慢,库存组说是价格慢。现在把 traceId 甩群里,谁慢一目了然。这个收益比技术本身还大。

参考