聊聊消息中间件Push模型和Pull模型优缺点
目录
一 、常用消息中间件支持模型
二、消费端Push模型优缺点
三、消费端Pull模型优缺点
四、两种模型在实际场景中的优缺点分析
五、常见消息中间Push和Pull实现实战和客户端源码分析
这篇文章前,先抛出个问题
- 1 所谓的推拉模型仅仅是指消费端模型吗?是否需要broker支持这种模型呢?
当然不是,如果没有服务端Broker支持,它如何知道消费端当前选取的消息消费模型,准确讲推拉模型是指消息的消费模型,需要服务端Broker支持,也需要消费端Client支持。(通常服务端Broker和消费端Client都是互相配合工作的,一起开发,一起作用的)。
一、常用消息中间件支持模型
中间件 | push模型 | pull模型 |
---|---|---|
RabbitMQ | 支持 | 支持 |
Kafka | --- | 支持(只有) |
RabbitMQ | 支持 | 支持 |
二、两种模型优缺点对比
Push 和 Pull 的区别
- 所谓 Push 模型,即当 Producer 发出的消息到达后,服务端马上将这条消息投递给 Consumer;
- 而 Pull 则是服务端收到这条消息后什么也不做,只是等着 Consumer 主动到自己这里来读,即 Consumer 这里有一个“拉取”的动作。
举个例子:
消息=食物;服务端Broker=老爸;消费端 = 儿子
如果采用Push模型:
优点:老爸一拿到食物就给儿子吃【食物送达及时】
缺点:儿子已经吃跑了,老爸还强塞儿子吃,可能导致儿子被撑死【儿子不堪重负,撑死了】如果采用Pull模型
优点:儿子饿了,主动找老爸要食物,不饿的时候不要。【儿子根据饥饿程度获取食物】
缺点:儿子饿了,发消息给老爸给我点食物,等老爸收到消息已经过了十分钟了,儿子等不及了饿死了。【食物没有及时送给儿子】
Push模型优缺点
-
Push模型优点
实时(因为服务端Broker一旦收到消息,就会发送给消费者,不管消费这准备好没有,消费者是死是活,缓存到消费端的BlockingQueue中) -
Push缺点
[1]、消息保存在服务端broker,容易造成消息堆积。(如何理解呢?为什么会存在服务端呢? 因为服务端Broker在和消费端第一次建立通信时就明确了该消费者的消费喜好,他选择的就是Push模型,那就不管三七二十一都发给你的缓存队列中去)。
[2]、服务端broker需要维护每次传输状态,遇到问题需要重试。(如何理解,为什么push模型broker要维护传输状态?Pull模型不需要吗?)
[3]、服务端broker需要依据订阅者消费能力做流控(流转机制)。(这个好理解,消费能力差时就不能疯狂塞消息给消费端,流转机制怎么做呢? RabbitMQ的做法是可以在消费者新建时,设置Qos,对服务端Borker提前表明消费端的消费能力,这样服务端最多推送指定数量的消息给消费者。)
Pull模型优缺点
-
Pull模型优点
[1] 保存在消费端,获取消息方便。(什么保存在消费端)
[2] 传输失败,不需要重试。(如何理解?)
[3] 消费端可以根据自身消费能力决定是否pull(流转机制) (这个好理解) -
Pull缺点
默认的端短询方式的实时性依赖于pull间隔时间,间隔越大,实时性越低,长轮询方式和push一致。(默认的端轮训指的是什么? 指的当长时间没有消息时,消费端实现的间隔时间去服务端轮训消息的过程)
三、两种模型在实际场景中的优缺点分析
选择 Push 还是 Pull
简要分析下 Push 和 Pull模型,在不同场景下各自存在的利弊。
场景 1:Producer 的速率大于 Consumer 的速率
对于 Producer 速率大于 Consumer 速率的情况,有两种可能性需要讨论:
- 第一种是Producer 本身的效率就要比 Consumer 高(比如说,Consumer 端处理消息的业务逻辑可能很复杂,或者涉及到磁盘、网络等 I/O操作);
- 另一种则是 Consumer 出现故障,导致短时间内无法消费或消费不畅。
Push方式由于无法得知当前 Consumer 的状态(),所以只要有数据产生,便会不断地进行推送,在以上两种情况下时,可能会导致 Consumer 的负载进一步加重,甚至是崩溃(比如生产者是 flume 疯狂抓日志,消费者是 HDFS+hadoop,处理效率跟不上)。除非Consumer 有合适的反馈机制能够让服务端知道自己的状况。(也可以 通过消费端限流方案,比如RabbitMQ消费者设置Qos,服务端Borker就会限制对消费端的发送流程,但是这个流量设置就需要衡量,不能太大也不能太小)
而采取 Pull 的方式问题就简单了许多,由于 Consumer 是主动到服务端拉取数据,此时只需要降低自己访问频率就好了。举例:如前端是 flume 等日志收集业务,不断往 CMQ 生产消息,CMQ 往后端投递,后端业务如数据分析等业务,效率可能低于生产者。
场景 2:强调消息的实时性
- 采用 Push 的方式时,一旦消息到达,服务端即可马上将其推送给服务端,这种方式的实时性显然是非常好的;
- 而采用 Pull 方式时,为了不给服务端造成压力(尤其是当数据量不足时,不停的轮询显得毫无意义),需要控制好自己轮询的间隔时间,但这必然会给实时性带来一定的影响。(Pull不会频繁拉取,设置一定间隔)。
场景 3:Pull 的长轮询
Pull 模式有什么问题呢?由于主动权在消费方,消费方无法准确地决定何时去拉取最新的消息。如果一次 Pull 取到消息了还可以继续去 Pull,如果没有 Pull 取到消息则需要等待一段时间再重新 Pull。
但等待时间就很难判定了。你可能会说,我可以有xx 动态拉取时间调整算法,但问题的本质在于,有没有消息到来这件事情决定权不在消费方。也许 1 分钟内连续来了 1000 条消息,然后半个小时没有新消息产生,可能你的算法算出下次最有可能到来的时间点是31分钟之后,或者 60 分钟之后,结果下条消息 10 分钟后到了,是不是很让人沮丧?
当然也不是说延迟就没有解决方案了,业界较成熟的做法是从短时间开始(不会对 CMQ broker 有太大负担),然后指数级增长等待。比如开始等 5ms,然后 10ms,然后 20ms,然后 40ms……直到有消息到来,然后再回到 5ms。即使这样,依然存在延迟问题:假设 40ms 到 80ms 之间的 50ms 消息到来,消息就延迟了 30ms,而且对于半个小时来一次的消息,这些开销就是白白浪费的。
总之就是消费端长时间没有消息消费的话,消费端轮训时间间隔如果太长,可能在轮训间隔中让部分消息延时消费,如果轮训时间太短,则频繁的请求在消耗服务端Broker,broker要应答消费端的请求(线程开销等)而造成服务端Broker的负担。
在腾讯云的 CMQ 里,有一种优化的做法-长轮询,来平衡 Pull/Push 模型各自的缺点。
基本方式是:消费者如果尝试拉取失败,不是直接 return,而是把连接挂在那里 wait,服务端如果有新的消息到来,把连接拉起,返回最新消息。
场景 4:部分或全部 Consumer 不在线
在消息系统中,Producer 和 Consumer 是完全解耦的,Producer 发送消息时,并不要求Consumer 一定要在线,对于 Consumer 也是同样的道理,这也是消息通信区别于 RPC 通信的主要特点;但是对于 Consumer不在线的情况,却有很多值得讨论的场景。
-
首先,在 Consumer 偶然宕机或下线的情况下,Producer 的生产是可以不受影响的,Consumer 上线后,可以继续之前的消费,此时消息数据不会丢失;但是如果 Consumer 长期宕机或是由于机器故障无法再次启动,就会出现问题,即服务端需不需要为 Consumer 保留数据,以及保留多久的数据等等。(消费端宕机,服务端Broker数据堆积)
-
采用 Push 方式时,因为无法预知 Consumer 的宕机或下线是短暂的还是持久的,如果一直为该 Consumer 保留自宕机开始的所有历史消息,那么即便其他所有的 Consumer 都已经消费完成,数据也无法清理掉,随着时间的积累,队列的长度会越来越大,此时无论消息是暂存于内存还是持久化到磁盘上(采用 Push 模型的系统,一般都是将消息队列维护于内存中,以保证推送的性能和实时性,这一点会在后边详细讨论),都将对 MQ 服务端造成巨大压力,甚至可能影响到其他 Consumer 的正常消费,尤其当消息的生产速率非常快时更是如此;但是如果不保留数据,那么等该 Consumer 再次起来时,则要面对丢失数据的问题。
折中的方案是:MQ 给数据设定一个超时时间,当 Consumer 宕机时间超过这个阈值时,则清理数据;但这个时间阈值也并太容易确定。
- 在采用 Pull 模型时,情况会有所改善;服务端不再关心 Consumer 的状态,而是采取“你来了我才服务”的方式,Consumer 是否能够及时消费数据,服务端不会做任何保证(也有超时清理时间)。
参考文章:
1 、选择 Push 还是 Pull(腾讯消息中间件CMQ技术文档)
2、 RabbitMQ之Consumer消费模式(Push & Pull)(厮大文章,抓包说明Push模型不会等待消费者是否消费完成前一个消息,就会发送第二个消息)。