第八部分:系统稳定性与故障排查(第 76~80 题)
进入「第三阶段架构师能力」。聚焦服务宕机、连接池耗尽、Redis 故障、MQ 堆积、线程池耗尽等典型线上故障的应急与根治。每题需给出:止血→定位→根因→根治→预案。
第 76 题:服务大面积宕机应急处理 宕机
- 1. 第一动作是什么?2. 怎么快速止血?3. 怎么定位根因?4. 发布即故障?5. 复盘怎么写?
第一步:分析。故障应急=「止血优先于定位」。先恢复业务,再查根因,避免边查边崩。
第二步:挑战。止血、定位、发布回滚、复盘。
第三步:架构。①止血:立即回滚上一稳定版本(K8s rollout undo)或摘流量;②保护:限流/降级防雪崩;③定位:看监控(OOM/CPU/内存曲线)+ 日志 + dump;④根因:本次为内存泄漏/参数错;⑤复盘:时间线 + 根因 + 改进项(加灰度/门禁)。
第四步:选型。K8s 回滚 + Prometheus(监控)+ 日志/链路 + 混沌演练。
第五步:一致性。回滚保持数据兼容(见灰度兼容式迁移)。
第六步:高可用。多副本+健康检查;回滚快;限流保部分可用。
第七步:优化。灰度发布 + 压测门禁防此类故障。
止血第一:业务停摆时,快速回滚(秒级)比现场排查更高效,因新版本已证明坏。保护手段:限流防健康实例被打垮(雪崩),降级非核心保主流程。根因:本次 OOM 多为新代码泄漏或 -Xmx 误改;dump 留现场。复盘价值:把故障变改进(灰度、门禁、监控缺口)。
// K8s 秒级回滚 kubectl rollout undo deploy/order-svc // 止血参数: 限流(网关) + 降级开关(Nacos) // 自动dump(发布前已配) -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data
追问 1:回滚数据不兼容?兼容式发布(Q35)保证新旧都可读写,回滚安全。
追问 2:回滚也崩?上上个版本 + 摘流量 + 降级;极端切备用集群。
追问 3:无监控怎么定位?补基础监控是前提;临时 arthas/jstack 上机。
追问 4:雪崩扩大?限流+舱壁(Q32)防健康实例被拖垮。
追问 5:复盘怎么落地?时间线+根因(5why)+ 改进项(责任人/期限)+ 演练验证。
- ❌ 先查根因再止血——边查边崩。正确:止血优先。
- ❌ 不回滚硬修——恢复慢。正确:秒回滚。
- ❌ 不复盘——重复踩坑。正确:复盘改进。
第 77 题:数据库连接池耗尽治理 连接池
HikariPool-1 - Connection is not available, request timed out,DB CPU 仅 30%,但应用拿不到连接,接口全超时。要求:定位并根治连接池耗尽。- 1. 连接池耗尽原因?2. 为什么 DB 不忙却拿不到?3. 慢 SQL 与池?4. 泄漏怎么查?5. 容量模型?
第一步:分析。拿不到连接=连接被占满未还。根因:慢 SQL 占连接久、连接泄漏(未关)、池太小、事务长。
第二步:挑战。占满、泄漏、容量。
第三步:架构。①止血:限流/降级减少请求;②定位:查慢 SQL(Q41)、连接泄漏(leakDetection)、活跃连接数;③根治:优化慢 SQL、try-with-resources 关连接、调池大小(按 DB 上限)、缩短事务;④预防:leakDetection + 监控 + 压测。
第四步:选型。HikariCP + 慢 SQL 监控 + 连接泄漏检测。
第五步:一致性。池是资源约束;耗尽致失败需降级。
第六步:高可用。池满快速失败(connectionTimeout)+ 限流。
第七步:优化。缩短事务/SQL;读走从库减主连接。
DB 不忙却拿不到:连接被应用侧「拿着不还不」占满——慢 SQL 执行中占连接、或泄漏未 close。DB 本身空闲。泄漏检测:HikariCP leakDetectionThreshold 报警未还连接堆栈。池大小:≤ DB max_connections/实例数(Q47)。事务长:事务内含远程调用=占连接久,需缩短。
// 连接泄漏检测 + 快速失败 HikariConfig c = new HikariConfig(); c.setLeakDetectionThreshold(5000); // 5s 未还报警 c.setConnectionTimeout(300); // 拿不到快速失败 // 必须 try-with-resources 关闭 try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 事务内勿含远程调用 }
追问 1:池调大就解决?掩盖,且可能超 DB 上限;先治慢 SQL/泄漏。
追问 2:事务内含 RPC?占连接久→池耗尽;拆:先 RPC 后开事务,或异步。
追问 3:从库连接也满?读多走缓存/本地;从库扩容或加只读副本。
追问 4:泄漏定位?leakDetection 堆栈 + 查未关的 Connection。
追问 5:预防?慢 SQL 门禁 + 泄漏检测 + 连接数监控 + 压测。
- ❌ 一味调大池——超 DB 上限。正确:治根因。
- ❌ 不查泄漏——渐耗尽。正确:leakDetection。
- ❌ 事务含 RPC——占连接久。正确:缩短事务。
第 78 题:Redis 故障与降级策略 Redis
- 1. 强依赖缓存怎么破?2. 切换期怎么降级?3. 哨兵切换时间?4. 本地缓存兜底?5. 预热恢复?
第一步:分析。缓存应是加速而非强依赖。故障时要降级到 DB/本地+限流,不能全挂。
第二步:挑战。强依赖、切换窗口、兜底、恢复。
第三步:架构。①解耦:缓存 miss/异常→走 DB(限流保护),非抛错;②本地兜底:Caffeine 存热点,Redis 挂仍可服务短窗;③高可用:哨兵/Cluster 多副本,切换 <10s;④限流:故障期降 QPS 防 DB 被打;⑤恢复:Redis 起后预热再放量。
第四步:选型。Redis Sentinel/Cluster + 本地缓存 + 降级开关 + 限流。
第五步:一致性。降级读 DB 源数据,保证正确;短暂旧值可接受。
第六步:高可用。多副本自动切换;降级保核心可用。
第七步:优化。故障注入演练验证降级。
强依赖之祸:把缓存当数据源(无缓存就错)是反模式。应缓存 miss 回源 DB。降级读 DB:Redis 异常时不抛错而走 DB,但需限流(否则 DB 被平时缓存挡的流量打垮)。本地兜底:热数据在本地,Redis 挂短窗仍可服务。切换窗口:哨兵默认秒级,配置 min-replicas 防脑裂。
// 缓存异常降级到DB(不抛错) public Item get(Long id){ try { Item v = redis.get(id); if (v!=null) return v; } catch (RedisException e) { metrics.incr("redis.down"); } return db.load(id); // 降级读源(限流保护) } // 本地缓存兜底(热数据) Item v = local.get(id, k -> db.load(k));
追问 1:降级打垮 DB?故障期限流(如降到日常 10%)+ 本地缓存扛热数据,DB 只承受限流量。
追问 2:哨兵切换慢?调 down-after-milliseconds + 并行 ping;多副本保切换 <10s。
追问 3:脑裂?min-replicas-to-write + min-replicas-max-lag 防主脑裂写。
追问 4:恢复预热?Redis 起后先预热热点再放量,避免开门击穿。
追问 5:演练?故障注入(杀主)验证降级+切换+恢复全流程。
- ❌ 缓存当数据源——挂即崩。正确:miss 回源。
- ❌ 降级不打限流——DB 崩。正确:限流保护。
- ❌ 无本地兜底——全靠 Redis。正确:本地热数据。
第 79 题:MQ 堆积应急与消费恢复 堆积
- 1. 积压怎么应急?2. 恢复后怎么追?3. 下游又挂?4. 丢消息风险?5. 预防?
第一步:分析。堆积应急=「先保不丢 + 快速追平 + 防下游再崩」。
第二步:挑战。追平速度、下游保护、不丢。
第三步:架构。①不丢:积压消息在 Broker,不丢;②追平:增分区+扩消费者(Q65)、批量消费、临时多消费组并行;③下游保护:消费限流/批量写,避免再打垮;④降级:非核心消息跳过/转冷存;⑤预防:lag 告警 + 消费能力预留。
第四步:选型。Kafka 增分区+扩容 + 限流 + 降级。
第五步:一致性。积压消息顺序保留;恢复后正常消费。
第六步:高可用。下游限流防再崩;转存防 broker 满。
第七步:优化。批量+并行;根因修复(下游高可用)。
不丢前提:消息在 Broker 保留(retention 足够),恢复即可消费,不会因堆积丢。追平关键:并行度受分区数限制,先增分区再扩实例;批量消费提吞吐。下游保护:消费快了但下游弱会再崩,需限流/合并写。预防:消费能力按峰值 2 倍预留 + lag 告警。
// 恢复后批量消费提吞吐 @KafkaListener(topics="inv", concurrency=64) public void on(List<ConsumerRecord> batch){ downstream.batchWrite(batch); // 限流保护下游 consumer.commitSync(); } // 监控 lag: 超阈值告警 + 自动扩容消费组
追问 1:分区已最大?无法再并行;改批量/优化消费逻辑,或临时多消费组分治 topic。
追问 2:下游再挂?消费限流 + 重试 + 死信;必要时暂停消费等下游恢复。
追问 3:积压过期丢?retention 设长(如 7d);过期需 DB 扫差补。
追问 4:非核心消息?降级转冷存,优先保核心追平。
追问 5:预防?消费能力预留 + lag 告警 + 下游高可用 + 压测。
- ❌ 猛冲打垮下游——二次故障。正确:限流。
- ❌ 只扩消费者不增分区——无效。正确:先增分区。
- ❌ 不预防——反复堆积。正确:告警+预留。
第 80 题:线程池耗尽与拒绝策略 线程池
RejectedExecutionException,任务丢弃致订单状态不更新。要求:设计线程池容量与拒绝策略。- 1. 线程池参数怎么定?2. 拒绝策略选哪个?3. 队列 vs 无队列?4. 监控?5. 任务丢弃怎么办?
第一步:分析。线程池=「core + max + queue + 拒绝」。参数错致拒绝/ OOM / 延迟。
第二步:挑战。容量、拒绝、队列、监控。
第三步:架构。①参数:按任务类型——CPU 密集 core≈核数,IO 密集 core 可大(如 2×核);②队列:用有界队列(防 OOM),配合理大小;③拒绝:核心任务用 CallerRuns(调用方线程执行,自然限流)或自定义(落 MQ 重试),勿直接丢弃;④监控:活跃线程/队列/拒绝数告警;⑤任务:拒绝的任务落 MQ/重试,不丢。
第四步:选型。ThreadPoolExecutor + 有界队列 + CallerRuns/自定义拒绝 + 监控。
第五步:一致性。拒绝的任务必须可恢复(MQ/重试),不丢状态。
第六步:高可用。CallerRuns 限流防雪崩;任务不丢。
第七步:优化。不同任务独立池(舱壁);监控驱动调参。
无队列(queue=0)+ max=core:满即拒,丢弃任务。应设有界队列缓冲 + 合理 max。CallerRuns:线程满时由提交线程(如 Tomcat 线程)自己执行,既不限丢又自然降提交速度(背压)。自定义拒绝:核心任务落 MQ 异步重试,保证不丢。监控:拒绝数突增=容量不足或下游慢,需告警。
// IO 密集型线程池 + 有界队列 + CallerRuns ThreadPoolExecutor pool = new ThreadPoolExecutor( 50, 200, 60, SECONDS, new LinkedBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); // 满则调用方执行(限流) // 核心任务自定义拒绝: 落MQ重试 new RejectedExecutionHandler() { public void rejected(Runnable r, ThreadPoolExecutor e){ mq.send(new RetryTask(r)); // 不丢 } }
追问 1:CallerRuns 拖慢提交方?是背压效果(提交方变慢),防止任务丢失与系统雪崩,可接受。
追问 2:队列太大 OOM?用有界队列,禁无界(Integer.MAX);大小按内存/延迟定。
追问 3:不同任务混池?独立池(舱壁)防互相影响;如订单/通知分池。
追问 4:监控看什么?活跃线程、队列长度、拒绝数、任务耗时 P99。
追问 5:core=max 还是动态?流量稳 core=max;波动大允许 max>core 弹性(短时扩容)。
- ❌ 无界队列——OOM。正确:有界队列。
- ❌ AbortPolicy 丢任务——状态丢。正确:CallerRuns/落MQ。
- ❌ 队列=0+max=core——频繁拒。正确:有界队列缓冲。