--- tags: - MQ create time: 2026-05-24 19:52 --- # MQ 背压与流控 ## 概述 当生产速率持续超过消费速率时,消息队列会面临内存溢出、磁盘满甚至消息丢失的风险。背压(Backpressure)机制让上游感知下游的处理能力,流控则是系统在过载时保护自身的手段。本文从 Broker 端和 Consumer 端两个维度,剖析主流 MQ 的背压与流控策略。 ## 正文 ### 问题:生产太快,消费太慢 想象一个场景:大促期间订单量暴增,Producer 疯狂写入消息,而下游 Consumer 处理能力有限。如果不加控制,会发生什么? 1. **内存溢出**:Broker 将消息堆积在内存中,最终 OOM 2. **磁盘满**:持久化消息写满磁盘,Broker 宕机 3. **消息丢失**:触发淘汰策略(TTL / 队列满丢弃),消息悄无声息地消失 > [!question] > 如果 MQ 天生就是一个缓冲区,消息堆积不是它的基本能力吗?为什么堆积到一定程度反而会出问题? 这涉及到一个关键认知:**缓冲区是有限的**。任何系统都有资源上限——内存、磁盘、CPU。无限制的堆积只是把问题延后,而不是解决。 ### 背压(Backpressure)概念 背压的核心思想:**让上游感知下游的处理能力,主动降速**。 ``` Producer → Broker → Consumer ↑ ↓ └──── 处理能力反馈 ────┘ ``` 这不是 MQ 的专利。TCP 的滑动窗口、HTTP/2 的流控、Reactive Streams 的 `request(n)` 都是背压的不同实现形式。 ### Broker 端流控 #### RabbitMQ 的信用机制(Credit Flow) RabbitMQ 使用 **信用机制** 控制消息流速。Producer 发送消息前需要有足够的 credit,Broker 处理完后归还 credit。当 Broker 积压过多,会暂停归还 credit,从而让 Producer 阻塞。 ``` Producer --msg1--> Broker (credit: 10→9) Producer --msg2--> Broker (credit: 9→8) ...积压严重... Broker 暂停归还 credit Producer 阻塞,停止发送 ``` #### Kafka 的 Producer 端背压 Kafka 通过两个参数实现背压: - `buffer.memory`:Producer 端缓冲区大小(默认 32MB) - `max.block.ms`:缓冲区满时,`send()` 方法的阻塞时间 当缓冲区满且超过 `max.block.ms`,Producer 抛出 `TimeoutException`。这是硬性的背压信号。 ```go // Kafka Producer 配置中的背压参数 config := sarama.Config{} config.Producer.RequiredAcks = sarama.WaitForAll config.Producer.Return.Successes = true // 缓冲区满时阻塞 1 秒,超时则报错 // 这就是 Kafka 的背压触发点 ``` #### 内存告警与磁盘告警 多数 Broker 有内置的资源监控: - **RabbitMQ**:内存高水位触发 flow control,阻塞所有连接 - **Kafka**:`log.retention.bytes` 限制分区大小,磁盘满时拒绝写入 - **RocketMQ**:`diskMaxUsedSpaceRatio` 触发磁盘保护 ### Consumer 端反压 Consumer 端同样需要流控,核心手段包括: **拉取速率控制**:Pull 模式天然支持背压——Consumer 按自己的节奏拉取,拉多少处理多少。RabbitMQ 的 `basicQos(prefetchCount)` 就是限制未 ACK 消息数的典型手段。 **处理能力反馈**:Consumer 可以动态上报自己的处理延迟或队列深度,上游据此调整推送速率。 **动态调整消费并发**:根据处理延迟自动扩缩 Consumer 实例数。Kubernetes HPA 基于队列深度的自动伸缩是常见方案。 ```mermaid graph LR P["Producer"] -->|"生产消息"| B["Broker"] B -->|"推送/拉取"| C["Consumer"] C -->|"ACK / 处理反馈"| B B -->|"Credit / 阻塞信号"| P style P fill:#4CAF50,color:#fff style B fill:#2196F3,color:#fff style C fill:#FF9800,color:#fff ``` ### 限流降级策略 当背压来不及响应时,需要更积极的流控手段: **令牌桶(Token Bucket)**:以恒定速率产生令牌,请求必须持有令牌才能通过。允许一定的突发流量(桶内预存令牌)。 **漏桶(Leaky Bucket)**:请求进入桶中,以恒定速率流出。严格平滑流量,但不允许突发。 **动态调整生产速率**:根据 Broker 的健康指标(队列深度、内存使用率)动态调整 Producer 的发送速率。 ```go // 带背压控制的 Producer 示例 // 利用 channel 的天然阻塞特性实现背压 func producer(ch chan<- string, done <-chan struct{}) { for { select { case <-done: return case ch <- "message": // channel 满时自动阻塞,实现背压 // 上游感知到下游处理不过来,自然降速 } } } func consumer(ch <-chan string) { for msg := range ch { // 模拟慢消费 time.Sleep(100 * time.Millisecond) _ = msg } } func main() { // 有界 channel 就是一个天然的背压缓冲区 ch := make(chan string, 100) done := make(chan struct{}) go producer(ch, done) go consumer(ch) // 当 consumer 处理不过来时, // channel 满后 producer 自动阻塞 time.Sleep(5 * time.Second) close(done) } ``` 这段代码的精髓在于 `make(chan string, 100)`——有界 channel 就是一个天然的背压装置。当缓冲区满时,发送方自动阻塞,不需要额外的信号传递。 > [!question] > 令牌桶和漏桶看起来很像,它们的核心区别是什么?什么场景下该用哪个? ## 关联笔记 - [[32-MQ-请求-回复模式]] - [[33-MQ-与流处理]] - [[34-事件驱动架构-EDA]]