1.设计目标
RocketMq被设计用来做一个消息中间件,这点与kafka不一样,kafka最初是用来做日志处理的(所以kafka允许丢消息,日志丢失是可以允许的,可以允许消 息堆积,日志很多,可能处理不及时),RocketMq由于设计初衷是作为一个消息中间件,所以他的事务机制做的特别好,并且支持消息的分类等功能。
2. 特性
- 经历过双十一的考验
- 支持集群
- 支持持久化(零拷贝/随机顺序存储/页缓存)
- 真正支持事务消息
- Netty Nio
- NameServer(自己开发的,类似与ZK用来实现服务协调)
- 可以指定消费某一类的消息
- 官方提供了监控平台
- 可以实现数据消费的pull和push
3.集群方式
单master
master挂了会导致不可用
多master
一个master挂了不影响整个系统的可用性,但是可能会丢失一部分数据
多master多slave(同步复制)
多master多slave(异步复制)
4.name server
nameserver在这里是一个注册中心的作用,但是这个注册中心是一个轻量级的注册中心,保证了AP,broker,消费者,生产者在启动的时候都会先访问nameserver并与nameserver保持长连接,每个30秒发送心跳来告诉nameserver健康状态。
上面这个故事,就讲述了NameServer路由注册的基本原理。
NameServer就相当于卫星,内部会维护一个Broker表,用来动态存储Broker的信息。
而Broker就相当于邮局,在启动的时候,会先遍历NameServer列表,依次发起注册请求,保持长连接,然后每隔30秒向NameServer发送心跳包,心跳包中包含BrokerId、Broker地址、Broker名称、Broker所属集群名称等等,然后NameServer接收到心跳包后,会更新时间戳,记录这个Broker的最新存活时间。
正常情况下,如果Broker关闭,则会与NameServer断开长连接,Netty的通道关闭监听器会监听到连接断开事件,然后会将这个Broker信息剔除掉。
异常情况下,NameServer中有一个定时任务,每隔10秒扫描一下Broker表,如果某个Broker的心跳包最新时间戳距离当前时间超多120秒,也会判定Broker失效并将其移除。
细心的人会发现一个问题,NameServer在清除失活Broker之后,并没有主动通知生产者,生产者每隔30秒会请求NameServer并获取最新的路由表,那么就意味着,消息生产者总会有30秒的延时,无法实时感知Broker服务器的宕机。所以在这个30秒里,生产者依旧会向失活Broker发送消息,那么消息发送的高可用性如何保证呢?
要解决这个问题得首先谈一谈Broker的负载策略,消息发送队列默认采用轮询机制,消息发送时默认选择异常重试机制来保证消息发送的高可用。当Broker宕机后,虽然消息发送者无法第一时间感知Broker 宕机,但是当消息生产者向Broker发送消息返回异常后,消息生产者会选择另外一个Broker上的消息队列,这样就规避了发生故障的Broker,结合重试机制,巧妙实现消息发送的高可用,同时由于不需要NameServer通知众多不固定的生产者,也降低了NameServer实现的复杂性。
在降低NameServer实现复杂性方面,还有一个设计亮点就是NameServer之间是彼此独立无交流的,也就是说NameServer服务器之间在某个时刻的数据并不会完全相同,但是异常重试机制使得这种差异不会造成任何影响。
5.集群大体工作流程
- nameserver启动,监听端口,等待producer,consumer,broker连接上来
- broker启动,与nameserver保持长链接,定期向nameserver发送心跳信息,包含broker的ip,端口,当前broker上topic的信息
- 创建topic(也可以自动创建)
- producer启动,与nameserver建立长链接,拿到broker的信息,然后就可以给broker发送消息了
- consumer启动,与nameserve建立长链接,拿到broker的信息,然后就可以建立通道,消费消息
6.生产者
同步发送
一条消息一定可以接收到一个mq的回执,在调用send方法之后,程序会一直阻塞,知道mq的回执返回之后才能开始后面的步骤
异步发送
一条消息可以先发送,然后提供一个回掉方法,程序不用等待mq的回执,等到mq回执来了会直接走回掉方法。
oneway
发送一条消息而不要回执,这种效率会非常高,但是可靠性不高,消息可能没有发送成功。
延迟发送
目前只支持1m,5m,10m,30m,1m,2m,3m,4m,5m,6m,7m,8m,9m,10m,30m,1h,2h不能自定义延迟。
7.消费者
广播消费(发布订阅)
集群消费(点对点)
同组的多个消费者,每个消费者负责固定的几个Queue(消费者数不要超过Queue数)
消费失败
当消费者消费失败的时候(这里的失败是我们手动定义的),rocketmq不会让失败的消息妨碍后面消息的正常消费,它会将失败的消息放入重试队列中,另起一个线 程去对失败的消息进行重试。
重试队列名为原消费者组的名称前加上%RETRY%(重试队列是针对消费组,而不是针对每个Topic设置的)。RocketMQ对于重试消息的处理是先保存至Topic名称为 “SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。现在我们已经知道消 费失败的消息会进入重试队列,那么多久重试一次呢?能进行多少次的重试呢?
考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时间,重试次数越多投递延时就越大。有一个参数 messageDelayLevel,这个参数是在服务器端的Broker上配置的,默认是
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
1
默认是最多可以重试16次
如果重试了16次之后,这条消息还是没有被成功消费,那么就认为这条消息是抢救不过来了,此时,消息队列不会立刻将消息丢弃,于是它被放入了死信队列中, 上面重试队列的图中你也可以看到死信队列,死信队列的名称是在原队列名称前加%DLQ%。如果你还是不死心的话,觉得这条消息还能抢救一下,可以开启一个后台 线程不断扫描死信队列然后继续重试,也可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费
ConsumeFromWhere
消费者从那个位置消费,分别为:
CONSUME_FROM_LAST_OFFSET:第一次启动从队列最后位置消费,后续再启动接着上次消费的进度开始消费
CONSUME_FROM_FIRST_OFFSET:第一次启动从队列初始位置消费,后续再启动接着上次消费的进度开始消费
CONSUME_FROM_TIMESTAMP:第一次启动从指定时间点位置消费,后续再启动接着上次消费的进度开始消费
以上所说的第一次启动是指从来没有消费过的消费者,如果该消费者消费过,那么会在broker端记录该消费者的消费位置,如果该消费者挂了再启动,那么自动从 上次消费的进度开始
消费流程
消费者配置
pushConsumer
consumerGroup DEFAULT_CONSUMER Consumer 组名,多个 Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组
messageModel CLUSTERING 消息模型,支持以下两种 1、集群消费 2、广播消费
consumeFromWhere CONSUME_FROM_LAST_OFFSET Consumer 启动后,默认从什么位置开始消费
allocateMessageQueueStrategy AllocateMessageQueueAveragely Rebalance 算法实现策略
subscription {} 订阅关系
messageListener 消息监听器
offsetStore 消费进度存储
consumeThreadMin 10 消费线程池数量
consumeThreadMax 20 消费线程池数量
consumeConcurrentlyMaxSpan 2000 单队列并行消费允许的最大跨度
pullThresholdForQueue 1000 拉消息本地队列缓存消息最大数
pullInterval 0 拉消息间隔,由于是长轮询,所以为 0,但是如果应用为了流控,也可以设置大于 0 的值,单位毫秒
consumeMessageBatchMaxSize 1 批量消费,一次消费多少条消息
pullBatchSize 32 批量拉消息,一次最多拉多少条
pullConsumer
consumerGroup DEFAULT_CONSUMER Consumer 组名,多个Consumer 如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组
brokerSuspendMaxTimeMillis 20000 长轮询,Consumer 拉消息请求在 Broker 挂起最长时间,单位毫秒
consumerTimeoutMillisWhenSuspend 30000 长轮询,Consumer 拉消息请求在 Broker 挂起超过指定时间,客户端认为超时,单位毫秒
consumerPullTimeoutMillis 10000 非长轮询,拉消息超时时间,单位毫秒
messageModel BROADCASTING消息模型,支持以下两种 1、集群消费 2、广播消费
messageQueueListener 监听队列变化
offsetStore 消费进度存储
registerTopics [] 注册的 topic 集合
allocateMessageQueueStrategy AllocateMessageQueueAveragely Rebalance 算法实现策略
- 事务消息
rocketmq支持事务消息,使用TransactionMQProducer构建一个事务消息生产者。
- 顺序发送/消费
- 幂等性
rocketmq无法保证幂等性,如果要想保证幂等性,要自己在业务系统中完成。
生产者幂等性:
为消息设置一个唯一的id,每次发送之前都去判断当前要发送的id是否已经发送过,发送完之后将当前消息的id再保存起来。
一般保存id可以使用redis的set集合。
消费者幂等性:
消息消费的时候每消费一条都把消费过消息的id存到redis中,每次消费的时候先判断这条消息是否已经消费。或者直接在数据库层面做去重,保证相同的数据不会重复添加
解决幂等性要引入更大的复杂性,如果不是对幂等性要求比较高,可以不做幂等性处理。
- 数据过期策略
可以按照时间和大小来清理
- 刷盘机制和复制机制
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。
RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种
写磁盘方式:
1)异步刷盘方式:在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,
吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入
2)同步刷盘方式:在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻
通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
同步刷盘还是异步刷盘,是通过Broker配置文件里的flushDiskType参数设置的,这个参数被设置成SYNC_FLUSH、ASYNC_FLUSH
中的一个
如果一个broker组有Master和Slave,消息需要从Master复制到Slave上,有同步和异步两种复制方式。
同步复制是等Master和Slave
均写成功后才反馈给客户端写成功状态;异步复制方式是只要Master写成功即可反馈给客户端写成功状态
这两种复制方式各有优劣,在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master
出了故障,有些数据因为没有被写入Slave,有可能会丢失;在同步复制方式下,如果Master出故障,Slave
上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。
同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成
ASYNC_MASTER、SYNC_MASTER、SLAVE三个值中的一个。
实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式,尤其是SYNC_FLUSH方式,由于频繁
的触发写磁盘动作,会明显降低性能。通常情况下,应该把Master和Slave设置成ASYNC_FLUSH的刷盘方式,
主从之间配置成SYNC_MASTER的复制方式,这样即使有一台机器出故障,仍然可以保证数据不丢。
- 失败问题
mq的使用一定会伴随消费失败,生产失败的问题,如何去处理这些问题成了保证程序健壮性的一大难题。
失败种类:
生产失败:
生产者在发送消息到mq的时候,首先会发送消息到mq,mq返回一个ack(除了在oneway的情况下生产者不接受ack),生产者接收到ack之后才知道自己这个消息是否发送成功,如何没成功生产者会自动进行重试(默认重试两次,加正常发送的一次,总共会发送3次)。但是在实际环境下,网络中的消息可能会丢失,会有几种情况:
- 生产者的消息没有发送到mq
- mq接收到消息并且返回ack,但是ack丢失没有发送到生产者
这两种情况下生产者都不会得到ack,那么生产者都会认为这条消息发送失败,进行重试或者其他失败措施。不同的是第二种情况是mq已经接收到消息,生 产者如果重发就会有多条相同的消息发往mq,导致mq中消息重复,这也就是幂等性问题。但是不管怎么说,如果不是网络崩溃很长世间,一般的网络抖动是不会出现消息丢失的,只是有可能消息重复(rocketmq没有解决幂等性问题)。
消费失败:
消费的时候
- 其他问题:
nameserver默认端口:9876
broker默认端口:10911
读队列和写队列?
读写队列,则是在做路由信息时使用。在消息发送时,使用写队列个数返回路由信息,而消息消费时按照读队列个数返回路由信息。在物理文件层面,只有写队列才会创建文件。举个例子:写队列个数是8,设置的读队列个数是4.这个时候,会创建8个文件夹,代表0 1 2 3 4 5 6 7,但在消息消费时,路由信息只返回4,在具体拉取消息时,就只会消费0 1 2 3这4个队列中的消息,4 5 6 7压根就没有消息。反过来,如果写队列个数是4,读队列个数是8,在生产消息时只会往0 1 2 3中生产消息,消费消息时则会从0 1 2 3 4 5 6 7所有的队列中消费,当然 4 5 6 7中压根就没有消息 ,假设消费group有两个消费者,事实上只有第一个消费者在真正的消费消息(0 1 2 3),第二个消费者压根就消费不到消息。
由此可见,只有readQueueNums>=writeQueueNums,程序才能正常进行。最佳实践是readQueueNums=writeQueueNums。那rocketmq为什么要区分读写队列呢?直接强制readQueueNums=writeQueueNums,不就没有问题了吗?
rocketmq设置读写队列数的目的在于方便队列的缩容和扩容。思考一个问题,一个topic在每个broker上创建了128个队列,现在需要将队列缩容到64个,怎么做才能100%不会丢失消息,并且无需重启应用程序?
最佳实践:先缩容写队列128->64,写队列由0 1 2 ......127缩至 0 1 2 ........63。等到64 65 66......127中的消息全部消费完后,再缩容读队列128->64.(同时缩容写队列和读队列可能会导致部分消息未被消费)
rocketmq实现了读写分离。master可以写,slaver只能读
rocketmq的数据存储与kafka有所区别,kafka是将所有的消息存储在segment里面,每个segment大小是固定的,会随着partition的增大不断分裂出来segment。rocket不一样,它将一个broker上的所有消息都存储在一个commitlog文件里,然后在consumerQueue里面存储每个queue中的数据文件,这个数据文件并没有实际的消息数据,而是存储了消息的元数据,比如该消息在commitlog中的offset(实际上consumerQueue中的内容就是一个索引的作用,让我们可以快速拿到commitlog中的消息或消息的元数据)。