一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
RocketMQ整体设计思想 80%借鉴 Kafka,因此大部分地方和kafka看起来都是类似的。
1 RocketMQ的角色划分
- 生产者Producer: 消息的生产者,也称为发布者
- 消费者Consumer:消息的消费者,也称为订阅者
- Broker:可以简单的理解为就是RocketMQ的应用实例
- NameServer:RocketMQ的注册中心,RocketMQ需要启动时要先启动NameServer再启动Broker,Broker启动时需要向所有NameServer注册,生产发送消息之前需要从NameServer获取Broker服务列表,然后根据负载均衡算法从列表中选择一台进行消息发送。Broker需要每30S向NameServer发送一次心跳,NameServer每10S检测所有服务列表,如果有Broker超过120S都没有发送成功的心跳就会认为宕机移出服务列表。消费者消费时也需要从NameServer获取到Broker列表以进行消息获取。
2 RocketMQ的概念
- 生产者组:生产者组只是一个概念,并没有什么特殊的处理,只是用来方便管理,任何一个生产者都可以无限制的发送消息。生产者分组一是用来标识一类生产者,用来管理。二是针对事务消息,发送事务消息时如果生产者中途宕机,Broker会主动回调生产者组内的任意一台机器来确认状态。
- Message ID:由RocketMQ生成的消息的全局唯一标识(通过机器IP和消息偏移量组成),值得注意的是Message ID存储重复的可能,如果需要严格的唯一标识,需要使用Message Key。
- Message Key:主动为消息设计的Key,发送消息时可以为消息设置此属性,后期可以通过这个Key来查找消息,RocketMQ专门创建了基于Hash索引的索引文件来快速查找目标消息,因此应该尽量保证唯一避免Hash冲突,也可以用这个作为业务方面的唯一标识以进行幂等处理。
- Tag:消息标签,相当于二级消息类型,用来对同一个Topic内的消息进一步分类,可以用这个来进行过滤,让消费者只消费同一Topic中自己感兴趣的消息。
- Queue: 接收到消息后,所有主题的消息都存储在commitlog文件中,以此来实现顺序写。消息存储到commitlog后,再一步转发到消息队列,也就是consumerqueue,Queue是数据分片的产物,数据分片可以提高消费效率(这一点和Kafka不同,Kafka没有Queue的概念),主题的MessageQueue数量可以设置。
3 RocketMQ为什么注册中心使用NameServer而不是直接使用Zookeeper
NameServer的核心就是收集Topic的信息,以供给生产者和消费者进行获取,这些Zookeeper是可以满足了,除此之外Zookeeper还具备服务发现,服务治理,集群Master选举等更强大的功能,但是这些额外强大的功能带来的却是Zookeeper对于RocketMQ太重了,有很多RocketMQ用不到的功能,不如重新做一个轻量级的注册中心。毕竟RocketMQ仅仅需要一个注册中心,而且对数据一致性要求不高,这一点上根据CAP理论,一个分布式系统最多只能满足两个点,Zookeeper是一个CP系统,如一个Zookeeper集群有三个节点A,B,C, 如果C和AB发生网络分区,即使C和客户端之间的网络是正常的,Zookeeper集群为了避免脑裂会将C下线。而如果恰好这个客户端是和C在一个分区,那么客户端又无法访问AB,或者说客户端仅仅配置了C的服务列表,就会导致这个客户端无法获得RocketMQ的Broker服务列表,同样的一旦Leader宕机需要重新选举,选举期间整个集群也是不可用的。这对于RocketMQ来说是不愿意见到的,RocketMQ不同于其他中间件的注册中心需要很好的一致性,对于RocketMQ来说可用性大于一致性,即使注册中心获取的数据是旧的,有瑕疵的,其操作的影响也有限,也好过完全不可用。
NameServer不需要像Zookeeper那样时刻关注状态并作出调整,甚至,NameServer都不需要有一个集群的管理者。以至于,NameServer看起来都不像一个集群。事实上,NameServer本质上来看,也不是一个集群。因为它的各个节点是独立的,不相关的,每个NameServer都是独立和Producer,Consumer或Broker来打交道,甚至同一时间NameServer之间的数据都不完全一样,节点中关于Broker、Topic的信息不会进行持久化,只会放在内存中。(支持持久化,但是一般不用)
4 RocketMQ的特点
- 消息的存储基于文件的顺序读写,这使得RocketMQ在消息发送上有极高的吞吐量。磁盘比想象中的快的多,但是也比想象中的慢的多,这取决于是否是顺序读写,磁盘的顺序读写和随机读写差距远超10倍百倍,目前高性能的磁盘顺序写速度能达到600M/S,超过一般网卡的传输速度,这是比想象中快的地方,而随机写速度只有100KB/S,相差了6000倍,这是比想象中慢的地方。
- 消息至少被消费一次:有相应的ACK机制,保证消息至少消费一次。
- 消息重试:消费发生异常,支持消息重新投递
- RocketMQ容忍设计缺陷,他不保证一个消息仅仅被消费一次,可能存在重复消费的情况,对于这种情况需要消费者自己进行处理。
- 多种消息类型:包括普通消息,定时消息,顺序消息,事务消息
- 定时消息:消息发送到Broker以后可以设置定时时间,不立即消费,而是到达指定时间再消费。不过这个过期时间并不支持任意指定,而是提供了几个定时间隔来选择。(Apache版本不支持,但是阿里内部版本支持自定义)
- 消息回溯:已消费的消息在有效期内可以重新消费
- 消息堆积:采用磁盘存储消息,从而拥有很强的消息堆积处理能力,同时存储文件过期删除机制来避免无限堆积,过期删除不在乎消息是否被消费,只在乎消息是否过期,这一点需要注意!
- 消息过滤:消费者可以根据过滤规则只消费自己需要的消息,同时支持服务端和消费端两种机制。
- RocketMQ能够严格的保证消息有序。
- 高可用:宕机场景下,RocketMQ能保证本机消息不丢失或者丢失少量(这取决于刷盘机制),另外支持主从,即使主机硬盘损坏,也能保证消息不丢失或者丢失少量(这取决于同步机制)
- 低延迟:RocketMQ支持推模式使得消息低延迟消费(实际上还是通过拉模式实现的,因为存储的推模式弊端太大了,这个放到后面再说)
5 RocketMQ的消息发送
对于普通消息来说有三种发送方式:
- 单向发送:发送方只负责发送消息,不等待服务器响应且没有回调函数触发,耗时极短,为微妙级别,但是存在消息丢失的风险。用于需要耗时短,可靠性不高的场景,如日志收集。
- 可靠同步发送:消息发出后,接到Broker明确的响应后再进行下一步,不会丢失消息。
- 可靠异步发送:消息发出后,不等响应直接返回,但是提供回调接口对发送结果进行处理,不会丢失消息。
消息发送的主要流程分为验证消息,查找路由,消息发送三个阶段:
- 验证阶段对主体名称消息体做校验,消息长度不能为0,但是也不能大于4M(默认可修改)
- 查找路由:客户端会缓存topic路由信息(每30S更新一次),如果是第一次发送消息,就先去NameServer尝试获取。路由消息包含Broker的MessageQueue的相关信息。生产者会随机选择一个MessageQueue来作为发送目标。
- 消息发送:选择消息队列发送消息,成功则返回。消息发送支持批量,但是依然不能大于4M,而且一次批量内要求是同一个Topic(Tag无限制)。如果发送失败则会采用规避原则(上一次失败后,一段时间内就不再选择该Broker上的消息队列,以此来提高发送成功率,避免Broker宕机但是客户端还未更新路由导致的连续失败)。发送失败重试次数在发送时通过retryTimesWhenSendFailed指定。
6 RocketMQ的消息消费
RocketMQ的消息消费分为集群消费和广播消费两种:
- 集群消费:集群消费模式下,消息队列中的一条消息只需要被集群内的任意一个消费者处理即可。消息处理失败重新投送时不保证路由到同一台机器。消费进度维护在服务端,可靠性更高。
- 广播消费:广播消费模式时,消息队列中的每条消息需要推送给集群内所有注册过的客户端,保证消息至少被每台机器消费一次。消费进度在客户端维护,出现重复的概率大于集群模式,不支持顺序消息,不支持重置消息位点,不会对失败的消息重投,客户端重启后会从最新消息消费,跳过重启之前未处理的消息。如果需要更可靠的广播模式,可以用集群描述模式,即为每一个消费者都建立一个消费者组。
7 RocketMQ的消费方式
7.1 推模式
代码上使用DefaultMQPushConsumer,这种模式下系统收到消息后会主动推送给客户端,自动保存offset,并且加入新的消费者后会自动做负载均衡。但是纯粹的推模式虽然实时性高,但是存在不可控,加大Server的工作量,各个Server消费能力不一致,如果产生堆积会存在各种潜在问题等弊端。因此RocketMQ在底层实现上,还是通过拉模式来实现的,RocketMQ通过长轮询的拉模式来实现推模式,这样即保证了Pull的优点,又确保了实时性。具体方式为客户端还是拉取消息,Broker端HOLD住客户端发过来的请求一段时间,在这个时间内(5S)有新消息到达就用现有的链接立即返回消息给Consumer,这种消息获取的主动权还掌握在Consumer手里,即使有大量的消息积压,也不会主动推给Consumer,因为长轮询本身的局限性,HOLD请求期间需要占用资源,因此适用于消息队列这种客户端链接数可控的场景。要注意推模式虽然是用拉取来实现,但是确不是一条一条的拉,而是批量的拉取,交给Consumer处理。
7.1.1 负载均衡
集群消费的场景下,会有多个消费者消费一个主题,一个主题又有多个MessageQueue。多个消费者消费多个队列,RocketMQ默认提供5种分配算法:假设有8个消息队列(q1,q2,q3,q4,q5,q6,q7,q8),有三个消费者(c1,c2,c3):
- 1) 平均分配(AllocateMessageQueueAveragely) c1:q1,q2,q3 c2:q4,q5,q6 c3:q7,q8,
- 2) 平均轮询分配(AllocateMessageQueueAveragelyByCircle) c1:q1,q4,q7 c2:q2,q5,q8 c3:q3,q6
- 3) 一致性 Hash(AllocateMessageQueueConsistentHash) 不推荐使用,因为消息队列负载均衡信息不容易跟踪
- 4) 根据配置(AllocateMessageQueueByConfig) 为每一个消费者配置固定的消费队列
- 5) 根据 Broker 部署机房名(AllocateMessageQueueByMachineRoom) 对每一个消费者负载不同 Broker 上的队列
一般采用平均分配或者平均轮询分配。RocketMQ每20S重新进行一次队列负载,重新负载时会先查询出当前所有的消费者,并对消费者,消费者队列进行排序。 这样有消费者上线或者下线都能够进行重新负载。
7.1.2 流量控制
推模式具有流量控制的功能,根据未处理的消息数,消息总大小,offset的跨度三个维护来控制,如果任一值超过设定的大小就隔一段时间再拉取消息,从而达到流量控制的效果。如果当前的MessageQueue正在处理的消息大于1000或者主题的不同队列的最大最小偏移量差距大于2000(避免一条消息阻塞导致进度无法向前推进导致的大量消息重复消费),那么这个队列的下一次拉取任务将在50毫秒后才加入拉取任务队列。注意推模式的底层还是拉取,流量控制这个功能是在消费者端通过延长启动拉取任务实现的,而不是Broker端实现的。
7.1.3 ACK机制
对于消费的成功和失败,推模式下业务实现消费回调式时,回调函数返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS即表示消费成功,ConsumeConcurrentlyStatus.RECONSUME_LATER表示消费失败,对于消费失败的信息,RocketMQ会将其放到一个名为%RETRY%+consumergroup 的重试队列中,这个重试队列RocketMQ内部自己创建,并在10S(默认)后重新将这条消息进行投递,如果消费失败次数达到16次(默认),就会将消息投递到DLQ死信队列,进入人工干预流程。
而对于消费进度,广播模式因为所有的消费者都需要消费主题下的所有消息,消费者的行为是独立的,因此消息进度存储在消费者本地。而集群模式下,消费者共同消费,因此消费进度存在服务器Broker上,集群模型下的消费者组会在每个MessageQueue中维护一个consumer offset来标识这个消费者组的消费进度,这样如果因为新增加或者减少了消费者,进行了重新负载均衡后,新的消费者就可以知道从哪里开始消费。消费者并不会在消费成功后就立即将消费进度同步到Broker,而是先更新到本地,如果消费者并发设置不是1,那么也是多个线程共享这个本地偏移量,然后由定时器同步到Broker,为了保证消息至少消费一次,本地又只会记录一批消息中最小的offset。这种方式相对于传统的单条ACK有了性能上的提升,但是也带了重复消费的风险:如果拉了2100-2200的消息,在这一批消息完全消费完成之前,消费进度只能维持在2101,即使2101后面的消息都已经消费完毕,只有在所有消息完全消费完成,才能将本地消费进度更新为2201,然后定时更新给Broker,如果在未完全消费时机器宕机或者线程被Kill,那么下次拉取时还是会从2101开始,这就造成了消息的重复的消费。因此RocketMQ多次强调消息要支持幂等。
7.1.4 批量消费
可以通过setConsumeMessageBatchMaxSize(maxSize)设置批量消费,默认为1,maxSize为最大消息量,并不是说一定要等到这个量才会消费,可以理解为批量消费要在消息有积压的情况下才算有效,如果没有积压,生产者投送一个消息,这边有空闲的消费者,那么就会被立即消费掉,导致看起来还是单条的样子。
7.2 拉模式
代码上使用DefaultMQPullConsumer,较之推模式使用起来更加复杂,需要自己去获取Topic的MessageQueue(一个Topic会有多个)并进行遍历获取,好处是可以选择指定的MessageQueue只消费哪一个,而且消费进度Offset需要客户端自己维护,在拉取时传入,而且针对不同的消息状态(FOUND获取到消息,NO_MATCHED_MSG没有匹配的消息,NO_NEW_MSG没有新消息,OFFSET_ILLEGAL非法偏移量)需要自己进行不同的处理。 这种方式虽然非常灵活,但是也带来了额外的工作量,因此如果没有特殊的需要,一般使用推模式。
8 顺序消息
全局顺序消息:全局计费消息需要把Topic的读写队列设置成1,然后生产者和消费者的并发设置也是1,这样就能实现全局顺序消息,缺点是吞吐量会很受限制,可以用作一下类似先到先得的场景上
部分顺序消息:对于一个Topic,消息根据Sharding Key进行区块分区,同一个分区内的消息会按照严格的FIFO的顺序发布和消费,Sharding Key的单独的一个字段,不是消息的Message Key。
9 延时消息
消息发送到Broker以后可以设置定时时间,不立即消费,而是到达指定时间后才能消费。使用于一些有时间窗口要求的场景,比如订单提交30分钟内不支付就取消。不过这个过期时间在Apache的RocketMQ上并不支持任意指定,而是提供了几个定时间隔来选择,分别是"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h", 使用是通过msg.setDelayTimeLevel(level)来设置, level标识等级, 1标识1S, 2标识5S,一直到18标识 2h,其他方面和普通消息一样。 不过在阿里云的RocketMQ中,消息的延迟是可以任意指定。
10 事务消息
如果需要同时投递到多个队列的消息,这些消息要么全部成功要么全部失败,或者说需要先投递一条,然后进行一些逻辑处理,投递的这个消息要和后续的逻辑保持同步,要么全部成功,要么全部失败。为此RocketMQ提供了事务消息。RocketMQ的事务消息采用类似于2PC的机制。
- 发送方向RocketMQ发送待确认(prepare)消息
- RocketMQ接到待确认消息后写入一个叫做RMQ_SYS_TRANS_HALF_TOPIC的主题中并进行持久化
- 发送方执行本地逻辑
- 发送方根据本地执行结果向RocketMQ发送Commit或者Rollback消息,RocketMQ只有收到Commit才会将消息移入实际的目标Topic中进行消费。
- 为了避免网络问题或者发送方宕机,对于RMQ_SYS_TRANS_HALF_TOPIC中的消息,每过一分钟,RocketMQ都会进行一次事务回查来确认消息状态来判定Commit还是Rollback,如果发送方宕机了,那么RocketMQ会去回查同一个Producer Group里的其他Producer 。
11 死信队列
如果消息的消费失败次数达到16次(默认),就会将消息投递到DLQ死信队列,进入人工干预流程。死信队列的有效期和正常消息相同,为3天,超过3天就会被自动删除,因此需要在3天内处理。每个消费者组只有一个死信队列,在第一个死信消息产生时创建,无论消息属于哪个Topic,只要是这个消费者组的,都会被放到这个死信队列中。
12 消息过滤
RocketMQ支持消息过滤,让消费者只消费自己感兴趣的消息
- Tag过滤:Consumer在订阅消息时除了制定Topic之外还能制定Tag,多个Tag可以用 || 分割。不要要注意的是服务端的过滤仅仅是通过Tag的HashCode进行比较,而不是严格的字符串比对,所以在客户端依然要对Tag进行重新比对确认
- SQL92过滤:默认关闭,通过SQL expression制定过滤方式
- 类过滤:4.3.0版本后已经不支持
13 RocketMQ的存储
MQ分为支持持久化和不支持持久化两种,ZeroMQ不支持持久化存储,ActiveMQ,RabbitMQ, Kafka,RocketMQ都是支持持久化的。
在持久化效率上,文件系统大于KV存储,KV存储又大于关系型数据库,但是可靠性上讲又是相反的。
13.1 消息存储结构
RocketMQ采用文件系统存储,主要的存储文件包含Commitlog文件,ConsumerQueue文件,IndexFile。
- Commitlog文件:所有主题的消息都存储在这个文件中,以此来确保消息发送时的顺序写,保证消息发送的高吞吐量。Commitlog文件默认一个的大小为1G,每个消息的前4个字节存储消息的总长度,但是因为消息内容不同,一个消息的存储长度是不固定的。
- ConsumerQueue文件:消费者订阅是基于主题的订阅,而所有主题的消息都放在Commitlog里面,直接检索麻烦且性能差,为了提高消费效率,RocketMQ会在消息写入Commitlog后异步的将消息写入ConsumerQueue中,一个主题有多个MessageQueue,每个MessageQueue对应一个ConsumerQueue文件。为了加速检索速度和存储空间。ConsumerQueue内并不会直接存储消息内容,仅仅会存储commitlog offset, size, tag hashcode三部分内容,commitlog offset用来指向该条消息在commitlog文件中物理存储的地址,size为消息的长度,tag hashcode记录tag标签的hashcode,用来执行消息过滤,从这里也能说明消息过滤是通过 tag的hashcode完成的,客户端依然要对Tag进行重新比对确认。因为ConsumerQueue存储的内容尺寸有限,因此大部分的Consumer都可以被读入内存(并不意味着不会写入磁盘,只是读入内存做缓存),即使因为宕机导致ConsumerQueue的部分消息丢失,因为Commitlog中有消息的全量信息,也可能进行恢复。
- IndexFile:IndexFile存的是索引文件,用来加快消息查询的速度,是一个hash索引,采用拉链法。要注意,key是MessageKey,而不是Message ID。
13.2 内存映射
内存映射就是由一个文件到一块内存的映射,文件的数据就是这块内存区域中对应的数据,读写文件的数据,直接对与内存对应的那块地址操作即可,减少了内存复制环节,效率要高,文件越大差距越大。RocketMQ就采用了这种方式,Commitlog,ConsumeQueue和IndexFile都被设计成固定长度,一个文件写满再创建一个新文件。
13.3 刷盘机制
数据要进行持久化,必不可少的要进行刷盘。RocketMQ提供了两种刷盘方式
异步刷盘:消息写入了内存成功就返回成功,写操作的返回快,吞吐量大,当内存里的消息积累到一定程度,统一进行刷盘。
同步刷盘:消息写入内存后开始写入磁盘,磁盘写入成功后才返回成功的状态。
刷盘方式是以Broker为维度配置的,通过Broker配置文件中的flushDiskType 来设定刷盘方式,可选值为 ASYNC_FLUSH (异步刷盘), SYNC_FLUSH 同步刷盘) 默认为异步。
14 过期文件删除
RocketMQ通过定时任务删除过期文件,通过判断文件最后一次更新时间距离当前的间隔,如果超过了fileReservedTime(默认42小时),那么就认为是过期文件可以删除。值得注意的是RocketMQ并不会关注这个文件上的消息是否已经被消费。删除文件是一个很耗费IO的操作,可能会影响到消息的插入,因此RocketMQ提供了deletePhysicFilesInterval参数,用来在同时有多个文件过期的场景下指定执行文件删除的间隔。另外如果在删除时发现这个文件还有线程引用,那么会通过destroyMapedFileIntervalForcibly设置文件最大的保存时间,在这时间内这个文件拒绝删除,当超过这个时 间时,会将引用每次减少 1000,直到引用 小于等于 0 为止,即可删除该文件。
删除定时任务定时任务每10s执行一次,但是并不是一天24小时执行,而是通过deleteWhen设置,默认为凌晨4点开始执行一个小时。除此之外如果磁盘空间占用达到85%(默认),也会触发过期文件删除任务。当空间占用率达到90%(默认),RocketMQ会阻止新消息写入。
15 主从同步
对于一个分布式系统来说,高可用(HA)是必不可少的,Broker的高可用通过主从同步来实现,Broker分为Master和Slave两个角色,Master接到消息后会同步给Slave,这样在Master宕机时,Slave可以接替其提供服务。NameServer的高可用则是通过部署多台来实现,上面也说了,NameServer之间相互独立,每个NameServer都是独立和Producer,Consumer或Broker来打交道,这就使得只要不是NameServer全挂,就依然能够提供服务。
集群模式下一个Topic对应的Queue尽量分布在集群中的多个节点,以减少节点故障时所造成的影响。对于Broker来说,Master负责读写,Slave仅仅负责读,类似MySQL的主备。 主从之间的数据同步,RocketMQ提供了异步复制和同步双写两种方式。同步双写保证数据不丢失,但是性能会比异步复制差一点。另外还记得上面提到的刷盘方式吗?要和同步方式区分开来,他们是不同的概念。对于数据同步要求较高的场景,可以设置成同步双写,刷盘设置为异步刷盘,这样既保证了数据的备份,有保证了吞吐量。
RocketMQ主从同步异步复制的方式是从服务器启动时主动向主服务器建立TCP长连接,然后获取服务器的commitlog最大偏移,以此偏移向主服务器主动拉取消息,主服务器根据偏移量,与自身文件的最大偏移量比较,如果大于从服务器提供的偏移量,则主服务器向从服务器返回一定数量的消息,该过程循环进行,达到主从的数据同步。
RocketMQ的读写分离与其他中间件完全不同,消费者首先去主服务器拉取消息,RocketMQ根据主服务器的压力以及主从同步状态,来建议消费者下次拉取是从主服务器还是从服务器拉取。一般拉取的消息超过主服务器的常驻内存大小,就会建议消费者下次从从服务器拉取。
开发成长之旅 [持续更新中...]
欢迎关注…