第九章 RocketMQ 技术架构与原理分析

image.png

核心架构

RocketMQ 体系中包括四个角色:
broker:消息服务器,存储消息,具有主从架构,后续使用 broker0 代表主
producer:消息生产者,产生消息
consumer:消息消费者,消费消息
nameServer:简称 namesrv,注册中心(类似 zk),存储路由信息表。

核心路由信息(前两个用于路由/第三个用于心跳探活)
<topic, List<QueueData>> topicQueueTable  其中QueueData包含brokerName以及需创建队列数量
<brokerName, BrokerData> brokerAddrTable  其中BrokerData中包含brokerName对应的主从服务地址
<brokerAddr, BrokerLiveInfo> brokerLivetable

心跳检测

线条检测存在于四大组件的两两关联组件中,心跳检测的作用:

  1. 检测组件活性
  2. 通过心跳包传递最新消息

简单的探活心跳可使用 Netty 的 IdleStateHandler,复杂的心跳可以自定义定时任务进行心跳发送逻辑

broker 与 namesrv 之间:

  • 所有 broker 向所有 namesrv 发送心跳,建立长链接,注册 topic 路由,每隔30s发送一次心跳,更新 topic 路由表信息;
  • namesrv 每隔 10s 扫描一次 brokerLivetable,若120s没有接收到来自 broker 的心跳,则认为该 broker 挂了,移除所有路由表中与该 broker 有关的信息

producer 与 namesrv 之间:

  • producer 随机连接一台 namesrv,channel 断后再重连其他。每30s拉一次 topic 路由信息,解析后缓存到本地

producer 与 broker 之间:

  • producer 仅连接含有其感兴趣的 topic 的 broker0,仅向已连接的 broker0 每隔30s发送一次心跳。不连接从 broker,从 broker 不处理写请求。

consumer 与 namesrv 之间:

  • consumer 随机连接一台 namesrv,channel 断后再重连其他。每30s拉一次 topic 路由信息,解析后缓存到本地

consumer 与 broker 之间:

  • consumer 连接其订阅的 topic 的所有 broker(包括主从,主从都会响应读,默认从主读,主繁忙时从从读),向 broker 注册信息消费者信息(broker 含有 topic->订阅的消费者信息列表 信息,用于后续重平衡),consumer 向已连接的 broker 每隔30s发送一次心跳。

整体流程

namesrv 启动

namesrv 可以写死几台机器,也可以在 http 静态服务器进行配置,后续其客户端可以写死 namesrv 的地址,也可以从指定的 http 静态服务器拉取

namesrv 核心做的事情:维护路由表

  1. 通过 broker 发送来的心跳信息构建路由表
核心路由信息(前两个用于路由/第三个用于心跳探活)
<topic, List<QueueData>> topicQueueTable  其中QueueData包含brokerName以及需创建队列数量
<brokerName, BrokerData> brokerAddrTable  其中BrokerData中包含brokerName对应的主从服务地址
<brokerAddr, BrokerLiveInfo> brokerLivetable
  1. 启动定时任务:每隔 10s 扫描一次 brokerLivetable,若120s没有接收到来自 broker 的心跳,则认为该 broker 挂了,移除所有路由表中与该 broker 有关的信息

broker 启动

人工通过后台创建 topic:创建 topic 时需要指定该 topic 要存储在哪些 broker 上,也可以在发送消息时自动创建 topic(推荐提前创建,关闭自动创建

  1. broker 启动时,所有 broker 向所有 namesrv 发送心跳,建立长链接,注册 topic 路由,每隔30s发送一次心跳,更新 topic 路由表信息
  2. 路由信息持久化:路由信息持久化在 broker 不是 namesrv,,broker 通过心跳包传递给 namesrv。topic 信息持久化在 topics.json 文件中(broker 接收到消息后,检查消息,若需要自动创建 topic,则自动创建,之后持久化 topic 到 topics.json)

producer 启动发送消息

  1. 获取更新路由表
    producer 随机连接一台 namesrv,启动时立即拉取路由信息,之后每30s拉一次topic路由信息,解析后缓存到本地。
本地缓存
<topic,List<messageQueue>>  messageQueue含有brokerName
<brokerName, <brokerId, brokerAddr>>

后续消息发送时,通过 topic 选 messageQueue,通过 messageQueue 获取所在 broker,之后向 broker0 发送消息

  1. 消息发送队列选择策略
    消息发送前,需要根据本地缓存通过 topic 选 messageQueue,再通过 messageQueue 获取所在 broker,之后向 broker0 发送消息。消息发送队列选择策略主要有两种:
  • 轮询:轮询获取 messageQueue,发送失败后进行重试(仅限同步机制,异步机制的重试在回调中进行),重试发送时如果选择到了上一次发送失败的 broker,会重新选择,尽量避开上一次失败的 broker
  • 故障规避:简易版的熔断策略,一次发送失败在未来的一段时间内不再选择该 broker 上的队列
  1. 批量消息
    批量消息发送,关键是编解码,了解编解码,才能更好的使用批量发送,否则单批消息可能会超过发送消息的消息体的最大限制。


    image.png

broker 接收消息进行存储

  1. topic 自动创建
    broker 接收到消息后,检查消息,若需要自动创建 topic,则自动创建,之后持久化 topic 到 topics.json,判断消息消费重试次数是否超过最大重试次数,若超过,则消息进入死信 topic 的队列。之后进行消息存储(可异步进行)
  2. 消息存储机制

核心消息文件

  1. commitLog 文件:存储消息主体。RocketMQ 为了追求极致的磁盘顺序写性能,所有 producer 的所有 topic 消息都写到一个文件,文件名是文件中的第一个消息的物理偏移量(第一个文件是0,第一个文件是1G,eg. 00000000001073741824 是第二个文件名),写完一个下一个,每个文件1G。这样,给定一个消息的物理偏移量,容易通过二分查找到 commitLog 文件,之后 消息物理偏移量-文件名=消息在 commitLog 文件中的物理地址
  2. consumequeue 文件:consumer 基于 topic 消费消息,该文件用于提高消费性能。每个 topic 有自己的 n 个 consumequeue,每个消息条目占 20 字节定长。每个 cq 文件存储 30w 个消息条目。这样,给定一个消息的 topic 和消费进度(consumequeue 逻辑偏移量 logicOffset,即第几个消息条目),就可以通过 logicOffset*20=该条目的起始偏移量,读取20字节获取一个完整消息条目
    image.png

    commitLog 与 consumequeue 文件的关联:消息直接进入 commitLog 文件,存储实际内容;之后 broker 通过定时任务 ReputService 每1ms将消息的偏移量写入 consumequeue。

内存映射机制


image.png
  • 传统的文件读写方式,需要在用户空间的缓冲区与内核空间的缓冲区进行消息复制(CPU-COPY),内存映射将用户空间的内存缓冲区地址直接映射为内核空间的缓冲区,避免了一次消息复制。
  • RocketMQ 为了减少 pagecache 的读写压力,提供了一种内存读写分离机制,可以让 producer 发来的写嫌存储到堆外内存,然后通过定时commit,将堆外内存中缓存的“一批”消息提交到pagecache中,减缓pagecache的写压力,而读还是直接发生在 pagecache。
  • 异步线程将堆外内存信息写入 pagecache,broker 进程意外退出,堆外内存中的数据会丢失,pagecache 中的不会(pagecache 属于操作系统进程,如果机器断电,则也丢失),每500ms异步线程刷盘,checkpoint 记录 commitlog/consumequeue/index 的刷盘点,辅助文件恢复
  • FileChannel.map 创建的文件使用的是 linux 的 pageCache。linux 会定时将 pagecache 数据持久化的磁盘,断电可能丢失。linux 在内存不够的情况下,会使用缓存置换算法,剔除相关 pagecache。

消息存储总体流程:

  • 获取创建消息文件:获取最后一个可写的 MappedFile 文件(commitLog 物理文件的程序对象),若没找到,则创建 MappedFile(一次性创建两个),借用堆外内存(如果开启了内存读写分离配置,则会在 broker 启动时创建一个堆外内存池),每个 MappedFile 创建真实物理文件和内存映射缓冲区(映射 linux 的 pageCache)
  • 消息追加到文件:若开启了内存隔离,放到堆外内存,否则,写入pageCache,更新队列位点 <topic-queueId, queueOffset>,如果异步刷盘,则此时返回,否则刷盘
  • 消息刷盘机制
    每500ms刷盘一次(如果开启了内存隔离,每200ms commit一次数据(从堆外内存提交到 pageCache 中)),使用 checkpoint 文件记录刷盘时间点,后续用于文件恢复
  1. 消息异步派发机制
    消息直接进入 commitLog 文件,存储实际内容;之后 broker 通过定时任务 ReputService(每1ms执行一次)
  • 创建或者获取 <topic,<queueId, consumequeue>> 中的 consumequeue
  • 之后将消息的偏移量写入 consumequeue

consumer 拉取消息进行消费(以 push 模式为例)

  1. 重平衡
  • consumer 随机连接一台 namesrv。启动后立即拉取,之后每30s拉一次 topic 路由信息,解析后缓存到本地 - 重平衡的 <topic,List<messagequeue>> 信息来源于这里。
本地缓存
<topic,List<messageQueue>>  messageQueue含有brokerName
<brokerName, <brokerId, brokerAddr>>

consumer 启动时依据上述的本地缓存信息连接其订阅的 topic 的所有 broker(包括主从,主从都会响应读,默认从主读,主繁忙时从从读),向 broker 注册信息消费者信息(broker 含有 topic->订阅的消费者信息列表 信息)- 重平衡的 <topic, <consumerGroup, List<Consumer>>> 信息来源于这里。

  • consumer 每20s执行一次重平衡。

eg. topicA 具有四个队列;
topicA:brokera-queueId1,brokera-queueId2,brokerb-queueId1,brokerb-queueId2 - 信息来自 namesrv
consumerGroupA:consumer1,consumer2 - 信息来自 broker
推荐的重平衡策略:将 broker a和b的队列平均分给 consumer,防止由于一台机器的原因,导致一个 consumer 消息挤压处理不过来(eg. 将 brokera 的消息全部分配给consumer1,brokerb 的消息全部分配给consumer2,假设 brokera 上的消息非常多,那么 consumer1 就会很累)
consumer1:brokera-queueId1,brokerb-queueId1
consumer2:brokera-queueId2,brokerb-queueId2

a. 从本地缓存根据 topic 获取消息队列列表 - 来自 namesrv
b. 从本地缓存中随机获取一台 broker(优先从主读,主繁忙才从从读),从 broker 上获取指定消费者组内的订阅了指定 topic 的 consumer 列表 - 来自 broker
c. 使用重平衡策略为当前的 consumer 分配消息队列:从 PullRequestQueue 中剔除原本属于自己此次分配给别人的队列(持久化该队列之前的消费进度),新增队列则创建 PullRequest,加入 PullRequestQueue

PullRequest 核心属性
> MessageQueue 消息队列
> ProcessQueue 处理队列
> nextOffset 待拉取的 MessageQueue 偏移量

d. 更新本地订阅主题版本号信息为当前时间,同时发送到 broker,更新 broker 上订阅主题版本号为当前时间(拉取消息时,会将当前本地存储的版本号与 broker 对比,如果小于,则无法拉取消息 - 由于重平衡是在 consumer 端做的,所以由于时间差在重平衡的时候,可能会将同一个队列分配给两个 consumer,所以使用版本号机制加以控制)

  1. 消息拉取
    a. consumer 阻塞从 PullRequestQueue 中拉取 pullRequest,不间断获取
    b. 根据 pullRequest.MessageQueue.brokerName 从本地缓存中获取一台 broker,之后带着 pullRequest.MessageQueue.queueId 向 broker 发出拉取请求
    c. broker 处理拉取消息请求,从 <topic, <queueId, ConsumeQueue>> 中获取 consumequeue,再根据要查询的 offset 从 consumequeue 和 commitLog 中拉取消息
    d. consumer拉取到消息后,首先将 PullRequest 的 offset 进行设置,然后将拉取到的消息提交给消息消费服务的消息处理线程池(执行消息消费监听逻辑),将 PullRequest 对象加到 PullRequestQueue 中,用于消息的后续拉取

  2. 延迟消息(重试机制的实现原理)

topic:SCHEDULE_TOPIC_XXX
队列:每个延迟级别对应一个队列

a. producer 发送消息延迟消息时,broker 会将消息真实 topic 和 queueId 封装到消息扩展属性中,用延迟队列的 topic 和 queueId 进行覆盖。
b. 消息消费时,从延迟消息扩展参数中将真实的 topic 和 queueId 解析出来,转发到对应的 topic 和 queueId 上,进行消费

高可用

namesrv

集群模式,还可通过 http 静态服务器,动态扩缩容

broker

  1. 集群模式 + 主从复制 + 主从切换
  2. 异步发送/单向发送,消息发送端会做并发控制

消息处理

  1. 消息发送重试机制(同步的重试在发送失败时;异步的重试在收到服务端响应包时进行,所以超时不重试)
  2. 消息发送故障规避机制(第一次发送失败,第二次发送时避开前一次发送失败的 broker,选择新的 broker)
  3. 消息消费重试机制

消息轨迹

记录消息生命周期,极好的用于问题排查,例如,典型的丢消息问题。

高性能

消息存储

commitlog:极致的磁盘顺序写 + 内存映射提高IO写效率,内存级别的读写分离,降低 pagecache 压力,实现了批量化消息写入 pagecache
consumequeue:定长,数组级别查找条目,提高消费性能

精细化的线程隔离

消息拉取线程与消费线程池隔离,各干各的,当发现消费慢了,可以考虑增加消费线程;使用 Netty 高效通信

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容