周末晚上七点,“辣得起”川菜馆进入了一天中最疯狂的时刻。后厨里三灶齐开,油花飞溅,一盘盘水煮鱼、辣子鸡像流水线一样被铲进盘子;传菜口那半米宽的不锈钢台面上,菜碟越摞越高,刚出锅的宫保鸡丁已经凉了,还有两盘差点被挤到地上——而服务员们正手忙脚乱,根本来不及端。如果你是大厨,你会怎么做?继续闷头猛炒?还是抬起头,看看传菜口的积压,顺着服务员们的节奏调整火候? 这个再平常不过的厨房难题,正是分布式系统世界里“背压机制(Backpressure)”要解决的终极命题:当上游生产数据的速度超过下游消费数据的速度,中间的缓冲区就像一个越堆越满的传菜台,迟早溢出、崩溃、丢数据。Reactive 响应式编程给出的答案是——让下游“点菜式”地告诉上游“我一次要几个”,上游按量产出,双方通过一张叫 Subscription 的“点菜单”达成契约。本文将从这家川菜馆出发,拆解背压的基础模型、响应式契约、请求量控制、传播链路、工程策略,以及它和限流、熔断这对“兄弟机制”的微妙区别。

岗位关联

背压是构筑高并发系统“自我保护”意识的最后一块拼图。理解它,你就能在流式架构、消息队列与 AI 推理管线中,让系统在洪峰下优雅降速而不是脆断崩溃——这是架构师从“能用”走向“可靠”的必修课。

目录
  1. 可视化演示
  2. 背压基础概念:传菜台为什么不够用
  3. 响应式契约:点菜单上的君子协定
  4. 请求量控制:一次报五道,而不是两百道
  5. 传播链路:从传菜口到采购间的节奏共振
  6. 策略与实现:传菜台满了,到底怎么办
  7. 实践与对比:限流、熔断与背压的三角关系
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示Reactive Streams的Subscription契约、请求量控制与背压传播链路,理解下游控制上游速率避免缓冲溢出的机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Subscription:请求驱动 request(n) 反向信号 request(n)→emit 按需拉取数据

背压基础概念:传菜台为什么不够用

七点的厨房里,大厨老王的手速是每秒 0.8 盘菜,而服务员小刘最多只能同时端三盘;传菜台总共只有 6 个位子——这就是活生生的“生产大于消费”。放到计算机世界,老王就是上游生产者(Publisher),小刘就是下游消费者(Subscriber),传菜台就是缓冲区(Buffer)。当生产速率超过消费速率,缓冲区会以肉眼可见的速度被填满,最终菜碟落地、汤汁四溅——术语叫缓冲区溢出,落在系统里就是内存飙升、消息丢失,甚至进程崩溃。

为了量化这种失衡,工程上引入“负荷率”指标:生产速率除以消费速率。

如何缓解失衡?传统“推模型”里,生产者闷头往外推数据,消费者只能被动接;而“拉模型”则反过来——消费者想要多少,生产者才给多少。背压机制正是建立在“拉模型”之上:它不是把缓冲区无限加大去“硬扛”,而是从源头调整生产节奏,让 ρ 天然趋近 1。就像老王不再闷头猛炒,而是看一眼传菜台的余量再动锅——背压的第一课是:别让缓冲区成为事故的接盘侠。

ρ=λ生产μ消费\displaystyle \rho = \frac{\lambda_{\text{生产}}}{\mu_{\text{消费}}}
ρ 大于 1,说明上游比下游快,缓冲区必然积压;ρ 小于 1,下游吃得下,系统安稳;ρ 等于 1,是最理想的“步调一致”。
  1. λ生产(分子)lambda 代表单位时间内上游生产的数据量,相当于大厨老王每分钟炒出的盘数。
    λ生产\displaystyle \lambda_{\text{生产}}
  2. μ消费(分母)mu 代表单位时间内下游最多能消化的数据量,相当于服务员小刘每分钟能端走的盘数。
    μ消费\displaystyle \mu_{\text{消费}}
  3. ρ 负荷率(比值)rho 是两者的商,代表整个“厨房”的忙闲程度:大于 1 说明超负荷运转,迟早出事故。
    ρ=λ生产μ消费\displaystyle \rho = \frac{\lambda_{\text{生产}}}{\mu_{\text{消费}}}

响应式契约:点菜单上的君子协定

光有“看一眼传菜台”的自觉还不行,人总有分神的时候。店长决定把节奏控制变成明文规定:每桌服务员必须先拿到一张点菜单,上面写清楚“我要几个菜”;后厨凭单炒菜,一份单最多出 N 道;菜上齐了菜单作废,出错了立刻叫停。这套规定,就是Reactive Streams 规范——一个跨语言的标准异步流处理协议,也是响应式编程(Reactive Programming)的地基。

规范定义了三个核心组件和四类信号:

  • 发布者(Publisher):数据源,相当于后厨,负责按“点菜单”出菜;
  • 订阅者(Subscriber):数据接收方,相当于服务员,负责消费并反馈需求;
  • 订阅契约(Subscription):连接双方的“点菜单”,承载唯一的流量控制指令 request(n);
  • 四类信号:onSubscribe(递上菜单,建立联系)、onNext(端上一道菜)、onError(菜做砸了直接上报)、onComplete(菜上齐了,合作结束)。

有了这套契约,无论中间隔着多少环节,系统的真实吞吐都被下游的消费能力牢牢钳制:

值得强调的是,onNext 里还可以继续调用 request(n)——服务员端完一道菜,可以随时在菜单上再添几道。这确保契约不是僵化的“一次定终身”,而是动态可调的双向对话。一份优秀的点菜单协议,让“辣得起”的后厨再也没出现过传菜台爆满的狼狈,这就是规范的力量。

T实际=min⁡(λ上游,μ下游)\displaystyle T_{\text{实际}} = \min(\lambda_{\text{上游}}, \mu_{\text{下游}})
背压的本质就是不接受任何超出下游消费能力的生产——实际吞吐永远等于两个速率中较小的那个,多出来的部分不是被缓冲,就是被拒收。
  1. λ上游上游(Publisher)的原始产出速率,即大厨不考虑传菜台限制时能炒出的最大盘数。
    λ上游\displaystyle \lambda_{\text{上游}}
  2. μ下游下游(Subscriber)的实际消化能力,即服务员端菜的速度上限。
    μ下游\displaystyle \mu_{\text{下游}}
  3. min 取小函数取两者中较小者,就像餐厅的“上菜速度”永远由慢的那一方决定——大厨再快,服务员端不过来,菜也到不了客人桌上。
    min⁡(λ上游,μ下游)\displaystyle \min(\lambda_{\text{上游}}, \mu_{\text{下游}})
  4. T实际整个链路的有效吞吐。这个公式道出背压的本质:它不是提升吞吐的魔法,而是守住“系统真实能力”底线的纪律。
    T实际\displaystyle T_{\text{实际}}

请求量控制:一次报五道,而不是两百道

有了点菜单,新问题来了:小刘每张单上报多少合适?如果她图省事,一次性报“给我 200 道”,后厨就会像脱缰野马般猛炒,传菜台瞬间淹没——这相当于亲手把背压机制关掉。反过来,一次只报 1 道,后厨刚热完锅就停火,效率又太低。请求量(request)的粒度,是背压工程的第一道调参题。

Reactive 社区常用动态窗口策略:消费者根据自己当前的“空闲槽位”和“在途数据”实时计算下一轮报数。比如小刘手里能同时端 3 盘、传菜台上还有 2 盘没拿,她就只报 1——刚好补齐空闲量,不贪多也不饿着。

除了动态窗口,批量拉取(batch fetch)也是常见手法:消费者攒够一批再统一 request,减少控制信号的通信次数,原理等同于小刘攒一批菜再统一上桌。请求量控制的核心心法只有一句:报得少会饿死吞吐,报得多会冲垮缓冲;跟着空闲槽位走,永远刚刚好。

request(kt)=⌊C空闲槽位max⁡(1, W‾在途人数)⌋\displaystyle \text{request}(k_t) = \left\lfloor \frac{C_{\text{空闲槽位}}}{\max(1,\ \overline{W}_{\text{在途人数}})} \right\rfloor
每一轮报数都在“手上还有多少盘子”和“路上还有多少菜”之间取平衡,既不给下游加压,也不让上游闲置。
  1. C空闲槽位消费者当前剩余的接收容量,相当于小刘空着的“手位”数量。
    C空闲槽位\displaystyle C_{\text{空闲槽位}}
  2. W在途人数已经报出、但还没消费完的数据量,即后厨正在炒的那批菜。
    W‾在途人数\displaystyle \overline{W}_{\text{在途人数}}
  3. max(1, ·)防止在途人数为 0 时除零;至少按 1 计算,保证公式恒成立。
    max⁡(1, W‾在途人数)\displaystyle \max(1,\ \overline{W}_{\text{在途人数}})
  4. ⌊ ⌋ 向下取整点单报数必须是整数道,不能报“0.7 道菜”,所以向下取整。
    ⌊⋅⌋\displaystyle \left\lfloor \cdot \right\rfloor
  5. k_t 本轮请求量第 t 轮实际报出的数字。它随系统状态动态变化,实现“按需分配、动态伸缩”。

传播链路:从传菜口到采购间的节奏共振

“辣得起”不止一层楼:传菜口上面是后厨,后厨后面是备菜间,备菜间连着仓库采购,层层叠叠,每一级都有自己的“小传菜台”。如果只有服务员对后厨喊停,而后厨还是拼命让备菜间切菜,备菜台的土豆丝照样堆成山。背压的威力,必须沿着整条链路逐级逆流而上——每一级算子都要把“我这边慢了”的信号传递给上一级,直到源头。

在 Reactive 编程中,一条链路通常由多个算子(Operator)串联而成:上游 → map 算子 → filter 算子 → 下游。每个算子既是消费者又是生产者:它先向下游申请额度,再向上游传递需求。下游慢了,它减少向上游的申请;上游快了,它用自带的线程调度器(Scheduler)与有界缓冲暂存。链路越长,节奏错位的风险越大,于是有了 Little 定律来量化积压:

全链路背压调节,本质上就是让每个环节的“传菜台余量”保持同步:服务员慢了,后厨慢炒;后厨慢了,备菜间少切;备菜间慢了,采购少进货——节奏在全链共振,而不是某一级独自硬扛。高吞吐的响应式系统,正是靠着这种层层传导的自律,才能在洪峰下保持优雅。

L=λ×W\displaystyle L = \lambda \times W
系统中“滞留”的数据量 L,等于单位时间到达的数据量 λ 乘以每个数据的平均停留时间 W。缓冲越大、停留越久,积压越恐怖。
  1. L(积压量)系统中平均攒着的数据量,即所有传菜台上静止的、正在端送的菜的总数。
    L\displaystyle L
  2. λ(到达率)单位时间流入链路的数据量,即后厨的出菜速率。
    λ\displaystyle \lambda
  3. W(停留时间)每个数据从进入链路到被消费的平均等待时间,即一道菜从出锅到上桌的平均耗时。
    W\displaystyle W
  4. L = λ × W(乘积关系)三条变量互相锁死——要降低积压 L,要么降速 λ,要么缩短等待 W;扩大缓冲只会让 W 变长,L 不降反升。
    L=λ×W\displaystyle L = \lambda \times W

策略与实现:传菜台满了,到底怎么办

即便有了点单契约和动态窗口,现实总有意外:隔壁公司团建包场,客流瞬间翻了三倍,传菜台还是爆了。这时候,店长有三条路可走——这对应着背压落地的三大策略。

第一条路:有界缓冲(Bounded Buffer)。传菜台从 6 位扩到 10 位,但绝不上不封顶。缓冲有上限,超出就触发背压或丢弃,系统内存可控,代价是极端洪峰下部分数据被拒。第二条路:溢出丢弃(Drop Policy)。传菜台满了,按规则倒掉最旧的菜(Drop Oldest)或最不急的菜(Drop Newest),保住最核心的客流——对应数据领域的“丢旧保新”“降精度采样”。第三条路:异步调度(Async Scheduler)。传菜台不够,就把部分菜委托给外包跑腿(独立线程池),用额外的执行资源换缓冲空间。

缓冲区随时间的变化,可以用一个积分式精确刻画:

实际框架中,RxJava 提供了 onBackpressureBuffer / onBackpressureDrop / onBackpressureLatest,Reactor 也有对应的自定义丢弃策略;当缓冲和丢弃都无法容忍时,还有终极手段——背压降级:把精确计算退化为近似值、把实时同步转为批量异步,以服务质量换系统存活。传菜台满不丢人,丢人的是明明可以提前规划,却在爆台时才手忙脚乱。

B(t)=B(0)+∫0t(λ进(s)−μ出(s))ds\displaystyle B(t) = B(0) + \int_{0}^{t} \left( \lambda_{\text{进}}(s) - \mu_{\text{出}}(s) \right) \mathrm{d}s
缓冲区的实时占用 = 初始占用 + 从 0 到 t 时刻“流入速率减流出速率”的累计效果。流入持续大于流出,积压必然持续上涨——策略的意义,就在于控制这个积分的增长趋势。
  1. B(0)(初始占用)初始时刻缓冲区的占用,通常为 0,即开张时传菜台是空的。
    B(0)\displaystyle B(0)
  2. λ进(s)(流入速率)s 时刻流入缓冲区的速率,即后厨往传菜台码菜的速度。
    λ进(s)\displaystyle \lambda_{\text{进}}(s)
  3. μ出(s)(流出速率)s 时刻流出缓冲区的速率,即服务员端走菜的速度。
    μ出(s)\displaystyle \mu_{\text{出}}(s)
  4. ∫ 积分(累计净流入)把从 0 到 t 每一瞬间的“净流入”累加起来,得到累计积压增量。
    ∫0t(λ进(s)−μ出(s))ds\displaystyle \int_{0}^{t} \left( \lambda_{\text{进}}(s) - \mu_{\text{出}}(s) \right) \mathrm{d}s

实践与对比:限流、熔断与背压的三角关系

“辣得起”还有两家分店,一家在门口叫号限流,一家在厨师集体请假时直接挂出暂停营业牌。这三种经营策略,恰好对应高并发架构里的三件套:限流、熔断与背压。它们目标一致(保护系统),但发力点截然不同:

  • 限流(Rate Limiting):在入口处主动拦截,超过阈值直接拒绝——门口叫号,进不来就是进不来;
  • 熔断(Circuit Breaking):下游故障达到阈值时,上游直接短路、快速失败——厨房着火,直接停业保护;
  • 背压(Backpressure):不拦截、不短路,而是让上游感知下游的消化速度,动态调整生产——服务员说“慢点炒”,大厨就放慢火候。

在真实系统中,三者往往组合使用。最典型的场景是流处理管道:假设一个实时风控系统,Kafka 里每秒涌入 10 万条交易事件,下游的规则引擎只能消化 2 万条。Flink 的背压机制会反向传导,Kafka 消费者自动降速,把积压留在消息队列里;同时入口的限流器把超过峰值的流量降级到离线批处理通道。而在 IO 密集场景(如数据库写入、第三方 API 调用)中,背压能防止“服务端响应慢 → 客户端疯狂重试 → 服务端更慢”的雪崩式恶性循环。定量的丢弃策略可以用一个简单的概率公式描述:

一次生产事故复盘很能说明问题:某金融公司晚间大促,下游清算服务响应时间从 30ms 飙到 3s,上游网关还在全速转发,最终引发内存溢出、整链崩溃。修复方案是三层防御:网关限流拦最外层、清算服务入口加熔断保护、消息管道启用背压让消费端按能力拉取——三者齐上,洪峰再来时系统只降级、不死机。背压不是银弹,而是纪律:它让系统诚实地面对自己的能力边界,并有尊严地降级。

P丢弃=max⁡(0,λ请求−μ服务上限λ请求)\displaystyle P_{\text{丢弃}} = \max\left(0, \frac{\lambda_{\text{请求}} - \mu_{\text{服务上限}}}{\lambda_{\text{请求}}}\right)
当请求速率超过服务上限时,超出部分按比例被丢弃或降级。这个比例,就是限流器与背压策略共同决定的“牺牲系数”。
  1. λ请求(到达速率)入口到达的请求速率,即门口排队的客流。
    λ请求\displaystyle \lambda_{\text{请求}}
  2. μ服务上限(处理能力)系统能稳定处理的最大速率,即餐厅同时最多能招待的桌数。
    μ服务上限\displaystyle \mu_{\text{服务上限}}
  3. 分子差值(过剩流量)超出服务上限的那部分“过剩流量”。
    λ请求−μ服务上限\displaystyle \lambda_{\text{请求}} - \mu_{\text{服务上限}}
  4. max(0, ·)(非负截断)请求速率没有超限时丢弃概率为 0;超限时才产生正数。
    max⁡(0,⋅)\displaystyle \max(0, \cdot)

AI 应用场景

大模型推理服务的显存保护

阶段:推理部署与在线服务

解决的问题:大模型推理引擎(如 vLLM、TensorRT-LLM)的吞吐受 GPU 显存和算力双重约束,请求洪峰稍不注意就会触发显存 OOM、进程崩溃。传统做法是硬编码最大并发数,要么浪费 GPU,要么被击穿。用背压思路重构后:推理引擎作为“订阅者”,按当前可用 KV cache 槽位动态向上游“申请”请求量,额度耗尽则新请求排队或返回 503,GPU 永不超卖——就像服务员按手头空位向后厨报数,后厨按单出菜。

具体案例:某 LLM 推理平台在晚高峰遭遇 5 倍请求洪峰,启用基于剩余显存的动态 request 窗口后,GPU 利用率稳定在 92%,P99 时延仅从 1.2s 升到 1.8s,零 OOM、零崩溃。

实时风控特征流的降速传导

阶段:实时特征工程与流式在线预测

解决的问题:营销大促时 Kafka 流量暴增,下游特征计算 + 模型推理扛不住,Flink 状态后端(RocksDB)内存膨胀甚至堆外 OOM。开启 Flink 背压后,上游算子的输出缓冲一旦填满,反压信号逐级传导,Kafka 消费者自动降速,积压被“憋”在消息队列里,而不是堆积在内存中。

具体案例:某支付公司的实时反欺诈系统,大促峰值 20 万 QPS,规则引擎只能稳定处理 5 万。开启背压与有界缓冲后,Flink 自动放缓消费,队列积压控制在分钟级以内,模型精度不受影响,系统在 4 小时大促中零故障。

训练数据管线的速率匹配

阶段:模型训练

解决的问题:GPU 计算快而数据加载慢,会导致 GPU 饥饿;反过来数据加载无限快,又会让内存队列爆炸。PyTorch DataLoader 的 buffered queue + prefetch 本质上就是一种背压:按 GPU 的消费节奏提前拉取固定批次,队列有界,满载时数据管线暂停生产,等待 GPU 消化。

具体案例:某大模型训练任务将 DataLoader 的 num_workers 从 4 调到 8 并设置 prefetch_factor=2 后,GPU 利用率从 61% 提升到 89%,且内存占用稳定在 8GB 以内,不再频繁 OOM。

Python 演示

python53 行
"""背压机制模拟: 消费者通过 request(n) 控制生产节奏"""class BackpressureChannel:    """有界通道: 生产者推送数据前必须先获得消费额度"""     def __init__(self, capacity=5):        self.capacity = capacity        self.credits = 0     # 消费者授予的额度        self.buffer = []     # 缓冲队列        self.dropped = 0     # 被拒收的个数     def request(self, n):        """消费者申请 n 个额度(背压信号沿链路逆流而上)"""        self.credits += n     def push(self, item):        """生产者尝试推送: 额度不足或缓冲已满则拒收"""        if self.credits > 0 and len(self.buffer) < self.capacity:            self.buffer.append(item)            self.credits -= 1            return True        self.dropped += 1        return False     def consume(self):        """消费者取走一个数据, 并立刻补充一个额度"""        if self.buffer:            item = self.buffer.pop(0)            self.request(1)            return item        return None  def simulate(produce_rate=1, consume_rate=3, total=15):    """produce_rate / consume_rate 越小越快(每 N 帧生产/消费一次)"""    ch = BackpressureChannel(capacity=5)    ch.request(3)              # 消费者先报数: 我要 3 个    produced = consumed = 0    for t in range(total):        if t % produce_rate == 0:            produced += 1            if not ch.push(produced):                print("t=%2d 背压拦截: 数据 %d 未获额度被拒收" % (t, produced))        if t % consume_rate == 0:            item = ch.consume()            if item is not None:                consumed += 1        print("t=%2d 缓冲=%d/%d 额度=%d" % (t, len(ch.buffer), ch.capacity, ch.credits))    print("统计: 生产 %d, 消费 %d, 拒收 %d" % (produced, consumed, ch.dropped))    return ch  if __name__ == "__main__":    simulate(produce_rate=1, consume_rate=3, total=15)
目的:用不到 50 行复刻“额度制”背压通道——消费者先报数 request(3),生产者每推送一个数据就消耗一个额度;额度耗尽后推送被拒收,模拟出上游被下游能力“反制”的全过程。
  1. ch.request(3)消费者开局“点单”:先向后厨要 3 个额度,相当于 subscription.request(n)。
  2. def push(self, item):生产者推送的入口,每次推送先检查额度和缓冲容量。
  3. if self.credits > 0 and len(self.buffer) < self.capacity:背压的“阀门”:无额度或缓冲满,直接拒收。
  4. self.credits -= 1每接收一个数据,消耗一个消费额度,对应规范中 Publisher 在 onNext 后扣减计数。
  5. def consume(self):消费者取数据,同时调用 self.request(1) 回补额度,维持动态窗口。
  6. if not ch.push(produced):推送失败说明背压生效,数据被拦截,本次生产“白做”。
  7. simulate(produce_rate=1, consume_rate=3)生产者比消费者快 3 倍,逼出背压拦截场景。

常见误区

误区“限流就是在入口限速,背压也是让上游慢点,两者一回事吧?我在网关加个 RateLimiter 是不是就等于实现了背压?”

正解限流是“入口单方面设定一个阈值,超过就拒绝”,流量方向是单向的、静态的;背压是“下游根据自身实时消费能力,动态向上游申请额度”,是反馈闭环、自适应的。用餐厅比喻:限流是门口保安说“今晚只能进 100 桌”;背压是服务员告诉后厨“我这桌现在只要 3 道菜”。限流保护的是入口,背压保护的是全链路。

为何常见:两者目标都是“别让系统被打爆”,且常常组合部署在同一入口,容易让人误以为是同一机制。

误区“背压问题不就是缓冲溢出吗?我把缓冲队列从 1000 调到 100 万,不就不会溢出了?”

正解无界缓冲只是把“爆炸点”从内存搬到延迟上。数据积压会让消费延迟飙升、数据过期失去实时性,最终内存依旧被耗尽(OOM 同样会崩)。按 Little 定律 L = λW,缓冲越大,停留时间 W 越长,积压 L 不降反升。背压的正解是让生产速率匹配消费速率(让 ρ ≤ 1),而不是无限扩容传菜台。

为何常见:数据积压是不可见的,不像崩溃那么刺眼;“扛一扛就过去了”的侥幸心理让很多人优先选择加内存,而不是改架构。

误区“既然 request 是背压的开关,那我一次 request(100000),让生产者火力全开,吞吐不就能拉满吗?”

正解request(n) 的本质是“下游承诺自己能处理 n 个”。n 超过实际消费能力时,数据会大量滞留在缓冲里,延迟攀升、内存吃紧,背压名存实亡。n 应基于“空闲槽位/在途数据”动态计算——报得少饿死吞吐,报得多冲垮缓冲,刚刚好才是最优。

为何常见:人们习惯把“吞吐越大越好”当铁律,忽略了延迟、内存与吞吐之间的制约关系。

误区“我的服务是同步阻塞调用的,慢就慢呗,不需要什么背压机制。”

正解同步阻塞其实是“最原始的背压”——调用方没处理完就阻塞着不发起下一个请求,天然限制了上游速率。但它的代价是线程被白白占住,线程数/连接数耗尽会引发雪崩。异步响应式场景通过显式 request(n) 用极少的线程承载高并发,同时保留背压控制,这才是它被重视的原因。

为何常见:背压一词在响应式编程中太常见,容易让人以为只有响应式场景才需要;实际上同步模型的“限速”效果正是背压的朴素形式。

面试题

Q1. 在餐饮比喻中,服务员通过“点菜”控制后厨出菜速度,对应背压机制的哪个核心操作?

单选题 · easy

  • A.生产者每满一点批量推送
  • B.消费者主动申请 request(n)← 正确答案
  • C.缓冲区快满时自动扩容
  • D.消费者永久阻塞等待数据

答案:B。背压的核心是消费者通过 Subscription.request(n) 主动向上游申请额度,生产者只有在收到额度后才推送。就像服务员拿起点菜单对后厨说“先上三道”,后厨才会动这三道——申请动作本身就决定了出菜节奏,而不是生产者自顾自地批量推送。

Q2. Reactive Streams 中,连接发布者与订阅者、承载流量控制指令的对象是?

单选题 · easy

  • A.Publisher 发布者对象
  • B.Subscriber 订阅者对象
  • C.Subscription 订阅契约← 正确答案
  • D.Disposable 一次性资源

答案:C。Subscription 是连接 Publisher 与 Subscriber 的“点菜单”,唯一的流量控制接口 request(n) 就定义在它上面。Publisher 负责产生数据,Subscriber 负责消费数据,而“按多少给多少”的节奏控制由 Subscription 承载。

Q3. 当系统负荷率 ρ = λ/μ = 1.5 时,以下描述正确的是?

单选题 · mid

  • A.生产速度低于消费速度
  • B.缓冲区将会被逐渐排空
  • C.生产速度高于消费速度← 正确答案
  • D.系统处于完美均衡状态

答案:C。ρ = λ/μ 大于 1 表示生产速率高于消费速率,缓冲区会持续积压直至溢出,绝不可能是“排空”或“均衡”。就像传菜台上新菜的速度比服务员端走的速度快,台面只会越堆越高。

Q4. 以下哪些属于 Reactive Streams 规范定义的信号?

多选题 · mid

  • A.onSubscribe 建立订阅契约← 正确答案
  • B.onNext 传递一个新的数据← 正确答案
  • C.onError 上报异常终止← 正确答案
  • D.onRequest 归还消费额度

答案:ABC。规范定义了四类信号:onSubscribe(建立契约)、onNext(传递数据)、onError(异常终止)、onComplete(正常完成)。规范里没有 onRequest 信号——消费者的“需求”不是信号,而是通过 subscription.request(n) 方法调用来表达。

Q5. 完全依赖“无界缓冲”处理速率不匹配,最可能引发的问题是?

单选题 · mid

  • A.延迟飙升与内存的耗尽← 正确答案
  • B.生产者线程持续的阻塞
  • C.消费者空转 CPU 飙升
  • D.网络带宽被瞬间占满

答案:A。无界缓冲只是把爆炸推迟:数据排队时间越来越长(延迟飙升),内存占用持续上涨(最终 OOM)。Little 定律 L = λW 指明,缓冲越大不代表积压越小,反而让数据在系统中滞留更久。背压的正解是调速而非扩容。

Q6. 背压机制与限流机制的本质区别是?

单选题 · mid

  • A.背压是双向反馈,限流是单向拦截← 正确答案
  • B.背压只用在消息队列的场景
  • C.限流更强调全链路的节流
  • D.两者完全等价没有区别

答案:A。背压是“下游能力反馈上游”的双向闭环,限流是“入口阈值拦截”的单向规则。限流像门口保安规定“最多进 100 桌”;背压像服务员实时告诉后厨“我这桌只要 3 道”。两者常配合使用,但机制本质不同。

Q7. 一条算子链“上游 → A算子 → B算子 → 下游”中,下游变慢时正确的做法是?

多选题 · hard

  • A.B 算子减少对 A 算子的请求量← 正确答案
  • B.A 算子继续全速向源头拉数据
  • C.B 算子用有界缓冲吸收尖峰← 正确答案
  • D.下游变慢立刻抛出异常终止

答案:AC。下游变慢时,正确做法是逐级传导:B 算子减少向上游 A 的申请量,并用有界缓冲吸收短暂尖峰;A 继续全速拉取或直接抛异常都是错误选择,前者让积压向源头蔓延,后者粗暴中断整个数据流。

Q8. 消费者执行 subscription.request(5) 后,发布者后续最多能推送多少个数据?

单选题 · easy

  • A.最多推送 5 个就须暂停← 正确答案
  • B.之后可无限推送数据
  • C.至少连续推送 5 个给下游
  • D.完全由系统负载情况决定

答案:A。request(5) 的含义是“我最多能处理 5 个”,发布者最多推送 5 个后必须停下等待新的请求。这是背压最基础的语义——额度是上限承诺,不是保底义务。

Q9. 大模型推理服务用背压保护 GPU 显存,下列哪些是有效手段?

多选题 · hard

  • A.按剩余显存动态发放请求额度← 正确答案
  • B.无上限缓冲堆积超量的请求
  • C.额度耗尽返回排队或 503← 正确答案
  • D.强制所有请求归为同一批

答案:AC。推理服务保护显存的有效手段包括:按剩余 KV cache/显存量动态发放请求额度(有界并发)、额度耗尽时让请求排队或返回 503(显式背压)。无上限堆积请求会撑爆显存,强制同一批处理则违背按需调度原则。

Q10. 按照 Reactive Streams 规范,订阅者调用 request(0) 会触发什么结果?

单选题 · extreme

  • A.被当作无效的请求直接忽略
  • B.发布者立即终止整条数据流
  • C.以参数异常上报 onError← 正确答案
  • D.自动按默认值取 1 个

答案:C。Reactive Streams 规范规定 request(n) 的 n 必须为正整数,n ≤ 0 视为非法参数,订阅者会收到携带 IllegalArgumentException 的 onError 信号并终止数据流。就像点菜单上不允许写“上 0 道菜”,协议的根本语义是“按需授予、数量为正”。