Files
cs-note/hhs/MQ/08-消息设计模式/28-MQ-Competing-Consumers-模式.md
T
2026-05-24 20:51:06 +08:00

6.1 KiB
Raw Blame History

tags, create time
tags create time
MQ
消息队列
设计模式
负载均衡
2026-05-24 19:52

MQ Competing Consumers 模式

概述

Competing Consumers(竞争消费者)模式通过多个消费者同时消费同一个队列来实现负载均衡,是提升消息处理吞吐量最直接的手段。本文深入讲解该模式的工作原理、分区分配算法、Consumer Rebalance 机制及其常见问题与优化方案。

正文

模式定义

传统单消费者模型中,一个队列只有一个消费者按顺序处理消息。当消息量增大时,单个消费者成为瓶颈。Competing Consumers 模式的核心思想很简单:让多个消费者"抢"同一个队列的消息,谁抢到谁处理,从而实现水平扩展。

graph LR
    P["Producer"] --> Q["Queue"]
    Q --> C1["Consumer 1"]
    Q --> C2["Consumer 2"]
    Q --> C3["Consumer 3"]

    style P fill:#4A90D9,color:#fff
    style Q fill:#F5A623,color:#fff
    style C1 fill:#6EC1E0,color:#fff
    style C2 fill:#6EC1E0,color:#fff
    style C3 fill:#6EC1E0,color:#fff

与传统单消费者的对比

维度 单消费者 Competing Consumers
吞吐量 受单机性能限制 线性扩展(理论上)
消息顺序 天然有序 需要额外保障(分区有序)
可用性 消费者故障则停摆 单个消费者故障不影响整体
复杂度 低 高(需要处理 Rebalance)

[!question] Competing Consumers 提升了吞吐量,但牺牲了全局顺序性。如果你的业务需要"同一用户的操作有序",该如何在分区级别保障顺序?

分区分配算法详解

在 Kafka 中,Consumer Group 内的消费者通过分区分配算法决定"谁消费哪个 Partition"。不同的分配策略直接影响负载均衡效果和 Rebalance 开销。

Range(范围分配)

将 Topic 的 Partition 按序号范围分配给消费者。例如 6 个 Partition、3 个消费者:Consumer 0 拿到 [0,1],Consumer 1 拿到 [2,3],Consumer 2 拿到 [4,5]。

优点是简单直观,但如果多个 Topic 使用 Range 策略,可能导致某个消费者被分配到多个 Topic 的"头部"分区,造成负载不均。

Round-Robin(轮询分配)

将所有 Partition 轮询分配给消费者。6 个 Partition、3 个消费者:Consumer 0 拿到 [0,3],Consumer 1 拿到 [1,4],Consumer 2 拿到 [2,5]。

分配更均匀,但要求同一 Consumer Group 内所有消费者订阅的 Topic 完全一致,否则会出现分配异常。

Sticky(粘性分配)

在 Round-Robin 的基础上增加"粘性":Rebalance 时尽量保留原有的分配关系,只迁移必要的 Partition。这大幅减少了 Rebalance 期间的 Partition 迁移量。

Cooperative Sticky(协作式 Rebalance)

传统 Rebalance 是"Stop-the-World"——所有消费者暂停消费,等待分配完成。Cooperative Sticky 采用增量式 Rebalance:只暂停被迁移的 Partition,其余 Partition 继续消费。

graph TD
    subgraph Before["Rebalance 前"]
        B1["Consumer 1"] --> BP1["Partition 0, 1, 2"]
        B2["Consumer 2"] --> BP2["Partition 3, 4, 5"]
    end

    subgraph After["Rebalance 后 (Cooperative Sticky)"]
        A1["Consumer 1"] --> AP1["Partition 0, 1"]
        A2["Consumer 2"] --> AP2["Partition 3, 4, 5"]
        A3["Consumer 3 (新加入)"] --> AP3["Partition 2"]
    end

    Before --> After

    style Before fill:#F5A623,color:#fff
    style After fill:#6EC1E0,color:#fff

注意图中:Cooperative Sticky 只迁移了 Partition 2,其余 Partition 在 Rebalance 期间保持消费不中断。

Consumer Rebalance 过程与问题

Rebalance 是 Competing Consumers 模式中最关键也最容易出问题的环节。

触发条件:消费者加入/离开 Group、消费者心跳超时、Topic Partition 数量变化。

Stop-the-World 问题:传统 Rebalance 期间,Group 内所有消费者必须停止消费,等待 Coordinator 完成重新分配。在分区数量多或消费者数量大时,这个过程可能持续数秒甚至数十秒。

Rebalance 风暴:当消费者处理消息过慢导致心跳超时,被踢出 Group 触发 Rebalance;Rebalance 期间积压更多消息;消费者重新加入后又因处理不过来被踢出——形成恶性循环。

Static Membership 减少不必要的 Rebalance

Kafka 2.3 引入 Static Membership 机制:每个消费者配置固定的 group.instance.id。消费者短暂断开重连时,只要在 session.timeout.ms 内恢复,就不会触发 Rebalance。

这在容器化部署中特别有用——Pod 重启时不会引发整个 Consumer Group 的 Rebalance 风暴。

[!question] Rebalance 期间消费者会暂停消费,在高并发场景下这会造成什么问题?如何缓解?

Go 代码:Kafka Consumer 分区分配配置

package main

import (
	"github.com/segmentio/kafka-go"
)

func main() {
	// 创建 Reader 时指定 Consumer Group 和分配策略
	r := kafka.NewReader(kafka.ReaderConfig{
		Brokers:  []string{"localhost:9092"},
		Topic:    "orders",
		GroupID:  "order-service",
		// 使用 Cooperative Sticky 分配策略,减少 Rebalance 迁移
		GroupBalancers: []kafka.GroupBalancer{
			kafka.CooperativeGroupBalancer{},
		},
		// 静态成员:Pod 重启时不触发 Rebalance
		GroupInstanceID: "order-consumer-pod-0",
	})
	defer r.Close()

	for {
		msg, err := r.ReadMessage(context.Background())
		if err != nil {
			// 处理错误,注意 Rebalance 期间会返回特定错误
			break
		}
		process(msg)
	}
}

关键配置说明:

  • GroupBalancers 设置分配策略,CooperativeGroupBalancer 对应 Cooperative Sticky。
  • GroupInstanceID 设置静态成员 ID,同一个 Pod 重启后保持相同的 ID,避免触发不必要的 Rebalance。

关联笔记