目录
概述
不同服务之间的通信
- RPC请求(HTTP请求):远程直接调用其他服务;
- 消息中间件:通过消息存储和转发实现通信。
消息中间件的应用场景
- 异步解耦
如果服务A直接和服务B、C、D连接,服务A是顺序调用服务B、C、D的,会造成服务B、C、D之间耦合。
- 削峰填谷
当客户端请求量超出业务系统的承受范围时,消息因为由消息中间件接收,请求不会丢失,消息只是堆积在消息中间件中,业务系统可以继续处理,不会故障。
- 消息分发
同一数据的大量访问,降低页面访问时间。
常见的消息中间件
关心指标:高吞吐,低延迟。
- ActiveMQ
Apache开源的比较老的消息中间件,完全支持JMS规范,API丰富,现在较少使用。
- RibbitMQ
实现了高级消息队列协议(AMQP)的开源消息代理软件(又称面向消息的中间件),使用Erlang语言编写,而集群和故障转移是构建在开放电信平台框架上的,所有主要的编程语言均有与代理接口通讯的客户端库。
- Kafka
Apache开源流处理平台,由Scala和Java编写,kafka是一种高吞吐量的分布式发布订阅消息系统,可以处理消费者在网站中的所有动作流数据,在大数据领域和日志处理解决方案用的比较多。
- RocketMQ
阿里巴巴开源的分布式消息中间件,目前已捐赠给Apache基金会,有高性能、低延时和高可靠性等特性。
淘宝内部的交易系统使用了淘宝自主研发的Notify消息中间件,使用MySQL作为消息存储媒介,可完全水平扩容;为了进一步降低成本,kafka出现后,对于使用在淘宝交易、订单、充值等场景下还有很多特性不满足,基于kafka使用Java编写了RocketMQ,定位于非日志的可靠性传输,多使用于订单、交易、充值、流计算、消息推送、日志流式处理、binlog分发等场景。
Kakfa | RocketMQ | RibbitMQ | |
定位 | 日志消息、监控数据 | 非日志的可靠消息传输 | 非日志的可靠消息传输 |
可用性 | 非常高 分布式、主从 | 非常高 分布式、主从 | 高 主从、采用镜像模式实现, 数据量大时可能性能有问题 |
消息可靠性 | 异步刷盘,容易丢数据 | 同步刷盘、异步刷盘 | 同步刷盘 |
单机吞吐量 | 百万级 | 十万级 | 万级 |
堆积能力 | 非常好 | 非常好 | 一般 |
顺序消费 | 支持 一台broker宕机后,消息会乱序 | 支持 顺序消息场景下,消费失败时消息队列将会暂停 | 支持 如果一个消费失败,此消息的顺序会被打乱 |
定时消息 | 不支持 | 支持 | 支持 |
事务消息 | 不支持 | 支持 | 不支持 |
消息重试 | 不支持 | 支持 | 支持 |
死信队列 | 不支持 | 支持 | 支持 |
访问权限 | 无 | 无 | 类似数据库,配置用户名和密码 |
核心特性
低延迟
1ms内响应延迟超过99.6%。
高稳定性
服务可用性99.95%,Region化,多可用区,分布式集群化部署,确保服务高可用,即使整个机房不可以仍可正常提交消息服务;
数据可靠性99.99999999%,同步双写,超三副本数据冗余与快速切换技术确保数据可靠。
高性能
在始终保证高性能前提下,支持亿级消息堆积,不影响集群的正常服务,在削峰填谷(蓄洪)、微服务解耦场景下尤为重要。
丰富的消息类型
当前支持的消息类型涵盖普通消息、顺序消息(全局顺序/分区顺序)、事务消息、定时消息/延时消息。
核心组件
运行模型
Producer
负责生产消息,一般由业务系统负责生产消息,一个消息生产者会把业务系统里产生的消息发送到Broker服务器。
RocketMQ提供多种发送方式,同步发送(顺序发送:只支持可靠同步发送)、异步发送、单向发送,同步和异步均需要Broker返回确认消息,单向发送不需要。
Consumer
负责消费消息,一般是由后台系统异步消费,一个消息消费者会从Broker服务器拉取消息提供给应用程序,从用户应用的角度而言提供给了两种消费方式:pull消费、push消费。
NameServer
名称服务充当路由消息的提供者,生产者或消费者能够通过名称服务查找各主题相应的Broker IP列表,多个NameServer实例组成集群,但互相独立,没有信息交换,Broker会将自己的信息注册到每个NameServer中,所以每个NameServer实例中都保存了完整的路由信息。
Broker
消息中转角色,负责存储消息、消息转发;代理服务器在其中负责接收从生产者推送的消息并存储,同时为消费者拉取请求做准备;代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移、主题和队列消息等。
一个集群中可以有多个Broker,通过Broker名称进行区分,不同的Broker之间存储的消息是不一样的;一般不同的Broker节点会部署在不同的服务器;对于同一个Broker可以有多个Slave,作为副本备份,其中Master负责读写消息,Slave只负责读,brokerId=0代表Master,非0就是Slave。
Topic
表示一类消息的集合,每个主题包含若干条消息,每个消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
Queue
为了提高消息处理的并发度,每个消息主题Topic会有多个Queue。
Message
消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题;RocketMQ中每条消息拥有一个唯一的Message ID,且可以携带具有业务标志的key,系统提供了通过Message ID和key查询消息的功能。
基本使用
入门
安装:docker安装RocketMQ_panpan_fighting的博客-CSDN博客
public class Producer {
public static void main(String[] args) throws Exception {
// 1. 创建生产者对象
DefaultMQProducer producer = new DefaultMQProducer("group01");
// 2. 配置nameserver
// 还可以通过 1.环境变量;2.JVM参数 的方式配置
producer.setNamesrvAddr("127.0.0.1:9876");
// 3. 启动生产者
producer.start();
// 4. 创建消息Message
Message message = new Message("hello01", "hello 222".getBytes());
// 5. 发送消息
SendResult result = producer.send(message);
System.out.println(JSON.toJSONString(result));
// 6. 关闭生产者
producer.shutdown();
}
}
public class Consumer {
public static void main(String[] args) throws Exception {
// 1. 创建一个消费者对象
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group01");
// 2. 设置nameserver地址
// 还可以通过 1.环境变量;2.JVM参数 的方式配置
consumer.setNamesrvAddr("127.0.0.1:9876");
// 3. 订阅topic(push方式)
consumer.subscribe("hello01", "*");
// 4. 创建监听器监听消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println("消费:" + new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5. 启动消费者
consumer.start();
}
}
若出现发送消息失败的情况,将brokerIP1设置为外网ip。
发送消息
类型
- 同步发送消息
发送消息给消息中间件的时候,会等待本次消息的处理结果,在同个线程等待。主要应用在重要的消息通知、短信通知等。
- 异步发送消息
发送消息给消息中间件时,发送消息的线程不会等待处理结果,而是继续执行自己的程序,当有结果时会通过异步的方式通知给生产者,生产者会由另一个专门的线程去接收结果。主要应用在对响应时间比较敏感的业务场景,需要Broker快速响应的情况。
异步发送需要注意接收结果的线程执行是否完成,可以使用计数器来等待结果接收(CountDownLatch2)。
- 一次性发送消息(单向发送)
发送消息给消息中间件,消息中间件不返回处理结果。主要应用在不需要关心发送结果的场景,如日志记录、大数据应用,其性能最高。
生产者组
一个生产者组包含多个生产者,同一个生产者组发送消息的类型和发送逻辑一致,可以提高并发量;若发送事务消息的原始生产者在发送完消息之后宕机,Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
消息封装
- tag:用来进行消息分类,如下单类、支付类,用来把不同类型的消息交给不同的消费者消费。
- key:区分业务消息的唯一性,如订单id。
接收消息
消费方式
- pull:由消费者主动发起请求去获取数据。
- push:消息中心主动把消息推送给消费者。
push方式使用监听器监听消息,可以指定消息的消费方式(并发、有序),监听器会启动多个子线程去接收消息,可以设置线程最大最小值;并发方式在线程数量限制内,选择某个线程接收某个Queue的一个消息,而有序方式使一个队列会只有一个线程消费;也就是说同一个队列的消息,并发方式可能会由多个线程消费,有序只会由同一个线程消费。
消费模式
- 集群模式:同一个消费组中的消费者,每个消费者消费的消息都是不一样的,相当于消费者的负载均衡(由Queue来做负载均衡,在Consumer会按均衡的将一个Queue绑定到一个Consumer,一个Consumer可以绑定多个Queue)。
- 广播模式:订阅了同一消息(topic+tag)的消费者,都会全量接收到消息,所有消费者的数据是一样的。注意,1. 广播模式是实时消费,在广播模式启动之前的消息是接收不到的。 2.广播模式发送失败的消息不会重试。
消息消费位置
消费消息会有消费偏移量,从偏移量之后才会进入消费位置选择,每个消费者组之间偏移量不一样,同组内消费者偏移量共用消费者组的偏移量;普通队列和重试队列偏移量隔离,互相独立。
- CONSUME_FROM_FIRST_OFFSET
从最开始的位置消费,会消费掉该Topic下面的所有有效数据,过期的数据会删除掉。
- CONSUME_FROM_LAST_OFFSET
如果该Topic下面没有过期的数据,则从最开始的位置进行消费;
如果该Topic下面存在过期的数据,则从最后面的位置开始消费,只会消费新加入的数据。
- CONSUME_FROM_TIMESTAMP
根据指定时间戳时间开始消费,如果不指定,则默认从半小时前开始消费。
消息确认
- pull消费
对于未提交的操作,Topic中Consumer订阅的消费偏移量是不会发生改变的,下次消费的时候会继续消费该数据。
可以通过consumer.setAutoCommit(false)设置是否自动提交,如果设置为手动,可以通过consumer.commitSync()提交。
- push消费
通过返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态表示消费成功,ConsumeConcurrentlyStatus.RECONSUME_LATER状态表示消费不成功,但消费偏移量会改变,消费不成功的消息会放入重试队列;默认重试采用服务端重试,默认重试次数16次(超过会放入死信队列)。
特殊消息处理
顺序消息
保证发送者发送的消息有序(使用Queue保证),同时消费者消费的消息有序(保证消费者的每一个消费队列都只有一个线程去消费)。
全局有序
一个生产者一个消费者,参与的Queue只有一个,则全局有序;如果多个Queue参与,则为分区有序,即相对每个Queue,消息都是有序的。
应用场景:对MySQL的binlog日志的分发,就需要使用全局有序的消息。
局部有序
将需要有序的消息放到同一个Queue中,可以保证局部有序。
延迟消息
发送时指定延迟多久被消费,指定了延迟级别后,生产者发送的消息会发送到延迟队列,每个延迟级别都有自己的Queue,RocketMQ的定时任务的方式不断读取不同延迟等级的队列对消息进行还原到原有Topic队列中。
messageDelayLevel | 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h |
应用场景
比如在电商里,提交了一个订单就可以发送一个延时消息,1h之后去检查这个订单的状态,如果还未付款就取消订单释放库存。
过滤消息
Tag消息过滤
在消费者订阅Topic时,指定只订阅某些tag的消息。
SQL过滤
可以使用SQL表达式筛选消息,可以实现一些简单的逻;发送消息时添加自定义属性,消费者订阅时通过SQL表达式来筛选订阅消息。
使用时需要在配置文件中启用enablePropertyFilter=true,默认是false关闭SQL过滤。
批量发送消息
批量发送能提高传递小消息的性能,限制是这些批量消息需要是相同的Topic,而且不能是延迟消息,此外,这一批消息大小不应超过4MB。若超出大小,可以切分消息发送,如ListSpliter。
原理
消息中心启动流程
NameServer
1. 执行命令,启动NameServer;
2. 设置NameServer相关参数(NamesrvConfig,NettyServerConfig 网络相关);
3. 创建一个NamesrvController;
4. 进行初始化配置(初始化线程池等),启动定时任务(定时检查Broker的状态);
5. 在RouteInfoManager中保存对应的配置信息(关键信息都在这);
6. 添加关闭线程池和定时任务的钩子函数;
7. 启动NamesrvController。
Broker
1. 执行命令,启动Broker;
2. 设置相关参数(NettyServerConfig(生产者消费者使用),NettyClientConfig 连接NameServer上报Topic等信息);
3. 创建BrokerController;
4. 进行初始化配置(加载topic信息、消费者偏移量、消费者生产者信息等),初始化消息的数据(CommitLog),初始化一系列线程池,启动一系列定时任务;
5. 添加对应的关闭线程池和定时任务的钩子函数;
6. 启动BrokerController。
NameServer检测Broker的状态(心跳机制):RouteInfoManager中保存Broker的状态信息(brokerLiveTable)其中包含Broker的最后更新时间,Broker会有定时任务定时发送心跳给NameServer(每隔10s),NameServer接收到心跳后会修改brokerLiveTable中的最后更新时间,同时NameServer会有一个定时任务,去扫描brokerLiveTable,一旦发现当前时间和最后更新时间的差值超过120s,就会剔除对应的Broker。
生产者启动流程
1. 创建生产者实例DefaultMQProducer(实例对象状态为CLIENT_JUST);
2. 启动生产者实例对象defaultMQProducerImpl#start
2-1. 修改实例对象的状态为START_FAILED,保证只启动一次;
2-2. 检查基本配置信息(生产者组的名称是否为空等);
2-3. 创建一个和Broker通信的客户端工厂对象mQCleintFactory;
2-4. 注册生产者信息到客户端工厂对象;
2-5. 启动通信客户端mQCleintFactory(运行一些定时任务保证和NameServer、Broker的通信正常,把服务状态调整为running状态等);
2-6. 向所有Broker发送心跳信息。
消息发送流程
1. 检查生产者服务状态是否是running;
2. 检查消息(是否超出大小等);
3. 设置超时时间,默认3s;
4. 根据Topic去NameServer查找对应的Broker路由信息;
5. 设置消息发送的总次数(同步消息有2次失败重试次数);
6. 获取上一次发送的Broker信息(重试时会发送到非上次的Broker);
7. 使用轮询策略选择对应的MessageQueue;
8. 发送消息(同步消息若失败会进入重试)。
架构
技术架构
rocketmq/architecture.md at master · apache/rocketmq · GitHub
消息存储
所有的消息都实际存储在CommitLog中,仅写入且是顺序写入的,CommitLog是一直追加的不会覆盖,但超过配置时间会删除文件;
消息根据Topic进行分类转发到ConsumerQueue中,ConsumerQueue存的不是实际数据而是CommitLog中数据的定位信息(CommitLogOffset、msgSize、tagHashCode);
消费者消费的信息根据Topic、偏移量等信息找到ConsumerQueue中的位置读取数据(顺序读)。
页缓存和内存映射
页缓存是OS对文件的缓存,用于加速对文件的读写。 OS将一部分内存用于PageCache,使用PageCache机制对读写操作进行了性能优化;数据写入,OS会将数据先写入Cache中,随后通过异步的方式由pdflush内核线程将数据从Cache刷盘至物理磁盘上;数据读取,会通过PageCache读取,如果未命中,OS从物理磁盘上访问文件的时候会顺序的对其他相邻的数据块一起读取到PageCache中(一次读取页大小可以配置)。
CommitLogQueue读取时是顺序的,在PageCache的机制下,性能几乎接近于读内存。
CommitLog会产生随机访问,RocketMQ主要通过MappedByteBuffer对文件进行读写操作,利用了NIO的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(0拷贝)。
消息刷盘
- 同步刷盘:消息持久化到磁盘之后Broker才会返回消息成功的ACK响应给Producer。
- 异步刷盘:消息写入PageCache之后Broker就会响应消息成功ACK给Producer,Broker后台异步进行刷盘。
负载均衡
Producer负载均衡
在发送消息时,默认不指定队列的情况下,会通过一定的负载策略将消息发送到不同的队列中,默认算法是轮询。
Consumer负载均衡
启动消费者的时候,对于集群模式下同一个组的消费者会根据一定的策略对不同消费者绑定对应的队列(当消费者>队列数时,会产生消费者的空消费),默认Queue是平均分配的。
触发重新分配:1. 增减消费者;2. 队列扩容/缩容的时候;3. Broker异常(宕机、网络)。
重新分配之后消费者会从Broker重新拉取信息(consumerOffset等)。
重新分配的影响:
- 暂停消费:触发重新分配后,所有Queue都会暂停消费,等待重新分配完成后才能继续消费。
- 重复消费:Queue存储的消费者偏移量是由消费者异步通知给Broker更新的,所以如果消费者消费之后未通知到Broker就宕机,Queue的偏移量未更新,就会存在重复消费。需要消费者端对业务进行幂等性处理。
- 消息突刺:如果暂停消费时间过长,导致积压了部分消息,会导致消费者在分配完成之后的一段时间内需要消费大量的消息。
集群
多主多从的Broker,多个NameServer(互相无影响)。
当Broker集群有主从时,Master和Slave中都会保存一个CommitLog,写入同步消息的时候,Master写入CommitLog之后,会将消息同步到Slave的CommitLog,直到所有节点的CommitLog一致后,才会返回消息写入成功。
端口的意义
启动Broker时,默认指定listenPort=10911,但真正运行的时候回启动三个端口,
listenPort=10911:负责生产者消费者和Broker的连接通信(Client Manager)。
haListenPort=10912:在数据进行主从同步之间通信的端口(HA Service)。
vip_Channel_port=10909:负责生产者创建Topic,发送Message的端口,没有启用vipChannel会使用listenPort。
高可用集群
通过多副本机制Dledger可以完成Master宕机之后,对于Slave角色可以自动切换成Master,完成角色的转换,实现高可用,RocketMQ使用raft一致性算法选举。
可以解决顺序消息的应用场景,同时可以提高吞吐量。
需要在启动Broker的配置文件添加配置开启(4.5版本及之后才可用)
# 开启主从切换的关键参数,把CommitLog交给Dledger管理
enableDLegerCommitLog=true
# 多副本相关的存储信息
storePathRootDir=/home/ztztdata/rocketmq-all-4.1.0-incubating/DLeger-store
# 对于不同的Broker需要添加不同的组,官方建议使用与broker name一样的名字。
dLegerGroup=broker-a
# 参与投票选举的通信操作
dLegerPeers=n0-192.168.0.1:40911;n1-192.168.0.2:40912;n2-192.168.0.3:40913
#这个要求是唯一id,根据节点分配,要与dLegerPeers保持一致。
dLegerSelfId=n0
raft算法
角色:Leader(领导者)、Follower(跟随者)、Candidate(候选者)、Election(选举者)。
1. 当集群中没有Leader的时候(心跳机制检测Follower -> Leader),集群中的所有节点会在一个随机时间(150ms-300ms)内,如果没有接收到Leader的心跳,就将自己的角色变为Candidate;
2. 并且发起一轮投票(发起者会将票投给自己),有一个初始任期(任期只有启动时会初始化,后面都是累加),将自己的投票结果广播给其他节点;
3. 当节点收到其他节点的投票结果广播时,如果自己没有投过票给其他节点或已经投过票的任期比这次广播的任期低,则会将票投给收到的广播节点,并且也将自己的结果广播出去,否则会投反对票;
4. 当某节点的赞成票数超过节点半数时,就会把自己变为Leader角色,通过广播通知其他节点;
5. 其他节点收到有Leader角色的广播后,会将自己的角色变为Follower;
至此投票结束。
若经过一轮投票之后,没有产生Leader角色,Candidate则会将自己的任期加1重新发起投票。