Skip to content

网络通信与降级方案:断网、慢网下,系统怎么活下去 ​

属于 S8 分布式理论 · 能力强化 · 第五篇(导师清单:网络通信断了的降级方案) 上一篇:gRPC 负载均衡详解 下一篇:限流与熔断

这篇解决什么问题:导师清单里最实战的一条——"网络通信断了"怎么办。网络是分布式系统里最不可靠的组件:会断、会慢、会乱序、会重复。本篇把降级方案讲成一个完整的防御体系:先看清网络故障长什么样(故障模型)→ 第一道防线超时(不让故障扩散)→ 第二道防线重试(自动恢复瞬时故障)→ 第三道防线降级(依赖挂了给兜底)→ 第四道防线隔离(舱壁,不让一个依赖拖垮全局)→ 最后与熔断配合(下一篇专讲)。核心认知:降级不是"出事后救火",而是"设计时就预留的逃生通道"。

一、网络故障模型:先定义"断了"是什么 ​

故障表现特征
延迟升高请求变慢但能通最常见;比"断"更难处理(超时判断难)
丢包/超时请求无响应可能重发后成功(瞬时故障,重试有效)
乱序/重复响应顺序错、请求被重发靠幂等 + 序列号解决
连接挂起(半开)TCP 连着但无响应靠 keepalive 探活(见 gRPC 负载均衡详解 5.4)
网络分区部分节点互相不可达,各自活着最阴险:节点分不清"自己断网了"还是"别人挂了"(见 S5 一致性与故障边界 二)

面试必背一句:"网络故障的可怕之处在于:'挂没挂'本身就不可知。 你只能通过超时和心跳去猜,而猜测必然有误差——所以防御体系必须容忍'猜错'(误判熔断、误判下线),而不是追求'猜准'。"

二、第一道防线:超时(没有超时的调用 = 定时炸弹) ​

2.1 为什么必须设超时 ​

一个慢下游会占满调用方的连接/线程/goroutine:假设下游从 50ms 变 5s,调用方 1000 个并发槽位全被占住 → 新请求进不来 → 调用方自己"被拖死" → 它的调用方再被拖死 → 级联雪崩。超时是切断这条链的第一刀。

2.2 三层超时(背这个结构) ​

层超时作用
连接超时DialTimeout建连不无限等
请求超时PerRPCTimeout单次调用不无限等
总超时(deadline)整个调用链的预算上游定总预算,逐层递减

2.3 全链路超时传播(gRPC/Go 的正确姿势) ​

核心原则:父超时 - 已耗时 = 剩余,不足即放弃。 用 Context 从入口一路传下去,任何一层都能感知"总预算还剩多少":

go
// 入口:用户请求分配 2s 总预算
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

// 第一跳:gRPC 自动把 deadline 传播给下游(grpc-timeout 头)
resp, err := clipClient.Generate(ctx, req) // 下游看到还剩 2s

// 下游处理到 800ms 后调再下游:
//   ctx 剩余 = 2s - 800ms = 1.2s,gRPC 自动透传剩余预算
//   再下游即使没有自己的超时设置,也会在总预算到期时被取消
  • gRPC 的 deadline 传播是协议级的(grpc-timeout 元数据头),Java 的 propagate、Go 的 context 同理;
  • 不要每层都设独立超时然后加起来(每层 500ms × 5 层 = 2.5s 失控);要单一总预算逐层递减;
  • 超时怎么定:看下游的 P99 延迟(不是平均值)——超时 ≈ 下游 P99 × 系数(如 3~5 倍),既覆盖绝大多数慢请求,又不让异常慢请求拖住系统。

三、第二道防线:重试(自动恢复瞬时故障) ​

3.1 重试的前提:只重试幂等请求 ​

重试会把"执行一次"变成"执行多次":下单接口重试 = 重复下单。所以铁律:只对幂等请求重试(查询、幂等键保护的写)。非幂等请求的重试要带幂等键(见 S5 场景题(中) 接口幂等)。

3.2 退避策略:指数退避 + jitter(必背) ​

问题:失败时所有客户端同时重试 → 重试风暴(羊群效应)。

text
① 简单重试:失败后立刻重试 N 次 —— 风暴(全都在同一时刻打)
② 指数退避:第 n 次重试等 base × 2^n(1s、2s、4s……)—— 错开但仍可能同步
③ 指数退避 + jitter:等 (base × 2^n) ± 随机抖动 —— 打散,生产标准
go
// 指数退避 + 抖动(gRPC 默认策略的简化版)
delay := base * time.Duration(1<<attempt) // 1s, 2s, 4s...
delay += time.Duration(rand.Int63n(int64(delay))) // 加 jitter 打散
time.Sleep(delay)

3.3 重试风暴:最典型的事故(背这个数字案例) ​

场景:QPS 10 万,下游 1% 超时,每个请求重试 3 次 → 下游实际承受 ≈ 10 万 × (1 + 3×1%) ≈ 10.3 万 → 算上重试放大甚至 40 万 QPS(如果退避不当全挤在同一时刻)→ 下游被打挂 → 更多人重试 → 雪崩。

防重试风暴五条(背):① 只重试幂等;② 指数退避 + jitter;③ 限总次数(如最多 3 次);④ 熔断优先于重试(Open 了就别再打,见下一篇);⑤ 全链路超时预算(总预算快用完就不重试)。

3.4 重试预算(retry budget,进阶) ​

服务网格(istio)里的成熟做法:重试次数受"总请求的百分比"限制(如重试流量 ≤ 总流量的 20%),从机制上防止重试流量反噬。面试提这一句是加分项。

四、第三道防线:降级(核心,导师清单点题) ​

4.1 降级的定义与分类 ​

降级 = 依赖故障/压力过大时,主动放弃一部分功能或精度,保住核心链路。 和熔断的区别:熔断是"不让打"(被动防护),降级是"换一种方式服务"(主动取舍)——通常熔断触发后,接住请求的就是降级逻辑。

分类维度类型例子
按触发方式静态降级(写死兜底)/ 动态降级(开关控制)开关在配置中心下发,可随时开/关
按降级对象读降级(返回缓存/默认值)/ 写降级(拦截写、进 MQ 缓冲)详情页读缓存、下单改异步
按范围功能降级(关非核心功能)/ 数据降级(用旧数据)推荐位关掉、用昨日数据

4.2 降级手段清单(面试能说出 5 种以上) ​

手段做法适用
返回兜底缓存依赖挂了 → 读本地/Redis 缓存(哪怕是旧数据)读多场景(详情页、Feed)
返回默认值依赖挂了 → 返回空列表/默认配置非关键数据(推荐、榜单)
关闭非核心功能把推荐、热搜、个性化降掉,只留搜索/下单大促、洪峰
异步化/削峰写请求进 MQ,慢慢消费(牺牲实时性)写高峰(秒杀下单)
Mock/桩服务依赖故障时切到桩实现(返回固定结果)联调环境、依赖开发中
拒绝对非核心调用方(限流变体)保核心调用方,拒绝低优先级多租户、多调用方
降级数据源MySQL 挂了切只读从库/本地文件读路径

4.3 降级开关:动态降级的工程实现 ​

降级必须是可开关的,不能只靠代码里写死(改代码发版太慢)。标准做法:

  • 关键点:开关状态放本地缓存(不能每次都查配置中心,否则配置中心挂了全挂——这也是 etcd 详解与工程实践 watch 风暴那节的实践);
  • 降级要有分级:一级降级(关推荐)、二级降级(关个性化)、三级降级(只保留读)——从轻到重,逐级触发;
  • 降级要能自动恢复:依赖恢复后自动切回(健康检查 + 开关联动)。

4.4 降级的故障边界(面试深挖) ​

场景现象处理
兜底缓存也没了无数据可返回返回空 + 打点告警(宁可空,不阻塞)
配置中心挂了降级开关状态无法更新本地缓存最后一份配置继续生效;配置中心恢复后补偿(AP 兜底,见 S5 一致性与故障边界 4.5)
降级误触发好好的功能被降了降级要打详细日志 + 指标(降级次数、降级时长),可观测才能复盘
降级后流量转移核心链路被降级流量打爆降级接口也要限流(见下一篇)

五、第四道防线:隔离(舱壁模式 Bulkhead) ​

问题:一个慢依赖占满所有资源,其他依赖也跟着遭殃。

舱壁思想(来自船舶:船舱分隔,一处进水不沉船):给每个依赖/下游分配独立的资源池(线程池/信号量/连接池),A 依赖吃光自己的池子,不影响 B 依赖。

隔离方式原理例子
线程池隔离每个下游一组线程经典 Hystrix 思路:clip-service 一组线程,auth-service 一组
信号量隔离不占线程,只限制并发数轻量:请求进来先抢信号量,抢不到快速失败
连接池隔离每个下游独立连接池gRPC subchannel 天然按实例隔离连接(见 gRPC 负载均衡详解)
故障域隔离物理/逻辑上分隔(不同机房、不同集群)核心链路与旁路分集群(见高可用机制全景)

面试金句:"线程池隔离用资源换隔离,信号量隔离用快速失败换轻量。 高并发下信号量 + 超时 + 快速失败是性价比最高的组合(不消耗线程,失败立即返回)。"

六、四道防线如何配合(完整链路图) ​

一句话总结:超时定上限 → 重试救瞬时 → 熔断止放大(下一篇)→ 降级给兜底 → 隔离防扩散。五件事是一个整体,单独上任何一个都不够。


串起来 ​

网络降级的本质是把"不可靠的网络"变成"可预期的行为":超时把"无限等待"变成"有限等待",重试把"瞬时失败"变成"自动恢复",降级把"依赖故障"变成"保底服务",隔离把"一个依赖的故障"变成"局部故障"。下一篇讲防雪崩的另一半——限流与熔断:不让流量进来(限流)和不让请求打向下游(熔断)。


面试追问 ​

  • 问:超时时间怎么定? 看下游 P99 延迟 × 系数(3~5 倍),而不是平均值;且必须全链路单一预算逐层递减,不能每层独立超时简单相加。
  • 问:重试会放大故障吗? 会——重试风暴。必须:只重试幂等、指数退避 + jitter、限次数、熔断优先、全链路预算。
  • 问:降级和熔断什么区别? 熔断是"不让打下游"(被动,保护下游),降级是"换一种方式服务"(主动,保护自己);通常熔断触发后由降级逻辑接住请求。
  • 问:降级开关放哪?怎么下发? 配置中心(etcd)下发 + 服务本地缓存 + Watch 更新;分级降级(从轻到重);自动恢复。
  • 问:网络分区时怎么判断"是自己断网还是对方挂了"? 无法确定——只能靠超时 + 多数派/租约等外部仲裁。这就是为什么 CP 系统要多数派(见 Raft 算法详解),注册中心要 AP 兜底(见 S5 一致性与故障边界)。

持续学习,持续构建。