一般认为,消息中间件属于分布式系统中一个子系统,关注于数据的发送和接收,利用高效可靠的异步消息传递机制对分布 式系统中的其余各个子系统进行集成。其特点如下:
高效:对于消息的处理处理速度快。
可靠:一般消息中间件都会有消息持久化机制和其他的机制确保消息不丢失。
异步:指发送完一个请求,不需要等待返回,随时可以再发送下一个请求,既不需要等待。
一句话总结,消息中间件不生产消息,只是消息的搬运工。
系统的耦合性越高,容错性就越低。
以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者 因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验 使用消息中间件,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队 列中,用户的下单操作正常完成。当物流系统恢复后,继续处理存放在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。
应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。
有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大 提到系统的稳定性和用户体验。
通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消 息队列中直接获取数据即可。 接口调用的弊端,无论是新增系统,还是移除系统,代码改造工作量都很大。 使用 MQ 做数据分发好处,无论是新增系统,还是移除系统,代码改造工作量较小
消息队列 RocketMQ 是阿里巴巴集团基于高可用分布式集群技术,自主研发的云正式商用的专业消息中间件,既可为分布式应用系统提供异步解耦 和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性,是阿里巴巴双 11 使用的核心产品。
RocketMQ 的设计基于主题的发布与订阅模式,其核心功能包括消息发送、消息存储(Broker)、消息消费,整体设计追求简单与性能第一
架构图如下:
NameServer 是整个 RocketMQ 的“大脑”,它是 RocketMQ 的服务注册中心,所以 RocketMQ 需要先启动 NameServer 再启动 Rocket 中的 Broker。
Broker 在启动时向所有 NameServer 注册(主要是服务器地址等),生产者在发送消息之前先从 NameServer 获取 Broker 服务器地址列表(消费者一 样),然后根据负载均衡算法从列表中选择一台服务器进行消息发送。
NameServer 与每台 Broker 服务保持长连接,并间隔 30S 检查 Broker 是否存活,如果检测到 Broker 宕机,则从路由注册表中将其移除。这样就可以实 现 RocketMQ 的高可用。
生产者也称为消息发布者,负责生产并发送消息至 RocketMQ。
消费者也称为消息订阅者,负责从 RocketMQ 接收并消费消息。
消息是生产或消费的数据,对于 RocketMQ 来说,消息就是字节数组。
RocketMQ 的核心,用于暂存和传输消息。
- NameServer 先启动
- Broker 启动时向 NameServer 注册
- 生产者在发送某个主题的消息之前先从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),然后根据负载均衡算法从列表中选择一台 Broker 进行消息发送。
- NameServer 与每台 Broker 服务器保持长连接,并间隔 30S 检测 Broker 是否存活,如果检测到 Broker 宕机(使用心跳机制,如果检测超过120S),则从路由注册表中将其移除。
- 消费者在订阅某个主题的消息之前从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),但是消费者选择从 Broker 中订阅消息,订阅 规则由 Broker 配置决定。
生产者:标识发送同一类消息的 Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。主要作用用于事务消息:
(事务消息中如果某条发送某条消息的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其它producer, 确认这条消息应该 commit 还是 rollback)
消费者:标识一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致。
同一个 Consumer Group 下的各个实例将共同消费 topic 的消息,起到负载均衡的作用。 消费进度以 Consumer Group 为粒度管理,不同 Consumer Group 之间消费进度彼此不受影响,即消息 A 被 Consumer Group1 消费过,也会再给 Consumer Group2 消费。
标识一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定 Topic。 区分消息的种类;
一个发送者可以发送消息给一个或者多个 Topic;一个消息的接收者可以订阅一个或者多个 Topic 消息
RocketMQ 支持给在发送的时候给 topic 打 tag,同一个 topic 的消息虽然逻辑管理是一样的。但是消费 topic1 的时候,如果你消费订阅的时候指定的 是 tagA,那么 tagB 的消息将不会投递。
简称 Queue 或 Q。消息物理管理单位。一个 Topic 将有若干个 Q。若一个 Topic 创建在不同的 Broker,则不同的 broker 上都有若干 Q,消息将物理地 存储落在不同 Broker 结点上,具有水平扩展的能力。 无论生产者还是消费者,实际的生产和消费都是针对 Q 级别。例如 Producer 发送消息的时候,会预先选择(默认轮询)好该 Topic 下面的某一条 Q 发送;Consumer 消费的时候也会负载均衡地分配若干个 Q,只拉取对应 Q 的消息。 每一条 message queue 均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log) 恢复回来。
RocketMQ 中,有很多 offset 的概念。一般我们只关心暴露到客户端的 offset。不指定的话,就是指 Message Queue 下面的 offset。
Message queue 是无限长的数组。一条消息进来下标就会涨 1,而这个数组的下标就是 offset,Message queue 中的 max offset 表示消息的最大 offset
Consumer offset 可以理解为标记 Consumer Group 在一条逻辑 Message Queue 上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的 最新消费的消息 offset+1,即实际上表示的是下次拉取的 offset 位置。
该消费模式中,一个 Consumer Group 中的各个 Consumer 实例分摊去消费消息,即一条消息只会投递到一个 Consumer Group 下面的一个实例。
实际上,每个 Consumer 是平均分摊 Message Queue 的去做拉取消费。例如某个 Topic 有 3 条 Q,其中一个 Consumer Group 有 3 个实例(可能是 3 个进程,或者 3 台机器),那么每个实例只消费其中的 1 条 Q。 而由 Producer 发送消息的时候是轮询所有的 Q,所以消息会平均散落在不同的 Q 上,可以认为 Q 上的消息是平均的。那么实例也就平均地消费消息了。 这种模式下,消费进度(Consumer Offset)的存储会持久化到 Broker。
该消费模式中,消息将对一个 Consumer Group 下的各个 Consumer 实例都投递一遍。即使这些 Consumer 属于同一个 Consumer Group, 消息也会被 Consumer Group 中的每个 Consumer 都消费一次。 实际上,是一个消费组下的每个消费者实例都获取到了 topic 下面的每个 Message Queue 去拉取消费。所以消息会投递到每个消费者实例。 这种模式下,消费进度(Consumer Offset)会存储持久化到实例本地。
消息消费时的权衡 :
集群模式:适用场景&注意事项
消费端集群化部署,每条消息只需要被处理一次。
由于消费进度在服务端维护,可靠性更高。
集群消费模式下,每一条消息都只会被分发到一台机器上处理。
如果需要被集群下的每一台机器都处理,请使用广播模式。
集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。
广播模式:适用场景&注意事项
广播消费模式下不支持顺序消息。
广播消费模式下不支持重置消费位点。
每条消息都需要被相同逻辑的多台机器处理。
消费进度在客户端维护,出现重复的概率稍大于集群模式。
广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消 费失败的情况。
广播模式下,客户端每一次重启都会从最新消息消费。
客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
目前仅 Java 客户端支持广播模式。
广播模式下服务端不维护消费进度,所以消息队列 RocketMQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ 可以严格的保证消息有序,可以分为分区有序或者全局有序。
顺序消费的原理解析:
在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列);而消费消息的时候从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。
但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个 queue 参与,则为分区有序,即相对每个 queue,消息都是有序的。
全局顺序:
分区有序:
使用顺序消息注意:首先要保证消息是有序进入 MQ 的,消息放 MQ 之前,比如对 id 等关键字进行取模,放入指定 messageQueue,consume 消费消息失败时, 不能返回 reconsume——later,这样会导致乱序,应该返回 suspend_current_queue_a_moment,意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里。
业务消费回调的时候,当且仅当此回调函数返回CONSUME_SUCCESS,RocketMQ 才会认为这批消息(默认是 1 条) 是消费完成的中途断电,抛出异常等都不会认为成功——即都会重新投递。
返回 RECONSUME_LATER,RocketMQ 就会认为这批消息消费失败了。 如果回调没有处理好而抛出异常,会认为是消费失败 RECONSUME_LATER 处理。 为了保证消息是肯定被至少消费成功一次,RocketMQ 会把这批消息重发回 Broker(topic 不是原 topic 而是这个消费组的 RETRY topic),在延迟的某个时间点(默认是 10 秒,业务可设置)后,再次投递到这个 ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认 16 次),就会投递到 DLQ 死信队列。应用可以监控死信队列来做人工干预。 另外如果使用顺序消费的回调 MessageListenerOrderly 时,由于顺序消费是要前者消费成功才能继续消费,所以没有 RECONSUME_LATER 的这个状态, 只有 SUSPEND_CURRENT_QUEUE_A_MOMENT 来暂停队列的其余消费,直到原消息不断重试成功为止才能继续消费
Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费, 该消息即延时消息。适用于消息生产和消费有时间窗口要求:
比如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在 30 分钟以 后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略。
注意:Apache RocketMQ 目前只支持固定精度的定时消息,因为如果要支持任意的时间精度,在 Broker 层面,必须要做消息排序,如果再涉及到持久化, 那么消息排序要不可避免的产生巨大性能开销。源码中如下:
是这 18 个等级(秒(s)、分(m)、小时(h)),level 为 1,表示延迟 1 秒后消费,level 为 5 表示延迟 1 分钟后消费,level 为 18 表示延迟 2 个 小时消费。生产消息跟普通的生产消息类似,只需要在消息上设置延迟队列的 level 即可。消费消息跟普通的消费消息一致。
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK),而且不能是延时消息。此外,这一批消息的总大小不应超过 4MB。
大致分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
(1) 发送消息(half 消息):图中步骤 1。
(2) 服务端响应消息写入结果:图中步骤 2。
(3) 根据发送结果执行本地事务(如果写入失败,此时 half 消息对业务不可见,本地逻辑不执行):图中步骤 3。
(4) 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见):图中步骤 4
(1) 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次“回查”:图中步骤 5。
(2) Producer 收到回查消息,检查回查消息对应的本地事务的状态:图中步骤 6。
(3) 根据本地事务状态,重新 Commit 或者 Rollback::图中步骤 6。 其中,补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况。
事务消息共有三种状态,提交状态、回滚状态、中间状态:
TransactionStatus.CommitTransaction: 提交状态,它允许消费者消费此消息(完成图中了 1,2,3,4 步,第 4 步是 Commit)。
TransactionStatus.RollbackTransaction: 回滚状态,它代表该消息将被删除,不允许被消费(完成图中了 1,2,3,4 步, 第 4 步是 Rollback)。
TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态(完成图中了 1,2,3 步, 但是没有 4 或者没有 7,无法 Commit 或 Rollback)。
该阶段主要发一个消息到 rocketmq,但该消息只储存在 commitlog 中,但 consumeQueue 中不可见,也就是消费端(订阅端)无法看到此消息 commit/rollback 阶段(确认阶段): 该阶段主要是把 prepared 消息保存到 consumeQueue 中,即让消费端可以看到此消息,也就是可以消费此消息。如果是 rollback 就不保存。
领域模型(Domain Model)是对领域内的概念类或现实世界中对象的可视化表示。又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
Message 是 RocketMQ 消息引擎中的主体。messageId 是全局唯一的。MessageKey 是业务系统(生产者)生成的,所以如果要结合业务,可以使用 MessageKey 作为业务系统的唯一索引。
另外 Message 中的 equals 方法和 hashCode 主要是为了完成消息只处理一次(Exactly-Once)。 Exactly-Once 是指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息在消费端也只被消费一次。
Tags 是在同一 Topic 中对消息进行分类 subTopics==Message Queue,其实在内存逻辑中,subTopics 是对 Topics 的一个拓展,尤其是在 MQTT 这种协议下,在 Topic 底下会有很多 subTopics。
Queue 是消息物理管理单位,比如在 RocketMQ 的控制台中,就可以看到每一个 queue 中的情况(比如消息的堆积情况、消息的 TPS、QPS)
对于每一个 Queue 来说都有 Offset,这个是消费位点。 Group 业务场景中,如果有一堆发送者,一堆消费者,所以这里使用 Group 的概念进行管理。
Message 与 Topic 是多对一的关系,一个 Topic 可以有多个 Message。
Topic 到 Queue 是一对多的关系,这个也是方便横向拓展,也就是消费的时候,这里可以有很多很多的 Queue。
一个 Queue 只有一个消费位点(Offset),所以 Topic 和 Offset 也是一对多的关系 Topic 和 Group 也是多对多的关系。
从上面模型可以看出,要解决消费并发,就是要利用 Queue,一个 Topic 可以分出更多的 queue,每一个 queue 可以存放在不同的硬件上来提高并发。
要确保消息的顺序,生产者、队列、消费者最好都是一对一的关系。
但是这样设计,并发度就会成为消息系统的瓶颈(并发度不够) RocketMQ 不解决这个矛盾的问题。理由如下:
1、 乱序的应用实际大量存在
2、 队列无序并不意味着消息无序 另外还有消息重复,造成消息重复的根本原因是:网络不可达(网络波动)。
所以如果消费者收到两条一样的消息,应该是怎么处理?
RocketMQ 不保证消息不重复,如果你的业务要严格确保消息不重复,需要在自己的业务端进行去重。 1、 消费端处理消息的业务逻辑保持幂等性
2、 确保每一条消息都有唯一的编号且保证消息处理成功与去重表的日志同时出现
RocketMQ 因为有高可靠性的要求(宕机不丢失数据),所以数据要进行持久化存储。所以 RocketMQ 采用文件进行存储
commitLog:消息存储目录
config:运行期间一些配置信息
consumerqueue:消息消费队列存储目录
index:消息索引文件存储目录
abort:如果存在改文件则 Broker 非正常关闭
checkpoint:文件检查点,存储 CommitLog 文件最后一次刷盘时间戳、consumerqueue 最后一次刷盘时间,index 索引文件最后一次刷盘时间戳。
RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件。
CommitLog:存储消息的元数据
ConsumerQueue:存储消息在 CommitLog 的索引
IndexFile:为了消息查询提供了一种通过 key 或时间区间来查询消息的方法,这种通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程
CommitLog 以物理文件的方式存放,每台 Broker 上的 CommitLog 被本机器所有 ConsumeQueue 共享,文件地址:$ {user.home} \store$ { commitlog} \ $ { fileName}。在 CommitLog 中,一个消息的存储长度是不固定的, RocketMQ 采取一些机制,尽量向 CommitLog 中顺序写 , 但是随机读。commitlog 文件默认大小为 lG ,可通过在 broker 置文件中设置 mappedFileSizeCommitLog 属性来改变默认大小。
Commitlog 文件存储的逻辑视图如下,每条消息的前面 4 个字节存储该条消息的总长度。但是一个消息的存储长度是不固定的。
每个 CommitLog 文件的大小为 1G,一般情况下第一个 CommitLog 的起始偏移量为 0,第二个 CommitLog 的起始偏移量为 1073741824 (1G = 1073741824byte)。每台 Rocket 只会往一个 commitlog 文件中写,写完一个接着写下一个。
ConsumeQueue 是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件, 文件地址在$ {$storeRoot} \consumequeue$ {topicName} $ { queueld} $ {fileName}。
ConsumeQueue 中存储的是消息条目,为了加速 ConsumeQueue 消息条目的检索速度与节省磁盘空间,每一个 Consumequeue 条目不会存储消息的全 量信息,消息条目如下:
ConsumeQueue 即为 Commitlog 文件的索引文件, 其构建机制是 当消息到达 Commitlog 文件后 由专门的线程产生消息转发任务,从而构建消息消 费队列文件(ConsumeQueue )与下文提到的索引文件。
存储机制这样设计有什么好处呢?
1 ) CommitLog 顺序写 ,可以大大提高写入效率。 (实际上,磁盘有时候会比你想象的快很多,有时候也比你想象的慢很多,关键在如何使用,使用得当,磁盘的速度完全可以匹配上网络的数据传 输速度。目前的高性能磁盘,顺序写速度可以达到 600MB/s ,超过了一般网卡的传输速度,这是磁盘比想象的快的地方 但是磁盘随机写的速度只有大概 lOOKB/s,和顺序写的性能相差 6000 倍!)
2 )虽然是随机读,但是利用操作系统的 pagecache 机制,可以批量地从磁盘读取,作为 cache 存到内存中,加速后续的读取速度。
3 )为了保证完全的顺序写,需要 ConsumeQueue 这个中间结构 ,因为 ConsumeQueue 里只存偏移量信息,所以尺寸是有限的,在实际情况中,大 部分的 ConsumeQueue 能够被全部读入内存,所以这个中间结构的操作速度很快,可以认为是内存读取的速度。此外为了保证 CommitLog 和 ConsumeQueue 的一致性, CommitLog 里存储了 Consume Queues 、Message Key、 Tag 等所有信息,即使 ConsumeQueue 丢失,也可以通过 commitLog 完全恢复出 来。
RocketMQ 还支持通过 MessageID 或者 MessageKey 来查询消息;使用 ID 查询时,因为 ID 就是用 broker+offset 生成的(这里 msgId 指的是服务端的), 所以很容易就找到对应的 commitLog 文件来读取消息。但是对于用 MessageKey 来查询消息,RocketMQ 则通过构建一个 index 来提高读取速度。
index 存的是索引文件,这个文件用来加快消息查询的速度。消息消费队列 RocketMQ 专门为消息订阅构建的索引文件 ,提高根据主题与消息检索消息的速度 ,使用 Hash 索引机制。
config 文件夹中 存储着 Topic 和 Consumer 等相关信息。主题和消费者群组相关的信息就存在在此。
topics.json : topic 配置属性
subscriptionGroup.json :消息消费组配置信息。
delayOffset.json :延时消息队列拉取进度。
consumerOffset.json :集群消费模式消息消进度。
consumerFilter.json :主题消息过滤信息。
abort :如果存在 abort 文件说明 Broker 非正常闭,该文件默认启动时创建,正常退出之前删除 checkpoint :文件检测点,存储 commitlog 文件最后一次刷盘时间戳、 consumequeue 最后一次刷盘时间、 index 索引文件最后一次刷盘时间戳。
删除过程分别执行清理消息存储文件( Commitlog )与消息消费 队列文件( ConsumeQueue 文件), 消息消费队列文件与消息存储文件 ( Commitlog )共用一套过期文件机制。
RocketMQ 清除过期文件的方法是 :如果非当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除, RocketMQ 不会关注 这个文件上的消息是否全部被消费。默认每个文件的过期时间为 42 小时(不同版本的默认值不同,这里以 4.8.0 为例) ,通过在 Broker 配置文件中 设置 fileReservedTime 来改变过期时间,单位为小时。
触发文件清除操作的是一个定时任务,而且只有定时任务,文件过期删除定时任务的周期由该删除决定,默认每 10s 执行一次。
文件删除主要是由这个配置属性:fileReservedTime:文件保留时间。也就是从最后一次更新时间到现在,如果超过了该时间,则认为是过期文件, 可以删除。
另外还有其他两个配置参数:
deletePhysicFilesInterval:删除物理文件的时间间隔(默认是 100MS),在一次定时任务触发时,可能会有多个物理文件超过过期时间可被删除, 因此删除一个文件后需要间隔 deletePhysicFilesInterval 这个时间再删除另外一个文件,由于删除文件是一个非常耗费 IO 的操作,会引起消息插入消 费的延迟(相比于正常情况下),所以不建议直接删除所有过期文件。
destroyMapedFileIntervalForcibly:在删除文件时,如果该文件还被线程引用,此时会阻止此次删除操作,同时将该文件标记不可用并且纪录当 前时间戳 destroyMapedFileIntervalForcibly 这个表示文件在第一次删除拒绝后,文件保存的最大时间,在此时间内一直会被拒绝删除,当超过这个时 间时,会将引用每次减少 1000,直到引用 小于等于 0 为止,即可删除该文件。
1)指定删除文件的时间点, RocketMQ 通过 deleteWhen 设置一天的固定时间执行一次。删除过期文件操作, 默认为凌晨 4 点。
2)磁盘空间是否充足,如果磁盘空间不充足(DiskSpaceCleanForciblyRatio。磁盘空间强制删除文件水位。默认是 85),会触发过期文件删除操作。
另外还有 RocketMQ 的磁盘配置参数:
1:物理使用率大于 diskSpaceWarningLevelRatio(默认 90%可通过参数设置),则会阻止新消息的插入。
2:物理磁盘使用率小于 diskMaxUsedSpaceRatio(默认 75%) 表示磁盘使用正常。
零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件 时节省 CPU 周期和内存带宽。
零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销
可以看出没有说不需要拷贝,只是说减少冗余[不必要]的拷贝。
比如:读取文件,再用 socket 发送出去,实际经过四次 copy。 伪码实现如下:
buffer = File.read()
Socket.send(buffer)
1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy 到应用程序的 buffer;
3、第三步:将 application 应用程序 buffer 中的数据,copy 到 socket 网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将 socket buffer 的数据,copy 到网卡,由网卡进行网络传输。
上述的过程,虽然引入 DMA 来接管 CPU 的中断请求,但四次 copy 是存在“不必要的拷贝”的。实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。 显然,第二次和第三次数据 copy 其实在这种场景下没有什么帮助反而带来开销(DMA 拷贝速度一般比 CPU 拷贝速度快一个数量级)。
同时,read 和 send 都属于系统调用,每次调用都牵涉到两次上下文切换:
总结下,传统的数据传送所消耗的成本:4 次拷贝,4 次上下文切换。 4 次拷贝,其中两次是 DMA copy,两次是 CPU copy。
硬盘上文件的位置和应用程序缓冲区(application buffers)进行映射(建立一种一一对应关系),由于 mmap()将文件直接映射到用户空间,所以实际文 件读取时根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内容从硬盘拷贝到内核空间的一个缓冲区。 mmap 内存映射将会经历:3 次拷贝: 1 次 cpu copy,2 次 DMA copy;
以及 4 次上下文切换
mmap()是在
如果按照传统的方式进行数据传送,那肯定性能上不去,作为 MQ 也是这样,尤其是 RocketMQ,要满足一个高并发的消息中间件,一定要进行优化。 所以 RocketMQ 使用的是 MMAP。 RocketMQ 一个映射文件大概是,commitlog 文件默认大小为 lG。
这里需要注意的是,采用 MappedByteBuffer 这种内存映射的方式有几个限制,其中之一是一次只能映射 1.5~2G 的文件至用户态的虚拟内存,这也是为何 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G 的原因了。
源码中:
Producer 端发送消息最终写入的是 CommitLog(消息存储的日志数据文件),Consumer 端先从 ConsumeQueue(消息逻辑队列)读取持久化消息的 起始物理位置偏移量 offset、大小 size 和消息 Tag 的 HashCode 值,随后再从 CommitLog 中进行读取待拉取消费消息的真正实体内容部分;
所有的 Topic 下的消息队列共用同一个 CommitLog 的日志数据文件,并通过建立类似索引文件—ConsumeQueue 的方式来区分不同 Topic 下面的不同 MessageQueue 的消息,同时为消费消息起到一定的缓冲作用(异步服务线生成了 ConsumeQueue 队列的信息后,Consumer 端才能进行消费)。这样,只 要消息写入并刷盘至 CommitLog 文件后,消息就不会丢失,即使 ConsumeQueue 中的数据丢失,也可以通过 CommitLog 来恢复。
发送消息时,生产者端的消息确实是顺序写入 CommitLog;订阅消息时,消费者端也是顺序读取 ConsumeQueue,然而根据其中的起始物理位置偏移量 offset 读取消息真实内容却是随机读取 CommitLog。 所以在 RocketMQ 集群整体的吞吐量、并发量非常高的情况下,随机读取文件带来的性能开销影响还是比较大的。
那么在 RocketMQ 中是怎么优化的?
1、本身无论是 Commitlog 文件还是 Consumequeue 文件,都通过 MMAP 内存映射。
2、本身存储 Commitlog 采用写时复制的容器处理,实现读写分离,所以很大程度上可以提高一些效率。
源码流程图: