排查一次下单失败,登了 7 台机器
4 月初有用户反馈下单失败,报错是"系统繁忙"。这个接口要经过 7 个服务:网关 → 订单 → 库存 → 价格 → 风控 → 优惠券 → 支付。
我们的排查方式是:先猜可能是哪个服务,登录它的机器 grep 日志,找不到就换下一个。那天从晚上八点查到十点半,最后在风控服务里找到一句 RiskRejectException,根因是风控规则表的一条配置被改错了。
2.5 小时。这个效率不能再忍了,那一周我开始做链路追踪。
选型:为什么是 SkyWalking
| 方案 | 接入方式 | 我们的顾虑 |
|---|---|---|
| SkyWalking 8.4.0 | Java Agent,字节码增强,零代码 | OAP 要单独部署,存储用 ES |
| Zipkin + Sleuth | Spring 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 传递链路上下文,一旦换了线程,上下文就丢了。我们有三处:
@Async异步方法- 自己创建的线程池
- 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);
}));
}
}
三个包装类对应三种场景:RunnableWrapper、CallableWrapper、SupplierWrapper。
对于 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 ms | 159 ms (+12%) | 147 ms (+3.5%) |
| P99 | 341 ms | 402 ms (+18%) | 352 ms (+3.2%) |
| CPU | 44% | 58% | 49% |
| 堆内存 | 2.1 GB | 2.4 GB | 2.2 GB |
| Full GC 次数 | 3 | 7 | 4 |
| Young GC 平均耗时 | 12 ms | 18 ms | 13 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 要提前配(
recordDataTTL7 天,指标 45 天),否则磁盘很快满。我们第一周没配,三天涨了 240 GB。 - 告警规则写在
alarm-settings.yml,webhook 转发到钉钉。
还有个副作用我没预料到:接入链路追踪之后,跨团队扯皮变少了。以前订单慢,订单组说是库存慢,库存组说是价格慢。现在把 traceId 甩群里,谁慢一目了然。这个收益比技术本身还大。