push
Push即服务端主动发送数据给客户端。在服务端收到消息之后立即推送给客户端。
当 Producer 发出的消息到达后,服务端马上将这条消息投递给 Consumer。
客户端连接到broker之后,启动一个线程,这个线程的任务就是循环调用方法从broker中拉取相应的消息至本地。如果是异步方法调用,则直接调用监听器方法,间接调用业务消费消息的方法,而不使用本地内存进行消息的缓存;所以这里的异步只是客户端的异步,而非broker的主动推送。通过这种方式既能解决多客户端的连接,也能解决类似服务端的push型的消息推送。在互联网中这种实现才具有普便性,因为这种方式即解决了性能问题又解决了异步消息的需求。
Push模型最大的好处就是实时性。因为服务端可以做到只要有消息就立即推送,所以消息的消费没有“额外”的延迟。
但是Push模式在消息中间件的场景中会面临以下一些问题:
-
在Broker端需要维护Consumer的状态,不利于Broker去支持大量的Consumer的场景
-
Consumer的消费速度是不一致的,由Broker进行推送难以处理不同的Consumer的状况
-
Broker难以处理Consumer无法消费消息的情况(Broker无法确定Consumer的故障是短暂的还是永久的)
-
大量的推送消息会加重Consumer的负载或者冲垮Consumer
pull
Pull模式由Consumer主动从Broker获取消息。
当服务端收到这条消息后什么也不做,只是等着 Consumer 主动到自己这里来读,即 Consumer 这里有一个“拉取”的动作。
客户端(指一个connection,一般情况指一个tcp的连接建立)连接到broker之后,启动一个线程,这个线程的任务就是循环调用方法从broker中拉取相应的消息至本地。如果是同步方法调用获取则将相应的消息存放在本地内存中,当同步方法消费消息时,则从该内存区中直接获取相应的消息进行消费;
这样带来了一些好处:
-
Broker不再需要维护Consumer的状态(每一次pull都包含了其实偏移量等必要的信息)
-
状态维护在Consumer,所以Consumer可以很容易的根据自身的负载等状态来决定从Broker获取消息的频率
场景1:Producer 速率大于 Consumer 速率
当 Producer 速率大于 Consumer 速率时,有两种可能性:一种是 Producer 本身的效率就要比 Consumer 高(例如,Consumer 端处理消息的业务逻辑可能很复杂,或者涉及到磁盘、网络等 I/O 操作);另一种是 Consumer 出现故障,导致短时间内无法消费或消费不畅。
Push 方式由于无法得知当前 Consumer 的状态,所以只要有数据产生,便会不断地进行推送,在以上两种情况下时,可能会导致 Consumer 的负载进一步加重,甚至是崩溃(例如生产者是 flume 疯狂抓日志,消费者是 HDFS+hadoop,处理效率跟不上)。除非Consumer 有合适的反馈机制能够让服务端知道自己的状况。
而采取 Pull 的方式问题就简单了许多,由于 Consumer 是主动到服务端拉取数据,此时只需要降低自己访问频率即可。举例:如前端是 flume 等日志收集业务,不断向 CMQ 生产消息,CMQ 向后端投递,后端业务如数据分析等业务,效率可能低于生产者。
场景2:强调消息的实时性
采用 Push 的方式时,一旦消息到达,服务端即可马上将其推送给消费端,这种方式的实时性显然是非常好的;而采用 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,对于半个小时来一次的消息,这些开销就是白白浪费的。
CMQ 提供了长轮询的优化方法,用以平衡 Pull/Push 模型各自的缺点。基本方式是:消费者如果尝试拉取失败,不是直接 return,而是把连接挂在那里 wait,服务端如果有新的消息到来,把连接拉起,返回最新消息。
场景4:部分或全部 Consumer 不在线
在消息系统中,Producer 和 Consumer 是完全解耦的,Producer 发送消息时,并不要求Consumer 一定要在线,对于 Consumer 也是同样的道理,这也是消息通信区别于 RPC 通信的主要特点;但是对于 Consumer 不在线的情况,却有很多值得讨论的场景。
首先,在 Consumer 偶然宕机或下线时,Producer 的生产是可以不受影响的,Consumer 上线后,可以继续之前的消费,此时消息数据不会丢失;但是如果 Consumer 长期宕机或是由于机器故障无法再次启动,就会出现问题,即服务端是否需要为 Consumer 保留数据,以及保留多久的数据等等。
采用 Push 方式时,因为无法预知 Consumer 的宕机或下线是短暂的还是持久的,如果一直为该 Consumer 保留自宕机开始的所有历史消息,那么即便其他所有的 Consumer 都已经消费完成,数据也无法清理掉,随着时间的积累,队列的长度会越来越大,此时无论消息是暂存于内存还是持久化到磁盘上(采用 Push 模型的系统,一般都是将消息队列维护于内存中,以保证推送的性能和实时性,这一点会在后边详细讨论),都将对 CMQ 服务端造成巨大压力,甚至可能影响到其他 Consumer 的正常消费,尤其当消息的生产速率非常快时更是如此;但是如果不保留数据,那么等该 Consumer 再次起来时,则要面对丢失数据的问题。
折中的方案是:CMQ 给数据设定一个超时时间,当 Consumer 宕机时间超过这个阈值时,则清理数据;但这个时间阈值也并不容易确定。
在采用 Pull 模型时,情况会有所改善;服务端不再关心 Consumer 的状态,而是采取“你来了我才服务”的方式,Consumer 是否能够及时消费数据,服务端不会做任何保证(也有超时清理时间)。
Dynamic Push/Pull
“在Broker一直有可读消息的情况下,long-polling就等价于执行间隔为0的pull模式(每次收到Pull结果就发起下一次Pull请求)。”
这是long-polling在服务端一直有可消费消息的处理情况。在这个情况下,一条消息如果在long-polling请求返回时到达服务端,那么它被Consumer消费到的延迟是:
假设Broker和Consumer之间的一次网络开销时间为R毫秒,
那么这条消息需要经历3R才能到达Consumer
第一个R:消息已经到达Broker,但是long-polling请求已经读完数据准备返回Consumer,从Broker到Consumer消耗了R
第二个R:Consumer收到了Broker的响应,发起下一次long-polling,这个请求到达Broker需要一个R
的时间
第三个R:Broker收到请求读取了这条数据,那么返回到Consumer需要一个R的时间
所以总共需要3R(不考虑读取的开销,只考虑网络开销)
另外,在这种情况下Broker和Consumer之间一直在进行请求和响应(long-polling变成了间隔为0的pull)。
考虑这样一种方式,它有long-polling的优势,同时能减少在有消息可读的情况下由Broker主动push消息给Consumer,减少不必要的请求。
消息中间件的Consumer实现
在消息中间件的Consumer中会有一个Buffer来缓存从Broker获取的消息,而用户的消费线程从这个Buffer中获取消费来消息,获取消息的线程和消费线程通过这个Buffer进行数据传递。
-
pull线程从服务端获取数据,然后写入到Buffer
-
consume线程从Buffer获取消息进行消费
有这个Buffer的存在,是否可以在long-polling请求时将Buffer剩余空间告知给Broker,由Broker负责推送数据。此时Broker知道最多可以推送多少条数据,那么就可以控制推送行为,不至于冲垮Consumer。
广播
最后来了解一下广播,先看一下广播的概念:
广播消息是指生产者产生的消息将分发给所有订阅这个消息的消费者,而普通的模式是:一批消息可以被多个人共同消费,如consumer1可能消费1,3,5记录,而consumer2可能消费的是2,4,6这种模块就是共同消费模块;而今天说的是广播消息,它是指一些消息同时被推送到多个订阅者,而这些订阅者收到的消息都是完整的,如consumer1收到的会是1,2,3,4,5,6,而consumer2回到的也会是1,2,3,4,5,6,这种就像广播一样,把消息广播给多人!
当然只有在消息到达服务器之前订阅的消费者才能收到,未订阅的消费组是无法收到消息的