概述
系列定位说明
本文是“高并发与稳定性工程”系列的第十篇,亦是收官之作。前九篇系统构建了限流、熔断、隔离、容量规划、混沌工程、秒杀架构、监控告警、降级预案与网关治理九大防线,将流量防御从入口网关逐层推进到业务节点与基础设施。本系列文章列表:
- 第1篇:限流算法与 Sentinel 落地
- 第2篇:熔断降级与 Resilience4j 深度
- 第3篇:服务隔离与线程池利特尔法则
- 第4篇:全链路压测与容量规划
- 第5篇:混沌工程与故障注入
- 第6篇:秒杀架构与高并发写优化
- 第7篇:稳定性监控与告警体系(SLI/SLO/SLA)
- 第8篇:降级预案与容错策略
- 第9篇:高性能网关 Spring Cloud Gateway 深度
现在,所有这些防御知识将被转化为故障排查的实战能力——当防线终究被突破、线上真的“炸了”时,你将拥有一套结构化排查方法论和导航式决策树,不再手足无措。
总结性引言
凌晨两点,Prometheus 告警骤响:订单服务 CPU 飙至 95%,P99 延迟从日常 100ms 攀升至 5s,错误率升至 8%,Error Budget 在 5 分钟内消耗过半。运维电话将你从睡梦中唤醒,Grafana 一片猩红。此时,从哪里开始?CPU 飙升是因还是果?是死循环、频繁 GC、数据库慢查,还是下游服务假死?若没有一套结构化排查方法论,你很可能在凌乱的指标中迷失,半小时过去仍未定位根因,Error Budget 耗尽,用户投诉如潮。
本文交付的正是这套方法论。 将前九篇的全部防御知识凝练为 15+ 高并发反模式的精确定义与排查手册,每种反模式均附错误示例、故障现象、排查思路、根因分析与修正方案。同时构建 CPU 飙升、RT 突增、错误率升高三大故障域的排查决策树,以及以 top -H + arthas thread -n + jstack 为核心的生产诊断命令链。最终,以“电商秒杀期间订单服务 CPU 飙升 95%”的完整故障为贯穿案例,还原一名 SRE 从告警触发到根因修复的 20 分钟全过程。
核心要点
- 六步排查法:现象描述 → 指标定位 → 工具诊断 → 根因确认 → 方案修正 → 验证恢复,每步附明确命令与输出。
- 15+ 反模式手册:限流阈值错误、熔断过松、线程池未隔离、定时任务无锁、缓存无 TTL、事务自调用、Tomcat 线程过高、Gateway 阻塞、连接池耗尽、超时缺失、探针误杀、降级未生效、MQ 幂等缺失、分片键倾斜。
- 三大决策树:CPU 飙升(
top -H→jstack→ 死循环/GC/阻塞)、RT 突增(SkyWalking → 慢 SQL → 连接池/下游)、错误率升高(Sentinel → Resilience4j → 下游健康)。 - 诊断命令链:
top -H -p+arthas thread -n 5+jstack+jmap -histo+jcmd GC.heap_dump+ MAT 的完整组合。 - 连锁推演:线程池未隔离 → Tomcat 满载 → 探针超时 → Pod 重启 → 雪崩,一条反模式链的完整还原。
- 电商贯穿案例:秒杀订单服务 CPU 95% 的 20 分钟完整排查与修复实录。
文章组织架构图
flowchart TD
A["1 高并发反模式排查方法论:六步排查法"] --> B["2 15+ 反模式逐一剖析(上):限流/熔断/隔离/缓存/事务"]
A --> C["3 15+ 反模式逐一剖析(下):Tomcat/Gateway/连接池/超时/探针/降级/MQ/分片"]
B --> D["4 三大故障域的排查决策树"]
C --> D
D --> E["5 生产诊断工具链实战"]
E --> F["6 反模式关联推演:连锁反应链"]
F --> G["7 贯穿案例:电商秒杀订单服务 CPU 飙升 95% 排查实录"]
G --> H["8 与前后系列的衔接"]
H --> I["9 面试高频专题"]
架构图说明
- 图表主旨概括:全文以六步排查法为方法论基石,系统归类 15+ 反模式,构建三大决策树与诊断命令链,通过连锁推演揭示反模式间的关联,最终以贯穿案例和面试题形成闭环。
- 逐模块说明:模块1确立排查的标准流程;模块2-3将前九篇的防御知识转化为具体反模式排查手册;模块4-5提供可复用的排查导航与命令组合;模块6揭示单一反模式如何引发级联故障;模块7用电商真实故障串联全部知识点;模块8-9缝合全系列并巩固知识体系。
- 设计原理映射:反模式的归纳源于线上真实故障的抽象,决策树是对经典性能诊断方法(USE、RED)的工程化简化,命令链则基于 Linux 内核与 JVM 的可观测性接口。
- 工程联系与关键结论:每一个线上故障都不是偶然的,它背后一定对应着一个被违背的设计原则或配置错误。六步排查法和三大决策树的价值,不是替代经验,而是将经验结构化、可复用化。
一、高并发反模式排查方法论:六步排查法
线上故障排查最忌“胡乱猜测、到处尝试”。一套标准化的流程能够大幅缩短平均恢复时间(MTTR),并确保排查动作可追溯、可复盘。本文提出的六步排查法,参考了 Google SRE 的故障排查流程(识别、诊断、修复、复盘),并针对高并发 Java 系统进行了细化。
1.1 六步排查法定义与对比
| 步骤 | 名称 | 核心动作 | 输入 | 输出 | 关键工具 |
|---|---|---|---|---|---|
| 1 | 现象描述 | 清晰刻画故障的表现、影响面、持续时间 | 告警通知、值班群反馈 | 一份结构化的故障现象单 | Prometheus Alert, Grafana Dashboard |
| 2 | 指标定位 | 在多维度指标中快速收缩故障范围,对比上下游时间线 | 故障现象单、Grafana 大盘 | 异常指标列表、嫌疑服务/组件列表 | Grafana, PromQL, SkyWalking |
| 3 | 工具诊断 | 根据初步定位的故障域(CPU/内存/网络/磁盘),选择对应的命令与工具深入挖掘 | 嫌疑 Pod、进程 PID、时间段 | 热点线程栈、慢 SQL 列表、大对象直方图 | top -H, arthas thread -n, jstack, jmap, SHOW FULL PROCESSLIST |
| 4 | 根因确认 | 将诊断发现与反模式库匹配,锁定根本原因,排除干扰项 | 诊断数据、反模式知识库 | 明确的根因声明(如“@Async 线程池未隔离”) | 反模式手册(本文模块2-3) |
| 5 | 方案修正 | 制定并实施修复方案,可能包括配置调整、代码修复、资源扩容等 | 根因、架构约束、紧急程度 | 修复后的配置/代码 | Nacos 配置中心、CI/CD 管道、Git |
| 6 | 验证恢复 | 通过监控指标确认故障消除,Error Budget 消耗停止,必要时进行压测验证 | 修复后的监控数据、压测报告 | 恢复确认通知、复盘文档 | Grafana, Prometheus, 压测平台 |
与 Google SRE 的故障排查流程对比:SRE 强调“检测、分诊、诊断、修复、事后剖析”。六步排查法更面向高并发 Java 系统,在步骤3中融入了特定诊断工具,步骤4强调了反模式库的匹配,使经验可复用。
1.2 每一步的详细操作指南
步骤1 现象描述
- 输入:Prometheus 告警内容(例如:
order-service CPU > 80% 持续5分钟,P99 > 3s),或值班群中业务方反馈“秒杀页面打不开”。 - 操作:登录 Grafana,打开该服务的标准监控面板(JVM、Tomcat、接口延迟、错误率),记录准确的时间点、异常数值、影响的服务范围(是否仅该服务,还是上下游均有)。
- 输出:一个结构化的故障描述,例如:“15:03 订单服务 CPU 从30%飙至95%,P99延迟由100ms升至5s,错误率8%,同时库存服务延迟升至2s,支付服务延迟升至4.5s,Redis 缓存命中率从95%跌至30%。”
步骤2 指标定位
- 在 Grafana 中使用 PromQL 查询:
rate(http_requests_total{service="order-service",status=~"5.."}[5m])观察错误率突增时间。histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service="order-service"}[5m]))确认 P99 延迟。rate(node_cpu_seconds_total{mode="user"}[5m])查看 CPU 飙升时间线。
- 打开 SkyWalking 拓扑图,查看该服务与上下游的调用关系,识别是自身问题还是下游传过来的延迟。
- 输出:圈定故障范围为“订单服务自身处理缓慢,下游支付服务严重超时(4.5s),Redis 缓存命中率骤降”,排除上游网关流量突增等原因。
步骤3 工具诊断
- 登录目标 Pod,使用工具进行深入诊断(参见第5章)。
- 根据现象选择初始工具:
- CPU 高 →
top -H -p <pid>定位线程,然后用arthas thread -n 5确认热点方法。 - 内存高 →
jmap -histo <pid> | head -20看对象分布,需要时 dump 堆转储用 MAT 分析。 - RT 高且疑似慢 SQL →
SHOW FULL PROCESSLIST,EXPLAIN。 - 错误率高 → 检查 Sentinel Dashboard,Resilience4j 状态端点。
- CPU 高 →
- 将发现的异常记录:例如“200个线程均在
PaymentService.confirm()阻塞”,或“内存中byte[]占80%”。
步骤4 根因确认
- 将步骤3的发现与反模式库(本文模块2-3)对照。例如:“线程池未隔离”对应的排查发现是“所有线程都在等待同一个慢下游,且线程池为默认共用池”,则确认根因为反模式2.3。
- 可能需要多个证据链相互印证。例如上述缓存无 TTL 会导致 Redis eviction 和 DB 慢查询,进而加剧线程池耗尽。
- 输出:明确的根因声明,例如:“根因1:@Async 线程池未隔离,支付服务慢调用占满线程池;根因2:订单缓存未配置 TTL,Redis OOM 触发大量 eviction,请求击穿数据库”。
步骤5 方案修正
- 根据根因选择修正方案(紧急修复 vs 永久修复):
- 线程池隔离:立即通过 Nacos 下发动态配置,为支付服务分配独立线程池,通过 Sentinel 限制支付调用的并发数。
- 缓存加 TTL:通过 Nacos 下发
spring.cache.redis.time-to-live=300,同时开启@RefreshScope刷新缓存配置。
- 若需代码变更,执行紧急发布流程(灰度→全量)。
步骤6 验证恢复
- 观察 Grafana 指标:CPU 回归正常水平(<50%),P99 降至 200ms 以内,错误率降至基线(<0.1%)。
- 确认 Error Budget 消耗停止。
- 可选:在预发环境压测验证修复方案有效。
- 输出:故障关闭通知,编写事后回顾(Postmortem)。
1.3 六步排查法流程图
flowchart TD
Start(["Prometheus 告警触发"]) --> S1["步骤1 现象描述<br/>记录服务名、异常指标、时间、影响范围"]
S1 --> S2["步骤2 指标定位<br/>Grafana + PromQL 对比上下游<br/>SkyWalking 调用链分析"]
S2 --> S3{"判定故障域"}
S3 -- "CPU 飙升" --> D1["步骤3 工具诊断<br/>top -H, arthas thread -n, jstack<br/>vmstat, perf top 等"]
S3 -- "RT 突增" --> D2["步骤3 工具诊断<br/>SkyWalking 慢 Span<br/>SHOW FULL PROCESSLIST, EXPLAIN"]
S3 -- "错误率升高" --> D3["步骤3 工具诊断<br/>Sentinel Dashboard, Resilience4j<br/>/actuator/health, Nacos 实例"]
D1 --> S4["步骤4 根因确认<br/>匹配反模式库,多证据链锁定根因"]
D2 --> S4
D3 --> S4
S4 --> S5["步骤5 方案修正<br/>调整配置/修复代码/资源扩容<br/>Nacos 动态下发或紧急发布"]
S5 --> S6["步骤6 验证恢复<br/>监控指标回归正常,Error Budget 停止消耗"]
S6 --> End(["故障关闭,复盘"])
classDef decision fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
classDef process fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#1e293b
classDef endpoint fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#0f172a
class S3 decision
class S1,S2,D1,D2,D3,S4,S5,S6 process
class Start,End endpoint
四层说明
- 图表主旨:展示六步排查法的闭环流程,每一步的输入输出与决策分支清晰可见,强调步骤3按故障域分流。
- 逐层分解:步骤1-2属于宏观感知层;步骤3属于深入诊断层,此处根据 CPU、RT、错误率三个主要故障域使用不同工具;步骤4根因确认借助反模式库加速;步骤5-6完成修复与验证。
- 设计原理映射:灵感来源于 Google SRE 的分诊诊断模型,结合 Java 生态特定工具形成可执行步骤。
- 关键结论:坚持六步排查法可以避免“试错式”修复,显著降低 MTTR。建议每个团队内建一个类似的反模式知识库,与步骤4紧密配合。
二、15+ 反模式逐一剖析(上)
本章以“错误示例 → 故障现象 → 排查思路 → 根因分析 → 修正方案 → 最佳实践”六段式,逐一拆解限流、熔断、隔离、缓存、事务五大领域的典型反模式。每种反模式均与前文系列知识形成映射,标注关联篇章。
2.1 限流阈值配置错误(关联第1篇、第4篇)
错误示例
# Sentinel 流控规则
FlowRule flowRule = new FlowRule("order_create");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(50000); // 单机限流 5万 QPS,但压测容量仅 1万
故障现象
集群实际 QPS 达 1.5 万时,订单服务响应突然全线超时,CPU 飙升至 100%,大量请求报错,而 Sentinel Dashboard 显示 blockQps 几乎为 0 —— 流量并未被限流,而是直接打垮了服务。
排查思路
- 步骤1-2:Grafana 显示订单服务 QPS 1.5w,CPU 100%,但 Sentinel block 数为 0。SkyWalking 显示服务自身处理耗时正常(约50ms),但大量请求直接返回 500 或超时,说明 Tomcat 线程池已满。
- 步骤3:
top -H发现所有 Tomcat 工作线程繁忙,arthas thread -n显示线程全部在执行正常的业务逻辑(如查询商品、校验库存),无慢调用或死锁。jstack线程栈无异常。说明仅仅是流量超出了系统容量。 - 步骤4:核对该服务的全链路压测报告(系列第4篇),发现容量拐点约为 1 万 QPS,当前限流阈值 5 万远超容量,形同虚设。
根因分析
限流阈值未基于压测数据,而是主观猜测或沿用历史值。限流器成了“马其诺防线”,一旦流量超过实际容量,服务迅速崩溃。
修正方案
根据第4篇压测结果,设置 count = 容量拐点 × 0.8,留出 20% 安全余量。
flowRule.setCount(8000); // 容量 10000 × 0.8
同时配置 maxQueueingTimeMs 和 warmUpPeriodSec,实现平滑预热。并开启 Sentinel 集群流控模式,防止单机限流不准确。
最佳实践
- 每次大促前执行全链路压测,自动更新 Sentinel 限流阈值(可通过 Nacos 动态下发)。
- 结合 HPA 弹性扩容,但必须保证限流阈值低于扩容不及时时的极限容量。
- 配合 Gateway 层的入口限流(第9篇),形成双重保障。
- 监控
sentinel_block_qps指标,若为 0 但服务已开始出现超时,应立即人工介入检查阈值。
2.2 熔断阈值过松(关联第2篇)
错误示例
# Resilience4j 配置
resilience4j.circuitbreaker:
instances:
paymentService:
failureRateThreshold: 90
waitDurationInOpenState: 2000
slidingWindowSize: 10
故障现象
支付服务发生瞬时故障(错误率 60%),但熔断器迟迟不打开,订单服务持续调用故障下游,自身线程池被大量超时请求占据,RT 逐渐升高,最终导致订单服务不可用,故障扩散至全站。
排查思路
- 步骤2:SkyWalking 显示
paymentService调用错误率 60%,平均 RT 3s。 - 步骤3:查看
/actuator/health及 Resilience4j 指标端点(/actuator/circuitbreakers),发现paymentService的state仍为CLOSED,failureRate显示 60%,但因阈值 90% 未触发。 - 步骤4:检查配置,熔断阈值 90% 过高,60% 错误率未能触发;
waitDurationInOpenState=2s过短,即使熔断后也立即半开,大量请求继续尝试失败。
根因分析
熔断器参数违背了总纲(系列第2篇)中“快速失败,谨慎恢复”原则。过高的阈值使得熔断器丧失了保护能力,过短的等待时间使下游未恢复就遭受探测流量。
修正方案
failureRateThreshold: 50
waitDurationInOpenState: 30000
permittedNumberOfCallsInHalfOpenState: 10
slidingWindowSize: 100
遵循:阈值设为 50% 敏感触发,等待时间足够下游重启或扩容。
最佳实践
- 熔断参数必须结合下游 SLA 设定,演练各种故障场景(第5篇混沌工程)验证。
- 熔断器状态变更应与告警联动,Open 状态立即通知下游团队。
- 在 Gateway 层也集成熔断(第9篇),实现多级防御。
2.3 @Async 线程池未隔离(关联第3篇)
错误示例
@Async
public CompletableFuture<PaymentResult> callPayment(PaymentRequest req) { ... }
@Async
public void updateInventory(InventoryRequest req) { ... }
配置:
spring:
task:
execution:
pool:
core-size: 50
max-size: 100
queue-capacity: 100
故障现象
支付服务偶发 4.5 秒延迟,所有 callPayment 线程阻塞等待;由于线程池被支付调用占满,updateInventory 任务无法获取线程执行,库存扣减堆积,Tomcat 请求线程也被迫等待,订单服务整体 RT 暴涨,CPU 飙高(上下文切换)。
排查思路
- 步骤2:Grafana 显示订单服务线程池队列大小持续增长,活跃线程数始终等于
max-size。 - 步骤3:
arthas thread -n 5输出 Top 5 线程全部为pool-1-thread-*,栈停留在PaymentService.confirm()等待 HTTP 响应。jstack显示 200 个线程中 180 个均为WAITING在 SocketRead 上。 - 步骤4:对比第3篇的利特尔法则,支付服务平均响应 4.5s × 并发 50 约需 225 线程,远超池容量,库存调用被饿死。
根因分析
多个依赖共用同一线程池,没有进行隔离。慢下游占满所有线程,导致核心库存业务也无法执行,引发连锁阻塞。
修正方案
为支付和库存分别定义独立线程池(TaskExecutor),并配置有界队列和 CallerRunsPolicy 拒绝策略。
@Bean("paymentExecutor")
public Executor paymentExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(20);
exec.setMaxPoolSize(50);
exec.setQueueCapacity(200);
exec.setRejectedExecutionHandler(new CallerRunsPolicy());
return exec;
}
@Bean("inventoryExecutor")
public Executor inventoryExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(10);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(100);
return exec;
}
调用时指定:
@Async("paymentExecutor")
public CompletableFuture<PaymentResult> callPayment(...) {...}
依据利特尔法则计算所需线程数:并发数 ≥ QPS × 平均响应时间(秒),并预留 20% 余量。
最佳实践
- 按依赖划分隔离舱壁(Bulkhead),至少保证核心链路线程池永不枯竭。
- 配合熔断器(第2篇)快速释放线程,防止慢调用堆积。
- 设置线程池监控,队列容量利用率 > 80% 时告警。
- 使用 Arthas 的
thread -n定期巡检,或通过 Prometheus 暴露executor_active_threads指标。
2.4 @Scheduled 分布式未加锁(关联第8篇降级任务)
错误示例
@Scheduled(cron = "0 0 2 * * ?")
public void settleOrders() {
List<Order> orders = orderRepo.findUnsettledOrders();
orders.forEach(this::settle);
}
故障现象
每天结算后,客服收到大量投诉:重复扣款、账户余额异常。数据库中结算记录出现相同订单号的重复行。
排查思路
- 步骤2:查看结算日志,相同订单在不同 Pod 的日志中均出现。
- 步骤3:
SELECT order_id, COUNT(*) FROM settlement GROUP BY order_id HAVING COUNT(*) > 1;确认重复。 - 步骤4:发现结算方法没有分布式锁保护,K8s 中多副本同时执行。
根因分析
Spring @Scheduled 仅保证单 JVM 内单线程执行,不提供跨实例互斥。多副本导致并发执行同一业务。
修正方案
引入 ShedLock 或 Redisson 分布式锁:
@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(name = "settleOrders", lockAtMostFor = "10m", lockAtLeastFor = "1m")
public void settleOrders() { ... }
同时将任务幂等化:使用订单状态机(UNSETTLED → SETTLING → SETTLED)避免重复结算。
最佳实践
- 所有分布式定时任务必须加锁,即使任务本身看起来“幂等”。
- 锁的超时时间必须大于任务最长执行时间。
- 定期进行降级演练(第8篇),验证锁的释放与续期机制。
- 定时任务执行时间需监控,若临近
lockAtMostFor说明可能需要拆分任务。
2.5 @Cacheable 无 TTL 导致内存溢出/击穿(关联第6篇、第7篇)
错误示例
@Cacheable(value = "orders", key = "#orderId")
public Order getOrder(Long orderId) { ... }
Redis 配置中未设置 TTL:
spring:
cache:
redis:
time-to-live: 0
故障现象
Redis 内存使用持续增长直至达到 maxmemory,触发大量 eviction。大量热门订单缓存被淘汰,请求直落 DB,DB 连接池瞬间耗尽,订单服务 RT 急剧上升,CPU 因大量等待和重试而飙升。
排查思路
- 步骤2:Grafana Redis 监控显示
used_memory接近maxmemory,evicted_keys攀升。 - 步骤3:
redis-cli info memory输出evicted_keys:12345678,确认大量淘汰。jmap -histo发现内存中byte[]占比异常(由于序列化等),SHOW FULL PROCESSLIST发现大量SELECT * FROM orders WHERE ...慢查询。 - 步骤4:缓存无 TTL 导致 Key 无限膨胀,加上业务热点 Key 未加互斥锁,击穿 DB。
根因分析
缓存没有过期策略,内存打满后 Redis 执行 eviction,大量请求缓存未命中,并发查询 DB 造成击穿。
修正方案
配置缓存 TTL,并添加随机偏移防止雪崩:
spring:
cache:
redis:
time-to-live: 300s
在热点 Key 上加互斥锁:
public Order getOrder(Long orderId) {
String cacheKey = "orders:" + orderId;
Order order = redisTemplate.opsForValue().get(cacheKey);
if (order != null) return order;
RLock lock = redisson.getLock("lock:order:" + orderId);
try {
lock.lock(5, TimeUnit.SECONDS);
order = redisTemplate.opsForValue().get(cacheKey);
if (order != null) return order;
order = orderRepo.findById(orderId);
redisTemplate.opsForValue().set(cacheKey, order,
300 + ThreadLocalRandom.current().nextInt(0, 60), TimeUnit.SECONDS);
return order;
} finally {
lock.unlock();
}
}
最佳实践
- 所有缓存必须设置 TTL,热点 Key 使用互斥锁 + 逻辑过期。
- 配置
maxmemory-policy为allkeys-lru或volatile-lru。 - 预热大促热点数据,避免冷启动击穿。
- 监控缓存命中率,命中率骤降应立刻告警。
2.6 @Transactional 自调用失效(关联微服务系列第20篇)
错误示例
@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
this.saveOrder(dto); // 自调用
}
@Transactional
public void saveOrder(OrderDTO dto) { ... }
}
故障现象
订单创建中出现部分数据不一致,如订单主表插入成功但明细表因异常回滚失败。
排查思路
- 步骤2:业务方反馈订单明细数据缺失,主表有记录。
- 步骤3:检查事务日志(开启
logging.level.org.springframework.transaction.interceptor=TRACE)未见saveOrder的事务边界日志。 - 步骤4:审查代码,发现自调用,由于 Spring AOP 代理机制,调用的是原始对象方法而非代理增强方法。
根因分析
@Transactional 基于 AOP 代理,同类方法调用绕过代理,注解失效。
修正方案
将事务方法移到独立 Service Bean 或使用 AopContext.currentProxy():
((OrderService) AopContext.currentProxy()).saveOrder(dto);
或更好:
@Service
public class TransactionalOrderService {
@Transactional
public void saveOrder(OrderDTO dto) { ... }
}
最佳实践
- 避免同类自调用事务方法;涉及事务的公有方法应集中在单独的
@Service中。 - 开启
expose-proxy=true仅作为临时方案,推荐注入独立 Bean。 - 在编码规范中强制检查,结合 Sonar 或 IDE 插件禁止自调用事务方法。
三、15+ 反模式逐一剖析(下)
接续剖析 Tomcat、Gateway、连接池、超时、探针、降级、MQ、分片八大领域反模式。
3.1 Tomcat maxThreads 配置过高(关联第9篇)
错误示例
server:
tomcat:
threads:
max: 2000 # 8核CPU
max-connections: 10000
故障现象
流量高峰时 CPU 使用率 100%,但吞吐量反而下降,vmstat 1 显示 cs(上下文切换)高达 50 万次/秒。系统负载(load average)远超 CPU 核心数。
排查思路
- 步骤2:CPU sys 态占比 > 30%,上下文切换量巨大。
- 步骤3:
vmstat 1观察cs列;top -H发现可运行线程数远超 CPU 核数,大量线程在Runnable状态但等待调度。 - 步骤4:根据 CPU 核数 8,I/O 密集型应用,推荐最大线程数 ≤ 8 × 25 = 200,实际 2000 线程导致调度开销急剧增加。
根因分析
Tomcat 线程数远超 CPU 能够高效调度的数量,产生上下文切换风暴,CPU 被调度器消耗而非处理业务。
修正方案
server:
tomcat:
threads:
max: 200 # 8核 × 25
max-connections: 2000
accept-count: 500
结合 HPA 伸缩,单 Pod 容量设为合理值,通过 Pod 数量增加总并发能力。
最佳实践
- 通过压测找到最优线程数(吞吐量拐点),而非一味提高。
- 监控
jvm.threads.states及系统context.switches。 - 使用 Gateway 限流防止突发流量直接击穿 Tomcat。
3.2 Gateway Netty 线程被阻塞(关联第9篇)
错误示例
@Component
public class BlockingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ResultSet rs = jdbcTemplate.query(...); // 阻塞
return chain.filter(exchange);
}
}
故障现象
网关 QPS 从正常的 5 万骤降至 2000,arthas thread -n 显示大量 reactor-http-epoll-* 线程处于 WAITING 或 BLOCKED 状态。
排查思路
- 步骤2:网关 CPU 不高,但请求排队严重,HTTP 连接数攀升。
- 步骤3:
arthas thread -n 5发现 Netty 工作线程在BlockingFilter.filter()等待数据库查询。 - 步骤4:Filter 中使用了阻塞数据库驱动,导致事件循环线程被占用。
根因分析
阻塞 I/O 调用绑定了 Netty 的单线程事件循环,违背了 Gateway 的响应式架构。
修正方案
将阻塞逻辑隔离到 Schedulers.boundedElastic() 或改用响应式 API(Spring Data R2DBC):
Mono.fromCallable(() -> blockingCall())
.subscribeOn(Schedulers.boundedElastic())
.flatMap(result -> ...)
最佳实践
- Gateway 中严禁直接使用阻塞客户端(RestTemplate、JDBC)。
- 所有 I/O 操作使用 WebClient、R2DBC 等异步驱动。
- 引入
spring-cloud-starter-circuitbreaker-reactor-resilience4j对下游调用加熔断。
3.3 连接池未设 maxActive 导致资源耗尽
错误示例
spring:
datasource:
hikari:
maximum-pool-size: 200
connection-timeout: 60000
故障现象
流量高峰时,大量请求报 CannotGetConnectionException: Wait millis 60000 exceeded,数据库 SHOW PROCESSLIST 显示连接数达到上限 max_connections。
排查思路
- 步骤2:错误日志中连接池等待超时。
- 步骤3:
SHOW VARIABLES LIKE 'max_connections'返回 200,而所有服务连接池总和超出。netstat -antp | grep 3306 | wc -l显示连接数已满。 - 步骤4:HikariCP
maximum-pool-size未合理计算,单个服务就占满数据库连接。
根因分析
连接池大小未根据数据库容量和应用节点数规划,总连接数超出数据库限制。等待超时过长使线程长时间阻塞。
修正方案
根据公式 maxPoolSize = (核心数 * 2) + (有效磁盘数) 初算,再结合 QPS × 单次DB耗时(秒) × 1.3 微调。全局总连接数必须小于数据库最大连接数。缩短超时:
maximum-pool-size: 20
connection-timeout: 3000
最佳实践
- 每个服务实例连接池大小不超过 20(常见指导值)。
- 监控
hikaricp_connections_active和hikaricp_connections_pending,排队数超过阈值告警。 - 使用数据库代理或连接池中间件统一管理。
3.4 超时未遵循传递链(关联第4篇)
错误示例
订单服务 -> 支付服务 (feign timeout: 5s)
支付服务 -> 银行网关 (RestTemplate 未设 timeout)
故障现象
银行网关假死,支付服务线程永久阻塞在 SocketInputStream.socketRead0,5 秒后订单服务超时,但支付服务仍在执行,其线程池逐渐耗尽,最终支付服务不可用。
排查思路
- 步骤3:
jstack支付服务 PID,大量http-nio-*线程在SocketRead状态,无任何超时异常。 - 步骤4:下游调用未设超时,导致上游超时后下游依然占用资源。
根因分析
超时传递链断裂:上游设定超时 > 所有下游超时之和,而此处下游无超时。
修正方案
设置下游 RestTemplate 超时:
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(1))
.setReadTimeout(Duration.ofSeconds(3))
.build();
}
并确保订单服务超时 > 支付服务超时 + 银行网关超时(如订单 6s,支付 4s,银行 2s)。
最佳实践
- 所有 HTTP 客户端、RPC 调用必须显式设置 connectTimeout 和 readTimeout。
- 全链路超时符合“上游超时 ≥ Σ 下游超时 + 余量”。
- 通过 SkyWalking 分析调用链真实耗时,动态调整超时。
3.5 livenessProbe 配置过短(关联第7篇、第8篇)
错误示例
livenessProbe:
httpGet:
path: /actuator/health/liveness
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
故障现象
服务正常运行但频繁重启,kubectl describe pod 显示 Liveness probe failed: timeout。
排查思路
- 步骤2:Pod Restart Count 增加,但应用日志无异常,CPU/内存正常。
- 步骤3:
kubectl describe pod观察到 liveness probe 失败,检查 GC 日志,发现 ParNew/Full GC 停顿时间可达 4s。 - 步骤4:
timeoutSeconds=3小于 GC 停顿,正常 GC 被误判为不健康。
根因分析
探针超时未考虑 JVM 的 Stop-The-World 停顿,导致 K8s 误杀健康 Pod。
修正方案
livenessProbe:
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 3
最佳实践
- 通过 GC 日志或
jstat -gcutil获取历史最大停顿时间,timeoutSeconds设为其 2 倍以上。 - 使用启动探针分离启动阶段。
- 结合 Prometheus 监控 GC 停顿,提前优化。
3.6 降级开关未生效(关联第8篇)
错误示例
@RefreshScope
@RestController
public class OrderController {
@Value("${degrade.recommend:false}")
private boolean recommendDegrade;
public Result recommend() {
// 未检查开关,始终调用推荐服务
return recommendService.getRecommendations();
}
}
故障现象
非核心推荐服务出现慢响应,但降级开关在 Nacos 中已配置为 true,订单服务却仍调用推荐服务,最终被拖慢。
排查思路
- 步骤2:Nacos 配置项
degrade.recommend=true,但 SkyWalking 追踪仍然显示调用推荐服务。 - 步骤3:检查 Controller 代码,发现未实际使用
@Value注入的开关变量。 - 步骤4:降级代码缺失或未正确读取配置,导致开关无效。
根因分析
开发未实现开关的判断逻辑,或者没有通过 @RefreshScope 确保动态刷新生效(但即使刷新,也未使用)。
修正方案
在需要降级的方法中显式判断:
if (recommendDegrade) {
return Result.fallback("推荐已降级");
}
并通过混沌演练(第5篇)强制打开降级开关,验证是否真的返回兜底数据。
最佳实践
- 降级开关必须有对应的判断代码,并通过自动化测试验证。
- 在接口层面使用 Sentinel 的
@SentinelResource(fallback = "fallbackMethod")做更细致的降级。 - 降级演练定期执行,确保所有开关有效。
3.7 MQ 消费未做幂等(关联第6篇)
错误示例
@RabbitListener(queues = "order.pay.success")
public void handlePaySuccess(OrderPaySuccessEvent event) {
accountService.debit(event.getUserId(), event.getAmount());
}
故障现象
消费者处理消息时发生异常,消息重试,导致重复扣款。对账不平。
排查思路
- 步骤2:用户投诉重复扣款,DB 中存在相同事件 ID 的多次扣款记录。
- 步骤3:
SELECT event_id, COUNT(*) FROM payment_log GROUP BY event_id HAVING COUNT(*) > 1; - 步骤4:消费者无幂等逻辑,重试时再次执行业务。
根因分析
消息中间件提供至少一次投递保证,消费者必须自行实现幂等。
修正方案
利用业务唯一键(如 eventId)配合数据库唯一索引或 Redis SETNX:
if (redisTemplate.opsForValue().setIfAbsent("idem:" + event.getEventId(), "1", Duration.ofHours(24))) {
accountService.debit(...);
}
或使用 INSERT ... ON DUPLICATE KEY UPDATE 实现幂等写入。
最佳实践
- 所有消息消费必须设计幂等;优先用数据库唯一约束兜底。
- 使用消费状态表记录已处理的消息 ID,允许重复消费但确保结果一致。
- 监控消息重试次数,超过阈值告警。
3.8 分片键选择不当导致数据倾斜(关联第6篇、分布式数据架构系列)
错误示例
# ShardingSphere 配置,按 order_status 分片
sharding:
tables:
order:
database-strategy:
standard:
sharding-column: status
故障现象
大促期间,大量订单处于“待支付”状态,分片 0(存放待支付订单)QPS 持续满载,而其他分片几乎空闲。该分片 RT 急剧上升,导致整个集群吞吐下降。
排查思路
- 步骤2:分片监控显示数据分布严重不均,0 分片 CPU 使用率 90%,其他 < 20%。
- 步骤3:
SHOW FULL PROCESSLIST指向分片 0 数据节点,大量慢查询。 - 步骤4:分片键
status基数极低,且业务热点集中,导致负载倾斜。
根因分析
分片键选择未考虑数据分布均匀性和查询路由,热点字段导致单分片过载。
修正方案
改用 user_id 或 order_id 等高基数、访问均匀的字段:
sharding-column: user_id
结合基因法或用户 ID 取模,确保请求均匀分散。
最佳实践
- 分片键应具备高区分度,且与大部分查询条件匹配。
- 定期分析分片 QPS 和容量,使用一致性哈希等策略避免热点。
- 对于必然倾斜的数据(如大商户),采用单独路由表或二次分片。
15+ 反模式分类全景图
flowchart LR
subgraph CPU_飙升域
A1[限流阈值过高 L2]
A2[线程池未隔离 L1]
A3[Tomcat maxThreads 过高 L2]
A4[Gateway 阻塞 L1]
A5[超时缺失 L1]
A6[分片键倾斜 L2]
end
subgraph RT_突增域
B1[熔断阈值过松 L1]
B2[缓存无 TTL L1]
B3[连接池耗尽 L1]
B4[降级未生效 L2]
B5[事务自调用 L3]
end
subgraph 错误率升高域
C1[限流阈值过低 L2]
C2[探针误杀 L1]
C3[MQ 幂等缺失 L1]
end
A2 --> B1
B2 --> B3
A5 --> B3
A6 --> B3
C2 --> A3
四层说明
- 主旨:将 15+ 反模式按主要影响的故障域(CPU、RT、错误率)进行分类,标注影响级别(L1致命/L2严重/L3中等)及相互关联。
- 逐元素分解:CPU 域多为资源争抢和阻塞导致;RT 域多为慢调用、缓存失效和资源耗尽;错误率域多为主动拒绝或误杀。箭头表示反模式间的因果触发。
- 设计原理:依据故障表征反向推导,反映了系统可观测性金字塔从“现象”到“根因”的关联。
- 关键结论:绝大多数 L1 故障的根源都在于资源隔离的缺失或超时/缓存策略的失效。修正这些反模式,能消除 80% 的线上重大故障。
四、三大故障域的排查决策树
基于六步排查法的核心诊断步骤(步骤3),提供针对 CPU 飙升、RT 突增、错误率升高的快速导航决策树。每个决策树不仅列出命令,还补充了判断条件、典型输出与下一步动作。
4.1 CPU 飙升决策树
flowchart TD
Start(["CPU 飙升告警"]) --> A["top -H -p PID<br>查看线程级 CPU,记录最高线程 TID"]
A --> B["printf '%x' TID<br>转为十六进制 nid"]
B --> C["jstack PID | grep -A 30 nid<br>获取线程栈"]
C --> D{"栈顶函数/线程名分析"}
D -- "GC task thread" --> E["GC 频繁<br>jstat -gcutil PID 1000 5 查看 FGC 频率"]
E --> E1["内存不足/泄漏<br>jmap -histo + MAT 分析"]
D -- "http-nio-* (RUNNABLE)" --> F["arthas thread -n 5<br>确认 CPU 热点方法"]
F --> G{"代码行为判断"}
G -- "死循环或密集计算" --> H["CPU 热点无限制<br>优化逻辑或加缓存"]
G -- "大量线程 WAITING 在锁" --> I["死锁或锁竞争<br>arthas thread -b 检测死锁<br>优化锁粒度"]
D -- "reactor-http-epoll-* (BLOCKED/WAITING)" --> J["Gateway Netty 线程阻塞<br>检查阻塞调用并隔离"]
D -- "pool-*-thread-* (WAITING socketRead)" --> K["下游调用无超时<br>设置 readTimeout"]
E1 --> L["修正方案:限制集合大小、加 TTL 等"]
H --> M["修正方案:逻辑修复、加本地缓存"]
I --> N["修正方案:无锁化、分段锁"]
J --> O["修正方案:异步化或 boundedElastic"]
K --> P["修正方案:设置超时"]
classDef decision fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
classDef process fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#1e293b
classDef endpoint fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#0f172a
class D,G decision
class Start endpoint
class A,B,C,E,E1,F,H,I,J,K,L,M,N,O,P process
决策树详细判定表
| 分支 | 线程栈特征 | 可能的反模式 | 下一步动作 | 常见输出/命令 |
|---|---|---|---|---|
| GC 线程 | "GC task thread" | 内存泄漏,对象生命周期过长 | jstat -gcutil 1s,若 FGC 频繁则 dump 堆 | jcmd <pid> GC.heap_dump |
| 业务 RUNNABLE | 业务方法在栈顶,无 IO 等待 | 死循环、密集计算 | arthas watch 观察方法参数和返回值 | 优化算法或缓存 |
| 锁竞争 | WAITING 在 synchronized / ReentrantLock | 锁粒度过大或死锁 | arthas thread -b 检测死锁,或 jstack 搜索 BLOCKED | 降低锁粒度、用无锁数据结构 |
| Netty 阻塞 | reactor-http-epoll-* 线程在 JDBC 或 HTTP 调用阻塞 | Gateway 阻塞(3.2) | 替换为 WebClient 或 R2DBC | subscribeOn(Schedulers.boundedElastic()) |
| 下游无超时 | pool-*-thread-* 在 SocketInputStream.socketRead0 | 超时缺失(3.4) | 为 RestTemplate 设置 readTimeout | setReadTimeout(3s) |
四层说明
- 主旨:以线程栈内容为核心分支依据,将 CPU 飙升快速归因到 GC、业务循环、锁、Netty 阻塞、下游超时五大类别。
- 逐层分解:前三步获取线程栈,第四步通过关键字(GC task、epoll、socketRead、业务方法)分流;每个分支继续细化直到给出修正建议。
- 设计原理映射:基于 USE 方法(Utilization, Saturation, Errors)的 Utilization 维度,Linux 线程调度与 JVM 线程转储结合。
- 关键结论:CPU 飙升不一定是代码死循环,多数情况是线程阻塞或大量上下文切换。学会阅读 jstack 中的线程状态是排查的关键。
4.2 RT 突增决策树
flowchart TD
Start(["RT 突增告警"]) --> A["SkyWalking 调用链<br>定位最慢的 Span"]
A --> B{"慢 Span 类型"}
B -- "数据库调用" --> C["SHOW FULL PROCESSLIST<br>查找执行时间 >1s 的 SQL"]
C --> D["EXPLAIN 分析执行计划<br>关注 type=ALL, key=NULL"]
D --> E{"慢 SQL 原因"}
E -- "缺少索引" --> F["添加合适索引"]
E -- "连接池等待" --> G["连接池耗尽<br>HikariCP pending > 0"]
G --> G1["增大连接池或分流"]
E -- "数据量大,全表扫描" --> H["缓存失效/击穿<br>Redis 命中率低"]
H --> H1["加互斥锁,预热缓存"]
B -- "RPC/HTTP 调用" --> I["跳转到下游服务监控<br>查看其 CPU/RT/错误率"]
I --> J{"下游状态"}
J -- "下游 RT 高" --> K["下游自身故障<br>继续下游排查决策树"]
J -- "熔断器 OPEN" --> L["降级未生效或半开探测<br>检查 Resilience4j state"]
J -- "连接超时/拒绝" --> M["连接池满或服务不可用<br>检查 Nacos 实例数"]
F --> N["索引添加完成"]
G1 --> N
H1 --> N
K --> N
L --> N
M --> N
classDef decision fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
classDef process fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#1e293b
classDef endpoint fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#0f172a
class B,E,J decision
class A,C,D,F,G,G1,H,H1,I,K,L,M,N process
class Start endpoint
决策树判定表
| 慢 Span | 下一步检查 | 命令/指标 | 典型异常发现 | 修正 |
|---|---|---|---|---|
| MySQL 查询 | SHOW FULL PROCESSLIST | SELECT ... FROM orders WHERE status='PAID' ORDER BY create_time | Sending data 状态持续很久 | 加索引 (status, create_time) |
| MySQL 查询 | HikariCP 指标 | /actuator/metrics/hikaricp.connections.pending | 排队数很高 | 扩容连接池或优化 SQL |
| Redis 查询 | redis-cli info stats | keyspace_hits/keyspace_misses | 命中率很低 | 查缓存 TTL 和 eviction |
| HTTP 调用 | SkyWalking 下游节点 | 下游 P99 > 1s | 下游服务 CPU 高 | 排查下游,增加熔断 |
四层说明
- 主旨:利用 APM 追踪调用链,将延迟归因到具体的 Span,再进一步下钻到数据库或下游服务。
- 逐层分解:SkyWalking 定位瓶颈 Span → 若为 DB,则用 MySQL 工具分析慢 SQL 及连接状况;若为 RPC,则检查下游健康和熔断状态。
- 设计原理:基于 RED 方法的 Duration 维度,层层剥离,通过标准化 SQL 分析命令实现快速判断。
- 关键结论:RT 突增 90% 与数据库或下游服务有关,尽快执行 SHOW FULL PROCESSLIST 和查看 SkyWalking 可缩短定位时间。
4.3 错误率升高决策树
flowchart TB
Start(["错误率升高"]) --> A["curl /actuator/health<br>检查各组件健康状态"]
A --> B["Sentinel Dashboard<br>查看 blockQps 是否突增"]
B --> C{"blockQps 增高?"}
C -- "是" --> D["限流拒绝<br>阈值过低或容量不足<br>反模式2.1"]
C -- "否" --> E["检查 Resilience4j 熔断器状态<br>/actuator/circuitbreakers"]
E --> F{"circuitBreaker.state == OPEN?"}
F -- "是" --> G["熔断拒绝<br>下游故障导致熔断<br>反模式2.2"]
F -- "否" --> H["检查 Nacos 服务列表<br>实例是否在线"]
H --> I{"实例健康?"}
I -- "否" --> J["Pod 重启/探针误杀<br>kubectl describe pod"]
I -- "是" --> K["检查 MQ 死信队列堆积<br>或下游错误率"]
K --> L["反模式3.7 幂等缺失或消息堆积<br>或下游错误扩散"]
D --> M["调整限流阈值或扩容"]
G --> N["调整熔断参数或修复下游"]
J --> O["修正探针配置"]
L --> P["实现幂等或处理死信"]
classDef decision fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
classDef process fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#1e293b
classDef endpoint fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#0f172a
class C,F,I decision
class A,B,D,E,G,H,J,K,L,M,N,O,P process
class Start endpoint
四层说明
- 主旨:按限流、熔断、实例健康、MQ 死信的顺序排查错误率飙升的根因。
- 逐层分解:优先检查限流块是否被触发;其次看熔断器是否因下游故障打开;再查服务注册发现中的实例状态;最后看异步消息是否有堆积或重复消费错误。
- 设计原理:服务错误率通常源自主动防御机制(限流、熔断)或基础设施问题(Pod 重启),决策树遵循自顶向下的检查顺序。
- 关键结论:错误率升高往往是因为某个防御机制被触发,而非代码 bug。确认 Sentinel blockQps 和 Resilience4j 状态是首要动作。
五、生产诊断工具链实战
本节详解四套命令组合的使用方法、典型输出与解读,提供可直接复制的脚本集。所有示例基于 JDK 8、Spring Boot 2.7.x、Arthas 3.x。
5.1 CPU 诊断三件套
命令序列
# 1. 找到 Java 进程 PID
jps -l | grep order-service
PID=12345
# 2. 查看线程级 CPU,找到最高线程 TID
top -H -p $PID -b -n 1 | head -20
# 输出示例:
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 16807 root 20 0 9345624 2.3g 13m S 95.0 15.3 1:23.45 java
# 16808 root 20 0 9345624 2.3g 13m R 94.0 15.3 1:23.44 java
# 3. 转换 TID 为十六进制
printf '%x\n' 16807 # 输出: 41a7
# 4. 从 jstack 抓取线程栈
jstack $PID | grep -A 30 "0x41a7"
典型输出与解读
"pool-1-thread-3" #45 prio=5 os_prio=0 tid=0x00007f8c0c001000 nid=0x41a7 runnable [0x00007f8b5a2f7000]
java.lang.Thread.State: RUNNABLE
at com.example.service.PaymentService.confirm(PaymentService.java:56)
at com.example.controller.OrderController.createOrder(OrderController.java:78)
...
解读:该线程属于 pool-1-thread-3,状态 RUNNABLE,正在执行 PaymentService.confirm()。若大量线程均在该方法,且状态为 WAITING (在 socketRead),则可能是下游服务无响应。如果是 RUNNABLE 且持续占用 CPU,可能是死循环或密集计算。
Arthas 快速定位
arthas 12345
[arthas@12345]$ thread -n 5
"pool-1-thread-3" Id=45 cpu=82% RUNNABLE
at PaymentService.confirm(PaymentService.java:56)
at OrderController.createOrder(OrderController.java:78)
thread -b 可查找死锁。
perf top 补充(可观测 CPU 消耗在哪个函数,包括 JVM 内部):
perf top -p $PID
如果看到大量 _raw_spin_unlock_irqrestore,说明存在激烈的锁竞争。
5.2 内存诊断三件套
对象直方图
jmap -histo $PID | head -20
输出示例:
num #instances #bytes class name
1: 12345678 9876543210 [C
2: 2345678 2345678901 java.lang.String
3: 345678 456789012 org.springframework.cache.concurrent.ConcurrentMapCache$1
[C (char[]) 占比异常高通常意味着大量字符串堆积,可能来自缓存或日志。如果是 ConcurrentMapCache 相关对象占用大,说明本地缓存无上限。
生成堆转储
jcmd $PID GC.heap_dump /tmp/heap.hprof
MAT 分析
打开 hprof 文件,查看 Dominator Tree:定位保留堆最大的对象。例如:
- 如果
org.springframework.cache.concurrent.ConcurrentMapCache下的store占用 2GB,表明本地缓存未限制大小。 - 使用
Path to GC Roots追踪引用链,找到是哪个线程或静态变量持有。
GC 日志分析(-Xlog:gc* 或 -XX:+PrintGCDetails):
查看 GC 日志中的 Full GC (Allocation Failure) 频率和耗时,结合 jstat -gcutil。
5.3 GC 诊断
jstat -gcutil $PID 1000 10
输出:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 12.34 45.67 78.90 92.10 89.56 123 1.234 5 2.345 3.579
若 FGC 持续增长且 FGCT 高,说明存在内存泄漏或大对象频繁晋升。可用 MAT 分析 GC Roots 定位。
5.4 网络诊断
netstat -antp | grep $PID | wc -l # 总连接数
ss -s # socket 统计
连接数异常高,结合 netstat 的状态分布(如大量 TIME_WAIT 或 CLOSE_WAIT)判断是连接泄露还是未关闭。对于数据库连接,netstat -antp | grep 3306 | awk '{print $6}' | sort | uniq -c 查看状态分布。
一键诊断脚本示例
#!/bin/bash
PID=$1
echo "=== CPU Top Threads ==="
top -H -p $PID -b -n 1 | head -10
echo "=== Arthas CPU Top ==="
arthas $PID -c "thread -n 5" --no-hup 2>/dev/null
echo "=== Memory Histogram ==="
jmap -histo $PID 2>/dev/null | head -20
echo "=== GC Stats ==="
jstat -gcutil $PID 1000 2
echo "=== Network Connections ==="
ss -antp | grep $PID | wc -l
六、反模式关联推演:连锁反应链
线上故障很少由单一反模式导致,通常是多个反模式级联引爆。以下列举三条常见连锁路径,每条路径标注故障时序、各阶段指标变化与阻断点,并附应急措施。
连锁路径 1:线程池未隔离 → Pod 重启雪崩
时序:
- 支付服务偶发慢调用(因第三方故障),耗时 4.5s。
- 订单服务 @Async 默认线程池(大小 50)被支付调用占满,库存更新任务无法获取线程。
- Tomcat 工作线程(200个)全部在等待库存/支付返回,新请求排队。
- 订单服务 CPU 飙升(上下文切换),RT 升至 5s。
- K8s livenessProbe 超时(配置
timeoutSeconds=3),误杀 Pod。 - 剩余 Pod 流量翻倍,压力更大,雪崩。
阻断点:
- 在步骤2前:为支付调用设置独立线程池,并配置熔断,慢调用快速失败。
- 在步骤5前:调整
livenessProbe.timeoutSeconds到 10s,防止误杀。 - 应急措施:通过 Sentinel Dashboard 对支付调用限流降级,或直接关闭支付功能开关。
连锁路径 2:缓存无 TTL → DB 击穿 → 全站瘫痪
时序:
- 订单缓存
@Cacheable无 TTL,Redisused_memory持续增长。 - Redis 达到
maxmemory,开始 eviction,热点 Key 被淘汰。 - 大量请求缓存未命中,并发查询 DB,慢 SQL 增加。
- DB 连接池耗尽,HikariCP pending 队列增长。
- 所有依赖该数据库的服务超时,全站不可用。
阻断点:
- 步骤1:设置
spring.cache.redis.time-to-live=300。 - 步骤3:在
getOrder中加入互斥锁,防止击穿。 - 应急措施:手动重启 Redis 或通过
CONFIG SET maxmemory-policy allkeys-lru更激进淘汰,同时执行数据库 kill 慢查询,临时限流。
连锁路径 3:超时缺失 → 全链路雪崩
时序:
- 下游服务(银行网关)假死,连接建立但无响应。
- 支付服务调用银行网关 RestTemplate 未设
readTimeout,线程永久等待。 - 支付服务线程池满,自身不可用。
- 订单服务调用支付超时(5s),但支付仍在执行,订单服务线程池逐渐耗尽。
- 订单服务不可用,上游网关超时,调用方线程池也满。
- 全链路雪崩。
阻断点:
- 步骤2:为 RestTemplate 设置
readTimeout=3s,配合熔断器快速失败。 - 应急措施:重启下游或手动熔断,临时关闭支付功能。
通过以上链路的反模式关联推演,可见 单一配置遗漏会像多米诺骨牌一样,逐级传导并放大破坏力。建立全链路防御观,并在排查时追溯连锁链,是解决复杂故障的核心能力。
七、贯穿案例:电商秒杀订单服务 CPU 飙升 95% 排查实录
场景:大促秒杀活动 15:00 开始,订单服务、支付服务、库存服务组成核心链路,架构如下:
flowchart LR
User([用户]) --> GW[Gateway]
GW --> Order[订单服务]
Order --> Pay[支付服务]
Order --> Inv[库存服务]
Order --> Redis[(Redis 缓存)]
Order --> DB[(MySQL)]
Pay --> Bank[银行网关]
时间线:
flowchart TD
T0["15:03 Prometheus 告警: CPU 95%, P99 5s"] --> T1["15:03-15:05<br/>步骤1-2 指标定位<br/>SkyWalking显示支付调用4.5s<br/>Redis命中率30%"]
T1 --> T2["15:06-15:11<br/>步骤3 工具诊断<br/>top -H + arthas 发现200线程在PaymentService.confirm<br/>jmap -histo byte[]占80%<br/>redis-cli evicted_keys 500万"]
T2 --> T3["15:12-15:15<br/>步骤4 根因确认<br/>1.线程池未隔离 2.缓存无TTL"]
T3 --> T4["15:16-15:19<br/>步骤5 方案修正<br/>支付线程池隔离<br/>缓存加TTL和互斥锁<br/>限流8000 QPS"]
T4 --> T5["15:20 步骤6 验证恢复<br/>CPU 40%, P99 150ms<br/>Error Budget停止消耗"]
classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
步骤1-2:现象与指标定位(15:03-15:05)
Prometheus 告警内容:order-service CPU > 80% 持续2分钟,P99 > 3s。打开 Grafana “订单服务标准面板”,CPU 曲线在 15:02:30 迅速攀升,P99 延迟同步飙升,错误率随之而来。使用 PromQL:
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service="order-service"}[5m]))
SkyWalking 拓扑显示:order-service -> payment-service avg RT 4.5s,order-service -> inventory-service avg RT 2s(正常50ms),order-service -> Redis 命中率从 95% 跌至 30%。断定故障范围:支付服务慢 + Redis 缓存失效。
步骤3:工具诊断(15:06-15:11)
登录订单服务 Pod (PID 42):
top -H -p 42 -b -n1 | head -10
# CPU 100% 线程 TID 16807, 16808...
转换 nid 后用 jstack 查看,栈均在 PaymentService.confirm() 的 SocketInputStream.socketRead0。
Arthas:
thread -n 5
# 输出全部为 pool-1-thread-* 在 PaymentService.confirm
thread 命令显示线程池 pool-1(@Async 默认池)线程数 50,全部 BUSY,队列满。
内存检查:
jmap -histo 42 | head -20
# [C 和 java.lang.String 共占 80%,同时有大量 Cache 相关对象
Redis 命令:
redis-cli info memory
# used_memory_human:4.00G,maxmemory:4.00G
# evicted_keys:5234567
redis-cli info keyspace
# db0:keys=3000000,expires=0,avg_ttl=0
验证:缓存 Key 数 300 万且无 TTL。
步骤4:根因确认(15:12-15:15)
结合现象,确定双重根因:
- 根因1:默认线程池未隔离,支付服务因银行接口抖动延迟 4.5s,占满线程池,库存调用阻塞,Tomcat 线程等待,CPU 上下文切换飙升(反模式2.3)。
- 根因2:
@Cacheable无 TTL,Redis 内存打满,触发大量 eviction,缓存击穿,DB 慢查询加剧延迟,进而拖慢所有线程(反模式2.5)。
步骤5:方案修正(15:16-15:19)
紧急修复(Nacos 动态配置下发):
- 线程池隔离:新增
paymentExecutor(core 20, max 50, queue 200) 和inventoryExecutor(core 10, max 20, queue 100),通过 Sentinel 流控配合。 - 缓存 TTL:
spring.cache.redis.time-to-live=300,并在代码中为热点getOrder增加 Redisson 互斥锁。 - 临时限流:Sentinel Dashboard 设置
order_create接口 QPS 8000。 - 探针:
livenessProbe.timeoutSeconds调整为 10。
步骤6:验证恢复(15:20)
Grafana 显示 CPU 降至 40%,P99 恢复至 150ms,错误率归零。Redis 内存使用量下降,缓存命中率回升至 92%。Error Budget 消耗停止。
完整命令序列摘要(含预期输出):
# 1. top -H -p 42 -b -n1 | head -10
# 输出:多个线程 CPU 100%,TID 16807等
# 2. printf '%x\n' 16807
# 输出:41a7
# 3. jstack 42 | grep -A 30 0x41a7
# 输出:pool-1-thread-3 WAITING at SocketInputStream.socketRead0
# 4. arthas 42 -c "thread -n 5"
# 输出:Top5 均为 pool-1-thread-* 在 PaymentService.confirm
# 5. jmap -histo 42 | head -20
# 输出:byte[] 和 String 实例数巨大,Cache 对象多
# 6. redis-cli info memory | grep evicted_keys
# 输出:evicted_keys:5234567
# 7. 修正配置通过 Nacos 推送,观察 CPU 下降。
八、与前后系列的衔接
- 本文作为收官之作,将第1-9篇的防御体系转化为排查能力:限流阈值依赖于第4篇的压测数据;熔断参数依据第2篇推导;线程池隔离基于第3篇利特尔法则;降级开关排查关联第8篇;Gateway 阻塞排查关联第9篇。
- 反模式修复后,技术债务的治理可纳入《微服务与云原生架构系列》第17篇(治理与标准化),将其作为架构评审必检项。
- 本文与微服务系列第20篇(Spring 设计哲学与反模式)互补:该篇侧重设计期编码误区,本文侧重高并发运行时故障,两者共同构成 Spring 生态的避坑指南。
- 后续可基于本文构建“故障演练平台”,集成混沌工程(第5篇)和排查决策树,实现半自动化诊断。
九、面试高频专题
9.1 线上 CPU 突然飙升,从哪些命令开始排查?每一步的输出如何解读?
-
一句话回答:先用
top -H定位高 CPU 线程,转十六进制后用jstack抓栈,最后用 Arthasthread -n确认热点方法;根据线程栈状态(RUNNABLE/WAITING)分流诊断 GC、死循环或 IO 阻塞。 -
详细解释
步骤 1:jps -l | grep <service>获取 Java 进程 PID。
步骤 2:top -H -p <PID> -b -n1 | head -20输出线程级 CPU 使用率。典型输出如:PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 16807 root 20 0 9.3g 2.3g 13m R 95.0 15.3 1:23.45 java此线程 TID=16807,CPU 95%。
步骤 3:十六进制转换printf '%x\n' 16807→41a7。
步骤 4:jstack <PID> | grep -A 30 0x41a7获取该线程完整栈帧。若输出为:"pool-1-thread-3" #45 prio=5 os_prio=0 tid=0x00007f... nid=0x41a7 runnable java.lang.Thread.State: RUNNABLE at com.example.PaymentService.confirm(PaymentService.java:56)说明线程正在执行业务逻辑,可能为死循环或密集计算;若状态为
WAITING (parking)或BLOCKED,则表明线程在等待锁或 IO。
步骤 5(快速法):使用 Arthasthread -n 5,直接输出 Top 5 繁忙线程栈,无需手动转换。arthas <PID> thread -n 5若发现大量线程停留在
SocketInputStream.socketRead0,则指向下游超时或连接池耗尽。
步骤 6:补充命令。vmstat 1查看系统cs(上下文切换)列,若大于 CPU 核数×10000,说明线程过多,调度风暴;perf top -p <PID>可看到 JVM 内部函数消耗,如_raw_spin_unlock高则表示锁竞争严重;jstat -gcutil <PID> 1s监控 GC 频率,若 FGC 频繁则转向内存诊断。 -
多角度追问
- 如果高 CPU 线程是
VM Thread或GC task thread,下一步该做什么? - 如何区分是代码死循环还是下游调用慢导致线程数量暴增?
- 在没有 Arthas 的情况下,如何快速找到最耗 CPU 的 Java 线程?
top -H中观察到多个线程 CPU 均不高,但总 CPU 高,可能是什么原因?
- 如果高 CPU 线程是
-
加分回答
可结合 JVM 的诊断命令jcmd <PID> PerfCounter.print查看 JVM 内部的性能计数器,如sun.rt.safepointTime等。对于死循环,还可以用jstack连续抓取三次栈,对比同一个线程的栈是否始终在同一位置,若不变则大概率是死循环。业界实践推荐在服务中集成micrometer暴露线程状态指标到 Prometheus,实现自动预警。
9.2 RT 突增的排查决策树是怎样的?如何区分是数据库慢还是下游服务慢?
-
一句话回答:通过 SkyWalking 定位调用链中最慢 Span,若为 DB 则执行
SHOW FULL PROCESSLIST及EXPLAIN;若为 RPC 则检查下游 RT 和熔断状态;再利用 MySQLsys库或performance_schema深入分析。 -
详细解释
- 打开 SkyWalking UI,选择受影响的服务,查看“端点”或“拓扑”的响应时间。点开耗时的调用链,找到耗时占总比 >80% 的 Span。
- 若 Span 为数据库操作(如
SELECT * FROM orders),则登录数据库执行:查看状态为SHOW FULL PROCESSLIST;Sending data、Creating sort index或Waiting for table metadata lock的线程,记录其Time和Info。 - 对嫌疑 SQL 执行
EXPLAIN SELECT ...,关注type列(ALL表示全表扫描)和key列(NULL表示未使用索引)。若rows列数值极大,说明扫描行数过多。 - 进一步使用
sys.库查询:sys.statement_analysis或sys.schema_tables_with_full_table_scans快速找到全表扫描的表。 - 若慢 Span 是 RPC 调用,点击该 Span 跳转至下游服务监控,查看下游服务的 CPU、RT、错误率。同时检查本服务的 Resilience4j 熔断器状态(
/actuator/circuitbreakers),若状态为OPEN,则延迟可能是由于排队等待半开探测。 - 若下游 RT 正常但调用量极大,可能是本服务没有足够连接导致线程等待,需检查 HTTP 连接池大小(如
RestTemplate的maxTotal和maxPerRoute)。
-
多角度追问
- 如何利用
performance_schema.events_statements_summary_by_digest分析慢 SQL? - 如果 SkyWalking 显示 DB 耗时仅 10ms,但服务总 RT 却高达 5s,可能的原因是什么?
- 数据库连接池(HikariCP)活跃连接数正常,但 SQL 变慢,可能是什么原因?
- 如何通过 Sentinel 追踪 RT 突增是由于限流排队还是业务阻塞?
- 如何利用
-
加分回答
可以采用“全链路追踪 + 日志”联合分析:在 SkyWalking 上为慢 Trace 关联日志(通过traceId),查看业务日志中是否出现“获取连接超时”或“连接重置”等异常。对于数据库,可开启slow_query_log并设置long_query_time=0.5捕获所有慢查,结合pt-query-digest工具分析慢查询报表。设计层面,为关键接口实现“备用数据源”(如只读副本),当主库变慢时可自动切换。
9.3 错误率突然升高,如何快速判断是限流、熔断还是下游故障?
-
一句话回答:按 Sentinel blockQps → Resilience4j 熔断器状态 → 下游健康探针(/actuator/health)→ Nacos 实例在线数 → MQ 死信队列堆积的顺序逐层排查。
-
详细解释
- 登录 Sentinel Dashboard,查看嫌疑资源的
blockQps趋势。若blockQps与请求错误率的上升同步,则很可能是流控规则触发导致的主动拒绝。结合curl模拟请求,观察返回体是否包含FlowException。 - 若
blockQps很低或为零,检查 Resilience4j 熔断器指标:GET /actuator/circuitbreakers,返回 JSON 中关注state(OPEN/FORCED_OPEN/HALF_OPEN/CLOSED)和failureRate。若state=OPEN,所有调用直接失败,错误率 100%。 - 若熔断器未打开,检查服务注册发现。调用 Nacos API
/v1/ns/healthy?serviceName=xxx查看健康实例数。若实例数少于预期,则可能是 Pod 重启或探针失败导致实例下线,流量打到了不健康节点。 - 若实例正常,检查消息队列(如 RocketMQ/RabbitMQ)的死信队列或重试队列是否堆积,消费异常可能导致业务错误(如重复消费失败)。
- 最终,通过
kubectl top pod或 Grafana 逐个比对上下游错误率,定位源头。
- 登录 Sentinel Dashboard,查看嫌疑资源的
-
多角度追问
- 限流和熔断同时发生如何解读?
- 如何通过 Prometheus 配置告警区分限流错误和业务异常?
- 下游服务健康探针正常但返回 500,可能的原因是什么?
- 如何设计一个“错误率雷达图”来快速识别故障域?
-
加分回答
在代码层面,可以自定义 Spring Boot 的ErrorController或@ExceptionHandler,为不同的异常类型(BlockException、CallNotPermittedException、HttpClientErrorException)分别设置 Metrics 指标。同时,将 Sentinel 的 block 页面重定向到统一的降级页面,并在前端埋点区分“限流降级”和“系统异常”。业界经验:错误率升高时,应当先查看“异常”的种类分布而非平均值,使用rate(http_requests_total{status=~"5.."}[1m]) by (uri)快速定位具体接口。
9.4 top -H + jstack 如何配合定位 CPU 热点线程?十六进制转换的作用是什么?
-
一句话回答:
top -H获得十进制线程 ID,需转为十六进制才能在jstack输出中匹配线程栈;二者结合可精准定位 CPU 消耗最高的代码行。 -
详细解释
在 Linux 中,内核调度和top -H显示的是轻量级进程(LWP)ID,即十进制整型;而jstack从 JVM 内部获取的nid(Native Thread ID)是以十六进制输出。必须将top看到的十进制转为十六进制才能对应。
完整操作链:# 1. 找到进程 jps -l | grep order-service # 2. 查看线程级 CPU top -H -p 12345 -b -n1 | awk '{if($9>50) print $1, $9}' | head # 输出:16807 95.1 # 3. 转换 printf '0x%x\n' 16807 # 0x41a7 # 4. 抓取该线程栈 jstack 12345 | grep -A 50 "0x41a7"若出现大量不同线程的
nid但栈顶相同,则说明此方法是热点。结合arthas thread -n 5可更便捷,但原理相同。
此外,也可以使用jcmd <PID> Thread.print,其输出格式与jstack一致。 -
多角度追问
- 如果十六进制 nid 在
jstack中找不到对应线程,可能是什么原因? - 为什么有些高 CPU 线程在
jstack中显示为WAITING? - 没有
top权限时,如何从 JVM 内部获取线程的 CPU 时间? - 如何利用
jstack快照分析线程状态分布(RUNNABLE vs WAITING vs BLOCKED)?
- 如果十六进制 nid 在
-
加分回答
可以结合pidstat -t -p <PID> 1查看线程级 CPU 利用率,更精细化。若需要长期分析,利用async-profiler生成火焰图,可以直接定位 JVM 的 CPU 热点函数,甚至包括 JIT 编译代码。十六进制转换的本质是 Linux 与 JVM 之间的线程 ID 表示不一致,理解这一点有助于定位 Native Memory 泄漏等更复杂问题。
9.5 Arthas 的 thread -n 5 和 thread -b 分别解决什么问题?
-
一句话回答:
thread -n 5按 CPU 使用率排序输出 Top 5 线程栈,用于快速找到热点方法;thread -b检测死锁,输出死锁线程相互等待的锁链。 -
详细解释
thread -n 5内部通过 JMX 获取ThreadMXBean的线程 CPU 时间,计算出 Delta CPU 使用率并排序,直接打印栈帧。比手动top -H+jstack快捷,而且能自动识别线程名称,如pool-1-thread-3、http-nio-8080-exec-1。
示例输出:"pool-1-thread-3" Id=45 cpu=82% RUNNABLE at com.example.PaymentService.confirm(PaymentService.java:56) at ...thread -b则调用ThreadMXBean.findDeadlockedThreads()检测死锁,输出类似:Found one Java-level deadlock: "thread-A": waiting to lock monitor 0x00007f... (object 0x00000007...) which is held by "thread-B" "thread-B": waiting to lock monitor 0x00007f... (object 0x00000007...) which is held by "thread-A"此外,
thread命令不带参数可列出所有线程的状态汇总,结合thread --state BLOCKED过滤阻塞线程。 -
多角度追问
thread -n显示的 CPU 使用率是如何计算的?与top的 %CPU 有何不同?- 如果怀疑某个方法执行慢但不是 CPU 高,该用 Arthas 的什么命令?
thread -b只能检测 Java 层死锁吗?如何发现 JNI 或 Native 层死锁?- 如何在生产环境安全地使用 Arthas 而不过多影响服务?
-
加分回答
Arthas 的profiler命令可以生成异步采样火焰图,适合分析 CPU 高且热点不明显的情况。对于慢方法,可使用trace com.example.Service method -n 5 --skipJDKMethod false追踪方法调用树和每个节点的耗时。在容器环境,推荐通过kubectl exec进入 Pod 使用 Arthas,或集成arthas-spring-boot-starter开启远程连接,但务必配置安全验证。
9.6 jmap -histo 和 MAT Dominator Tree 如何配合分析内存泄漏?
-
一句话回答:
jmap -histo展示堆中各类实例的数量与浅堆大小,快速发现可疑对象类;MAT 的 Dominator Tree 则计算每个对象的保留堆大小,结合 GC Roots 追踪最终定位泄漏的引用链。 -
详细解释
jmap -histo:live <PID> | head -20(建议加:live触发一次 Full GC 以去除可回收对象)输出:num #instances #bytes class name 1: 12345678 9876543210 [C 2: 2345678 2345678901 java.lang.String 3: 345678 456789012 com.example.cache.UserCache若
UserCache实例数和占用异常,则怀疑该缓存对象持有过多数据。之后用jcmd <PID> GC.heap_dump /tmp/heap.hprof生成堆转储,MAT 打开后查看 Dominator Tree。
Dominator Tree 按保留堆大小降序排列,找到最顶层的大对象,右键选择Path To GC Roots→exclude weak/soft references查看是谁在引用它。典型的泄漏路径:某个静态HashMap或ConcurrentHashMap不断put从未移除。 -
多角度追问
jmap -histo中的[C和java.lang.String占比高,如何进一步定位是哪个功能生成的?- 为什么推荐使用
jcmd而非jmapdump? - MAT 的 OQL(对象查询语言)如何帮助排查?
- 如果堆转储文件过大无法下载到本地,有哪些替代分析手段?
-
加分回答
可使用jhat或在线heaphero.io分析,但更推荐在服务器上使用 MAT 的命令行脚本ParseHeapDump.sh生成报告。另外,借助 Spring Boot Actuator 的heapdump端点可以远程触发并下载。内存泄漏排查过程中,结合jstat -gcutil观察 Old 区持续增长而不下降,是泄漏的确凿旁证。
9.7 什么是反模式的连锁反应?线程池未隔离如何最终导致 Pod 重启雪崩?
-
一句话回答:反模式连锁反应是多个不当设计相互耦合,一个微小的故障通过层层放大最终造成全局崩溃;典型路径为:线程池耗尽 → Tomcat 满载 → 健康探针超时 → Pod 被误杀 → 剩余 Pod 过载 → 雪崩。
-
详细解释
以电商订单服务为例:- 订单服务中
@Async使用默认线程池(core=50, max=100),支付调用和库存调用共享。 - 银行网关抖动,支付调用平均耗时从 200ms 升至 4.5s。
- 支付请求占满线程池所有线程,新来的库存更新任务被阻塞在队列中或直接被拒绝。
- Tomcat 的
http-nio线程等待Async返回,导致 Tomcat 工作线程全部被占用(maxThreads=200),新请求得不到处理开始堆积。 - K8s livenessProbe 请求因为 Tomcat 没有空闲线程处理而超时(
timeoutSeconds=3)。 - Kubelet 判定 Pod 不健康,杀掉并重启。
- 流量转移到剩余 Pod,同样因线程池耗尽而被拖垮,进入重启循环,全站雪崩。
阻断措施:步骤 1 就应采用隔离线程池;步骤 5 可增大探针超时并设置
failureThreshold;中间还可通过熔断器提前打开,拒绝支付调用释放线程。 - 订单服务中
-
多角度追问
- 如何通过监控提前发现线程池即将耗尽?
- 若无法立即修改代码,有哪些临时措施可以阻断这个连锁反应?
- 除了线程池,还有哪些资源耗尽会导致类似的雪崩(如连接池、文件句柄)?
- 如何利用混沌工程模拟这种级联故障验证系统韧性?
-
加分回答
可借用“利特尔法则”预测线程需求:所需线程数 = QPS × 平均响应时间,结合限流将 QPS 控制在安全范围内。在 Kubernetes 中,使用 PodDisruptionBudget 和反亲和策略防止所有 Pod 同时重启。设计层面应遵循“舱壁隔离”原则,将关键链路独立部署或至少独立线程池,并配合快速失败的超时机制。
9.8 为什么 @Cacheable 必须配置 TTL?不配置会导致什么连锁故障?
-
一句话回答:无 TTL 导致 Redis 内存无限增长直至
maxmemory,触发 eviction 淘汰热点 Key,缓存击穿使流量直接冲击数据库,造成连接池耗尽、全站瘫痪。 -
详细解释
Redis 默认不会自动删除未设置过期时间的 Key。若@Cacheable方法不断被调用产生新 Key,Redis 的used_memory最终达到maxmemory。根据配置的maxmemory-policy(通常默认noeviction或allkeys-lru),要么写操作失败,要么开始淘汰 Key。
淘汰过程中,包括热点订单数据的 Key 会被移除,此时大量请求缓存未命中,并发查询数据库。由于数据库连接池(如 HikariCPmaxActive=20)容量有限,瞬间高并发查询将连接池耗尽,导致其他服务也无法获取连接,形成全站性的数据库依赖故障。
正确做法:必须设置spring.cache.redis.time-to-live,同时为热点 Key 添加逻辑过期或互斥锁,防止缓存击穿。如使用 Redisson 的RLock加双重检查。 -
多角度追问
- Redis 的
maxmemory-policy各参数有何适用场景?如何选择? - 如果必须缓存大对象且不可过期,如何设计防止 OOM?
- 如何实现两级缓存(Caffeine + Redis)来降低 Redis 压力?
- 如何通过监控 Redis 内存和缓存命中率提前预警?
- Redis 的
-
加分回答
可以结合 Spring Cache 的CacheManagerCustomizer动态调整 TTL,或使用 Redis 的volatile-ttl策略,优先淘汰 TTL 较短的 Key。大对象可采用“压缩+分片”存储,并监控slowlog观察大 Key 的访问性能。在架构上,采用“缓存旁路”模式,设置代码审阅卡口,禁止在@Cacheable上遗漏 TTL 配置,可用自定义注解处理器在编译期检查。
9.9 (故障排查题 1)某电商系统大促期间,订单服务 CPU 飙至 90%,P99 延迟从 100ms 飙至 3s,错误率 5%。已排除代码死循环和 GC 问题。请给出你的完整排查步骤,要求:(1)列出使用的诊断命令与预期输出;(2)画出排查决策树;(3)给出可能的根因(至少 3 种)及各自的修正方案。
- 回答:
(1) 诊断命令与预期输出
top -H -p <PID>:预期多个线程 CPU 高,但线程状态可能为WAITING(socketRead) 或BLOCKED。arthas thread -n 5:预期栈顶阻塞在RestTemplate.execute或HikariPool.getConnection()。jstack <PID> | grep -E "WAITING|BLOCKED" -A 10 | head -50:预期大量线程在java.net.SocketInputStream.socketRead0或com.zaxxer.hikari.pool.HikariPool.getConnection。curl localhost:8080/actuator/metrics/hikaricp.connections.pending:预期返回数值 >0,表示等待获取连接的线程。SkyWalking:查看最慢 Span,若为数据库调用,SQL 执行时间 >2s;若为 RPC 调用,下游服务 P99 >2s。redis-cli info stats | grep evicted_keys:若值较大,说明缓存有淘汰。
(2) 排查决策树
flowchart TD
Start[CPU 90%,P99 3s] --> A[top -H + jstack 确认线程状态]
A --> B{线程状态分布}
B -- 大量 WAITING (socketRead) --> C[SkyWalking 追踪]
C --> D{慢 Span 类型}
D -- RPC 调用 --> E[下游服务 CPU/RT 检查]
E -- 下游慢 --> F[问题在下游,协助排查或熔断]
E -- 下游正常 --> G[检查 HTTP 连接池是否耗尽]
D -- 数据库调用 --> H[SHOW FULL PROCESSLIST]
H --> I[EXPLAIN 慢 SQL]
I --> J[慢 SQL 原因:缺索引/锁等待/全表扫描]
B -- 大量 BLOCKED --> K[检查锁竞争或死锁]
B -- 少量线程 RUNNABLE 但总 CPU 高 --> L[上下文切换过多,线程数过多]
(3) 可能根因与修正
-
根因 1:线程池未隔离,支付慢调用占满共享线程池。修正:为支付调用建立独立线程池(如
paymentExecutor),并配置熔断超时快速失败。 -
根因 2:Redis 缓存未设置 TTL 导致内存满 eviction,大量请求击穿 DB。修正:为缓存配置 300s TTL 并增加随机偏移,热点 Key 加互斥锁(Redisson RLock)。
-
根因 3:数据库连接池
maxActive过大导致数据库端连接数耗尽,争抢激烈。修正:根据节点数合理计算maxActive,设置connection-timeout=3000,并确保总连接数小于数据库max_connections。 -
根因 4:下游 HTTP 调用未设置超时,线程无限等待。修正:RestTemplate 设置
connectTimeout=1s, readTimeout=2s,并配合熔断器在半开状态快速探测。 -
多角度追问:若同时在 Grafana 看到网络流量也飙升,是否可能是 DDoS?若 Redis CPU 也高,怎么办?
9.10 (故障排查题 2)某系统在流量高峰时频繁出现 Pod 重启,kubectl describe pod 显示 Liveness probe failed: context deadline exceeded。已知应用 GC 最大停顿时间 4s。请分析可能原因并给出修正方案,同时说明如何防止此类问题再次发生。
-
一句话回答:livenessProbe 超时时间小于 GC 停顿时间,导致正常 GC 期间探针无响应被 K8s 误杀;需增大
timeoutSeconds并优化 GC。 -
详细解释
根本原因:应用的 livenessProbe 配置timeoutSeconds: 3,而 G1 或 CMS 的 Full GC 可能引发 4s 的 Stop-The-World 停顿。在此期间所有应用线程被挂起,包括健康检查的 HTTP 处理线程,探针客户端在 3s 后超时,kubelet 认为容器不再存活,执行重启。
修正方案:- 调整探针配置:
timeoutSeconds: 10(≥ 最大停顿时间×2),failureThreshold: 3,periodSeconds: 10。 - 优化 GC:切换到低停顿收集器(如 Shenandoah 或 ZGC,JDK 11+),或调整 G1 参数
-XX:MaxGCPauseMillis=200。 - 添加启动探针(startupProbe)给应用充分的启动时间,避免初始化阶段的重启。
预防措施:
- 通过 JMX 或
jstat监控 GC 停顿时间,设置 Prometheus 告警:jvm_gc_pause_seconds_max > 3。 - 在混沌工程中注入 GC 停顿(使用 ChaosBlade 的
jvm delay或full-gc)验证探针配置。 - 使用 Spring Boot 的
management.endpoint.health.probes.enabled=true并确保/actuator/health/liveness只包含轻量检查。
- 调整探针配置:
-
多角度追问
- 如果 livenessProbe 和 readinessProbe 配置不当同时失败会怎样?
- 有没有可能因为线程池满导致健康检查无法响应,而非 GC?如何区分?
- 如何设计一个自适应的探针,根据当前负载动态调整超时?
- 在排查时,如何快速确认是 GC 导致还是死锁导致?
-
加分回答
可以在应用内自定义健康检查逻辑,通过独立的“健康检查线程池”处理/actuator/health请求,与业务线程池隔离。对于 GC 停顿,可使用-Xlog:gc*导出 GC 日志,用GCViewer分析停顿分布。在 Kubernetes 1.24+ 中,可使用GRPC探针代替 HTTP,减少探针开销。
9.11 (系统设计题)设计一个高并发秒杀系统的降级熔断与隔离方案。要求画出架构图与时序图,并说明在突增流量下如何保证核心下单链路可用。
- 回答:
架构图
flowchart TD
CDN[CDN 静态资源]
LB[负载均衡]
GW[Spring Cloud Gateway<br>全局入口限流 + 路由]
Order[订单服务]
Pay[支付服务]
Inv[库存服务]
Redis[(Redis 集群<br>热点缓存 + 库存预扣)]
DB[(MySQL 分库分表)]
MQ[RocketMQ<br>异步落单]
Sentinel[Sentinel 控制台]
Nacos[Nacos 配置中心]
Resilience4j[Resilience4j]
CDN --> LB
LB --> GW
GW -- 限流规则 --> Sentinel
GW --> Order
Order -- @Async paymentExecutor --> Pay
Order -- @Async inventoryExecutor --> Inv
Order -- 缓存读写 --> Redis
Order -- 降级开关 --> Nacos
Order -- 熔断 --> Resilience4j
Pay --> Bank[银行网关]
Inv --> Redis
Order --> MQ --> DB
设计要点
- 网关层:Spring Cloud Gateway 集成 Sentinel 网关流控,设定全局 QPS(如 10万),超过阈值返回静态排队页面。
- 服务隔离:订单服务中,支付与库存更新分别使用独立线程池
paymentExecutor(core 20, max 50)和inventoryExecutor(core 10, max 20),防止支付慢调用拖垮库存。 - 熔断降级:使用 Resilience4j 对支付服务调用熔断,错误率 50% 打开熔断器,半开 10 次探测。同时 Nacos 动态配置降级开关,当库存服务响应变慢时,关闭推荐等非核心接口。
- 缓存设计:商品信息、库存热点数据缓存到 Redis,设置 TTL 300s,热点商品使用 Redisson 互斥锁防击穿。
- 异步削峰:下单后通过 MQ 异步落库,应用层快速返回用户“订单处理中”。
时序图(正常流程与降级流程)
sequenceDiagram
participant U as 用户
participant GW as Gateway
participant O as 订单服务
participant P as 支付服务(独立线程池)
participant I as 库存服务(独立线程池)
participant Redis as Redis
participant MQ as RocketMQ
U->>GW: POST /seckill/order
GW->>GW: Sentinel 流控检查
alt 超过限流阈值
GW-->>U: 429 "活动太火爆,请稍后"
else 通过
GW->>O: 转发创建订单
O->>Redis: 检查库存热点缓存
alt 库存不足
O-->>U: "已售罄"
else 库存充足
O-->>P: 异步发起支付(paymentExecutor)
O-->>I: 异步扣减库存(inventoryExecutor)
O-->>U: 返回“排队中”
par 支付处理
P->>Bank: 调用银行
Bank-->>P: 结果
P-->>O: 支付回调
and 库存处理
I->>Redis: 扣减 Redis 库存
I-->>O: 扣减结果
end
O->>MQ: 发送落单消息
MQ->>DB: 异步写入订单
end
end
降级流程:当支付熔断器打开时,订单服务不再发送支付请求,直接返回“支付稍后重试”页,用户可稍后在订单列表手动支付。当库存服务不可用时,通过 Nacos 下发的降级开关,OrderController 返回固定“活动已结束”的静态页。
突增流量下的保证:
- 入口网关限流保证总流量不超过后端处理能力。
- 线程池隔离确保核心流程(库存扣减)不受非核心(支付)影响。
- 熔断快速失败,避免线程堆积。
- 缓存与异步化减少同步 IO 等待,提升吞吐。
- 降级预案确保在最极端情况下提供有损服务而非完全崩溃。
9.12 如何设计一个全链路超时传递链?以订单 → 支付 → 银行网关为例,给出具体配置和公式。
-
一句话回答:遵循
上游超时 ≥ Σ 下游超时 + 余量原则,在每个服务客户端显式设置 connectTimeout 和 readTimeout,形成链式递减。 -
详细解释
公式:Timeout_upstream ≥ Timeout_downstream_a + Timeout_downstream_b + ... + Δ
其中 Δ 为序列化、网络抖动、GC 停顿等预留,一般取 20%-50%。
示例:- 银行网关:最下游,处理耗时通常 500ms,设
readTimeout = 2s。 - 支付服务调用银行网关:使用 RestTemplate,
connectTimeout=1s, readTimeout=2s,合计 3s。 - 支付服务自身内部逻辑耗时 200ms,故对外暴露的接口超时设为 4s。
- 订单服务调用支付服务(Feign):
connectTimeout=1s, readTimeout=4s,合计 5s。 - 订单服务超时 = 5s > (支付超时 4s) + 余量,合理。
若使用Resilience4j TimeLimiter,还可动态调整,但基础链保持。配置文件示例:
# 订单服务调用支付 feign: client: config: payment-service: connectTimeout: 1000 readTimeout: 4000// 支付服务调用银行网关 @Bean public RestTemplate bankRestTemplate() { return new RestTemplateBuilder() .setConnectTimeout(Duration.ofSeconds(1)) .setReadTimeout(Duration.ofSeconds(2)) .build(); } - 银行网关:最下游,处理耗时通常 500ms,设
-
多角度追问
- 如果下游超时动态变化,如何自适应调整上游超时?
- 如何处理重试导致的超时放大?
- 全链路超时与分布式追踪的 Span 超时有何关联?
- 为什么建议设置连接超时小于读取超时?
-
加分回答
借助 SkyWalking 采集真实 RT 分布,使用脚本动态修正超时配置,或通过 Resilience4j 的TimeLimiter配合动态配置实现。重试必须考虑总超时预算,可以为重试设置独立的“总超时”,防止雪上加霜。在 Netflix 的 Hystrix 中就有“超时传播”机制,可参考其思路设计中间件。
9.13 解释 Sentinel 的流控效果与使用场景,如何避免阈值配置错误?
-
一句话回答:流控效果包括快速失败、Warm Up、排队等待;避免配置错误须通过全链路压测确定单机容量,阈值设为容量拐点的 80%,并动态调整。
-
详细解释
快速失败:当 QPS 超过阈值,直接抛出FlowException,适用于对延迟敏感的关键接口。
Warm Up:通过warmUpPeriodSec和coldFactor让系统在冷启动时阈值逐步上升,防止瞬间流量打垮刚启动的服务,适合秒杀刚开始时的预热。
排队等待:超过阈值的请求排队,设置超时时间(maxQueueingTimeMs),当队列满或超时则失败,适合需要流量整形、可容忍一定延迟的场景。
避免配置错误:- 进行全链路压测(第4篇),找到 CPU/RT 突增的 QPS 拐点(例如 10000 QPS)。
- 设置
count = 拐点 × 0.8 = 8000,留出安全余量。 - 配合集群流控模式(
ClusterFlowRule),避免单机不均匀导致的过早拒绝。 - 配置告警规则,当
sentinel_block_qps在未达阈值时就出现业务异常(如 RT 突增),说明阈值可能过高,应立即人工介入。
-
多角度追问
- 集群流控的 Token Server 高可用如何保障?
- 如何实现按来源应用(如来自 IOS 和 Android)的精细化流控?
- Sentinel 的滑动窗口算法是如何实现的?
- 如何避免配置中心故障导致流控规则失效?
-
加分回答
可以通过 Nacos 动态刷新流控规则,并实现规则的灰度发布。Sentinel 还支持热点参数流控,对于秒杀中的商品 ID 做单独限流。在实现层面,Sentinel 使用 LeapArray 滑动窗口统计,了解其原理有助于解释“临界突发”问题。
9.14 在分库分表场景下,如何选择分片键以避免热点?请举出反例并给出优化方案。
-
一句话回答:选择高基数、查询频率均匀、大部分核心查询能携带的字段作为分片键(如
user_id),避免低基数或业务热点字段(如status);对于不可避免的热点,可采用二级映射或基因法打散。 -
详细解释
反例:选择order_status作为分片键。订单状态仅有待支付、已支付、已发货等几个值,数据分布极度不均。大促时大量订单处于“待支付”状态,全部路由到同一个分片,该分片 CPU 100%,而其他分片空闲,整体集群吞吐暴跌。
优化方案:- 改用
user_id取模分片,保证数据均匀。user_id的基数非常高,访问也会随用户自然分布。 - 若仍需要按状态查询,可建立异构索引,例如通过 Canal 同步数据到 ES,按状态查询走 ES。
- 对于确实有超级热点用户(如大商户),可添加基因法:在
user_id后附加随机数再取模,但会影响查询,需要配合映射表。
配置示例(ShardingSphere):
sharding: tables: t_order: actualDataNodes: ds$->{0..3}.t_order_$->{0..15} tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: order-inline - 改用
-
多角度追问
- 如果既要按
user_id查询又要按order_id查询,如何设计分片? - 基因法具体如何实现?会引入哪些限制?
- 分片键选择后如何进行在线扩容和数据迁移?
- 如何通过监控发现分片热点?
- 如果既要按
-
加分回答
可引入“分区表 + 分片”的二级路由策略,或者使用 TiDB 等 NewSQL 数据库彻底解决分片问题。在监控上,使用PolarDB或ShardingSphere-Proxy暴露各分片的 QPS 和延迟,通过 Prometheus 采集。设计上可遵循“一个分片键原则”,尽量避免多维度查询,将不同维度的查询引导到不同的异构存储。
9.15 如何构建一套生产故障排查的自动化体系?结合本文的工具和决策树,描述流程。
-
一句话回答:将决策树编码为诊断脚本/Operator,告警触发后自动执行
top -H + arthas + jstack + jmap等命令收集证据,通过规则引擎匹配反模式并生成诊断报告,辅助甚至代替人工初步排查。 -
详细解释
- 触发:Prometheus 告警通过 Alertmanager 发送到自动化诊断服务(可以是 Kubernetes Job 或 Serverless 函数)。
- 收集:诊断服务通过 SSH 或 K8s API exec 进入目标 Pod,执行标准诊断脚本(参见第五节脚本),收集:
- CPU:
top -H,arthas thread -n 5,jstack - 内存:
jmap -histo,jcmd GC.heap_dump - GC:
jstat -gcutil - 网络:
ss -antp - 应用指标:
/actuator/metrics
- CPU:
- 分析:将收集的文本输出传给分析引擎。引擎使用正则表达式和关键词匹配(如 “GC task” → 内存问题,“SocketRead” → 下游超时)以及数值阈值(FGC>5 → 内存泄漏)。并可结合 LLM(大语言模型)解读
jstack和慢 SQL。 - 报告:生成包含可能根因(匹配的反模式)、修复建议的 Markdown 报告,并发送到值班群或创建工单。
- 人工确认与修复:值班 SRE 审核报告,确认后执行修正。
- 闭环:修复动作通过配置管理平台自动记录,事后复盘自动纳入知识库。
-
多角度追问
- 如何保证诊断脚本本身不会影响生产环境(如
jmap导致 STW)? - 如果目标 Pod 已经频繁重启,如何收集信息?
- 如何让自动化诊断适应不同服务(不同框架、语言)?
- 有没有开源的类似方案可供参考?
- 如何保证诊断脚本本身不会影响生产环境(如
-
加分回答
可借鉴阿里开源的ChaosBlade和Arthas的脚本能力,封装成 Kubernetes Operator。对于内存 dump,可使用-F强制 dump 但慎用。对于重启场景,可利用日志收集系统和持久化卷保存堆转储。Google SRE 的“事后剖析无责备”文化与此体系契合,自动化诊断的目的不是替代人,而是加速信息收集,将 MTTR 从小时级降至分钟级。
Demo 代码与延伸阅读
反模式错误与修正配置对比(节选)
限流
# 错误:count: 50000
# 修正:count: 8000 # 基于压测拐点 × 0.8
熔断
# 错误:failureRateThreshold: 90, waitDurationInOpenState: 2000
# 修正:failureRateThreshold: 50, waitDurationInOpenState: 30000
缓存
# 错误:spring.cache.redis.time-to-live: 0
# 修正:spring.cache.redis.time-to-live: 300
排查决策树速查表
| 故障现象 | 可能原因 | 诊断命令 | 确认指标 | 修正方案 |
|---|---|---|---|---|
| CPU 飙升,线程 RUNNABLE | 死循环/密集计算 | top -H + jstack | 栈顶为业务方法无 IO | 修复逻辑或加缓存 |
| CPU 飙升,线程 WAITING (socketRead) | 下游无超时 | netstat + jstack | 大量 SocketRead 线程 | 设置 readTimeout |
| RT 突增,SkyWalking 显示 DB 慢 | 缺少索引或连接池满 | SHOW FULL PROCESSLIST, EXPLAIN | type=ALL / pending连接 | 添加索引或扩容连接池 |
| 错误率升高,Sentinel blockQps 高 | 限流阈值过低 | Sentinel Dashboard | blockQps > limit | 调整限流阈值或扩容 |
| Pod 重启频繁 | 探针超时 | kubectl describe pod | timeout 错误 | 增大 timeoutSeconds |
诊断命令链脚本
#!/bin/bash
PID=$1
echo "=== CPU Top Threads ==="
top -H -p $PID -b -n 1 | head -10
echo "=== Arthas CPU Top ==="
arthas $PID -c "thread -n 5" --no-hup 2>/dev/null
echo "=== Memory Histogram ==="
jmap -histo $PID 2>/dev/null | head -20
echo "=== GC Stats ==="
jstat -gcutil $PID 1000 2
echo "=== Network Connections ==="
ss -antp | grep $PID | wc -l
延伸阅读
- 《Systems Performance: Enterprise and the Cloud》第1-6章(方法论与工具)
- 《Java Performance: The Definitive Guide》第4-5章(JVM 监控与调优)
- Arthas 官方文档(
thread、dashboard、vmtool命令) - 《Site Reliability Engineering》第12-14章(故障排查)
- MAT 官方文档
全文总结:掌握六步排查法,熟知 15+ 反模式,活用三大决策树与诊断命令链,你就能在下一个午夜告警中,从容应对,将 MTTR 从小时级压缩到分钟级。这正是整个“高并发与稳定性工程”系列知识向实战能力的终极转化。