← 大纲 第八部分 · 系统稳定性与故障排查 · 第 76~80 题

第八部分:系统稳定性与故障排查(第 76~80 题)

进入「第三阶段架构师能力」。聚焦服务宕机、连接池耗尽、Redis 故障、MQ 堆积、线程池耗尽等典型线上故障的应急与根治。每题需给出:止血→定位→根因→根治→预案。

本页目录: #76 服务宕机#77 连接池耗尽#78 Redis故障 #79 MQ堆积#80 线程池耗尽

第 76 题:服务大面积宕机应急处理 宕机

1.【真实企业业务场景】
凌晨发布后,订单服务 50 个 Pod 在 10 分钟内陆续 OOM 重启,注册中心实例列表抖动,网关大量 503。要求:给出应急止血与根因定位的标准动作。
2.【面试官问题】
    1. 第一动作是什么?2. 怎么快速止血?3. 怎么定位根因?4. 发布即故障?5. 复盘怎么写?
3.【候选人的标准回答】

第一步:分析。故障应急=「止血优先于定位」。先恢复业务,再查根因,避免边查边崩。

第二步:挑战。止血、定位、发布回滚、复盘。

第三步:架构。止血:立即回滚上一稳定版本(K8s rollout undo)或摘流量;②保护:限流/降级防雪崩;③定位:看监控(OOM/CPU/内存曲线)+ 日志 + dump;④根因:本次为内存泄漏/参数错;⑤复盘:时间线 + 根因 + 改进项(加灰度/门禁)。

第四步:选型。K8s 回滚 + Prometheus(监控)+ 日志/链路 + 混沌演练。

第五步:一致性。回滚保持数据兼容(见灰度兼容式迁移)。

第六步:高可用。多副本+健康检查;回滚快;限流保部分可用。

第七步:优化。灰度发布 + 压测门禁防此类故障。

4.【架构设计】
告警 → 止血(回滚/摘流量) → 保护(限流降级) → 定位(监控/日志/dump) → 根因(内存/参数) → 复盘(时间线+改进)
5.【技术方案深度解析】

止血第一:业务停摆时,快速回滚(秒级)比现场排查更高效,因新版本已证明坏。保护手段:限流防健康实例被打垮(雪崩),降级非核心保主流程。根因:本次 OOM 多为新代码泄漏或 -Xmx 误改;dump 留现场。复盘价值:把故障变改进(灰度、门禁、监控缺口)。

6.【关键技术点】
应急止血回滚限流降级OOM定位灰度发布复盘
7.【Java 实现示例】
// K8s 秒级回滚
kubectl rollout undo deploy/order-svc
// 止血参数: 限流(网关) + 降级开关(Nacos)
// 自动dump(发布前已配)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data
8.【面试官可能继续追问】
9.【常见错误回答】
  • ❌ 先查根因再止血——边查边崩。正确:止血优先。
  • ❌ 不回滚硬修——恢复慢。正确:秒回滚。
  • ❌ 不复盘——重复踩坑。正确:复盘改进。
10.【架构师评分标准】
初级 0~40
无应急概念。
中级 40~60
知回滚,无保护/复盘。
高级 60~80
止血+保护+定位+根因+复盘。
架构师 80~100
再加:①灰度/门禁预防;②兼容式回滚;③雪崩防护;④复盘驱动改进闭环。

第 77 题:数据库连接池耗尽治理 连接池

1.【真实企业业务场景】
大促中订单服务报错 HikariPool-1 - Connection is not available, request timed out,DB CPU 仅 30%,但应用拿不到连接,接口全超时。要求:定位并根治连接池耗尽。
2.【面试官问题】
    1. 连接池耗尽原因?2. 为什么 DB 不忙却拿不到?3. 慢 SQL 与池?4. 泄漏怎么查?5. 容量模型?
3.【候选人的标准回答】

第一步:分析。拿不到连接=连接被占满未还。根因:慢 SQL 占连接久、连接泄漏(未关)、池太小、事务长。

第二步:挑战。占满、泄漏、容量。

第三步:架构。止血:限流/降级减少请求;②定位:查慢 SQL(Q41)、连接泄漏(leakDetection)、活跃连接数;③根治:优化慢 SQL、try-with-resources 关连接、调池大小(按 DB 上限)、缩短事务;④预防:leakDetection + 监控 + 压测。

第四步:选型。HikariCP + 慢 SQL 监控 + 连接泄漏检测。

第五步:一致性。池是资源约束;耗尽致失败需降级。

第六步:高可用。池满快速失败(connectionTimeout)+ 限流。

第七步:优化。缩短事务/SQL;读走从库减主连接。

4.【架构设计】
报错超时 → 查活跃连接/慢SQL/泄漏 → 优化SQL+关连接+调池 止血: 限流降级; 预防: leakDetection+监控
5.【技术方案深度解析】

DB 不忙却拿不到:连接被应用侧「拿着不还不」占满——慢 SQL 执行中占连接、或泄漏未 close。DB 本身空闲。泄漏检测:HikariCP leakDetectionThreshold 报警未还连接堆栈。池大小:≤ DB max_connections/实例数(Q47)。事务长:事务内含远程调用=占连接久,需缩短。

6.【关键技术点】
连接池耗尽HikariCPleakDetection慢SQLconnectionTimeout容量模型
7.【Java 实现示例】
// 连接泄漏检测 + 快速失败
HikariConfig c = new HikariConfig();
c.setLeakDetectionThreshold(5000);  // 5s 未还报警
c.setConnectionTimeout(300);        // 拿不到快速失败
// 必须 try-with-resources 关闭
try (Connection conn = ds.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
  // 事务内勿含远程调用
}
8.【面试官可能继续追问】
9.【常见错误回答】
  • ❌ 一味调大池——超 DB 上限。正确:治根因。
  • ❌ 不查泄漏——渐耗尽。正确:leakDetection。
  • ❌ 事务含 RPC——占连接久。正确:缩短事务。
10.【架构师评分标准】
初级 0~40
不知连接占用。
中级 40~60
知调池,不论泄漏。
高级 60~80
定位(慢SQL/泄漏)+调池+超时+预防。
架构师 80~100
再加:①容量模型(DB 上限约束);②事务优化;③监控体系;④压测验证。

第 78 题:Redis 故障与降级策略 Redis

1.【真实企业业务场景】
Redis 主节点宕机,哨兵切换 8s,期间商品详情接口 100% 超时(强依赖缓存)。要求:设计 Redis 故障时的降级与高可用。
2.【面试官问题】
    1. 强依赖缓存怎么破?2. 切换期怎么降级?3. 哨兵切换时间?4. 本地缓存兜底?5. 预热恢复?
3.【候选人的标准回答】

第一步:分析。缓存应是加速而非强依赖。故障时要降级到 DB/本地+限流,不能全挂。

第二步:挑战。强依赖、切换窗口、兜底、恢复。

第三步:架构。解耦:缓存 miss/异常→走 DB(限流保护),非抛错;②本地兜底:Caffeine 存热点,Redis 挂仍可服务短窗;③高可用:哨兵/Cluster 多副本,切换 <10s;④限流:故障期降 QPS 防 DB 被打;⑤恢复:Redis 起后预热再放量。

第四步:选型。Redis Sentinel/Cluster + 本地缓存 + 降级开关 + 限流。

第五步:一致性。降级读 DB 源数据,保证正确;短暂旧值可接受。

第六步:高可用。多副本自动切换;降级保核心可用。

第七步:优化。故障注入演练验证降级。

4.【架构设计】
Redis挂 → 捕获异常 → 走DB(限流保护) / 本地缓存兜底 高可用: 哨兵多副本(切换<10s) 恢复: 预热 → 放量
5.【技术方案深度解析】

强依赖之祸:把缓存当数据源(无缓存就错)是反模式。应缓存 miss 回源 DB。降级读 DB:Redis 异常时不抛错而走 DB,但需限流(否则 DB 被平时缓存挡的流量打垮)。本地兜底:热数据在本地,Redis 挂短窗仍可服务。切换窗口:哨兵默认秒级,配置 min-replicas 防脑裂。

6.【关键技术点】
缓存降级哨兵本地兜底限流保护故障注入预热
7.【Java 实现示例】
// 缓存异常降级到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));
8.【面试官可能继续追问】
9.【常见错误回答】
  • ❌ 缓存当数据源——挂即崩。正确:miss 回源。
  • ❌ 降级不打限流——DB 崩。正确:限流保护。
  • ❌ 无本地兜底——全靠 Redis。正确:本地热数据。
10.【架构师评分标准】
初级 0~40
强依赖缓存。
中级 40~60
知降级,无限流。
高级 60~80
解耦+降级+本地+限流+高可用。
架构师 80~100
再加:①哨兵/脑裂配置;②故障注入演练;③预热恢复;④降级 SOP。

第 79 题:MQ 堆积应急与消费恢复 堆积

1.【真实企业业务场景】
消费者依赖的下游 DB 宕机 20 分钟,Kafka 积压 8000 万,恢复后消费追不上,下游库存延迟 1 小时。要求:应急追平积压且不二次故障。
2.【面试官问题】
    1. 积压怎么应急?2. 恢复后怎么追?3. 下游又挂?4. 丢消息风险?5. 预防?
3.【候选人的标准回答】

第一步:分析。堆积应急=「先保不丢 + 快速追平 + 防下游再崩」。

第二步:挑战。追平速度、下游保护、不丢。

第三步:架构。不丢:积压消息在 Broker,不丢;②追平:增分区+扩消费者(Q65)、批量消费、临时多消费组并行;③下游保护:消费限流/批量写,避免再打垮;④降级:非核心消息跳过/转冷存;⑤预防:lag 告警 + 消费能力预留。

第四步:选型。Kafka 增分区+扩容 + 限流 + 降级。

第五步:一致性。积压消息顺序保留;恢复后正常消费。

第六步:高可用。下游限流防再崩;转存防 broker 满。

第七步:优化。批量+并行;根因修复(下游高可用)。

4.【架构设计】
积压(不丢) → 增分区+扩消费者+批量 → 下游限流保护 非核心: 降级转冷存; 预防: lag告警+能力预留
5.【技术方案深度解析】

不丢前提:消息在 Broker 保留(retention 足够),恢复即可消费,不会因堆积丢。追平关键:并行度受分区数限制,先增分区再扩实例;批量消费提吞吐。下游保护:消费快了但下游弱会再崩,需限流/合并写。预防:消费能力按峰值 2 倍预留 + lag 告警。

6.【关键技术点】
积压应急增分区消费扩容下游限流降级转存lag告警
7.【Java 实现示例】
// 恢复后批量消费提吞吐
@KafkaListener(topics="inv", concurrency=64)
public void on(List<ConsumerRecord> batch){
  downstream.batchWrite(batch); // 限流保护下游
  consumer.commitSync();
}
// 监控 lag: 超阈值告警 + 自动扩容消费组
8.【面试官可能继续追问】
9.【常见错误回答】
  • ❌ 猛冲打垮下游——二次故障。正确:限流。
  • ❌ 只扩消费者不增分区——无效。正确:先增分区。
  • ❌ 不预防——反复堆积。正确:告警+预留。
10.【架构师评分标准】
初级 0~40
无应急。
中级 40~60
知扩容,不保护下游。
高级 60~80
不丢+追平+下游限流+降级+预防。
架构师 80~100
再加:①retention 与补差;②多消费组分治;③下游高可用;④容量预留与告警。

第 80 题:线程池耗尽与拒绝策略 线程池

1.【真实企业业务场景】
订单服务用固定线程池(core=20, queue=0)处理异步任务,流量涨时频繁 RejectedExecutionException,任务丢弃致订单状态不更新。要求:设计线程池容量与拒绝策略。
2.【面试官问题】
    1. 线程池参数怎么定?2. 拒绝策略选哪个?3. 队列 vs 无队列?4. 监控?5. 任务丢弃怎么办?
3.【候选人的标准回答】

第一步:分析。线程池=「core + max + queue + 拒绝」。参数错致拒绝/ OOM / 延迟。

第二步:挑战。容量、拒绝、队列、监控。

第三步:架构。参数:按任务类型——CPU 密集 core≈核数,IO 密集 core 可大(如 2×核);②队列:用有界队列(防 OOM),配合理大小;③拒绝:核心任务用 CallerRuns(调用方线程执行,自然限流)或自定义(落 MQ 重试),勿直接丢弃;④监控:活跃线程/队列/拒绝数告警;⑤任务:拒绝的任务落 MQ/重试,不丢。

第四步:选型。ThreadPoolExecutor + 有界队列 + CallerRuns/自定义拒绝 + 监控。

第五步:一致性。拒绝的任务必须可恢复(MQ/重试),不丢状态。

第六步:高可用。CallerRuns 限流防雪崩;任务不丢。

第七步:优化。不同任务独立池(舱壁);监控驱动调参。

4.【架构设计】
任务 → 线程池(core/max/有界队列) → 满 → 拒绝策略 核心: CallerRuns(限流) / 落MQ重试(不丢) 监控: 活跃/队列/拒绝数
5.【技术方案深度解析】

无队列(queue=0)+ max=core:满即拒,丢弃任务。应设有界队列缓冲 + 合理 max。CallerRuns:线程满时由提交线程(如 Tomcat 线程)自己执行,既不限丢又自然降提交速度(背压)。自定义拒绝:核心任务落 MQ 异步重试,保证不丢。监控:拒绝数突增=容量不足或下游慢,需告警。

6.【关键技术点】
线程池参数有界队列CallerRuns拒绝策略背压任务不丢
7.【Java 实现示例】
// 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)); // 不丢
  }
}
8.【面试官可能继续追问】
9.【常见错误回答】
  • ❌ 无界队列——OOM。正确:有界队列。
  • ❌ AbortPolicy 丢任务——状态丢。正确:CallerRuns/落MQ。
  • ❌ 队列=0+max=core——频繁拒。正确:有界队列缓冲。
10.【架构师评分标准】
初级 0~40
不会配线程池。
中级 40~60
知参数,用 Abort 丢。
高级 60~80
有界队列+CallerRuns+不丢+监控。
架构师 80~100
再加:①IO/CPU 密集区分;②舱壁分池;③背压原理;④监控与容量。