之前博客《一文理解为什么需要使用消息队列》提到过,系统引入消息队列后,需要考虑如何保证消息队列的高可用。
本篇文章将围绕几个常见的消息队列中间件(RabbitMQ,RocketMQ,Kafka)进行逐个讲解。
RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。
单机模式,一般是开发者本地启动调试使用,不会应用到生产环境。
普通集群模式下,在多台机器上分别启动一个RabbitMQ实例。新创建的queue,只会放在其中一个RabbitMQ实例上,但是每个实例都同步queue的元数据(元数据是queue的一些配置信息,例如通过元数据可以找到queue所在实例)。在消费者进行消费的时候,如果连接到的实例没有指定的queue,那么这个实例会从queue所在实例上拉取数据过来。
这种模式下,要么消费者每次随机连接一个实例然后拉取数据,要么固定连接指定queue所在实例消费数据,前者有数据拉取的开销,后者可能会导致单实例性能瓶颈。
缺点:
普通集群模式相对单机模式,提升了消费速度,提高了吞吐量。
这种模式下RabbitMQ才能实现高可用。跟普通集群模式不一样的是,在镜像集群模式下,创建的queue,无论元数据还是queue里的消息都会完整存在于多个实例上。
这样就可以保证任何一个节点宕机后,其他节点还包含了这个queue的完整数据,consumer可以到其他的节点上去消费数据。
缺点:
注:
镜像集群模式相对普通集群模式,提升了可用性,但对吞吐量没有改善。
RocketMQ的集群方式分为:
RocketMQ中由NameServer集群、Broker 集群、Producer 集群和Consumer集群组成。
RocketMQ是通过Broker主从机制来实现高可用的。相同Broker名称,不同Brokerid的机器组成一个Broker组,BrokerId=0表明这个Broker是Master,BrokerId>0表明这个Broker是Slave。
消息生产的高可用:创建Topic时,把Topic的多个message queue创建在多个broker组上。这样当一个Broker组的Master不可用后,Producer仍然可以给其他组的Master发送消息。
消息消费的高可用:Consumer并不能配置从Master读还是Slave读。当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave读。这样当Master出现故障后,Consumer仍然可以从Slave读,保证了消息消费的高可用。
可以理解为:Rocketmq是通过多个Master实现写入容灾,通过主从实现读取容灾。
上图中,Broker Master1和Broker Slave1 是主从结构,实例之间会进行数据同步。同时每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer中。
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master建立长连接,且定时向Broker发送心跳。Producer只能将消息发送到Broker Master;是Consumer则不一样,它同时和提供Topic服务的Master和Slave建立长连接,既可以从Broker Master订阅消息,也可以从Broker Slave订阅消息。
在RocketMQ里面,1台机器要么是Master,要么是Slave,这在初始的机器配置里面就确定了。其中Master的Broker id = 0,Slave的Broker id > 0。有点类似于MySQL的主从概念,Master挂了以后,Slave仍然可以提供读服务,但是由于有多主的存在,当一个Master挂了以后,可以写到其他的Master上。
2.1 多Master模式
只有Master,无Slave。某个实例挂了,该实例在重启前未被消费的消息无法被消费。
优点:配置简单,性能最高。
缺点:单台机器重启或宕机期间,该机器下未被消费的消息在机器恢复前不可订阅,影响消息实时性。
2.2. 多Master多Slave异步模式
每个Master配一个Slave,有多对Master-Slave,集群采用异步复制方式,主备有短暂消息延迟,毫秒级
优点:性能同多Master几乎一样,实时性高,主备间切换对应用透明,不需人工干预。
缺点:Master宕机或磁盘损坏时会有少量消息丢失。
2.3. 多Master多Slave同步模式
每个Master配一个Slave,有多对Master-Slave,集群采用同步双写方式,主备都写成功,才向应用返回成功
优点:服务可用性与数据可用性非常高。
缺点:性能比异步集群略低(大约低10%)。
kafka 0.8以前,是没有HA机制的,就是任何一个Broker宕机了,那个Broker上的Partition就没法写也没法读,无法实现高可用性。
Kafka 0.8版本以后,增加了replica副本机制,从而实现了Kafka的高可用性。
如果对Kafka不了解的话,可以先看这篇博客《一文快速了解Kafka》。
Producer在发布消息到某个Partition时,先通过ZooKeeper找到该Partition的Leader,然后无论该Topic的Replication Factor为多少,Producer只将该消息发送到该Partition的Leader。Leader会将该消息写入其本地Log。每个Follower都从Leader pull数据。这种方式上,Follower存储的数据顺序与Leader保持一致。Follower在收到该消息并写入其Log后,向Leader发送ACK。当Leader收到了ISR中的所有Replica的ACK,该消息就被认为已经commit了,Leader将增加HW并且向Producer发送ACK。
为了提高性能,每个Follower在接收到数据后就立马向Leader发送ACK,而非等到数据写入Log中。因此,对于已经commit的消息,Kafka只能保证它被存于多个Replica的内存中,而不能保证它们被持久化到磁盘中,也就不能完全保证异常发生后该条消息一定能被Consumer消费。
Consumer读消息也是从Leader读取,只有被commit过的消息才会暴露给Consumer。
Kafka Replication的数据流如下图所示:
每个Partition的数据都会同步到其他机器上,形成自己的多个replica副本。然后所有replica会选举一个leader出来,那么生产和消费都跟这个leader打交道,然后其他replica就是follower。写的时候,leader会负责把数据同步到所有follower上去,读的时候就直接读leader上数据即可。只能读写leader?很简单,要是你可以随意读写每个follower,那么就要care数据一致性的问题,系统复杂度太高,很容易出问题。kafka会均匀的将一个partition的所有replica分布在不同的机器上,这样才可以提高容错性。
kafka的这种机制,就有所谓的高可用性了,因为如果某个broker宕机了,也没事儿,因为那个broker上面的partition在其他机器上都有副本的,那么此时会重新选举一个新的leader出来,大家继续读写那个新的leader即可。这就有所谓的高可用性了。
写数据的时候,生产者就写leader,然后leader将数据落地写本地磁盘,接着其他follower自己主动从leader来pull数据。一旦所有follower同步好数据了,就会发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)
消费的时候,只会从Leader去读,但是只有当一个消息已经被所有follower都同步成功并返回ack的时候,这个消息才能够被消费者读到。
Kafka通过Zookeeper来进行协调,而RocketMq通过自身的namesrv进行协调:
Kafka的多文件并发写入相对RocketMQ的单文件写入,Kafka的性能要好很多。但Kafka的大量文件存储会导致一个问题:当Broker中包含Partition特别多的时候,磁盘的访问会发生很大的瓶颈,毕竟单个文件看着是append操作,但是多个文件之间必然会导致磁盘的寻道。