分布式系统为什么需要背压?
简化版
背压是下游把自身剩余处理能力反馈给上游,让上游减速、排队、拒绝或降级,避免请求无限进入并耗尽线程、连接和内存。它解决的是生产速率长期大于消费速率时的稳定性问题;队列只能吸收短时突发,不能替代背压。
详细版
当到达速率 λ 大于处理速率 μ,积压会持续增长,延迟、超时和重试又会进一步放大流量,最终形成级联故障。背压应贯穿整条链路:下游暴露容量或通过并发窗口、队列水位、消费确认体现压力;上游据此限制并发、降低读取速度、暂停拉取或快速失败。
常见机制包括有界队列、信号量/并发窗口、Reactive Streams 的 demand、消息消费者暂停拉取、TCP 流量控制,以及网关限流。策略要明确队列满后怎么办:阻塞会占用资源,丢弃要选丢新还是丢旧,拒绝需可识别错误码,降级则要保证业务安全。重试必须使用指数退避、抖动和预算,否则会抵消背压。
设计时用排队论和容量数据校准:观察到达率、完成率、在途数、队列长度、排队时间、拒绝率和超时率。背压信号要尽量靠近瓶颈,并能向调用链上游传播;只在最下游排队会让上游资源先被占满。
完整版教学
一、为什么“先收下再慢慢处理”会失效
系统资源有限,排队空间也有限。若入口每秒收到 1500 个请求,而下游稳定只能处理 1000 个,每秒会新增 500 个积压;10 分钟就是 30 万个。每个排队请求若占 4KB 上下文,仅对象内存约 1.2GB,还没计算连接、线程栈和序列化缓冲。
延迟随队列增长后,调用方开始超时重试,入口速率可能从 1500 进一步升高。于是一个容量不足问题演变成连接池耗尽、GC 抖动和全链路雪崩。背压的目标不是让所有请求都成功,而是在过载时守住可恢复的工作区间。
心法:队列解决“暂时来得快”,背压解决“持续处理不过来”;没有容量上限的队列只是延迟故障。
二、用公式理解积压和延迟
最基本的稳定条件是长期平均到达率小于处理率,即 λ < μ。当 λ >= μ 时,不管队列多大,持续足够久都会填满。Little 定律 L = λW 说明系统在途请求数 L、吞吐 λ 和平均停留时间 W 彼此关联。
若稳定吞吐 λ = 800 请求/秒,平均停留 W = 0.25 秒:
平均在途 L = 800 × 0.25 = 200 个请求
若资源只允许 400 个在途,请求停留时间升到 1 秒时:
所需在途约 800 个,系统必然排队或拒绝
公式不是为了精确预测所有长尾,而是帮助设定并发上限和告警。实际还要看 P95/P99 延迟、服务时间分布与共享资源争用。
三、背压信号如何沿链路传播
背压可以是显式的:消费者向生产者声明还需要 N 个元素;也可以是隐式的:信号量拿不到、队列到达高水位、HTTP 返回 429/503。关键是上游收到信号后必须真正减速,而不是立刻换一条路径重试。
客户端 -> 网关 -> 服务A -> 服务B -> 数据库
限流 并发窗 有界队列 连接池
^-------- 压力逐级向上反馈 --------|
若数据库连接池只有 50 个连接,服务 B 却放行 500 个并发,多出的请求只会占着线程等待。把 B 的并发窗口限制在接近可用连接和实际查询能力的位置,能更早拒绝并释放上游资源。
四、不同背压机制怎么选择
同步 RPC 适合并发信号量、请求队列和快速拒绝;流式处理适合基于 demand 或窗口的拉取;消息队列适合暂停分区消费、减小批次或延长处理间隔。TCP 的接收窗口只能保护字节传输缓冲,不理解业务线程池和数据库是否过载,因此不能替代应用层背压。
| 机制 | 适合场景 | 优点 | 主要风险 |
|---|---|---|---|
| 有界队列 | 短突发 | 削峰、实现简单 | 队列过长抬高延迟 |
| 并发窗口 | RPC/数据库访问 | 直接限制在途量 | 上限需随容量校准 |
| 拉取配额 | 流/消息消费 | 消费方掌握节奏 | 处理失败时需正确确认 |
| 429/503 | 跨服务拒绝 | 快速释放资源 | 客户端可能无脑重试 |
| 丢弃/采样 | 遥测、非关键事件 | 保护主流程 | 必须定义可丢语义 |
五、队列满了之后必须有确定策略
有界队列只是把无限风险变成一个决策点。满后阻塞生产者适合同进程可控线程,但可能把阻塞传播到请求线程;拒绝适合在线请求,但应返回明确、可重试的错误;丢弃适合允许损失的遥测数据;降级则适合用缓存或简化计算返回可接受结果。
假设队列容量 5000,净积压速度每秒 500 条,它只能提供 10 秒缓冲。若扩容至少需要 2 分钟,这个队列无法靠扩容自救。应在高水位前限流,并为关键与非关键请求设置不同优先级,防止低价值流量占满全部容量。
丢弃策略也有语义:实时监控可能丢旧数据保留最新值,审计日志通常一条都不能丢,必须转入持久化缓冲或让业务失败。不能只按中间件默认策略处理。
六、重试为什么容易破坏背压
下游过载时立即重试,相当于把一次失败变成多次新流量。若 1000 QPS 中 30% 超时,每次最多重试 2 次,最坏会额外产生 600 QPS,而且这些请求集中在下游最脆弱的时候。多层都重试时放大更严重。
重试要有指数退避和随机抖动,避免客户端同一时刻再次冲击;还要设置全链路重试预算,例如额外流量不超过原请求的 5%。服务端若返回 Retry-After,客户端应尊重;对于非幂等操作则不能盲目重试。
delay_n = min(cap, base × 2^n) + random(0, jitter)
示例:base=100ms,cap=2s,可得到约 100ms、200ms、400ms、800ms……
七、怎样验证背压真的有效
压测应包含持续过载,而不只是瞬时峰值。逐步把入口从 800 提升到 1500 QPS,观察完成率是否稳定在系统容量附近、队列是否受上限约束、拒绝是否在入口出现,以及恢复后积压能否在目标时间内消化。
核心指标包括到达率与完成率之差、在途请求数、队列使用率、排队时间、拒绝率、重试率和资源饱和度。若 CPU 只有 30% 但连接池已满,继续加实例或线程未必有效;背压必须绑定真实瓶颈。
还应验证优先级隔离:在批量任务制造过载时,关键在线请求的 P99 和成功率仍符合目标。否则系统虽然没有崩溃,却没有保护正确的业务。
八、常见误区与追问
- 误区:把队列调得足够大就不会过载。 当到达率长期大于处理率时任何有限队列都会填满,大队列只会增加内存和等待时间。
- 误区:TCP 已经有流量控制,应用无需背压。 TCP 只知道接收缓冲,不知道业务线程、连接池和下游依赖的容量。
- 误区:返回 429 就完成了背压。 客户端必须减速并遵守退避,否则拒绝响应会触发更强重试。
- 误区:背压就是限流。 限流是入口决策,背压更强调下游容量信号向上游传播,两者常配合使用。
- 追问:队列容量怎么定? 根据允许排队时间、到达峰值、处理速率和内存预算计算,再通过持续过载压测校准。
- 追问:消费者能否通过加线程解决积压? 只有瓶颈在可并行计算且下游仍有容量时有效,否则会加剧争用。
- 追问:消息消费如何背压? 控制拉取批次和并发、暂停分区拉取,并确保心跳与消费位点处理不会引发重复风暴。
九、加强记忆
把背压记成“测容量、限在途、定满载、控重试”。先找到真正的处理瓶颈,用并发窗口和有界队列限制在途工作;队列满时按业务价值明确拒绝、降级或丢弃;最后让压力信号逐级向上游传播,并约束重试放大。只要生产长期快于消费,就必须减少进入量,而不能靠更大的等待区逃避这个事实。