首页 研习RocketMQ01之高可用与消息存储
文章
取消

研习RocketMQ01之高可用与消息存储

常见的主流消息队列有以下三款,各自定位也有所不同

  • RocketMQ:高性能可靠消息传输
  • RabbitMQ:可靠消息传输
  • Kafka:系统间的数据流通道(海量的数据通过它走通道,比如大数据日志分析,要的就是大量数据的吞吐量)

可以看出,RocketMQ 和 RabbitMQ 是扛业务流量的(属于OLTP),Kafka 偏重数据分析领域(属于OLAP)

三者的区别,大体如下:

支持分布式则说明扩展能力好,扩展能力好就能存很多消息,能存的消息多则表示堆积能力好

堆积能力好就能更好的实现削峰填谷(即把请求先缓存在MQ,再慢慢消费,它考验的是 MQ 的堆积能力)

 RocketMQRabbitMQKafka
数据可靠性
性能非常高
可用性分布式、主从主从分布式、主从
堆积能力非常好一般非常好
延时消息只支持特定Level死信队列实现不支持
事务消息支持不支持不支持
消息过滤支持不支持支持
消息查询支持不支持不支持
批量发送不支持不支持支持
消费失败重试支持支持不支持

高可用

上面是 RocketMQ 的拓扑图和架构图,可以看出,它主要有四个结构:

  • NameServer:集群管理
  • Broker:存储消息
  • Producer:生产者
  • Consumer:消费者

其特点如下:

  1. 每组Broker都是主从部署的,且都会注册自身信息到nameserver
  2. 每组Broker之间是没有数据同步的(即各个master之间都是独立的)
  3. 消费消息时,consumer会从nameserver获取topic所在的broker信息,然后建立broker连接,消费消息
  4. 生产消息时,producer会从nameserver获取可以存储topic的borker信息,然后建立broker连接,投递消息
  5. producer只会把消息投递到主broker(不会投递到从),而consumer则主从都会去消费(这是高可用的一个手段)
  6. nameserver集群的各个节点之间相互独立,且无任何的数据通信(它们并不知道彼此的存在)
    所以broker在注册时要注册到nameserver的所有节点上,不过nameserver节点很少,再加上还有hearbeat
    即便注册失败,还会通过心跳不停的注册来保证nameserver的数据一致性(所以它的节点很快就对齐了)
    同样,在做服务发现时,随便连到某一个节点上就可以找到broker了

虽然nameserver集群的实现方式有点偷懒,但broker节点的变化频率并不高,所以这并不会消耗过多的资源

而其分布式就体现在:可以很方便的扩展出一组Broker,然后注册到NameServer

所以说它的堆积能力强就是这个原因(现有的Broker写满了,可以很方便的扩出一组来,扩展性非常好)

可靠性

数据可靠,无非就是说数据不丢

这通常要从两个角度来看:固化(即刷盘,保证本地的数据可靠)和同步(即broker的主从部署,避免单点)

固化方式有两种:

  • 同步刷盘:性能低,可靠性高
         消息到达broker后,就先写到磁盘上,然后才会返回给producer说消息发送成功
  • 异步刷盘:性能高,可靠性低
         消息到达broker后,先在缓冲区攒着,然后直接返回给producer说消息发送成功
         具体刷盘的动作,是由异步线程在触发了某个阈值之后,再把缓冲区数据写到磁盘
         这个阈值一般就是时间和空间(每隔多长时间写一次,空间达到多少写一次)
         而丢数据的话,最多也就是丢两次刷盘之间的数据,但是性能高

通过刷盘,保证了本地的数据的可靠,但这还不够,因为分布式系统,就要避免单点
现在主库的数据可靠了,那从库呢(或者说多副本)?
所以要做到一致性写入,就得来看一下数据同步的方式

同步方式也有两种:

  • 同步双写:性能低,可靠性高(比如master挂了,没关系,slave有全量消息,能保证被消费)
  • 异步复制:性能高,可靠性低

实际部署时,除了根据数据一致性的要求来选择不同的固化和同步方式外,还要考虑机柜

部署这种存储产品时,两台机器都不会放到同一个机柜里面,而是各自独立放在不同的机柜

因为机柜同时掉电的概率太低了,这就等于是间接的保证了可靠性(也就没必要非得同步刷盘同步双写了)

实际生产环境用的异步写更多一些

可用性

这里有一个很重要点:broker主从模式如果master宕机,那么broker就会变成可读不可写的状态

这个特性能保证mstaer剩余未消费的消息,通过slave得到消费(消费的偏移量也会同步到slave)

对于未同步到slave的消息,如果此时broker可写的话,那么这些消息就会被跳过,就会造成它丢了

所以此时要求slave不可写,除非我们人为的把slave提升成master

对于broker的集群搭建方式,有以下不同:

  • 单master模式(相当于线下的测试环境,它没什么可用性,挂了就挂了)
  • 多master模式(可用性稍好些,但若挂了一个master,里面数据容易丢,所以这个模式意义不大)
  • 多master多slave模式(具体固化和同步方式,根据实际情况选择)

消息存储

这里有几个概念:

  • CommitLog:存储消息主体(虽然名字里有log,但不是日志,它存的是消息数据)
  • ConsumeQueue:消息消费队列
  • IndexFile:消息索引文件(它跟存储没啥太大关系,是给运维用的)

commitlog

所有producer生产的消息,都会追加写到commitlog里面

注意:这里是谁先到commitlog,谁就先写进去,保证了它是顺序写的

所以可能会出现:前俩消息是 topic1 的,第三个是 topic2 的,第四个是 topic1 的,第五六个是 topic2 的交叉情形

消费队列

由于commitlog并没有做什么优化(比如按照topic分类),所以就有了消费队列

在追加写commitlog的过程中,dispatch线程会按照偏移量一点点往下分发

每来一个消息,它都会根据topic来把消息分发到某个队列里面(注意是某个队列,不是所有队列)

而且一个topic可以对应到多个队列,具体分发给哪个队列则由负载均衡决定(该方案是在producer端做的)

这样一来,消息的写入和消费就会很快,因为它不是由固定队列来承担某个topic的所有消息,而是分摊的

实际消费

队列里存的不是消息实体(如果存消息内容,那commitlog也就没啥意义了)

而是消息的索引(即该消息在commitlog里的偏移量,以及消息实体的大小,和tags)

所以实际消费时就会根据偏移量到commitlog找到消息,然后取出消息内容,接着被消费端消费

随机消费

这里就有一个问题:topic是顺序写的,而消费则不是顺序消费的

即同一个topic连续投递过来的两个顺序消息,可能会被分发到不同的队列,导致消费不一定是连续的

所以会出现topic是顺序写到commitlog的,而消费则是在commitlog的一段范围内随机读的

虽然顺序写随机读这个问题,不如顺序读性能高,但其实影响不是很大(只是在做时间轮时有点影响)

重复消费

它只保证消息不丢,但不保证消息不重复

因为consumer在从消费队列取数据时,不是一条一条取的,而是一次取 N 条,然后去慢慢消费

若这个过程中consumer挂了,那么下次消费时,所取的数据还是会包括这一次的消息

直到consumer显式的回复 ack 给消费队列,消费队列才会去偏移(即移动offset)

所以,如果我们不能接受重复消息,那就得做幂等

优化

如果有优化需求,可以考虑从以下三个角度着手:

  • CommitLog文件切分(默认1G)
    假设某业务堆积了很多消息,然后它突然开始消费,此时堆积消息可能位于commitlog的中部或顶部
    那么就要加载一个很大的文件来读到那些堆积消息(代价有点大),所以做切分,按需加载就行了
  • MMap提升文件访问性能
    内存文件映射机制,可以认为它做了一次类似于零拷贝,减少了内核态和用户态的切换
    其实就是对写入做了一次优化,多次读直接读内存,减少了系统调用
  • SSD(就算是普通的机械盘,一般也不会有什么性能问题)
本文由作者按照 CC BY 4.0 进行授权