面试官追问:分布式系统最难的到底是共识还是故障处理?你经历过的最大分布式事故
提出问题
分布式系统面试到了 P7/P8 级别,面试官会问一个很"虚"但很要命的问题:你觉得分布式系统最难的到底是什么?
大多数人第一反应是"共识算法,Paxos/Raft 太难了"。但稍微深入想一想:共识算法是数学上可证明的,只要工程实现正确,它的行为是确定的。而故障处理是不可穷举的——网络分区、时钟漂移、磁盘慢 IO、内存 OOM、GC 停顿、内核 bug、硬件故障,这些故障的组合才是真正的噩梦。
共识算法解决的是"在故障下如何达成一致",故障处理解决的是"在故障下系统如何不崩溃"。后者更难,更考验工程师的实战能力。
如果你现在做 Agent 工程,这个结论更加成立——Agent 调用链比传统微服务长得多:LLM → 工具调用 → 外部 API → 上下文持久化,每一环都可能出故障,而且 LLM 的"不确定性"本身就是一种新型故障模式。共识算法至少是确定的,LLM 的输出你连确定性都保证不了。
共识算法到底难在哪——是"难理解"还是"难用对"?
难在理解算法本身,但工程上已有成熟封装
Paxos 的论文(The Part-Time Parliament, Leslie Lamport, 1998)确实难懂——Lamport 用了希腊议会隐喻,读三遍才明白说的是什么。但工程上你根本不需要自己实现 Paxos。业界有成熟的实现:
| 组件 | 底层协议 | 应用场景 | 封装层级 |
|---|---|---|---|
| etcd | Raft | 服务发现、配置中心 | 完整的 HTTP/gRPC API,客户端只需要 put/get |
| Consul | Raft + SWIM | 服务发现 + 健康检查 | Agent 模型,开发者只需注册服务 |
| ZooKeeper | ZAB | 分布式协调、锁、配置 | 树形 ZNode,客户端用 Curator 封装 |
| TiKV | Multi-Paxos (Raft) | 分布式 KV 存储 | 完全屏蔽共识细节,业务层当单机用 |
| Nacos | Distro + Raft (CP 模式) | 注册中心 + 配置 | AP 模式用 Distro,CP 模式用 Raft |
你看,无论哪个组件,应用层都不需要关心日志复制、Leader 选举、任期变更这些细节。Raft 的日志匹配机制(Log Matching Property)——只要两条日志的 index 和 term 相同,那么它们之前的所有日志必然一致——这个特性确保了一致性,但开发者只需要知道"etcd 保证了线性一致性读"就够了。
共识算法的核心思想是多数派(Quorum)——只要超过半数(N/2 + 1)的节点存活,系统就能正常运转。这个定理背后是 FLP 不可能原理(Fischer, Lynch, Paterson, 1985):在异步网络中,即使只有一个节点可能崩溃,也不存在一个确定性算法能在有限时间内达成共识。所以实际系统都用同步假设(心跳超时)来绕过这个限制——Paxos 用 Proposal ID 和 Accept 阶段,Raft 用 Leader 和心跳超时。
结论:共识算法是"难在理解,但容易用对"。
面试官追问:Raft 的日志复制如果遇到网络分区怎么办?
这是 P7 面试的经典追问。Raft 的网络分区处理流程:
时间线:
t=0: 集群 5 节点,Leader 在 A 机房
t=1: A 机房与 B/C 机房网络断开(分区)
t=2: A 机房(Leader + 2 Follower)仍能通信,B 机房(2 Follower)无法联系 Leader
t=3: B 机房的 Follower 选举超时(election timeout,默认 150-300ms 随机),发起新选举
t=4: B 机房选出新 Leader,但只有 2 节点,无法达到多数派(3/5)
t=5: B 机房的新 Leader 无法提交任何日志(commit 需要多数派确认)
t=6: 网络恢复,A 机房原 Leader 发现 term 更高的节点,让步降级为 Follower
t=7: B 机房的未提交日志被回滚,A 机房的已提交日志被同步到 B这个流程的关键点:
- 分区内的少数派不能提交日志,只能接收客户端请求但无法确认,客户端会超时
- 分区恢复后,少数派的未提交日志自动回滚,由多数派的新日志覆盖
- 客户端需要自己处理重试和幂等,Raft 不保证 exactly-once 的执行语义
面试加分项:如果你能说出"Raft 的 PreVote 机制可以防止网络分区恢复后的 disruptive leader",那就证明你读过 etcd 的源码。PreVote 在发起选举前先问其他节点"我能不能当 Leader",如果当前 Leader 还活着(心跳正常),PreVote 请求会被拒绝,避免不必要的 term 递增和 Leader 切换。etcd 3.4+ 默认开启 PreVote。
故障处理为什么比共识算法更难?
难题一:故障的不可枚举性
你能列出的故障类型是有限的,但实际出问题的永远是"没想到的那个"。我见过的一个真实案例:
现象: 某服务每隔 2 小时就 OOM 一次
排查: jmap dump 分析 → 看不出问题,内存使用正常
继续: 加 NMT(Native Memory Tracking) → 发现 Direct Buffer 持续增长
根因: Netty 的 ByteBuf 在某个异常路径下没有 release,导致堆外内存泄漏
结局: 这个 bug 在 Netty 4.1.48 修复,我们用的是 4.1.42再比如:GC 停顿导致心跳超时 → 节点被集群踢出 → 重新加入时触发全量数据同步 → 同步过程中磁盘 IO 飙升 → 影响了其他业务 → 连锁反应导致整个集群不可用。这个链条包含 6 个环节,每个环节都可能被不同的故障触发。
对 Agent 工程师的启示:Agent 系统的故障链更长,也更容易出"没想到的故障"。比如 LLM 调用超时 → 重试触发 API 限流 → 备用模型切换 → 上下文窗口溢出 → Agent 输出乱码 → 下游系统解析失败。每个环节的故障概率虽然不大,但乘起来概率就很高了。
难题二:故障的传播性与雪崩模型
一个模块的故障会像多米诺骨牌一样扩散。用时序图描述一次典型的雪崩:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 缓存服务 │ │ 业务服务 │ │ 数据库 │ │ 上游 │
│ (Redis) │ │ (Java) │ │ (MySQL) │ │ (网关) │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
t=0 │ 正常响应 1ms │ │ │
│─────────────>│ │ │
│ │ │ │
t=1 │ Redis 节点宕机│ │ │
│ (1/3 宕机) │ │ │
│ │ │ │
t=2 │ 请求超时 5s │ │ │
│<── timeout ──│ │ │
│ │ │ │
t=3 │ │ 请求降级到 DB │
│ │───────────────────────────>│
│ │ │ │
t=4 │ │ DB 连接池 200 满,排队 3s │
│ │<── 慢查询 ─────────────────│
│ │ │ │
t=5 │ │ Tomcat 线程池 200 满 │
│ │ (业务线程卡在 DB 等待) │
│ │ │ │
t=6 │ │ │ │ 新请求
│ │ │ │───────>
t=7 │ │ │ │ 连接拒绝
│ │ │ │<─── 503
│ │ │ │
t=8 │ 网关重试,又打过来 3 倍流量 │
│ (重试风暴) │ │ │这个链条里,根因是 1 个 Redis 节点宕机,但最终结果是整条链路 503。这其中任何一环的防御措施(线程池隔离、超时时间、重试策略、限流)只要有一个做对了,后果都不一样。
雪崩的放大系数:一次故障在每层链路可能会被放大。上面的例子中,网关重试 3 倍流量 == 放大系数 3。如果每一层都做重试,放大系数是乘法关系:客户端重试 3 次 × 网关重试 3 次 × 服务重试 3 次 = 27 倍流量。这就是为什么重试必须只在最外层做,内层只做一次调用,失败就抛异常。
难题三:故障的不可复现性与"幽灵故障"
很多分布式故障只在特定时间窗口、特定流量下发生。你事后看日志,一切正常。你想重现,怎么都重现不了。
我经历过一个"幽灵故障":某接口每周三下午 3:15 左右超时 3-5 秒,持续 10 分钟,然后自动恢复。排查了两个月:
- 第一周:怀疑是数据库定时任务,查了所有 cron 脚本,没有
- 第二周:怀疑是 GC,加了 GC 日志,发现 Full GC 频率正常
- 第三周:怀疑是网络抖动,ping 监控一切正常
- 第四周:怀疑是硬件,检查了虚机宿主机负载,正常
- 第五周:把监控粒度从 1 分钟降到 1 秒,发现了——每 15 分钟有一次 2 秒的 IO 抖动,但 1 分钟粒度平均后看不出来
- 第六周:追查 IO 抖动,发现是宿主机上的另一个 VM 在做每周三的定时全量备份,IO 竞争导致
- 第七周:运维调整备份时间到凌晨,问题消失
根因:宿主机 IO 竞争。排查周期:7 周。
关键教训:监控粒度决定了你能看到什么。1 分钟粒度的 P99 监控,只能发现持续 > 1 分钟的故障。你的监控粒度必须比业务要求的 SLA 至少细 10 倍。如果业务要求 99.9%(1 分钟不可用),监控粒度至少 6 秒。
难题四:故障的处理时间窗口极短
SLA 99.99% 意味着全年不可用时间不超过 53 分钟。一次故障从发现到止血,给你 10 分钟算宽裕了。如果你的架构没有自动降级能力,靠人手工操作,10 分钟登录 3 台机器输命令都不够。
用真实数据说话:Google SRE 报告的 2019 年数据,人工故障处理平均 MTTR 是 47 分钟,自动化系统的 MTTR 是 7 分钟。差了 6.7 倍。
Agent 系统的 SLA 更难做:LLM 的 P99 延迟可能是 5-10 秒(取决于模型供应商),比传统 RPC 的 50ms 差了 100 倍。你不可能用传统微服务的超时阈值去套 Agent 系统。Agent 的故障处理需要更长的超时、更灵活的重试策略、以及更复杂的降级逻辑(比如从 GPT-4 降级到 GPT-3.5)。
共识算法 vs 故障处理:正面硬刚
| 维度 | 共识算法 | 故障处理 |
|---|---|---|
| 确定性 | 行为确定,可数学证明 | 不可预测,不可穷举 |
| 学习成本 | 高(论文难读),但一次搞定终身受用 | 高(需要多年实战踩坑) |
| 工具化程度 | 成熟组件直接可用(etcd/consul/zk) | 依赖架构设计 + 监控告警 + 预案演练,没银弹 |
| 事故后果 | 实现错误 → 数据不一致或脑裂 | 任何环节都可能崩,且会传播 |
| 排查难度 | 低(共识层问题通常有明确日志) | 高(从现象到根因可能要跨 5 层) |
| 面试区分度 | 知道就是知道,不知道就是不知道 | 编不出来,有过实战就是有 |
| Agent 时代影响 | 几乎不变(Agent 也用 etcd/zk) | 更复杂(LLM 不确定性 + 更长调用链) |
一句话总结:共识算法是数学问题,可以证明;故障处理是工程问题,只能靠经验、预案和敬畏心。
一个真实案例:某次跨机房分布式事务雪崩复盘
事故背景
某金融业务(日均订单 50 万笔),部署在 3 个机房(A/B/C),核心链路:用户下单 → 扣减库存 → 生成订单 → 调用支付网关。底层依赖:
- MySQL(跨机房主从,A 为主库,B/C 为从库)
- Redis(跨机房缓存,Cluster 模式,3 机房各 3 节点)
- RocketMQ(跨机房消息,主从同步 + 异步刷盘)
- Etcd(3 节点,部署在 A 机房)
时间线还原
14:00 交换机 A-01 配置错误,A→B 路由被改
14:01 A 机房到 B 机房延迟从 2ms 跳到 200ms(约 100 倍)
14:02 Etcd 客户端心跳超时(阈值 50ms),报错 "etcdserver: request timed out"
14:03 订单服务发现 Etcd 不可用 → 触发重试逻辑(每次重试也超时)
14:04 同一个订单请求被重复投递到 RocketMQ(平均 3.5 次/单)
14:05 MQ 积压从 0 飙到 3 万条/分钟,10 分钟后到 50 万
14:06 支付网关消费到重复消息 → 重复扣款(幂等未实现)
14:07 部分订单因为并发写入 MySQL 时没拿到分布式锁 → 状态更新失败(订单状态=已支付,实际=未支付)
14:08 下单成功率从 99.9% 降到 60%
14:09 用户反馈:支付成功但订单显示"未支付",部分用户收到重复扣款短信
14:10 监控告警触发排查过程
Step 1:全局监控(2 分钟)
打开 Grafana 大盘,看四个维度的状态:
| 指标 | 状态 | 结论 |
|---|---|---|
| CPU/内存/磁盘 | 各机房正常 | 不是硬件问题 |
| 数据库连接数 | 正常(200/500) | 数据库没挂 |
| Redis 命中率 | 99% 正常 | 缓存没崩 |
| MQ 积压 | 0 → 50 万 | 问题入口 |
Step 2:定位 MQ 积压原因(3 分钟)
Consumer 消费速度正常(5000/s),但生产速度暴增到 18000/s。正常下单量是 800/s,说明同一个订单被重复发布了。
Step 3:追查订单服务的重试逻辑(5 分钟)
查看订单服务日志,发现 14:02-14:15 之间频繁出现:
ERROR [etcd-client] io.grpc.StatusRuntimeException: UNAVAILABLE:
io exception, channel is in TRANSIENT_FAILURE state
WARN [OrderService] Etcd endpoint unreachable, retrying... (attempt 1/3)
ERROR [OrderService] All retries exhausted, using fallback config但 Etcd 集群本身是健康的——etcdctl endpoint health 返回 3 节点正常。
Step 4:深挖 Etcd 连接超时(5 分钟)
Etcd 客户端报错是连接超时,但服务端正常——说明问题在客户端到服务端的网络路径上。检查跨机房网络延迟:
ping 10.0.1.1 (A 机房 → B 机房 Etcd 节点)
min=198ms, avg=210ms, max=245ms
ping 10.0.2.1 (A 机房 → C 机房 Etcd 节点)
min=2ms, avg=3ms, max=5msA→B 延迟 200ms,A→C 正常。Etcd 的所有节点都在 A 机房,但订单服务部署在 B 机房,B→A 的流量被绕路了。
Step 5:根因定位(网络团队介入,30 分钟)
A 机房某台交换机配置错误,导致 A→B 的流量绕路到了 C 机房再回 B,延迟暴增 100 倍。
Step 6:连锁反应总结
交换机配置错误
└→ A→B 网络延迟 200ms
└→ Etcd 心跳超时 50ms
└→ 服务发现不可用
└→ 订单服务重试(每次重试耗时 5s ≈ 请求超时)
└→ 同一订单被重复投递 MQ(3.5 倍放大)
├→ 支付网关重复扣款(无幂等)
└→ 订单状态并发写入失败(无分布式锁)临时止血
- 15:00 手动将订单服务切换到不依赖 Etcd 的降级模式(本地缓存服务列表,TTL 30s 刷新)
- 15:05 清空 MQ 积压 50 万条,按 idempotent_key 去重后重新消费
- 15:10 对支付网关的重复消息做紧急幂等处理(数据库加唯一索引:order_id + payment_serial_no)
- 15:30 交换机配置回滚,网络延迟恢复
- 16:00 系统恢复,下单成功率回到 99.9%
MTTR(从故障发生到恢复):约 2 小时。 其中真正止血操作只用了 30 分钟,剩下 1.5 小时是排查时间。
长期改进
| 改进项 | 具体措施 | 优先级 | 预期效果 |
|---|---|---|---|
| 网络层监控 | 跨机房延迟监控,>50ms 自动告警 | P0 | 提前发现,不用等业务告警 |
| 服务发现降级 | 本地缓存兜底 → DNS 兜底 → 文件配置兜底,三级降级自动切换 | P0 | Etcd 挂了也不影响服务发现 |
| 重试幂等 | 全局请求 ID(UUID),服务端用 Redis 做幂等表,TTL 24h | P0 | 重试不会导致重复数据 |
| 支付幂等 | 数据库唯一约束(order_id + payment_serial_no) | P0 | 彻底杜绝重复扣款 |
| 分布式锁 | 下单接口加 Redis 分布式锁,key=order_id+user_id,TTL=5s | P1 | 防止并发写入 |
| 故障演练 | 每季度做一次"跨机房网络故障"演练 | P1 | 验证降级链路是否真的走得通 |
| 重试限流 | 重试总次数限制(3 次),重试间隔指数退避(100ms/200ms/400ms)+ 30% 抖动 | P2 | 减少重试风暴 |
事后反思
如果再来一次,最应该提前做的是:重试机制的幂等设计和降级策略的自动化演练。这两个问题在所有分布式系统里都是高发区,但平时没人会主动去测。
为什么? 因为开发的时候,你测的是"一切正常"的路径。降级策略只在测试环境跑过一次,而且测试环境的网络延迟是 1ms,根本测不出问题。等到了生产环境,200ms 延迟一来,Etcd 客户端超时,降级策略才第一次被真正触发——然后发现本地缓存是空的,因为没人测试过"从 Etcd 不可用到本地缓存"这个切换路径。
代码示例:一个带幂等的重试客户端
public class RetryableServiceClient {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 100;
private final List<String> endpoints;
private final AtomicInteger currentIndex = new AtomicInteger(0);
private final LoadingCache<String, List<String>> localEndpointCache;
public RetryableServiceClient(List<String> initialEndpoints) {
this.endpoints = new ArrayList<>(initialEndpoints);
// 本地缓存兜底,30s 刷新一次,Etcd 挂了也不影响
this.localEndpointCache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build(key -> refreshEndpointsFromEtcd());
}
// 核心:幂等 key 由调用方传入,服务端去重
public <T> T executeWithRetry(String idempotentKey, Supplier<T> operation) {
RetryPolicy<T> retryPolicy = RetryPolicy.<T>builder()
.handle(RuntimeException.class)
.withMaxRetries(MAX_RETRIES)
.withDelay(Duration.ofMillis(BASE_DELAY_MS))
.withBackoff(DelayBackoff.EXPONENTIAL)
.withJitter(0.3) // 30% 随机抖动,避免惊群效应
.build();
return Failsafe.with(retryPolicy)
.onRetry((result, failure, ctx) -> {
log.warn("Retry #{}, endpoint={}, idempotentKey={}, failure={}",
ctx.getAttemptCount(),
selectEndpoint(),
idempotentKey,
failure.getMessage());
})
.onFailure(failure -> {
// 所有重试失败后,切到降级方案
log.error("All retries exhausted for idempotentKey={}, falling back to degraded mode",
idempotentKey);
degradedModeManager.activate("ORDER_SERVICE");
})
.get(ctx -> {
String endpoint = selectEndpoint();
return callWithIdempotency(endpoint, idempotentKey, operation);
});
}
private String selectEndpoint() {
// 优先从本地缓存获取,Etcd 挂了也不影响
List<String> cached = localEndpointCache.get("endpoints");
if (cached != null && !cached.isEmpty()) {
return cached.get(currentIndex.getAndIncrement() % cached.size());
}
// 兜底:用启动时传入的初始列表
return endpoints.get(currentIndex.getAndIncrement() % endpoints.size());
}
private <T> T callWithIdempotency(String endpoint, String idempotentKey, Supplier<T> operation) {
// 设置请求超时,避免线程被永久阻塞
// 超时时间 = 5000ms,超过了 Etcd 的心跳超时 50ms,但远小于用户等待极限 10s
return CompletableFuture.supplyAsync(operation)
.orTimeout(5, TimeUnit.SECONDS)
.exceptionally(e -> {
throw new RuntimeException("Service call failed: " + endpoint, e);
})
.join();
}
}服务端幂等处理:
@RestController
public class OrderController {
@PostMapping("/order/create")
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
String idempotentKey = request.getIdempotentKey(); // UUID,调用方生成
// 1. 检查幂等 key 是否已处理(Redis 幂等表,TTL 24h)
// 选 Redis 而不是 MySQL:幂等检查是高频操作,MySQL 查一次 2-5ms,Redis 0.1ms
IdempotentRecord record = idempotentService.get(idempotentKey);
if (record != null) {
log.info("Idempotent hit, key={}, status={}", idempotentKey, record.getStatus());
return ResponseEntity.ok(record.getResponse());
}
// 2. 分布式锁防并发(同一个幂等 key 可能被多个请求同时打到)
// 锁超时 10s,远大于业务执行时间 500ms,防止锁提前释放
boolean locked = distributedLock.tryLock("order:" + idempotentKey, 10, TimeUnit.SECONDS);
if (!locked) {
// 并发冲突,让客户端重试(客户端会走到步骤 1 直接返回)
return ResponseEntity.status(HttpStatus.CONFLICT).build();
}
try {
// 3. 再次检查(双重检查,防并发)
// 场景:A 和 B 同时到步骤 1,都没找到记录,A 拿到锁,B 在步骤 2 等待
// A 执行完业务写入记录,B 拿到锁,如果不检查,B 会重复执行
record = idempotentService.get(idempotentKey);
if (record != null) {
return ResponseEntity.ok(record.getResponse());
}
// 4. 执行业务逻辑
OrderResponse response = orderService.create(request);
// 5. 记录幂等结果(Redis + MySQL 双写,Redis 用于快速检查,MySQL 用于持久化)
idempotentService.save(idempotentKey, response);
return ResponseEntity.ok(response);
} finally {
distributedLock.unlock("order:" + idempotentKey);
}
}
}面试官可能的追问与应对
Q1:你提到的降级策略,降级和熔断有什么区别?
核心区别:
| 维度 | 降级(Degradation) | 熔断(Circuit Breaker) |
|---|---|---|
| 触发条件 | 主动策略(如大促时关闭非核心功能) | 被动响应(错误率 > 50% 自动触发) |
| 恢复方式 | 手动恢复或定时恢复 | 自动半开探测(Half-Open) |
| 粒度 | 功能级别(如"关闭评论功能") | 调用级别(如"订单服务调用超时") |
| 典型实现 | 配置中心 + 开关 | Resilience4j CircuitBreaker / Sentinel |
| 适用场景 | 已知的容量规划 | 未知的故障保护 |
实操建议: 降级和熔断配合使用。熔断是"被动防御的最后一道防线",降级是"主动控制的策略"。比如双十一,先主动降级"历史订单查询"功能(降级),再配合 Sentinel 熔断"库存扣减"接口(熔断)。
Q2:如果网络分区时间超过了你降级策略的缓存刷新时间怎么办?
回答: 本地缓存降级有三种模式,按场景选择:
- 短期缓存(30s TTL):适合网络分区 < 30s 的短暂抖动。Etcd 恢复后,缓存自动更新。
- 长期缓存(5min TTL)+ 后台刷新:适合网络分区 30s-5min 的中等故障。缓存过期前,后台线程尝试刷新;刷新失败,继续用旧数据。
- 静态配置(重启不丢失):适合网络分区 > 5min 的严重故障。把关键服务的地址列表写在配置文件里,随应用发布。Etcd 不可用时,读取静态配置。
实际案例: 我们最终用的是模式 2 + 模式 3 组合。本地缓存 5min 过期,后台每 30s 刷新一次 Etcd 的服务列表。如果 Etcd 连续 3 次刷新失败,告警但不切换——继续用旧缓存数据。只有本地缓存也过期了(5min),才 fallback 到静态配置。
Q3:你案例里提到"交换机配置错误",这个怎么提前发现?
回答: 三层监控:
- 应用层:业务接口的 P99 延迟监控(我们的 P99 是 50ms,翻到 200ms 就能告警)
- 中间件层:Etcd/Redis/MySQL 的客户端连接超时统计(不能只看服务端指标)
- 网络层:跨机房延迟探针(每 10s 发一次 ping,>50ms 告警)
我们当时缺的是第 2 层——Etcd 服务端指标正常,但客户端视角的网络延迟没人监控。后来加了 Etcd 客户端指标暴露(Prometheus 的 etcd_client_grpc_sent_failed_total),就能提前发现连接问题了。
Q4:你现在做 Agent 工程,传统分布式系统的故障处理经验哪些还能用?
这个问题是面试官在考察你的知识迁移能力。 回答思路:
- 幂等设计仍然是最重要的——Agent 的 LLM 调用天然有不确定性,同一个 prompt 可能返回不同结果,幂等 key 要加在 tool call 层面
- 重试策略需要调整——LLM 调用超时通常 30-60s,不能用传统 5s 超时;指数退避的基数要更大(500ms 起步,不是 100ms)
- 降级策略更加重要——模型不可用时,要从 GPT-4 降级到本地小模型,再从本地小模型降级到缓存回复
- 熔断机制需要适配——LLM 的 429 限流和 503 不可用,触发熔断的条件和传统 RPC 不同
- 新的故障模式需要关注——LLM 幻觉、上下文窗口溢出、tool call 格式错误、模型输出截断,这些都是传统分布式系统没有的
总结
分布式系统最难的从来不是共识算法——算法是确定的,你只需要理解它、用好它。真正难的是故障处理,因为故障是不确定的、不可穷举的、会传播的。
要想在分布式系统里"打过仗"而不是"纸上谈兵",你需要:
- 建立故障处理的系统思维:不只看表象,要追根溯源。从"MQ 积压"追到"交换机配置错误",中间经过了 6 层排查。每层都有明确的证据和排除逻辑。
- 做好预案和降级:每一个依赖都必须有降级方案。Etcd 挂了 → 本地缓存兜底;Redis 挂了 → 本地锁兜底;DB 挂了 → 读缓存数据。降级方案要真正演练过,不是写在文档里就算完。
- 重视幂等设计:这是分布式系统最容易踩的坑,也是面试官最看重的实战细节。重试不带幂等 = 制造故障。幂等用 Redis 做检查表 + MySQL 做持久化,双重保障。
- 定期做故障演练:不要在线上出事了才第一次测试降级链路。FMEA(故障模式与影响分析)是每个分布式系统团队应该做的功课。每季度至少一次跨机房演练,把 Etcd 停掉、把 Redis 停掉、把网络切掉,看看系统能不能扛住。
- Agent 时代需要重新审视故障处理:LLM 的不确定性给分布式系统带来了新的故障模式。传统系统的经验(幂等、重试、降级、熔断)仍然有用,但参数和策略需要调整。如果你能从传统分布式系统的故障处理经验,迁移到 Agent 系统的故障处理,面试官会认为你具备了系统性的工程思维。
最后,回到面试官那个问题——"你经历过的最大分布式事故是什么?" 一个好的回答不是讲一个惊险的故事,而是展示你面对故障时的系统性思考能力:发现问题 → 定位根因 → 临时止血 → 长期改进 → 事后反思。这才是 P7/P8 级别的工程师该有的样子。