背压、雪崩与故障恢复:局部过载怎样不演变成系统崩溃
从非阻塞背压、准入控制、队列上限、重试放大、负载削减和恢复坡度解释级联故障的形成与终止。
Reactive Streams 定义异步流处理中非阻塞背压的标准接口;它不替业务定义跨网络协议、优先级或负载削减语义。
雪崩从资源等待超过截止时间开始
下游变慢后,上游线程、连接和请求进入队列;排队让更多请求超过 deadline;超时触发重试,重试增加到达率;实例健康检查也变慢并被摘除,剩余实例负载更高。扩容若需要启动、注册、预热和连接,会晚于故障增长。级联失败不是一个异常,而是多个正反馈环。
首先限制每层并发和队列,使系统在安全容量外显式拒绝。队列只能吸收短突发,长度应由允许排队时间与服务速率推导。队列里的请求如果剩余 deadline 已不足以完成,应在开始工作前丢弃,避免无效消耗。
背压让消费者控制上游生产
Reactive Streams 订阅者通过 request(n) 声明需求,发布者不得发送超过累计需求的元素;cancel 停止关系。这个协议解决同一异步流中的非阻塞需求协调,不自动跨 HTTP、消息 Broker 或数据库传播。跨边界要把剩余配额映射为窗口、拉取批次、prefetch 或 admission token。
如果上游无法减速,例如公网突发或日志采集,系统只能缓存有限量后丢弃、采样或降级。负载削减按业务优先级,不按随机异常:支付提交与推荐刷新使用不同资源池和拒绝语义。丢弃必须可计数,不能静默。
重试要成为受预算控制的新流量
所有失败请求立即重试会让恢复中的下游再次过载。重试率设置全局和每租户上限,使用指数退避与抖动,服从原 deadline;熔断 OPEN 后不积累无限等待,而是快速失败或明确降级。对结果未知的写操作,先查询或使用幂等键。
重试预算可以定义为正常成功流量的一小部分,错误率越高允许的额外尝试越少。指标分开 original requests 与 retry attempts,计算 amplification;只看 QPS 会误把重试风暴当业务增长。
恢复阶段比宕机阶段更容易二次击穿
服务恢复可达时,积压、客户端重试、缓存回源和健康探测同时涌入。先用少量 probe 验证正确性,再按 10%、25%、50%、100% 增加流量,每一步观察尾延迟、错误率、连接池、数据库和净排空速率。下游预算是坡度上限,不是消费者能跑多快。
净排空速率等于安全消费速率减当前新流量,只有为正才能估计恢复时间。若恢复时间超过业务截止,优先丢弃过期或低价值任务、重建派生数据、转人工,而不是追求队列数字归零。故障演练要在持续入口流量下验证收敛曲线。
用证据边界拆开“不知道”和“做不到”
结果未知必须持久化 operationId、目标实体、预期版本和查询入口。自动重试只有在动作尚未开始或目标以相同 id 幂等时安全。用异常类名直接判断“肯定没执行”会在网络断点后制造重复提交。
状态机必须拥有终态、租约和人工出口
每个非终态都要有下一次唤醒来源:消息重投、租约到期、协调器扫描、对账任务或人工队列。只有状态而没有唤醒,流程会永久卡住;只有重试而没有终态,会永久自旋。
背压链断在任何一层都会转成隐藏队列
Subscriber 只请求 5 条,但应用把每条再异步提交到无界线程池,协议层背压已经被内部队列绕过;HTTP 客户端限制连接,却让入口线程无限等待连接池,也只是换了排队位置。需要从入口 admission、业务执行器、连接池、Broker prefetch 到数据库并发逐层核对上限与 deadline。
恢复演练记录每分钟 incoming、admitted、shed、retry、completed、oldestAge 与下游利用率,实测净排空曲线是否符合 600 秒预测。若增加消费者后数据库延迟上升导致净速率下降,控制器必须停止扩容或回退,而不是继续追求实例数。
用两个 Java 状态模型固定分布式不变量
下面的 Java 17 模型不模拟真实网络或共识实现,而是把本篇关键版本、租约、状态和容量关系变成确定输出。随后用真实数据库、Broker、锁服务或多进程环境注入延迟、重复、分区与崩溃。
javac --release 17 -Xlint:all -Werror examples/backend-development/distributed-systems/backpressure-recovery/DemandBackpressureDemo.java examples/backend-development/distributed-systems/backpressure-recovery/DistributedRecoveryDemo.java
java -cp examples/backend-development/distributed-systems/backpressure-recovery DemandBackpressureDemo
java -cp examples/backend-development/distributed-systems/backpressure-recovery DistributedRecoveryDemorequested=5 produced=5 consumed=5 extraProduced=0 protocolViolation=false
incomingPerSecond=900 safePerSecond=500 shed=400 backlog=120000 netDrainPerSecond=200 recoverySeconds=600 ramp=[10,25,50,100]输出变化时必须解释哪条一致性、epoch、幂等或容量不变量被修改。真实实验还要验证结果未知、旧所有者恢复、状态清理和人工修复路径。
容量、观测与安全要围绕协调成本
同步协调增加往返、日志刷盘、锁持有和 quorum 等待;异步收敛增加积压、版本、去重和对账存储。容量模型至少包含峰值操作率、参与者数、每次尝试、超时窗口、重试放大、状态保留、复制倍数与恢复净速率。平均值无法覆盖分区热点和长尾暂停。
反向实验要打在决定落盘的前后
故障注入必须有范围、自动停止和数据清理。生产演练从只读、单租户、单分片和低比例开始,保留旁路与回滚。完成标准不是组件重新可达,而是业务不变量保持、未知结果已对账、积压按预测下降且旧所有者无法继续写。
演进从缩小协调域开始
小规模优先单库本地事务、模块化单体和单一调度 owner。增长后先按业务冲突域分片、用批量与读模型减少跨域协调,再引入 Outbox、租约或 Saga。不要为了“分布式化”把一个本地不变量拆成跨服务事务。
任何新协调机制都写决策记录:解决什么不变量、网络分区时拒绝什么、权威证据在哪、超时后如何查询、状态保留多久、容量上限、故障演练与移除路径。系统复杂度只有在可验证收益覆盖恢复成本时才值得增加。
数据模型要为并发和恢复保留足够字段
替代方案要比较协调域而不是比较组件列表
团队门禁要阻止不可恢复的捷径
代码审查要求状态转换与 SQL 条件同时出现,集成测试要求在决定落盘前后杀进程,发布检查要求新旧版本互读,运行看板要求展示最老非终态而不只展示总数。分布式正确性不是某次设计评审的结论,而是持续被反例验证的工程属性。
