@@ -272,18 +272,20 @@ RocketMQ 中的消息模型就是按照 **主题模型** 所实现的。那么 *
272272
273273** 负载均衡策略对比**
274274
275+ 下面对比的是** 同一消费者组内的集群消费(共享消费)** 。负载均衡粒度取决于客户端类型和消费协议,不能只看 Broker 的版本:传统 3.x/4.x Remoting SDK 的消费者采用队列粒度;5.x 新版 SDK 的 PushConsumer、SimpleConsumer 采用消息粒度;PullConsumer 仍采用队列粒度。
276+
275277``` mermaid
276278flowchart TB
277- subgraph Queue["队列粒度负载均衡 4.x "]
279+ subgraph Queue["队列粒度:3 个队列,4 个消费者 "]
278280 style Queue fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
279281 direction TB
280282 Q1["队列1"] --> C1["消费者1"]
281283 Q2["队列2"] --> C2["消费者2"]
282284 Q3["队列3"] --> C3["消费者3"]
283- Q4["队列4"] -.-> C4["消费者4<br/>(无队列可消费 )"]
285+ C4["消费者4<br/>(无队列可分配 )"]
284286 end
285287
286- subgraph Message["消息粒度负载均衡 5.x Push/Simple "]
288+ subgraph Message["消息粒度:同一队列的不同普通消息由组内消费者分担 "]
287289 style Message fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
288290 direction TB
289291 MQ1["队列1"] --> MC1["消费者1<br/>消费消息1"]
@@ -295,33 +297,46 @@ flowchart TB
295297 classDef consumer4x fill:#E99151,color:#fff,rx:10,ry:10
296298 classDef consumer5x fill:#00838F,color:#fff,rx:10,ry:10
297299
298- class Q1,Q2,Q3,Q4, MQ1 queue
300+ class Q1,Q2,Q3,MQ1 queue
299301 class C1,C2,C3,C4 consumer4x
300302 class MC1,MC2,MC3 consumer5x
301303
302304 linkStyle default stroke-width:1.5px,opacity:0.8
303305```
304306
305- - ** 队列粒度负载均衡(4.x 默认策略) ** :一个队列只会被一个消费者消费 。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。队列数通常要大于或等于消费者数,消费者少于队列是常见情况,只是单个消费者会分到多个队列; 如果消费者数多于队列数,多出来的消费者会空闲。这种模式的缺点是容易产生 ** 长尾效应 ** :如果某个消费者处理速度较慢,会导致其对应的队列消息堆积,而其他消费者却处于空闲状态 。
306- - ** 消息粒度负载均衡(5.x PushConsumer/SimpleConsumer 默认策略) ** :RocketMQ 5.x 中,PushConsumer 和 SimpleConsumer 默认使用消息粒度负载均衡,同一消费者分组内的多个消费者可以按照消息粒度分摊主题中的消息;PullConsumer 仍然是队列粒度。消费者获取某条消息后,服务端会将该消息加锁,保证这条消息对其他消费者不可见,直到该消息消费成功或消费超时。这种模式有效解决了长尾效应问题,因为消息不再静态绑定到某个消费者,而是动态分配给空闲的消费者 。
307+ - ** 队列粒度负载均衡** :在分配稳定时,同一消费者组内,一个队列只分配给一个消费者实例;一个消费者可以分到多个队列 。如果某个消费者挂掉,会通过重平衡把它的队列分配给组内其他消费者。 如果消费者数多于队列数,就会有消费者分不到队列。这种模式容易出现负载不均:某个消费者处理速度较慢,或者分到的队列消息较多,就可能出现该消费者积压、其他消费者空闲的情况。不同消费者组可以分别消费同一个队列,不受组内独占分配的限制 。
308+ - ** 消息粒度负载均衡** :5.x 新版 SDK 的 PushConsumer、 SimpleConsumer 可以让同一消费者组内的多个消费者分担同一个队列中的不同消息。图中只是一次分配示例,消息不会固定绑定到某个消费者。消息被取走后,在不可见时间内不会再投递给 ** 同组 ** 其他消费者;消费失败或超时仍可能触发重试,因此不代表绝不会重复消费。这种方式可以缓解队列分配不均和消费者处理能力差异造成的积压,但不能消除业务处理慢等所有瓶颈。顺序消息还受消息组顺序约束,不能把同一消息组内的消息任意并行处理 。
307309
308- 消费者个数小于队列个数并不是问题,只是并发能力受消费者数量限制。真正需要避免的是队列数太少,导致后续扩容消费者时没有足够的队列可分配。如下图 。
310+ 下面这张图仅适用于 ** 队列粒度负载均衡 ** 。 消费者个数小于队列个数并不是问题,一个消费者可以消费多个队列;如果希望扩容后的消费者都能分到队列,就需要有足够的队列。消息粒度负载均衡下,消费者实例数则不受队列数的直接限制 。
309311
310312![ ] ( https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3850c808d707.jpg )
311313
312- ** 每个消费组在每个队列上维护一个消费位置** ,为什么呢?
314+ ** 为什么不同消费者组要分别记录每个队列的消费进度?**
315+
316+ 因为不同消费者组订阅同一个 Topic 时,需要各自消费这些消息,消费速度也可能不同。以传统队列粒度的集群消费为例,消费位点按 ** 消费者组 + Topic + 队列** 分别记录,组内消费者共享该进度;发生重平衡时,接手队列的消费者可以从已提交的位点继续消费。
317+
318+ 消费位点(ConsumerOffset)表示该组在该队列上下一次继续消费的位置。它不是消息 ID,也不是整个 Topic 共用的序号。下面假设两个组订阅同一个 Topic,没有过滤和重试,消息按队列顺序消费并已提交位点:
319+
320+ | 消费者组 | 队列 Q0 的消费进度 | 队列 Q1 的消费进度 |
321+ | -------- | ----------------------------------------- | ----------------------------------------- |
322+ | Group A | offset 为 3:已消费 0~2,下一条从 3 开始 | offset 为 1:已消费 0,下一条从 1 开始 |
323+ | Group B | offset 为 1:已消费 0,下一条从 1 开始 | offset 为 4:已消费 0~3,下一条从 4 开始 |
324+
325+ Q0 和 Q1 的 offset 分别在各自队列内递增。Group A 推进 Q0 的位点,不会改变 Group B 的位点,也不会改变自身 Q1 的位点。
326+
327+ ** 位点更新不能简单理解为“每成功一条就加一”,也不能保证不重复消费。** 批量拉取、并发处理、消息过滤和位点提交都会影响进度的推进;例如前面的消息还没处理完,后面的消息即使先完成,也不能直接越过前面的消息提交进度。消费已成功但位点尚未持久化时发生故障,就可能再次消费。5.x 新版 SDK 的 PushConsumer、SimpleConsumer 通过逐条消息确认,由服务端管理消费状态,也不能套用“客户端每成功一条就将 offset 加一”的解释。
313328
314- 因为我们刚刚画的仅仅是一个消费者组,我们知道在发布订阅模式中一般会涉及到多个消费者组,而每个消费者组在每个队列中的消费位置都是不同的。如果此时有多个消费者组,那么消息被一个消费者组消费完之后是不会删除的(因为其它消费者组也需要呀),它仅仅是为每个消费者组维护一个 ** 消费位移(offset) ** ,每次消费者组消费完会返回一个成功的响应,然后队列再把维护的消费位移加一,这样就不会出现刚刚消费过的消息再一次被消费了 。
329+ 消息被某个组消费成功后不会立即从存储中删除,也不是必须等所有组都消费完才删除。RocketMQ 按保存时间、存储空间等清理策略回收消息,未消费的消息也可能被清理。消费进度管理、消息保留和业务幂等是不同的问题 。
315330
316- ![ ] ( https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3857fefaa079.jpg )
331+ 参考官方文档: [ 消费者负载均衡 ] ( https://rocketmq.apache.org/zh/docs/featureBehavior/08consumerloadbalance/ ) 、 [ 消费进度管理 ] ( https://rocketmq.apache.org/zh/docs/featureBehavior/09consumerprogress/ ) 、 [ 消息存储和清理机制 ] ( https://rocketmq.apache.org/zh/docs/featureBehavior/11messagestorepolicy/ ) 。
317332
318333可能你还有一个问题,** 为什么一个主题中需要维护多个队列** ?
319334
320- 答案是 ** 提高并发能力** 。的确, 每个主题中只存在一个队列也是可行的。你想一下,如果每个主题中只存在一个队列,这个队列中也维护着每个消费者组的消费位置,这样也可以做到 ** 发布订阅模式 ** 。如下图 。
335+ 答案是 ** 提高并发能力** 。 每个主题中只存在一个队列也是可行的,不同消费者组仍可以分别订阅它,并维护各自的消费进度。下图展示的是队列粒度负载均衡下的单队列场景 。
321336
322337![ ] ( https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38600cdb6d4b.jpg )
323338
324- 但是,这样我生产者是不是只能向一个队列发送消息?又因为需要维护消费位置所以一个队列只能对应一个消费者组中的消费者,这样是不是其他的 Consumer 就没有用武之地了?从这两个角度来讲,并发度一下子就小了很多 。
339+ 只有一个队列时,生产者的消息都写入这个队列,无法利用多个队列分散到不同 Broker。在队列粒度负载均衡下,同组也只有一个消费者实例能分到该队列,其他实例无法分担;这是队列分配策略的限制,不是维护消费位点必然导致的。消息粒度负载均衡允许组内多个消费者分担单队列中的不同消息,多队列仍有利于存储和读写负载的分散 。
325340
326341所以总结来说,RocketMQ 通过** 使用在一个 Topic 中配置多个队列并且每个队列维护每个消费者组的消费位置** 实现了 ** 主题模式/发布订阅模式** 。
327342
@@ -1010,7 +1025,7 @@ PushConsumer 消费时,不允许使用以下方式处理消息:
10101025
101110262 . ** 错误方式二** :在消费监听器内将消息再次分发到自定义的其他线程,消费监听器提前返回消费结果。此时如果消息消费失败,RocketMQ 服务端同样无法感知,因此也不会进行消费重试。
10121027
1013- ** Push 模式工作原理:**
1028+ ** 传统 Remoting SDK 的 Push 模式工作原理(对应上面的 DefaultMQPushConsumer 示例) :**
10141029
101510301 . ** 负载均衡** :RebalanceService 线程根据队列数量和消费者个数做负载均衡,将分配到的队列发布 pullRequest 到 pullRequestQueue
101610312 . ** 消息拉取** :PullMessageService 线程不断从 pullRequestQueue 获取 pullRequest,从 Broker 拉取消息并缓存到 ProcessQueue
@@ -1212,11 +1227,11 @@ flowchart TB
12121227 direction TB
12131228 style ConsumerGroup fill:#F0F2F5,stroke:#E0E6ED,stroke-width:1.5px
12141229
1215- subgraph Cluster["集群消费模式"]
1230+ subgraph Cluster["集群消费模式(以队列粒度为例) "]
12161231 direction TB
12171232 style Cluster fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
1218- CG["消费者组"] --> C1["消费者1<br/>消费队列1、2 "]
1219- CG --> C2["消费者2<br/>消费队列3、4 "]
1233+ CG["消费者组"] --> C1["消费者1<br/>消费队列1"]
1234+ CG --> C2["消费者2<br/>消费队列2 "]
12201235 CG --> C3["消费者3<br/>空闲"]
12211236 Note1["任意一条消息<br/>只需被消费组内<br/>任意一个消费者处理"]
12221237 end
@@ -1623,9 +1638,9 @@ public class ConsumerAddViewHistory implements RocketMQListener<Message> {
16231638
16241639我们可以从多个角度去思考解决这个问题,当流量到峰值的时候是因为生产者生产太快,我们可以使用一些 ** 限流降级** 的方法,当然你也可以增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增。如果消费者消费过慢的话,我们可以先检查 ** 是否是消费者出现了大量的消费错误** ,或者打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。
16251640
1626- > 当然,最快速解决消息堆积问题的方法还是增加消费者实例,不过 ** 同时你还需要增加每个主题的队列数量 ** 。
1641+ > 增加消费者实例通常能缓解消费能力不足造成的堆积,但是否需要增加队列,要看负载均衡策略和现有队列数量 。
16271642>
1628- > ** 注意** :在 RocketMQ 4.x 及之前的版本中 ,** 一个队列只会被一个消费者消费 ** ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况(部分消费者没有队列可消费) 。
1643+ > ** 注意** :传统 Remoting SDK 的队列粒度负载均衡下 ,** 同组内一个队列只分配给一个消费者实例 ** 。只有扩容后的消费者数超过可分配队列数时,才会出现部分消费者没有队列可消费的情况 。
16291644>
16301645> RocketMQ 5.x 的 PushConsumer 和 SimpleConsumer 默认使用** 消息粒度负载均衡策略** ,同一消费者分组内的多个消费者可以按照消息粒度共同消费同一个队列中的消息,因此即使消费者数量多于队列数量,仍能提升消费并行度。PullConsumer 仍是队列粒度,扩容时还是要关注队列数。
16311646
0 commit comments