RocketMq与Kafka相关知识汇总

RocketMq与Kafka相关知识汇总

    • 架构
    • 对比
    • 消息持久化
    • 服务可用性保障——复制与 failover 机制
    • 消息队列的高级特性
      • 顺序消息
      • 事务消息
      • 消息回放
      • 影响单机性能的因素
      • 硬件层面
      • 应用层面
    • 最佳实践
      • Producer最佳实践
      • Consumer最佳实践
      • 其他配置

架构

在Kafka中,是1个topic有多个partition,每个partition有1个master + 多个slave。
broker是个物理概念,1个broker就对应1台机器。在Kafka里面,Master/Slave的选举,有2步:第1步,先通过ZK在所有机器中,选举出一个KafkaController;第2步,再由这个Controller,决定每个partition的Master是谁,Slave是谁。
这里的Master/Slave是动态的,也就是说:当Master挂了之后,会有1个Slave切换成Master。
创建Topic时,对于kafka来说,你指定topic,它的partition个数,它的master/slave配比,然后系统自动从所有机器中,为每个topic_partition分配1个master + 多个slave。

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic

RocketMq与Kafka相关知识汇总_第1张图片
RocketMQ
在RocketMQ里面,1台机器只能要么是Master,要么是Slave。这个在初始的机器配置里面,就定死了。Broker是个逻辑概念,1个broker = 1个master + 多个slave。所以才有master broker, slave broker这样的概念。其架构拓扑图如下:
RocketMq与Kafka相关知识汇总_第2张图片
在RocketMQ中,不需要选举,Master/Slave的角色也是固定的。当一个Master挂了之后,你可以写到其他Master上,但不会说一个Slave切换成Master。消费者从slave消费消息,但slave不能写消息。创建Topic时,对于RokcetMQ来说,你指定topic,它的queue个数,它对应的cluster。然后系统自动建立这个cluster(多个master + 多个slave) 和你的topic之间的映射关系。
RocketMQ的关键设计:
分布式集群化
强数据安全
海量数据堆积
毫秒级投递延迟(推拉模式)

对比

数据可靠性
RocketMQ支持异步实时刷盘,同步刷盘,同步复制,异步复制
Kafka使用异步刷盘方式,异步复制/同步复制

总结:RocketMQ的同步刷盘在单机可靠性上比Kafka更高,不会因为操作系统Crash,导致数据丢失。Kafka同步Replication理论上性能低于RocketMQ的同步Replication,原因是Kafka的数据以分区为单位组织,意味着一个Kafka实例上会有几百个数据分区,RocketMQ一个实例上只有一个数据分区,RocketMQ可以充分利用IO组Commit机制,批量传输数据,配置同步Replication与异步Replication相比,性能损耗约20%~30%,Kafka没有亲自测试过,但是个人认为理论上会低于RocketMQ。

性能对比
Kafka单机写入TPS约在百万条/秒,消息大小10个字节
RocketMQ单机写入TPS单实例约7万条/秒,单机部署3个Broker,可以跑到最高12万条/秒,消息大小10个字节
总结:Kafka的TPS跑到单机百万,主要是由于Producer端将多个小消息合并,批量发向Broker。 RocketMQ为什么没有这么做?
缓存过多消息,GC是个很严重的问题
Producer调用发送消息接口,消息未发送到Broker,向业务返回成功,此时Producer宕机,会导致消息丢失,业务出错,Producer通常为分布式系统,且每台机器都是多线程发送,我们认为线上的系统单个Producer每秒产生的数据量有限,不可能上万。
缓存的功能完全可以由上层业务完成。

单机支持的队列数
Kafka单机超过64个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长。Kafka分区数无法过多的问题
RocketMQ单机支持最高5万个队列,负载不会发生明显变化
队列多有什么好处?
单机可以创建更多话题,因为每个主题都是由一批队列组成
消费者的集群规模和队列数成正比,队列越多,消费类集群可以越大

消息投递实时性
Kafka使用短轮询方式,实时性取决于轮询间隔时间,0.8以后版本支持长轮询。
RocketMQ使用长轮询,同Push方式实时性一致,消息的投递延时通常在几个毫秒。

消费失败重试
Kafka消费失败不支持重试。
RocketMQ消费失败支持定时重试,每次重试间隔时间顺延
总结:例如充值类应用,当前时刻调用运营商网关,充值失败,可能是对方压力过多,稍后再调用就会成功,如支付宝到银行扣款也是类似需求。这里的重试需要可靠的重试,即失败重试的消息不因为Consumer宕机导致丢失。

严格的消息顺序
Kafka支持消息顺序,但是一台代理宕机后,就会产生消息乱序
RocketMQ支持严格的消息顺序,在顺序消息场景下,一台Broker宕机后,发送消息会失败,但是不会乱序,MySQL的二进制日志分发需要严格的消息顺序

定时消息
Kafka不支持定时消息
RocketMQ支持两类定时消息
开源版本RocketMQ仅支持定时级别,定时级用户可定制,阿里云MQ指定的毫秒级别的延时时间

分布式事务消息
Kafka不支持分布式事务消息
RocketMQ支持分布式事务消息

消息查询
Kafka不支持消息查询
RocketMQ支持根据消息标识查询消息,也支持根据消息内容查询消息(发送消息时指定一个消息密钥,任意字符串,例如指定为订单编号)
总结:消息查询对于定位消息丢失问题非常有帮助,例如某个订单处理失败,是消息没收到还是收到处理出错了。

消息回溯
Kafka理论上可以按照偏移来回溯消息
RocketMQ支持按照时间来回溯消息,精度毫秒,例如从一天之前的某时某分某秒开始重新消费消息
总结:典型业务场景如consumer做订单分析,但是由于程序逻辑或者依赖的系统发生故障等原因,导致今天消费的消息全部无效,需要重新从昨天零点开始消费,那么以时间为起点的消息重放功能对于业务非常有帮助。

消费并行度
Kafka的消费并行度依赖Topic配置的分区数,如分区数为10,那么最多10台机器来并行消费(每台机器只能开启一个线程),或者一台机器消费(10个线程并行消费)。即消费并行度和分区数一致。
RocketMQ消费并行度分两种情况
顺序消费方式并行度同卡夫卡完全一致
乱序方式并行度取决于Consumer的线程数,如Topic配置10个队列,10台机器消费,每台机器100个线程,那么并行度为1000。

消息轨迹
Kafka不支持消息轨迹
RocketMQ支持消息轨迹

开发语言友好性
Kafka采用scala编写
RocketMQ采用的Java语言编写

消息过滤
Kafka不支持代理端的消息过滤
RocketMQ支持两种代理端消息过滤方式
根据消息变量来过滤,相当于子主题概念
向服务器上传一段Java代码,可以对消息做任意形式的过滤,甚至可以做Message身体的过滤拆分。

消息堆积能力
理论上Kafka要比RocketMQ的堆积能力更强,不过RocketMQ单机也可以支持亿级的消息堆积能力,我们认为这个堆积能力已经完全可以满足业务需求。

开源社区活跃度
Kafka社区更新较慢
RocketMQ的GitHub的社区有250个个人,公司用户登记了联系方式,QQ群超过1000人。

商业支持
Kafka原开发团队成立新公司,目前暂没有相关产品看到
RocketMQ在阿里云已经商业化,目前以云服务形式供大家商用,并向用户承诺99.99%的可靠性,同时彻底解决了用户自己搭建MQ产品的运维复杂性问题

成熟度
Kafka在日志领域比较成熟
RocketMQ在阿里集团内部有大量的应用在使用,每天都产生海量的消息,并且顺利支持了多次天猫双十一海量消息考验,是数据削峰填谷的利器。

消息持久化

Kafka 会在 Broker 上为每一个 topic 创建一个独立的 partiton 文件,Broker 接受到消息后,会按主题在对应的 partition 文件中顺序的追加消息内容。
而 RocketMQ 则会创建一个 commitlog 的文件来保存分片上所有主题的消息。Broker 接收到任意主题的消息后,都会将消息的 topic 信息,消息大小,校验和等信息以及消息体的内容顺序追加到 Commitlog 文件中,Commitlog 文件一般为固定大小,当前文件达到限定大小时,会创建一个新的文件,文件以起始便宜位置命名。同时,Broker 会为每一个主题维护各自的 ConsumerQueue 文件,文件中记录了该主题消息的索引,包括在 Commitlog 中的偏移位置,消息大小及校验和,以便于在消费时快速的定位到消息位置。ConsumerQueue 的维护是异步进行的,不影响消息生产的主流程,即使 ConsumerQueue 没有及时更新的 情况下,服务异常终止,下次启动时也可以根据 Commitlog 文件中的内容对 ConsumerQueue 进行恢复。
这样的文件结构也就决定了,在同步刷盘的场景下,RocketMQ 是顺序写,而 Kafka 是随机写。通常情况下,我们认为顺序写的性能远高于随机写,尤其时对于传统的机械硬盘来讲更是如此。 且当 Broker 上的 topic 数量增多时,RocketMQ 在写消息的性能上几乎不会受到影响,而对 Kafka 的影响则会较大。

而在消费时,因为可以根据一定的缓存策略将热数据提前缓存到内存中,所以不管哪种方式对于磁盘的要求都不是太高。不过对于 RocketMQ 来说,内存加载时会加载一整个 Commitlog 文件,如果同一个 Broker 上的两个主题,一个主题的消息积压了很长时间开始才开始消费,而另一个主题在及时消费新发送的消息时,Broker 可能会频发的读取文件更新到缓存中,造成磁盘性能损耗,进而影响到生产时的发送性能。所以虽然 RocketMQ 支持海量消息积压,但如果是在共享的集群中,还是建议用户最好能做到及时消费,保证集群中所有主题都在消费相近时间段的消息,这样命中内存缓存的概率会比较高,消费时不会带来额外的磁盘开销。

需要补充说明的是,在做技术选型时,还需要考虑到硬件的发展。现今固态硬盘虽然价格较机械硬盘还是高出很多,但普及度越来越高。而固态硬盘在乱序写时,性能表现比机械硬盘会好很多,特别是多线程同时进行写操作时,性能也会比单线程顺序写强。对于需要同步刷盘保证数据可靠性的应用,磁盘读写性能的重要性一般来讲也会远高于磁盘的空间大小。 成本上来讲,如果可以显著的提高单机性能,虽然单价来看固态硬盘更加昂贵,但是如果可以节省部分 CPU,内存和机架位置,还是很划算的。

服务可用性保障——复制与 failover 机制

复制的实现,最简单的方式就是一主一从的结构,开源版本的 RocketMQ 即使用了这种模式。由两个 Broker 实例组成一组服务,一个作为主节点,提供读写服务,一个作为从节点,在正常情况下只从主节点同步数据不提供读写服务,且每个 topic 都会分配到多个 Broker 分组上。当某个从节点发生故障时,可以禁止主节点的写入,依然允许消费者继续消费该节点中未处理完成的消息。而生产者有新消息过来时,由其它主从都健康的分组提供服务, 直到故障机器恢复后主节点重新提供读写服务。如果故障机器无法恢复,只需等积压消息全部消费完,替换故障机器即可。 如果主节点故障,则可以在从节点进行消费,其它处理方式与从节点故障处理方式一致。 这种方式的优点是逻辑简单,实现也简单,简单意味着稳定,隐藏的 bug 少。且数据只需要一份冗余,对磁盘空间的开销相对较少,可以保证大多数情况下的数据可靠性和服务可用性。

Kafka 的复制策略,使用的是 ISR(可用服务列表)的方式,可以把他看成主从结构的升级版。对于每一个 partiton,可以分配一个或多个 Broker。 其中一个作为主节点,剩余的作为跟随者,跟随者会保存一个 partition 副本。生产者将消息发送到主节点后,主节点会广播给所有跟随者,跟随者收到后返回确认信息给主节点。 用户可以自由的配置副本数及当有几个副本写成功后,则认为消息成功保存。且同时,会在 ZooKeeper 上维护一个可用跟随者列表,列表中记录所有数据和主节点完全同步的跟随者列表。当主节点发生故障时,在列表中选择一个跟随者作为新的主节点提供服务。在这种策略下,假设总共有 m 个副本,要求至少有 n 个(0

还有一种实现是基于 Raft 算法实现的多副本机制,具体细节可以参考官方的 paper。Raft 集群一般由奇数节点构成,如果要保证集群在 n 个节点故障的情况下可用,则至少需要有 2n+1 个节点。 与 ISR 方式相比,Raft 需要耗费更多的资源,但是整个复制和选举过程都是集群中的节点自主完成,不需要依赖 ZooKeeper 等第三者。 理论上 Raft 集群规模可以无限扩展而 ISR 模式下集群规模会受限于 ZooKeeper 集群的处理能力。

消息队列的高级特性

顺序消息

一般情况下,因为消息分布在不同的 Broker 上,且有多个客户端同时消费,各实例间的网络状态和处理能力都是不一定的,所以分布式消息系统是没有办法保证消息的处理顺序的。但如果你了解了一般消息队列的文件结构,你就会发现不管是 Kafka 的 partition 那种方式,还是 RocketMQ 的方式,都可以保证同一个 partition 或者同一个 ConsumerQueue 内的消息是可以保证顺序的。剩下的,我们需要做的就是将需要保证顺序的消息放入到同一个 partiton 或者 queue 中就好了, 最简单的方式是我们只为主题分配一个 partition 或者 queue,这样就可以保证严格的顺序,但是这样就不能体现分布式系统的性能优势了,集群的处理能力没有办法横向扩展。

在实际的生产中,大多数情况下我们其实并不需要所有的消息都顺序处理,更多时候只要求具有相同特征的消息保证顺序,如电商系统中,一般要求具有相同订单号的消息需要保证顺序,不同的订单之间可以乱序,也就是说我们只要保证有办法将具有相同订单编号的消息放入到同一个队列或者 partition 中即可。

Kafka 提供了指定 partition 发送的功能,使用者可以在客户端根据业务逻辑自行处理,还有的消息队列支持根据某个字段的值,将消息 hash 到消息指定消息队列中。 指定 partition 和 hash 两种方式的主要区别,就是当有某个分片故障时,指定 partition 的方式会导致部分消息发送失败,而 hash 的方式有可能造成少量消息的乱序。

事务消息

事务消息主要指消息生产过程中,需要确保发送操作和其它业务逻辑处理结果的一致性,要么都成功要么都失败。 比如要同时执行写入 MySQL 数据库和发送消息两种操作,要保证写库成功同时发送消息也成功,如果写库失败,消息也要取消发送。事务消息的实现一般是依赖两步提交策略。 已写库并发消息为例,首先客户端将消息发送到 Broker,Broker 收到消息后,给客户端返回一个确认信息。 这时消息在服务端是处于一种中间状态,消费者不可以消费这种状态的消息。 客户端收到确认消息后,执行写数据库的操作,写库成功后,向 Broker 再发送一个提交信息。 服务端收到提交信息后将消息更改就绪状态,允许消费者正常消费。 同时,生产者客户端还要提供一个回调方法,当 Broker 收到消息后,长时间没有收到确认信息时,调用客户端提供的回调方法进行回滚,如重置数据库。

消息回放

有时,消费者可能会因为系统问题或其它原因,需要重新消费已经消费完的消息。大部分消息队列都可以实现这个功能,一般将次功能称为消息回放。要实现消息回放的功能,需要保证消息不会在消费成功之后立刻删除,而是保存一段时间后,根据一定策略,如一周后删除。同时,还需要对消费者当前消费的消费位置进行记录,RocketMQ 和 Kafka 都会通过一个 Offset 文件来记录消费者的消费位置,当消费者消费完成功,更新并提交 Offset。一般来说,Offset 文件中还需要记录最大消费位置,即已经入队的最新一条消息所在的位置和最小消费位置,即还没删除的最老的消息所在的位置。Offset 文件可以保存在服务端,也可以保存在客户端,也可以保存在 ZooKeeper 中,或者其它如 Redis 之类的第三方存储。 早期版本(0.9.0 之前)的 Kafka 是将消息保存在 ZooKeeper 中,之后为了减轻 ZooKeeper 的负担,将 Offset 保存到 Broker 对应的 topic 中。 RocketMQ 则支持有两种模式,默认是集群模式,topic 中的每条消息只会集群中的一个实例消费,这种模式由服务端管理 Offset,还有一种是广播模式,集群中的所有实例都会消费一份全量消息,这种模式由客户端管理 Offset。

影响单机性能的因素

Kafka 和 RocketMQ 都是优秀的分布式消息系统,当需要服务于有较高高吞吐量要求的服务时,都可以通过扩容来解决需求。虽然如此,我们也不应放弃对单机吞吐量的追求,毕竟单机处理能力越高,意味着可以节省更多资源。而决定单机性能的因素,我能想到的主要有下面几个方面:

硬件层面

硬盘:一般来说消息队列的瓶颈主要在磁盘 IO,更好的硬盘会带来更高的性能,正常情况性能由高到低排序为 NVMe > 传统 SSD(Non-Volatile Memory express) >SAS >SATA。 对于 SAS 盘和 SATA 盘这种机械硬盘来说,还要看具体硬盘的转速。
Raid 卡: Raid 卡的型号和性能,以及是否带有 Raid 卡缓存,Raid 卡的鞋策略是 WriteThough 还是 WriteBack 也会影响到服务的 I/O 性能进而影响到吞吐量。
系统层面
Raid 级别:当有 4 块盘时,Raid0,Raid5 和 Raid10 三种形式的 Raid 会对 I/O 性能造成不同的影响。Raid0 因为不需要任何其它操作,速度是最快的,几乎等于单盘写速度的四倍。Raid10 需要写一份数据和一份镜像,写性能是略小于单盘写速度的两倍的;Raid5 每次写入可以有三个盘提供写服务,另一个盘来存放校验和,由于计算校验和存在一定的性能损耗,写速度略小于单盘写速度的三倍,而且随着硬盘数量的增多,Raid5 计算校验和造成的开销会随之增大,如果没有 Raid 卡缓存支撑的话,对于 SSD 硬盘,由于 partial-stripe 些问题,raid5 性能可能会低于 Raid10 的。
Linux I/O 调度算法:Linux 内核包含 Noop,Deadline,CFG, Anticipatory 四种 I/O 调度算法,需要结合应用特性和硬件选择合适的调度算法。 据说 SSD 硬盘更适合使用 Noop 算法。
文件系统的 block size:调整合适的文件系统的 block size 也会提高吞吐量。
SWAP 的使用: SWAP 空间使用过程中会造成一定的 I/O 开销,如果内存充足的情况下,可以关闭 SWAP 功能。

应用层面

文件读写的方式:一般来说,顺序读写速度远高于随机读写,且一次性读写的文件越大相对来说效率越高。应用可以据此来对文件结构和读写方式做一定优化。
缓存策略: 应用可以通过一定的缓存策略,提前将可能用到的数据读到内存中,当收到请求时,如果能命中缓存中的数据,在缓存中直接读取效率远高于读写磁盘。同样,写操作时也可以通过缓存将零散的写操作进行汇集,提高写操作的效率。 所有适合的缓存策略将显著提高 Broker 的处理能力。

rocketmq在发送消息时,会先去获取topic的路由信息,如果topic是第一次发送消息,由于nameserver没有topic的路由信息,所以会再次以“TBW102”这个默认topic获取路由信息,假设broker都开启了自动创建开关,那么此时会获取所有broker的路由信息,消息的发送会根据负载算法选择其中一台Broker发送消息,消息到达broker后,发现本地没有该topic,会在创建该topic的信息塞进本地缓存中,同时会将topic路由信息注册到nameserver中,那么这样就会造成一个后果:以后所有该topic的消息,都将发送到这台broker上,如果该topic消息量非常大,会造成某个broker上负载过大,这样消息的存储就达不到负载均衡的目的了。

在Broker进程被Kill的场景, Kafka和RocketMQ都能在保证吞吐量的情况下,不丢消息,可靠性都比较高。
在宿主机掉电的场景,Kafka与RocketMQ均能做到不丢消息(同步刷盘),此时Kafka的吞吐量会急剧下跌(Kafka本身是没有实现任何同步刷盘机制,可以通过修改异步刷盘的频率来实现。设置参数log.flush.interval.messages=1,即每条消息都刷一次磁盘),几乎不可用。RocketMQ则仍能保持较高的吞吐量。

这是因为Kafka的每个Topic、每个分区都会对应一个物理文件。当Topic数量增加时,消息分散的落盘策略会导致磁盘IO竞争激烈成为瓶颈。而RocketMQ所有的消息是保存在同一个物理文件中的,Topic和分区数对RocketMQ也只是逻辑概念上的划分,所以Topic数的增加对RocketMQ的性能不会造成太大的影响。

最佳实践

Producer最佳实践

1、一个应用尽可能用一个 Topic,消息子类型用 tags 来标识,tags 可以由应用自由设置。只有发送消息设置了tags,消费方在订阅消息时,才可以利用 tags 在 broker 做消息过滤。
2、每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。由于是哈希索引,请务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。
3、消息发送成功或者失败,要打印消息日志,务必要打印 sendresult 和 key 字段。
4、对于消息不可丢失应用,务必要有消息重发机制。例如:消息发送失败,存储到数据库,能有定时程序尝试重发或者人工触发重发。
5、某些应用如果不关注消息是否发送成功,请直接使用sendOneWay方法发送消息。

Consumer最佳实践

1、消费过程要做到幂等(即消费端去重)
2、尽量使用批量方式消费方式,可以很大程度上提高消费吞吐量。
3、优化每条消息消费过程

其他配置

线上应该关闭autoCreateTopicEnable,即在配置文件中将其设置为false。
RocketMQ在发送消息时,会首先获取路由信息。如果是新的消息,由于MQServer上面还没有创建对应的Topic,这个时候,如果上面的配置打开的话,会返回默认TOPIC的(RocketMQ会在每台broker上面创建名为TBW102的TOPIC)路由信息,然后Producer会选择一台Broker发送消息,选中的broker在存储消息时,发现消息的topic还没有创建,就会自动创建topic。后果就是:以后所有该TOPIC的消息,都将发送到这台broker上,达不到负载均衡的目的。
所以基于目前RocketMQ的设计,建议关闭自动创建TOPIC的功能,然后根据消息量的大小,手动创建TOPIC。

RocketMQ消息订阅有两种模式,一种是Push模式,即MQServer主动向消费端推送;另外一种是Pull模式,即消费端在需要时,主动到MQServer拉取。但在具体实现时,Push和Pull模式都是采用消费端主动拉取的方式。

从上面的分析我们已经知道:对于ConsumeQueue,是完全的顺序读写。可是对于CommitLog,Producer对其“顺序写”,Consumer却是对其“随机读”。
对于这样的一个大型文件,又要随机读,如何提高读写效率呢?
答案就是“内存映射文件”。关于内存映射文件的原理,以后有机会,可以在LInux的篇章中,详细分析。此处就不展开了。
对于RocketMQ来说,它是把内存映射文件串联起来,组成了链表。因为内存映射文件本身大小有限制,只能是2G。所以需要把多个内存映射文件串联成一个链表,来和一个屋里文件对应起来。
Commit Log,一个文件集合,每个文件1G大小,存储满后存下一个,为了讨论方便可以把它当成一个文件,所有消息内容全部持久化到这个文件中;Consume Queue:一个Topic可以有多个,每一个文件代表一个逻辑队列,这里存放消息在Commit Log的偏移值以及大小和Tag属性。
RMQ的Consume Queue是不会存储消息的内容,任何一个消息也就占用20 Byte,所以文件可以控制得非常小,绝大部分的访问还是Page Cache的访问,而不是磁盘访问。正式部署也可以将Commit Log和Consume Queue放在不同的物理SSD,避免多类文件进行IO竞争。

你可能感兴趣的:(java,中间件)