Spring Cloud Alibaba 微服务线上故障应急排查手册
本手册覆盖 Nacos、Sentinel、Seata、Dubbo、RocketMQ、Spring Cloud Gateway 六大核心组件,每个章节以真实生产场景驱动,给出从告警触发到根因定位的完整排查路径、可直接复制执行的命令,以及参数级修复方案。
一、应急响应总则
1.1 故障分级与响应时效
| 级别 | 判定标准 | 响应时效 | 处置原则 |
|---|---|---|---|
| P0 致命 | 核心交易链路全部不可用,大面积用户受影响 | 1 分钟内响应,5 分钟内止血 | 先恢复再排查:限流、降级、回滚、切流 |
| P1 严重 | 核心链路部分功能不可用,或非核心链路全部不可用 | 3 分钟内响应,15 分钟内止血 | 限流降级优先,定位后精准修复 |
| P2 一般 | 局部功能异常,影响范围可控 | 10 分钟内响应,30 分钟内修复 | 深入定位根因,避免遗留隐患 |
| P3 轻微 | 个别请求异常,无用户感知 | 当日处理 | 记录工单,纳入迭代修复 |
1.2 黄金十分钟排查路径
收到告警后的标准动作顺序,遵循"由外向内、先快后深"原则:
告警触发
│
├─ 第 1 分钟:确认告警真实性
│ └─ 查看监控大盘,确认指标是否真实异常(排除监控自身故障)
│
├─ 第 2-3 分钟:判断影响范围
│ └─ 通过全链路追踪确认异常服务节点、错误率、RT 飙升曲线
│
├─ 第 4-5 分钟:止血决策
│ ├─ 单节点故障 → 摘除流量(Nacos 下线实例 / Gateway 路由屏蔽)
│ ├─ 依赖故障 → 降级开关(Sentinel 熔断 / 配置中心动态降级)
│ ├─ 全量异常 → 版本回滚
│ └─ 容量不足 → 弹性扩容
│
├─ 第 6-10 分钟:根因定位
│ └─ 根据异常组件类型,进入对应章节的专项排查流程
│
└─ 10 分钟后:根因修复 + 复盘准备
1.3 止血手段优先级
止血不等于修复,目标是在排查期间将业务影响降到最低。按优先级从高到低:
- 流量摘除 — 将异常节点从负载均衡中移除,请求转发到健康节点
- 熔断降级 — 对异常依赖启用 Sentinel 熔断或手动降级开关,阻止故障扩散
- 限流削峰 — 降低入口 QPS,保护下游不被压垮
- 回滚版本 — 若故障与近期发布时间高度吻合,直接回滚到上一稳定版本
- 弹性扩容 — 容量瓶颈导致的 RT 飙升,快速增加实例数
二、通用排查工具箱
2.1 JVM 层排查命令
线上问题的第一步通常是判断是应用本身的问题还是外部依赖的问题。以下命令覆盖了 80% 的 JVM 层快速诊断需求。
# 查看所有 Java 进程,确认目标服务 PID
jps -lvm
# 查看 JVM 堆内存概况(新生代/老年代/元空间各区域使用情况)
jmap -heap <pid>
# Dump 堆内存用于离线分析内存泄漏(生产环境注意文件体积,建议加 live 参数只 dump 存活对象)
jmap -dump:live,format=b,file=/tmp/heap_$(date +%s).hprof <pid>
# 查看线程堆栈,排查死锁、线程阻塞、线程池耗尽
jstack <pid>
jstack -l <pid> # -l 额外打印锁信息,排查死锁时必加
# GC 统计,每 1 秒采样 1 次,共 10 次,观察 Full GC 频率和耗时
jstat -gcutil <pid> 1000 10
# 查看进程内各线程 CPU 占用,定位高 CPU 线程
top -H -p <pid>
# 将高 CPU 线程 ID(十进制)转为十六进制,用于在 jstack 输出中定位对应线程
printf "%x\n" <tid>
2.2 Arthas 在线诊断
Arthas 是 Alibaba 开源的 Java 诊断工具,无需重启应用即可在线观测方法执行、修改日志级别、热更新代码。生产环境使用时需注意:观测完毕后必须执行 stop 或 reset 还原字节码增强。
# 启动 Arthas,选择目标进程
java -jar arthas-boot.jar
全局诊断
# 实时数据面板:线程、内存、GC、运行时环境一览
dashboard
# 查看 JVM 详细信息(类加载器、GC、编译器、运行参数)
jvm
# 查看各内存区域使用情况
memory
线程排查
# 查看所有线程状态
thread
# 查看最忙的 3 个线程(CPU 占用最高的 3 个)
thread -n 3
# 查看指定线程的堆栈
thread <thread-id>
# 检测死锁(查找阻塞其他线程的线程)
thread -b
方法级诊断(高频使用)
# 方法执行监控:统计 10 秒内的调用次数、成功率和平均 RT
monitor com.example.order.service.OrderService createOrder -c 10
# 方法内部调用路径及耗时:逐层展示方法内部各子调用的耗时,定位慢调用根源
trace com.example.order.service.OrderService createOrder
# 方法执行数据观测:查看入参、返回值、抛出的异常
watch com.example.order.service.OrderService createOrder "{params, returnObj, throwExp}" -x 2
# 方法调用栈:查看谁在调用这个方法,定位异常调用来源
stack com.example.order.service.OrderService createOrder
# 时空隧道:记录每次方法调用,可事后回放查看入参和返回值
tt -t com.example.order.service.OrderService createOrder
类与字节码操作
# 查看已加载的类信息(来源 jar 包、类加载器)
sc -d com.example.order.service.OrderService
# 反编译已加载的类,确认线上运行代码与预期一致
jad com.example.order.service.OrderService
# 临时热更新代码(验证修复方案,不可作为正式发布手段)
redefine /tmp/OrderService.class
动态日志与运行时修改
# 在线修改日志级别(故障排查时临时开启 DEBUG,无需重启)
logger --name com.example.order --level DEBUG
# 查看和修改 JVM 系统属性
sysprop
sysprop spring.profiles.active
# 查看环境变量
sysenv
内存与性能分析
# Dump 堆内存到文件
heapdump /tmp/heap.hprof
# 生成火焰图(async-profiler 集成),用于 CPU 性能分析
profiler start
# 采集 30 秒后停止并生成 HTML 报告
profiler stop --format html
2.3 日志排查技巧
# 按时间范围过滤日志(排查特定时段的异常)
grep "2026-07-15 10:[0-5]" /opt/app/logs/app.log | grep -i "error\|exception"
# 统计错误类型分布(快速定位最高频异常)
grep "Exception" /opt/app/logs/app.log | awk -F: '{print $1}' | sort | uniq -c | sort -rn | head -20
# 通过 traceId 追踪全链路日志
grep "traceId=a1b2c3d4" /opt/app/logs/app.log
# 统计某接口在指定时段的 P99 响应时间
grep "/api/order/create" access.log | awk '{print $NF}' | sort -n | awk 'BEGIN{c=0} {a[c++]=$1} END{print a[int(c*0.99)]}'
三、Nacos 服务注册与配置中心
3.1 场景:服务注册失败,消费端报 “No instances found”
现象描述:order-service 重新部署后,user-service 调用时报 No instances found for order-service,Nacos 控制台服务列表中该服务实例数为 0。
排查步骤:
第一步,确认网络连通性。Nacos 2.x 需要 3 个端口全部可达,gRPC 端口不可达是最常见的注册失败原因。
# Nacos 2.x 需要开放的三个端口
# 8848 — HTTP API 主端口
# 9848 — 客户端 gRPC 端口(主端口 + 1000)
# 9849 — 服务端 gRPC raft 端口(主端口 + 1001)
telnet <nacos-ip> 8848
telnet <nacos-ip> 9848
telnet <nacos-ip> 9849
# 确认防火墙是否放行
firewall-cmd --list-ports
第二步,查看应用端 Nacos 客户端日志,定位注册阶段的具体报错。
# 查看注册相关日志
grep -i "register\|failed\|exception" /opt/app/logs/nacos/naming.log
# 典型报错及含义:
# "failed to register" — 注册请求失败,检查网络或鉴权
# "invalid token" — 鉴权 token 失效,需重新生成
# "metadata too long" — 实例元数据超过 512 字节限制
# "Connection refused" — Nacos 服务端不可达
第三步,确认 Namespace 使用的是 ID 而非名称。这是一个高频踩坑点:Nacos 的 Namespace 在配置时必须填 ID(如 a1b2c3d4-e5f6-...),不能填名称(如 production),否则客户端会注册到错误的 Namespace 导致消费端找不到。
# 错误写法(使用名称)
spring:
cloud:
nacos:
discovery:
namespace: production # 错误!Nacos 不认名称
# 正确写法(使用 Namespace ID)
spring:
cloud:
nacos:
discovery:
namespace: a1b2c3d4-e5f6-7890-abcd-ef1234567890 # 在 Nacos 控制台 → 命名空间 列表中获取
根因与修复:
| 根因 | 特征日志 | 修复方法 |
|---|---|---|
| gRPC 端口不通 | Connection refused |
防火墙放行 9848/9849 端口 |
| 鉴权 token 失效 | invalid token |
检查 nacos.core.auth.* 配置,重新生成令牌 |
| 元数据超限 | metadata too long |
精简实例 metadata,控制在 512 字节内 |
| Namespace 填名称 | 注册成功但消费端找不到 | 改用 Namespace ID |
| 集群脑裂 | leader not found |
强制重新选举:curl -X POST http://<ip>:8848/nacos/v1/ns/operator/resetCluster |
3.2 场景:服务已下线但仍被调用(推空保护)
现象描述:payment-service 的某个实例已在 Nacos 控制台手动下线,但 order-service 仍然向该实例发送请求,导致调用失败。
排查步骤:
第一步,登录 Nacos 控制台,确认该实例在服务列表中的状态。如果控制台显示实例已下线但消费端仍调用,进入第二步。
第二步,检查消费端是否存在推空保护机制。推空保护是 Nacos 客户端的一个安全策略:当注册中心返回空实例列表时,客户端不会清空本地缓存,而是保留旧实例信息,防止注册中心故障导致所有服务不可用。但在实例真正下线时,这个机制会导致已下线实例仍被调用。
# 查看消费端缓存的服务实例
# 通过 Arthas 查看客户端缓存
java -jar arthas-boot.jar
ognl '@com.alibaba.nacos.client.naming.core.HostReactor@serviceInfoMap' -x 2
# 查看 Nacos 客户端推送日志
grep "push\|notify" /opt/app/logs/nacos/naming.log
第三步,确认推送是否到达消费端。如果 Nacos 服务端已移除实例但消费端未收到推送通知,检查消费端与 Nacos 的 gRPC 长连接是否正常。
# 查看消费端与 Nacos 的连接状态
netstat -anp | grep 9848 | grep ESTABLISHED
修复方法:
# 方案一:关闭推空保护(需评估风险,注册中心故障时可能导致全部实例被清空)
spring:
cloud:
nacos:
discovery:
# Nacos 客户端 2.x 默认开启推空保护
naming-load-cache-at-start: false
# 方案二:重启消费端进程,强制重新拉取服务列表
# (生产环境慎用,仅在其他方法无效时执行)
# 方案三:通过 Nacos API 手动触发实例下线
curl -X PUT "http://<nacos-ip>:8848/nacos/v1/ns/instance?serviceName=payment-service&ip=<instance-ip>&port=8080&enabled=false"
3.3 场景:配置修改后不生效
现象描述:在 Nacos 控制台修改了 order-service 的配置项(如将超时时间从 3000ms 改为 5000ms),应用日志中未出现配置刷新记录,行为未变化。
排查步骤:
第一步,验证 dataId、group、namespace 三要素是否完全匹配。这是配置不生效最常见的根因,控制台显示的配置和应用实际读取的配置可能不在同一个 namespace 下。
# 通过 API 直接查询配置内容,确认应用读取的是这份配置
curl "http://<nacos-ip>:8848/nacos/v1/cs/configs?dataId=order-service-prod.yml&group=ORDER_GROUP&tenant=<namespace-id>"
第二步,检查应用日志中是否有配置监听和刷新记录。
# 查看配置刷新日志
grep "config\|refresh\|listener" /opt/app/logs/nacos/config.log
# 如果日志中没有 "refresh" 记录,说明客户端未收到推送
# 检查长连接是否正常
netstat -anp | grep 9848 | grep ESTABLISHED
第三步,如果是集群部署,检查各节点配置一致性。
# 对比集群各节点同一份配置的 MD5,确认是否一致
for ip in 192.168.1.10 192.168.1.11 192.168.1.12; do
echo -n "$ip: "
curl -s "http://$ip:8848/nacos/v1/cs/configs?dataId=order-service-prod.yml&group=ORDER_GROUP&tenant=<namespace-id>" | md5sum
done
第四步,确认配置格式。如果配置使用 .yaml 扩展名但内容实际是 .properties 格式,或反之,Nacos 会静默解析失败。
根因排查表:
| 根因 | 验证方法 | 修复方法 |
|---|---|---|
| 三要素不匹配 | 用 API 查询配置确认 | 对齐 dataId/group/namespace |
| 长连接断开 | netstat 检查 9848 端口 |
重启应用恢复长连接 |
| 集群不一致 | 对比各节点 MD5 | 排查 DB 同步延迟,必要时强制推送 |
| 配置格式错误 | 检查文件扩展名与内容格式 | 统一格式,YAML 用 .yml/.yaml,属性用 .properties |
@RefreshScope 缺失 |
检查使用 @Value 的 Bean 是否加了注解 |
在需要动态刷新的 Bean 上添加 @RefreshScope |
3.4 场景:Nacos 集群脑裂
现象描述:Nacos 集群 3 个节点中,1 个节点脱离集群后独立运行,出现两个 leader,配置写入后在部分节点可见、部分节点不可见。
排查步骤:
# 查看集群各节点状态
for ip in 192.168.1.10 192.168.1.11 192.168.1.12; do
echo -n "$ip: "
curl -s "http://$ip:8848/nacos/v1/ns/operator/metrics" | jq '.leader'
done
# 查看集群日志,搜索选举和脑裂相关关键字
grep -i "election\|leader\|term\|unavailable" /opt/nacos/logs/cluster-alert.log
# 检查 Raft 日志
grep -i "raft\|vote\|term" /opt/nacos/logs/protocol-raft.log
修复方法:
# 1. 停止异常节点(脱离集群独立运行的节点)
ssh <abnormal-ip> "sh /opt/nacos/bin/shutdown.sh"
# 2. 等待 15 秒确保 Raft 选举完成(观察 leader 日志确认新 leader 产生)
sleep 15 && grep "leader" /opt/nacos/logs/protocol-raft.log | tail -5
# 3. 清理异常节点数据后重新加入集群
# 删除 data 目录下的 protocol/raft 快照(谨慎操作)
rm -rf /opt/nacos/data/protocol/raft/
# 4. 启动节点重新加入集群
ssh <abnormal-ip> "sh /opt/nacos/bin/startup.sh"
# 5. 如果脑裂严重无法自动恢复,强制重置集群状态(最后手段,需评估数据丢失风险)
curl -X POST "http://<leader-ip>:8848/nacos/v1/ns/operator/resetCluster"
3.5 Nacos 关键日志文件
| 日志文件 | 路径 | 排查时关注的关键字 |
|---|---|---|
| 主日志 | logs/nacos.log |
ERROR、Exception、reject |
| 命名服务日志 | logs/naming.log |
register、deregister、push |
| 配置服务日志 | logs/config-server.log |
sync failed、MD5 mismatch |
| 集群告警日志 | logs/cluster-alert.log |
election、term、unavailable |
| Raft 协议日志 | logs/protocol-raft.log |
leader、vote、term |
四、Sentinel 熔断限流
4.1 场景:限流规则配置后不生效
现象描述:在 Sentinel Dashboard 中为 order-service 的 /api/order/create 接口配置了 QPS 限流规则(阈值 100),但压测时 QPS 超过 200 仍未触发限流,接口正常处理。
排查步骤:
第一步,确认应用是否成功接入 Dashboard。如果 Dashboard 中的机器列表为空,说明应用未注册到 Dashboard,规则无法推送。
# 检查应用启动参数是否配置了 Dashboard 地址
ps -ef | grep java | grep -- '-Dcsp.sentinel.dashboard.server'
# 预期输出包含:
# -Dcsp.sentinel.dashboard.server=<dashboard-ip>:8080
# -Dproject.name=order-service
# 检查客户端与 Dashboard 的心跳连接
grep "dashboard" /opt/app/logs/sentinel/sentinel-record.log
第二步,确认资源名称与代码中一致。这是限流不生效的高频原因:Dashboard 中配置的资源名必须与代码中的资源定义完全一致。
// 情况一:Spring MVC 接口,Sentinel 默认资源名是 URL 路径
// Dashboard 中应配置资源名为:/api/order/create
// 情况二:使用 @SentinelResource 注解,资源名是注解的 value
@SentinelResource(value = "createOrder", blockHandler = "createOrderBlockHandler")
public OrderResult createOrder(OrderRequest request) { ... }
// Dashboard 中应配置资源名为:createOrder(不是 URL 路径)
// 情况三:自定义资源名
try (Entry entry = SphU.entry("customResourceName")) {
// 业务逻辑
} catch (BlockException e) {
// 限流处理
}
第三步,确认引入了正确的依赖。如果项目同时引入了 spring-cloud-starter-alibaba-sentinel 和 sentinel-core,版本不一致会导致规则不生效。
<!-- 确保只引入 spring-cloud-starter-alibaba-sentinel,它会传递依赖正确的 sentinel-core 版本 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 不要手动指定 sentinel-core 版本,避免版本冲突 -->
第四步,确认规则是否满足触发条件。Sentinel 的熔断规则有最小请求数限制:如果配置了 RT 熔断(阈值 500ms,最小请求数 5),但实际 1 秒内只有 3 个请求,即使全部超时也不会触发熔断。
# 通过 Dashboard 查看实时监控
# 菜单:簇点链路 → 选择资源 → 查看实时监控
# 关注:通过 QPS、拒绝 QPS、平均 RT、异常数
4.2 场景:熔断频繁误触发,正常请求被拒绝
现象描述:order-service 的 /api/order/create 接口配置了异常比例熔断(阈值 50%,最小请求数 5,时间窗口 10 秒),但正常业务量下频繁触发熔断,导致大量正常请求被拒绝。
排查步骤:
第一步,通过 Dashboard 查看异常类型分布。异常比例熔断统计的是所有异常,包括业务异常(如参数校验失败)和系统异常(如超时、空指针)。如果业务异常占比高,熔断阈值会被误触发。
# 查看 sentinel-record.log 中的熔断事件记录
grep "DegradeException\|FlowException" /opt/app/logs/sentinel/sentinel-record.log | tail -20
# 典型日志:
# DegradeException: max rt exceeds threshold — RT 熔断触发
# FlowException: qps exceeds threshold — QPS 限流触发
第二步,使用 Arthas 的 watch 命令查看方法实际抛出的异常,区分业务异常和系统异常。
# 观察方法入参、返回值和异常
watch com.example.order.service.OrderService createOrder "{params, returnObj, throwExp}" -x 2
# 如果发现大量 BusinessException(业务异常),应调整熔断策略
# 方案:改用异常数熔断并排除业务异常,或提高异常比例阈值
第三步,检查熔断策略与实际故障类型是否匹配。
// 常见误配场景:
// 问题:依赖服务超时,但配置了"异常比例"熔断
// 超时异常是系统级异常,但比例可能达不到阈值,导致熔断不及时
// 解决:超时场景应配置"慢调用比例(RT)"熔断
// Sentinel 熔断策略选择指南:
// 慢调用比例(RT) → 适用于下游响应慢的场景
// 异常比例 → 适用于下游偶发报错的场景
// 异常数 → 适用于需要严格保护、异常数超阈值立即熔断的场景
修复方法:
// 方案一:自定义异常忽略,排除业务异常不计入熔断统计
@SentinelResource(value = "createOrder",
blockHandler = "createOrderBlockHandler",
exceptionsToIgnore = {BusinessException.class, ParamValidException.class})
public OrderResult createOrder(OrderRequest request) { ... }
// 方案二:调整熔断规则,提高阈值或增加最小请求数
// 在 Dashboard 中修改规则:
// - 异常比例阈值:50% → 80%
// - 最小请求数:5 → 20
// - 统计时长:1000ms → 2000ms
4.3 场景:Dashboard 看不到应用
现象描述:应用启动后,Sentinel Dashboard 的机器列表中看不到该应用。
排查步骤:
# 1. 确认 Dashboard 地址配置正确
ps -ef | grep java | grep 'csp.sentinel.dashboard.server'
# 输出应包含:-Dcsp.sentinel.dashboard.server=<ip>:8080
# 2. 确认应用名配置正确
ps -ef | grep java | grep 'project.name'
# 输出应包含:-Dproject.name=order-service
# 3. 确认 Dashboard 端口可达
telnet <dashboard-ip> 8080
# 4. 查看客户端传输日志,确认心跳是否正常发送
grep "dashboard\|transport\|connect" /opt/app/logs/sentinel/sentinel-record.log
# 5. 确认 Sentinel 传输端口未被占用
# Sentinel 客户端默认使用 8719 端口与 Dashboard 通信,如被占用会自动递增
netstat -tlnp | grep 8719
修复方法:
# 如果传输端口被占用,手动指定端口
java -Dcsp.sentinel.transport.port=8720 -jar app.jar
# 如果 Dashboard 地址配错,修改后重启应用
# 确保配置在 application.yml 中也正确
spring:
cloud:
sentinel:
transport:
dashboard: <dashboard-ip>:8080
port: 8719 # 客户端与 Dashboard 通信端口
eager: true # 立即初始化,不再等首次请求
4.4 Sentinel 排查三原则
- 先看监控 — 通过 Dashboard 观察资源级别的 QPS、线程数、平均 RT、异常率,判断是限流还是熔断
- 再查日志 — 分析
sentinel-record.log中的限流/熔断事件,确认触发时间和触发规则 - 后调规则 — 根据实际流量和异常分布调整流控规则,避免凭直觉设置阈值
五、Seata 分布式事务
5.1 场景:分布式事务不回滚
现象描述:order-service 调用 stock-service 扣减库存和 account-service 扣减余额,account-service 抛出异常,但 stock-service 的扣减未回滚,库存数据不一致。
排查步骤:
第一步,确认 @GlobalTransactional 注解是否生效。注解失效的最常见原因是 Starter 依赖缺失或版本不匹配。
# 确认 Seata Starter 依赖
grep "seata-spring-boot-starter" pom.xml
# 确认 Seata Server 版本与 Client 版本一致
# 查看应用加载的 Seata 版本
java -jar arthas-boot.jar
sc -d io.seata.spring.annotation.GlobalTransactional
# 查看 classloader 中的 seata 版本
<!-- 确保 Seata 版本与 Server 端一致 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version> <!-- 与 Seata Server 版本一致 -->
</dependency>
第二步,确认 XID 是否在 Feign 调用链中正确传递。如果 XID 未传递,下游服务不在全局事务中,不会回滚。
# 在 account-service 中通过 Arthas 查看 XID
java -jar arthas-boot.jar
ognl '@io.seata.core.context.RootContext.getXID()'
# 如果返回 null,说明 XID 未传递到该服务
// 确保 Feign 拦截器配置正确,传递 XID
// Seata 自动配置了 SeataFeignClient 拦截器,但需确认未被覆盖
// 检查是否手动注册了 Feign.Builder 导致拦截器丢失
// application.yml 确认配置
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default # 对应 Seata Server 的 cluster 名称
第三步,确认业务库中 undo_log 表已创建。Seata AT 模式依赖 undo_log 表记录回滚数据,未建表会导致回滚失败。
-- 检查业务库是否有 undo_log 表
SHOW TABLES LIKE 'undo_log';
-- 如果不存在,执行以下建表语句(每个参与全局事务的数据库都要建)
CREATE TABLE `undo_log` (
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB COMMENT ='AT transaction mode undo table';
第四步,确认 rollbackFor 是否覆盖了实际抛出的异常类型。Spring 默认只对 RuntimeException 和 Error 回滚,如果业务抛出的是受检异常(checked exception),需显式指定。
// 错误写法:默认只回滚 RuntimeException
@GlobalTransactional
public void createOrder() {
orderService.create(); // 如果这里抛出 IOException(受检异常),不会回滚
stockService.deduct(); // 扣减库存
accountService.deduct(); // 扣减余额
}
// 正确写法:显式指定回滚的异常类型
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder() {
orderService.create();
stockService.deduct();
accountService.deduct();
}
5.2 场景:全局锁等待超时
现象描述:高并发下单场景下,部分全局事务报 Global lock wait timeout,事务失败回滚。
排查步骤:
-- 第一步:查看未完成的全局事务(status != 0 表示未完成)
SELECT xid, status, gmt_create, gmt_modified, timeout
FROM global_table
WHERE status != 0
ORDER BY gmt_create DESC
LIMIT 20;
-- 第二步:查看锁持有情况,找出长时间持有未释放的锁
SELECT * FROM lock_table
WHERE gmt_create < DATE_SUB(NOW(), INTERVAL 5 MINUTE);
-- 第三步:通过 xid 查看分支事务状态
SELECT * FROM branch_table
WHERE xid = '<xid-from-step-1>';
-- 第四步:查看某个资源(表)被哪些事务锁定
SELECT * FROM lock_table
WHERE resource_id = 'stock_table'
ORDER BY gmt_create;
根因分析:
全局锁是 Seata AT 模式在提交阶段获取的行级锁。当两个全局事务并发修改同一行数据时,后到的事务需要等待前一个事务释放锁。如果前一个事务执行缓慢或未正确释放,就会导致锁等待超时。
常见触发场景:
- 高并发秒杀场景,大量事务争抢同一商品库存行
- 前一个事务因下游调用超时迟迟未提交
- Seata Server 异常重启后,
global_table中残留未清理的事务记录导致锁未释放
修复方法:
## file.conf 调整锁等待参数
client {
## 获取全局锁重试间隔(毫秒),默认 10
lock.retryInterval = 30
## 获取全局锁最大重试次数,默认 30
lock.retryTimes = 60
## 全局事务超时时间(毫秒),默认 60000
tm.defaultGlobalTransactionTimeout = 120000
}
-- 清理残留的僵尸全局事务(手动修复,谨慎操作)
-- 1. 先确认事务确实已超时且不会恢复
SELECT xid, gmt_create FROM global_table WHERE status = 1 AND gmt_create < DATE_SUB(NOW(), INTERVAL 1 HOUR);
-- 2. 删除残留的全局事务记录
DELETE FROM global_table WHERE status = 1 AND gmt_create < DATE_SUB(NOW(), INTERVAL 1 HOUR);
-- 3. 清理对应的锁记录
DELETE FROM lock_table WHERE xid NOT IN (SELECT xid FROM global_table);
-- 4. 清理分支事务记录
DELETE FROM branch_table WHERE xid NOT IN (SELECT xid FROM global_table);
// 架构层面优化:高并发热点数据场景,从 AT 模式改为 TCC 模式
// TCC 不依赖全局锁,Try 阶段预留资源,Confirm/Cancel 阶段直接操作
@LocalTCC
public interface StockTccAction {
@TwoPhaseBusinessAction(name = "deductStock", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(BusinessActionContext context, @BusinessActionContextParameter String sku, @BusinessActionContextParameter Integer count);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
5.3 场景:undo_log 表数据异常导致回滚失败
现象描述:全局事务回滚时报 BranchRollbackFailed,undo_log 表中残留记录未清理,数据不一致。
排查步骤:
-- 检查 undo_log 表中的残留记录
SELECT * FROM undo_log;
-- 如果有残留记录,说明回滚未成功执行
-- 检查 log_status 字段
-- log_status = 0:正常状态
-- log_status = 1:防御状态(已被标记删除但未实际删除)
-- 查看 Seata Server 日志中的回滚失败记录
# 查看 Seata Server 回滚日志
grep "rollback\|undo\|BranchRollback" /opt/seata/logs/seata-server.log | tail -20
# 常见报错:
# "BranchRollbackFailed: undo_log not found" — undo_log 已被删除,无法回滚
# "undo_log dirty data" — undo_log 数据被篡改或损坏
# "lock_table row_key length insufficient" — lock_table 中字段长度不足,锁记录插入静默失败
修复方法:
-- 1. 确认 lock_table 字段长度足够(row_key 和 xid 字段)
ALTER TABLE lock_table MODIFY row_key VARCHAR(255) NOT NULL;
ALTER TABLE lock_table MODIFY xid VARCHAR(128) NOT NULL;
-- 2. 手动清理残留的 undo_log 记录(确认事务已结束且数据状态正确后执行)
DELETE FROM undo_log WHERE log_created < DATE_SUB(NOW(), INTERVAL 1 DAY);
-- 3. 手动补偿数据(如果回滚失败导致数据不一致,需根据 undo_log 中的 rollback_info 手动恢复)
-- rollback_info 是序列化后的前后镜像数据,需反序列化后对比恢复
5.4 Seata 核心表速查
| 表名 | 位置 | 作用 | 排查时关注 |
|---|---|---|---|
global_table |
Seata Server 库 | 记录全局事务状态 | status:0=开始,1=提交中,2=回滚中 |
branch_table |
Seata Server 库 | 记录分支事务状态 | status:0=注册,1=提交,2=回滚 |
lock_table |
Seata Server 库 | 记录全局行锁 | row_key:表名+主键值 |
undo_log |
各业务库 | 记录回滚数据 | log_status:0=正常,1=防御 |
distributed_lock |
Seata Server 库 | Seata 内部协调锁 | 用于 Server 集群选主等 |
六、Dubbo RPC 调用
6.1 四层排查法
Dubbo 调用失败时,按照"注册中心 → 消费端路由 → 网络 TCP → Provider 线程池"四层逐层排查,能快速定位故障层。
消费端发起调用
│
▼
① 注册中心层 — Provider 是否注册成功?Consumer 是否订阅成功?
│
▼
② 消费端路由层 — 路由规则是否过滤了所有 Provider?
│
▼
③ 网络 TCP 层 — 消费端与 Provider 的网络是否通畅?
│
▼
④ Provider 线程池层 — Provider 线程池是否已满?处理是否超时?
6.2 场景:No provider available
现象描述:order-service 调用 user-service 的 UserService.getUserInfo() 时报 No provider available for the service com.example.UserService。
排查步骤:
第一步,确认 Provider 已注册到注册中心。
# 如果使用 Nacos 作为注册中心
curl "http://<nacos-ip>:8848/nacos/v1/ns/instance/list?serviceName=providers:com.example.UserService:1.0.0"
# 如果使用 ZooKeeper 作为注册中心
zkCli.sh -server 127.0.0.1:2181
# 查看所有 Provider
ls /dubbo/com.example.UserService/providers
# 查看 Provider 注册详情(URL 参数)
get /dubbo/com.example.UserService/providers/dubbo%3A%2F%2F192.168.1.10%3A20880%2Fcom.example.UserService...
# 查看所有 Consumer
ls /dubbo/com.example.UserService/consumers
# 查看路由规则(排查是否被规则过滤)
ls /dubbo/com.example.UserService/routers
第二步,确认 Consumer 端订阅成功。如果 Provider 列表不为空但 Consumer 仍报错,可能是 Consumer 未成功订阅。
# 通过 Dubbo QoS(端口 22222)查看消费端缓存的服务列表
telnet 127.0.0.1 22222
# 列出所有服务
ls
# 查看特定服务的 Provider 列表
ls com.example.UserService
第三步,确认接口全限定名、分组、版本号三要素完全匹配。Dubbo 通过 接口名 + 分组 + 版本号 唯一标识一个服务,任一不匹配都会导致找不到 Provider。
// Provider 端配置
@DubboService(group = "production", version = "1.0.0")
public class UserServiceImpl implements UserService { ... }
// Consumer 端配置必须完全匹配
@DubboReference(group = "production", version = "1.0.0")
private UserService userService;
// 常见错误:
// 1. Consumer 没配 group,Provider 配了 group
// 2. Consumer 的 version 与 Provider 不一致
// 3. Consumer 引入的接口包名与 Provider 不同(如 com.example.api.UserService vs com.example.UserService)
第四步,Dubbo 3.x 默认序列化从 hessian2 变为 fastjson2,如果 Provider 和 Consumer 序列化方式不一致,会导致服务发现失败。
dubbo:
# 确认序列化方式一致
protocol:
serialization: hessian2 # 明确指定,避免版本默认值不一致
6.3 场景:线程池满,请求被拒绝
现象描述:user-service 大量报 Thread pool is EXHAUSTED,接口响应变慢甚至超时。
排查步骤:
第一步,查看日志确认线程池耗尽。
# 线程池耗尽的典型日志
grep "EXHAUSTED\|Thread pool" /opt/app/logs/dubbo/dubbo.log | tail -10
# 典型日志格式:
# Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-192.168.1.10:20880,
# Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200), Task: 45678
第二步,通过 jstack 分析线程在做什么。线程池满通常是因为业务逻辑执行慢(如数据库慢查询、外部 HTTP 调用超时),线程被阻塞无法释放。
# 统计 DubboServerHandler 线程数量
jstack <pid> | grep 'DubboServerHandler' | wc -l
# 查看线程状态分布
jstack <pid> | grep 'DubboServerHandler' | awk '{print $2}' | sort | uniq -c | sort -rn
# 查看各线程的堆栈,定位阻塞原因
jstack <pid> | grep -A 20 'DubboServerHandler' | head -80
# 常见阻塞原因:
# - java.net.SocketInputStream.socketRead0 → 等待数据库或 HTTP 响应
# - java.util.concurrent.locks.ReentrantLock → 等待获取业务锁
# - sun.misc.Unsafe.park → 等待异步任务完成
第三步,通过 Dubbo QoS 查看线程池实时状态。
telnet 127.0.0.1 22222
# 查看线程池和连接池状态
status -s
修复方法:
dubbo:
protocol:
# 方案一:调大线程池(临时缓解)
threads: 500 # 线程池大小,默认 200
threadpool: fixed # 固定线程池(默认),也可用 cached
# 方案二:启用线程池告警和拒绝策略
# 当线程池使用率超过阈值时告警
// 方案三:根治 — 找到慢调用的根因并修复
// 1. 数据库慢查询:加索引、优化 SQL、引入缓存
// 2. 外部 HTTP 调用超时:设置合理超时时间、增加重试、引入熔断
// 3. 同步转异步:将耗时的非核心逻辑异步化
// 方案四:关键接口隔离线程池,避免互相影响
// Dubbo 3.x 支持为不同服务配置独立的线程池
@DubboService(executor = "orderServiceExecutor")
public class OrderServiceImpl implements OrderService { ... }
// 配置自定义线程池
@Configuration
public class DubboThreadPoolConfig {
@Bean("orderServiceExecutor")
public ExecutorService orderServiceExecutor() {
return new ThreadPoolExecutor(
50, 100, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("order-service-%d").build()
);
}
}
6.4 场景:调用超时
现象描述:order-service 调用 user-service 报 Waiting server-side response timeout. timeout: 1000ms。
排查步骤:
第一步,区分超时类型,两种超时文案含义完全不同:
# 文案一:请求还没发出去(消费端线程池排队超时)
# 日志特征:Waiting server-side execution timeout
# 含义:消费端的请求在本地线程池排队等待发送,排队时间超过了超时阈值
# 根因:消费端线程池满,或消费端 GC 导致 STW
# 文案二:请求已发出但等不回响应
# 日志特征:Waiting server-side response timeout
# 含义:请求已发送到 Provider,但在超时时间内未收到响应
# 根因:Provider 处理慢、网络延迟高、Provider GC 停顿
第二步,查看 Provider 端日志确认是否收到了请求。
# 查看 Provider 日志中该次调用的耗时
grep "execute service.*cost.*ms" /opt/app/logs/dubbo/dubbo.log | tail -10
# 典型日志:[DUBBO] [Dubbo-Provider] execute service xxx cost 3001ms,
# this invocation almost (maybe already) timeout. Timeout: 1000ms
第三步,通过 Arthas 的 trace 命令定位 Provider 内部哪个子调用最耗时。
# 追踪方法内部各子调用的耗时
trace com.example.user.service.UserServiceImpl getUserInfo
# 输出示例:
# ├─[0.01ms] com.example.user.mapper.UserMapper.selectById()
# ├─[800ms] com.example.user.service.AccountService.getBalance() ← 罪魁祸首
# └─[0.02ms] com.example.user.converter.UserConverter.convert()
修复方法:
dubbo:
provider:
timeout: 3000 # Provider 默认超时 3 秒
retries: 1 # 默认重试 1 次(仅对查询类幂等操作有效)
consumer:
timeout: 5000 # Consumer 默认超时 5 秒
# 针对特定服务单独配置
check: false # 启动时不检查 Provider 是否可用
# 针对写操作配置不重试(避免重复扣款等副作用)
# 在 @DubboReference 上单独指定
// 写操作必须配置 retries=0,避免重复执行
@DubboReference(timeout = 5000, retries = 0)
private OrderService orderService;
// 查询操作可以配置重试
@DubboReference(timeout = 2000, retries = 2)
private UserService userService;
6.5 场景:序列化异常
现象描述:Dubbo 调用时报 java.io.NotSerializableException 或序列化相关异常。
# 典型日志
grep "Serializable\|SerializeException" /opt/app/logs/dubbo/dubbo.log | tail -5
修复方法:
// 1. 确保传输的数据对象实现了 Serializable 接口
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
// ...
}
// 2. Dubbo 3.x 默认使用 fastjson2 序列化,对无参构造器和 getter/setter 有要求
// 如果报 "no default constructor",添加无参构造器
public class UserDTO implements Serializable {
public UserDTO() {} // 无参构造器
}
// 3. 如果存在循环引用,使用 fastjson2 的 @JSONField(serialize = false) 排除
七、RocketMQ 消息队列
7.1 场景:消息积压
现象描述:告警显示 order-topic 消费积压量超过 10 万条且持续增长,下游订单状态更新延迟。
排查步骤:
第一步,确认积压量和消费 TPS,判断是消费速度跟不上还是消费者完全停止消费。
# 查看消费组消费进度(最关键命令)
sh mqadmin consumerProgress -n <nameserver-addr> -g order-consumer-group
# 输出示例:
# #Topic #Broker Name #QID #Broker Offset #Consumer Offset #Diff
# order-topic broker-a 0 1523456 1423456 100000 ← 积压 10 万
# order-topic broker-a 1 1523456 1453456 70000
# Consume TPS: 0 ← 消费 TPS 为 0,消费者完全停止
# 关键指标:
# Consume TPS = 0 → 消费者完全停止,检查消费者进程是否存活
# Consume TPS 很低 → 消费者处理慢,检查消费逻辑或扩容
# Diff (TOTAL) 持续增长 → 消费速度跟不上生产速度
第二步,如果 TPS 为 0,检查消费者进程是否存活,以及是否抛出了阻塞消费的异常。
# 检查消费者进程
ps -ef | grep rocketmq
# 查看消费者日志
grep "exception\|error\|reconsume" /opt/app/logs/rocketmq/consumer.log | tail -20
# 常见阻塞原因:
# "MessageExt object is null" → 消息已过期被清理(超过 fileReservedTime)
# "IllegalStateException" → 消费逻辑抛出未捕获异常,线程池停止
# "too many times" → 消息重试次数耗尽,进入死信队列
第三步,检查消费线程数和批量拉取配置是否合理。
# 通过 Arthas 查看消费者线程池状态
java -jar arthas-boot.jar
thread | grep ConsumeMessageThread
紧急处理方案:
# 方案一:调高消费线程数和批量拉取数(最直接,无需重启)
# 通过 Nacos 动态修改配置
spring:
rocketmq:
consumer:
consume-thread-min: 50 # 最小消费线程数,默认 20
consume-thread-max: 200 # 最大消费线程数,默认 64
consume-message-batch-max-size: 16 # 一次拉取 16 条消息,默认 1 条(吞吐量翻 16 倍)
# 方案二:增加 Topic 队列数(决定最大并发度,消费者实例数不能超过队列数)
sh mqadmin updateTopic -n <nameserver-addr> -t order-topic -r 16 -w 16
# r=readQueueNums, w=writeQueueNums
# 方案三:水平扩容消费者实例
# 注意:消费者实例数 ≤ 队列数,超过队列数的实例不会分配到队列
# 例如 16 个队列最多 16 个消费者实例并发
# 方案四:积压严重时的紧急处理 — 临时跳过积压消息(慎用,会丢失未处理的消息)
# 1. 停止当前消费者
# 2. 创建一个临时的消费者组,快速消费积压消息并跳过处理
# (仅将消息标记为已消费,不执行业务逻辑)
# 3. 重置位点到最新位置
sh mqadmin resetOffsetByTime -n <nameserver-addr> -t order-topic -g order-consumer-group -s now
# -s now 表示重置到最新位点
# 方案五:重置位点从某个时间点开始消费(积压消息中大部分已过期无用时)
sh mqadmin resetOffsetByTime -n <nameserver-addr> -t order-topic -g order-consumer-group -s 20260715120000
# -s 格式:yyyyMMddHHmmss,从该时间点开始消费
7.2 场景:消息消费失败
现象描述:消费者反复消费同一条消息,日志中持续报错,消息进入重试队列。
排查步骤:
# 查看消费失败日志
grep "CONSUME_FAILED\|reconsume\|exception" /opt/app/logs/rocketmq/consumer.log | tail -20
# 查看重试队列中的消息
sh mqadmin queryMsgByKey -n <nameserver-addr> -t "%RETRY%order-consumer-group" -k <msgKey>
# 查看死信队列(重试 16 次后进入)
sh mqadmin queryMsgByKey -n <nameserver-addr> -t "%DLQ%order-consumer-group" -k <msgKey>
根因分析:
// 常见消费失败原因及修复
// 原因一:消费逻辑抛出未捕获异常
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer-group")
public class OrderConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
// 错误:未捕获异常,异常会触发重试
OrderDTO order = JSON.parseObject(message, OrderDTO.class);
orderService.process(order); // 如果这里抛异常,消息会重试
// 正确:捕获异常并分类处理
try {
OrderDTO order = JSON.parseObject(message, OrderDTO.class);
orderService.process(order);
} catch ( BusinessException e) {
// 业务异常:记录日志,返回消费成功(不重试)
// 消息进入人工处理队列
log.error("业务异常,消息不再重试: {}", message, e);
return; // 正常返回,不抛异常,消费成功
} catch (Exception e) {
// 系统异常:抛出异常触发重试
throw e;
}
}
}
// 原因二:消息体反序列化失败(生产者与消费者实体类不一致)
// 修复:统一实体类版本,或使用 JSON 格式传输并配置宽容解析
// 原因三:数据库唯一键冲突(消息重复消费)
// 修复:消费端实现幂等性
幂等消费实现:
@Override
public void onMessage(MessageExt message) {
String msgId = message.getMsgId();
String msgKey = message.getKeys(); // 业务唯一标识(如订单号)
// 1. 先查询是否已消费(Redis 或数据库)
String consumeKey = "mq:consumed:" + msgKey;
if (Boolean.TRUE.equals(redisTemplate.hasKey(consumeKey))) {
log.info("消息已消费,跳过: msgKey={}", msgKey);
return; // 已消费,直接返回成功
}
// 2. 执行消费逻辑
try {
OrderDTO order = JSON.parseObject(new String(message.getBody()), OrderDTO.class);
orderService.process(order);
// 3. 标记已消费(设置过期时间,避免 Redis key 无限增长)
redisTemplate.opsForValue().set(consumeKey, "1", 24, TimeUnit.HOURS);
} catch (Exception e) {
// 系统异常才重试,业务异常直接消费成功
if (e instanceof BusinessException) {
redisTemplate.opsForValue().set(consumeKey, "error", 24, TimeUnit.HOURS);
return;
}
throw e; // 触发重试
}
}
7.3 场景:消息丢失
现象描述:生产者发送的消息在消费者端未收到,或 Broker 端查询不到。
全链路排查:
| 环节 | 保障措施 | 排查方法 |
|---|---|---|
| 生产者端 | 同步发送 + 重试 + 发送日志 | 检查发送日志是否有 SendStatus.SEND_OK |
| Broker 端 | 同步刷盘 + 主从同步 | 检查 flushDiskType 和 brokerRole |
| 消费者端 | 消费重试 + 死信队列 + 幂等 | 检查消费日志是否有异常 |
# 第一步:确认生产者发送状态
grep "SEND_OK\|SEND_FAIL\|FLUSH_DISK_TIMEOUT" /opt/app/logs/rocketmq/producer.log | tail -20
# 第二步:通过 msgId 在 Broker 端查询消息是否存在
sh mqadmin queryMsgById -n <nameserver-addr> -i <msgId>
# 第三步:查看消息轨迹(需开启消息轨迹功能)
sh mqadmin queryMsgTrace -n <nameserver-addr> -i <msgId>
生产环境消息不丢失配置:
# 生产者端配置
rocketmq:
producer:
# 同步发送(确保 Broker 收到后才返回)
send-message-timeout: 3000
# 发送失败重试次数
retry-times-when-send-failed: 3
# 异步发送失败重试次数
retry-times-when-send-async-failed: 3
# Broker 端配置(broker.conf)
# 同步刷盘(确保消息写入磁盘后才返回)
flushDiskType=SYNC_FLUSH
# 主从同步复制(确保消息复制到从节点)
brokerRole=SYNC_MASTER
# 消息保存时间(默认 72 小时,生产建议 168 小时=7 天)
fileReservedTime=168
7.4 RocketMQ 关键端口
| 端口 | 服务 | 说明 |
|---|---|---|
| 9876 | NameServer | 路由注册与发现 |
| 10911 | Broker | 对外服务端口(ListenPort) |
| 10909 | Broker VIP | VIP 通道(某些客户端使用) |
| 10912 | Broker HA | 主从同步端口(haListenPort) |
八、Spring Cloud Gateway 网关
8.1 场景:503 Service Unavailable
现象描述:通过 Gateway 访问 order-service 时返回 503,但直接访问 order-service 的实例 IP 和端口正常。
排查步骤:
第一步,确认 Gateway 是否引入了负载均衡依赖。这是 Spring Cloud Gateway 503 最常见的根因:Gateway 通过 lb://service-name 路由到下游服务时,需要 LoadBalancer 将服务名解析为实际 IP,缺少依赖会导致无法解析。
<!-- 必须!Gateway 默认不包含 LoadBalancer 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
第二步,确认路由配置格式正确。uri 必须以 lb:// 开头,后跟服务名(Nacos 中注册的 spring.application.name)。
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service # 必须用 lb:// 前缀,不能用 http://
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
第三步,确认下游服务在 Nacos 中注册成功且健康状态正常。
# 通过 Nacos API 查看服务实例
curl "http://<nacos-ip>:8848/nacos/v1/ns/instance/list?serviceName=order-service"
# 检查返回的 instances 数组:
# - healthy: true(健康检查通过)
# - enabled: true(实例已启用)
# - weight > 0(权重不为 0,权重为 0 不会被调用)
第四步,检查 Gateway 的 WebFlux 线程池和连接池是否耗尽。
# 查看 Gateway 进程线程状态
jstack <gateway-pid> | grep 'reactor\|http\|netty' | awk '{print $2}' | sort | uniq -c | sort -rn
# 查看是否有大量线程处于 WAITING 状态
jstack <gateway-pid> | grep -c "WAITING"
修复方法:
spring:
cloud:
gateway:
# 调整网关连接池配置(防止连接池耗尽)
httpclient:
connect-timeout: 3000 # 连接超时 3 秒
response-timeout: 30s # 响应超时 30 秒
pool:
max-connections: 500 # 最大连接数,默认 500
acquire-timeout: 5000 # 获取连接超时 5 秒
type: elastic # 弹性连接池(推荐)
8.2 场景:后端服务正常但网关返回 503
现象描述:order-service 实例健康、Gateway 路由配置正确、LoadBalancer 依赖已引入,但网关仍然返回 503。
深层排查:
第一步,检查 TLS 握手。如果下游服务启用了 HTTPS 但证书过期或 SNI 不匹配,TLS 握手会失败导致连接异常。
# 测试 TLS 握手
openssl s_client -connect <order-service-ip>:443 -servername order-service
# 如果输出 "verify error:num=10:certificate has expired",证书已过期
第二步,检查后端服务是否启用了 HTTP/2 但 Gateway 仅支持 HTTP/1.1(或反之)。
# 确认 Gateway 和下游服务的 HTTP 版本一致
curl -v --http2 http://<order-service-ip>:8080/health
curl -v --http1.1 http://<order-service-ip>:8080/health
第三步,确认 Nacos 中服务名称没有下划线。Nacos 默认不支持服务名包含下划线,会导致注册成功但 Gateway 路由失败。
# 错误:服务名含下划线
spring:
application:
name: order_service # Nacos 可能无法正确处理
# 正确:使用连字符
spring:
application:
name: order-service # 推荐使用连字符命名
第四步,检查 Gateway 的全局过滤器是否有异常。如果某个 GlobalFilter 抛出未捕获异常,会导致整个请求链路中断返回 503。
# 查看 Gateway 日志中的异常
grep "ERROR\|Exception\|GlobalFilter" /opt/gateway/logs/gateway.log | tail -20
# 开启 Gateway 调试日志
# 通过 Arthas 动态修改日志级别
logger --name org.springframework.cloud.gateway --level DEBUG
8.3 场景:网关响应延迟异常升高
现象描述:Gateway 的 P99 响应时间从正常的 50ms 飙升到 2000ms,但下游服务响应时间正常。
排查步骤:
# 第一步:确认是否 GC 导致 STW
jstat -gcutil <gateway-pid> 1000 5
# 如果 Full GC 频繁或耗时 > 500ms,说明堆内存不足
# 第二步:查看 Netty 事件循环线程是否被阻塞
# Gateway 基于 WebFlux,事件循环线程不应执行阻塞操作
jstack <gateway-pid> | grep 'reactor-http' -A 20
# 如果线程堆栈中包含 SocketInputStream.socketRead0,说明在执行阻塞 IO
# 第三步:通过 Arthas 追踪网关请求处理耗时
trace org.springframework.web.reactive.DispatcherHandler handle
修复方法:
# JVM 调优(网关是 IO 密集型,建议使用 G1GC)
java -Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar gateway.jar
// 确保网关中不执行阻塞操作
// 错误:在过滤器中同步调用数据库
@Component
public class BadFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 错误!同步阻塞调用
String result = restTemplate.getForObject("http://config-service/config", String.class);
return chain.filter(exchange);
}
}
// 正确:使用响应式编程
@Component
public class GoodFilter implements GlobalFilter {
private final WebClient webClient;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return webClient.get()
.uri("http://config-service/config")
.retrieve()
.bodyToMono(String.class)
.flatMap(config -> {
exchange.getRequest().mutate().header("X-Config", config).build();
return chain.filter(exchange);
});
}
}
九、故障速查总表
9.1 组件级速查
| 组件 | 典型现象 | 关键日志关键字 | 首选排查命令 |
|---|---|---|---|
| Nacos | 服务注册失败 | failed to register、invalid token |
curl http://<ip>:8848/nacos/v1/ns/instance/list?serviceName=xxx |
| Nacos | 配置不生效 | sync failed、MD5 mismatch |
curl "http://<ip>:8848/nacos/v1/cs/configs?dataId=xxx&group=xxx" |
| Nacos | 集群脑裂 | leader not found、election |
curl http://<ip>:8848/nacos/v1/ns/operator/metrics |
| Sentinel | 限流不生效 | 无限流日志 | Dashboard → 机器列表 → 确认应用是否注册 |
| Sentinel | 熔断误触发 | DegradeException、FlowException |
grep "Degrade|Flow" sentinel-record.log |
| Seata | 事务不回滚 | BranchRollbackFailed |
SELECT * FROM global_table WHERE status != 0 |
| Seata | 锁等待超时 | Global lock wait timeout |
SELECT * FROM lock_table WHERE gmt_create < NOW() - INTERVAL 5 MINUTE |
| Dubbo | 无 Provider | No provider available |
telnet 127.0.0.1 22222 → ls <service-name> |
| Dubbo | 线程池满 | Thread pool is EXHAUSTED |
`jstack |
| Dubbo | 调用超时 | Waiting server-side response timeout |
trace <class> <method> (Arthas) |
| RocketMQ | 消息积压 | 无 | sh mqadmin consumerProgress -n <addr> -g <group> |
| RocketMQ | 消费失败 | CONSUME_FAILED、reconsume |
sh mqadmin queryMsgByKey -t "%RETRY%<group>" |
| Gateway | 503 | ServiceUnavailableException |
确认 spring-cloud-starter-loadbalancer 依赖 |
| JVM | OOM | OutOfMemoryError |
jmap -dump:live,format=b,file=heap.hprof <pid> |
| JVM | CPU 飙高 | 无 | top -H -p <pid> → printf "%x\n" <tid> → `jstack |
9.2 常见端口速查
| 端口 | 组件 | 说明 |
|---|---|---|
| 8848 | Nacos | HTTP API 主端口 |
| 9848 | Nacos 2.x | 客户端 gRPC 端口 |
| 9849 | Nacos 2.x | 服务端 gRPC raft 端口 |
| 8080 | Sentinel | Dashboard 端口 |
| 8719 | Sentinel | 客户端传输端口 |
| 8091 | Seata | Server 端口 |
| 20880 | Dubbo | 默认协议端口 |
| 22222 | Dubbo | QoS 诊断端口 |
| 9876 | RocketMQ | NameServer 端口 |
| 10911 | RocketMQ | Broker 服务端口 |
| 2181 | ZooKeeper | 客户端端口 |
9.3 应急止血操作速查
| 止血手段 | 操作命令 | 适用场景 |
|---|---|---|
| Nacos 实例下线 | curl -X PUT "http://<nacos-ip>:8848/nacos/v1/ns/instance?serviceName=xxx&ip=<ip>&port=<port>&enabled=false" |
单节点异常,摘除流量 |
| Sentinel 手动降级 | Dashboard → 熔断规则 → 新增降级规则 | 依赖服务故障,阻止故障扩散 |
| RocketMQ 重置位点 | sh mqadmin resetOffsetByTime -n <addr> -t <topic> -g <group> -s now |
消息积压严重,跳过历史消息 |
| Gateway 路由屏蔽 | 修改 Nacos 中 Gateway 路由配置,注释异常路由 | 下游服务故障,临时屏蔽 |
| JVM 紧急 dump | jmap -dump:live,format=b,file=/tmp/heap.hprof <pid> |
OOM 前紧急 dump 内存 |
| Arthas 动态降级 | logger --name com.example --level DEBUG |
临时开启 DEBUG 日志定位问题 |