作者:斜阳
RocketMQ 完结了灵敏的多分区和多副本机制,有用的防止了集群内单点毛病关于全体服务可用性的影响。存储机制和高可用战略是 RocketMQ 安稳性的中心,社区上关于 RocketMQ 现在存储完结的剖析与讨论一向是一个热议的话题。近期我一向在担任 RocketMQ 音讯多副本和高可用才能的建造,和大家同享下一些有趣的想法。
本文想从一个不一样的视角,着重于谈谈我眼中的这种存储完结是在处理哪些杂乱的问题,因而我从本文开端的版别中删去了冗杂的代码细节剖析,由浅入深的剖析存储机制的缺点与优化方向。
RocketMQ 的架构模型与存储分类
先来简略介绍下 RocketMQ 的架构模型。RocketMQ 是一个典型的发布订阅体系,经过 Broker 节点中转和耐久化数据,解耦上下流。Broker 是实在存储数据的节点,由多个水平布置但不必定彻底对等的副本组构成,单个副本组的不同节点的数据会到达终究一致。关于单个副本组来说同一时刻最多只会有一个可读写的 Master 和若干个只读的 Slave,主毛病时需求推举来进行单点毛病的容错,此刻这个副本组是可读不可写的。
NameServer 是独立的一个无状态组件,承受 Broker 的元数据注册并动态保护着一些映射关系,一起为客户端供给服务发现的才能。在这个模型中,咱们运用不同主题 (Topic) 来差异不同类别信息流,为顾客设置订阅组 (Group) 进行更好的办理与负载均衡。
如下图中间部分所示:
- 服务端 Broker Master1 和 Slave1 构成其间的一个副本组。
- 服务端 Broker 1 和 Broker 2 两个副本组以负载均衡的办法共同为客户端供给读写。
RocketMQ 现在的存储完结能够分为几个部分:
-
元数据办理
-
- 具体指当时存储节点的主题 Topic,订阅组 Group,消费进展 ConsumerOffset。
- 多个装备文件 Config,以及为了毛病康复的存储 Checkpoint 和 FileLock。
- 用来记载副本主备身份的 Epoch / SN (sequence number) 文件等(5.0-beta 引进,也能够看作 term)
-
音讯数据办理,包含音讯存储的文件 CommitLog,文件版守时音讯的 TimerLog。
-
索引数据办理,包含按行列的次序索引 ConsumeQueue 和随机索引 IndexFile。
元数据办理与优化
为了进步全体的吞吐量与供给跨副本组的高可用才能,RocketMQ 服务端一般会为单个 Topic 创立多个逻辑分区,即在多个副本组上各自保护部分分区 (Partition),咱们把它称为行列 (MessageQueue)。同一个副本组上同一个 Topic 的行列数相同并从 0 开端接连编号,不同副本组上的 MessageQueue 数量能够不同。
例如 topic-a 能够在 broker-1 主副本上有 4 个行列,编号 (queueId) 是 0-3,在 broker-1 备副本上彻底相同,可是 broker-2 上或许就只需 2 个行列,编号 0-1。在 Broker 上元数据的安排办理办法是与上述模型匹配的,每一个 Topic 的 TopicConfig,包含了几个中心的特点,称号,读写行列数,权限与许多元数据标识,这个模型类似于 K8s 的 StatefulSet,行列从 0 开端编号,扩缩行列都在尾部操作(例如 24 个行列缩分区到 16,是留下了编号为 0-15 的分区)。这使得咱们无需像 Kafka 一样对每个分区独自保护状态机,一起大幅度的简化了关于分区的完结。
咱们会在存储节点的内存中简略的保护 Map 的结构来将 TopicName 直接映射到它的具体参数。这个规划足够的简略,也隐含了一些缺点,例如它没有完结一个原生 Namespace 机制来完结存储层面上多租户环境下的元数据的阻隔,这也是 RocketMQ 5.0 向云原生年代跨进进程中一个重要的演进方向。
当 Broker 接收到外部管控指令,例如创立或删去一些 Topic,这个内存 Map 中就会对应的更新或许删去一个 KV 对,需求马上序列化一次并向磁盘覆盖,否则就会形成丢掉更新。关于单租户的场景下,Topic (Key) 的数量不会超越几千个,文件巨细也只需数百 KB,速度是十分快。
可是在云上大多租的场景下,一个存储节点的 Topic 能够到达十几 MB。每次改动一个 KV 就全量向磁盘覆盖写这个大文件,这个操作的开支十分高,尤其是在数据需求跨集群,跨节点搬迁,或许应急状况下扩容逃生场景下,同步写文件严重延长了外围管控指令的呼应时刻,也成为云上大同享模式下严峻的应战之一。在这个布景下,两个处理计划很天然的就发生了,即批量更新接口和增量更新机制。
- 批量更新指每次服务端能够承受一批 TopicConfig 的更新,这样 Broker 刷写文件的频率就显著的下降。
- 增量更新指将这个 Map 的耐久化换成逻辑替换成 KV 型的数据库或完结元数据的 Append 写,以 Compaction 的办法保护一致性。
除了最重要的 Topic 信息,Broker 还办理着 Group 信息,消费组的消费进展 ConsumerOffset 和多个装备文件。Group 的改动和 Topic 类似,都是只需新建或许删去时才需求耐久化。而 ConsumeOffset 是用来保护每个订阅组的消费进展的,结构如 Map>。这儿咱们从文件自身的效果和数据结构的视点进行剖析下,Topic Group 虽然数量多,可是改动的频率仍是比较低的,而提交与耐久化位点时时刻刻都在进行,进而导致这个 Map 几乎在实时更新,可是上一更新后的数据 (last commit offset) 对当时来说又没有什么用,并且答应丢少数更新。
所以这儿 RocketMQ 没有像 Topic Group 那样采纳数据改动时刷写文件,而是运用一个守时使命对这个 Map 做 CheckPoint。这个周期默许是 5 秒,所以当服务端主备切换或许正常发布时,都会有秒级的音讯重复。
那么这儿还有没有优化的空间呢?事实上大部分的订阅组都是不在线的,每次咱们也只需求更新位点有改动的这部分订阅组。所以这儿咱们能够采纳一个差分优化的战略(参加过 ACM 的选手应该更熟悉,搜索差分数据传输),在主备同步 Offset 或许耐久化的时分只更新改动的内容。假设此刻咱们除了知道当时的 Offset,还需求一个历史 Offset 的提交记载怎么办,这种状况下,运用一个内置的体系 Topic 来保存每次提交(某种意义上的自举完结,Kafka 便是运用一个内部 Topic 来保存位点),经过回放或查找音讯来追溯消费进展。由于 RocketMQ 支撑海量 Topic,元数据的规划会愈加大,选用现在的完结开支更小。
所以选用哪种完结彻底是由咱们所面临的需求决议的,完结也能够是灵敏多变的。当然,在 RocketMQ 元数据办理上,怎么在上层确保分布式环境下多个副本组上的数据一致又是别的一个令人头疼的难题,后续文章会愈加详细的讨论这点。
音讯数据办理
许多文章都说到 RocketMQ 存储的中心是一个极致优化的次序写盘,以 append only 的办法不断的将新的音讯追加到文件末尾。
RocketMQ 运用了一种称为 MappedByteBuffer 的内存映射文件的办法,将一个文件映射到进程的地址空间,完结文件的磁盘地址和进程的一段虚拟地址关联,实践上是运用了NIO 中的 FileChannel 模型。在进行这种绑定后,用户进程就能够用指针(偏移量)的办法写入磁盘而不必进行 read / write 的体系调用,削减了数据在缓冲区之间来回仿制的开支。当然这种内核完结的机制有一些限制,单个 mmap 的文件不能太大 (RocketMQ 挑选了 1G),此刻再把多个 mmap 的文件用一个链表串起来构成一个逻辑行列 (称为 MappedFileQueue),就能够在逻辑上完结一个无需考虑长度的存储空间来保存悉数的音讯。
这儿不同 Topic 的音讯直接进行混合的 append only 写,比较于随机写来说功能的进步十分显著的。还有一个重要的细节,这儿的混合写的写扩大十分低。当咱们回头去看 Google 完结的 BigTable 的理论模型,各种 LSM 树及其变种,都是将原来的直接保护树转为增量写的办法来确保写功能,再叠加周期性的异步兼并来削减文件的个数,这个动作也称为 Compaction。
RocksDB 和 LevelDB 在写扩大,读扩大,空间扩大都有几倍到几十倍的开支。得益于音讯自身的不可变性,和非堆积的场景下,数据一旦写入中间署理 Broker 很快就会被下流消费掉的特性,此刻咱们不需求在写入时就保护 memTable,防止了数据的分发与重建。比较于各种数据库的存储引擎,音讯这样近似 FIFO 的完结能够节省许多的资源,一起削减了 CheckPoint 的杂乱度。关于同一个副本组上的多个副本之间的数据仿制都是悉数由存储层自行办理,这个规划类似于 bigtable 和 GFS,azure 的 Partation layer,也被称为 Layered Replication 分层架构。
单条音讯的存储格局
RocketMQ 有一套相对杂乱的音讯存储编码用来将音讯目标序列化,随后再将一个非定长的数据落到上述的实在的写入到文件中,值得注意的存储格局中包含了索引行列的编号和方位。
存储时单条音讯自身元数据占用的存储空间为固定的 91B + 部分特点,而音讯的 payload 一般大于 2K,也便是说元数据带来的额定存储开支只增加了 5%-10% 左右。很明显,单条音讯越大,存储自身额定的开支(份额)就相对的越少。但假如有大音讯的诉求,例如想在 body 中保存一张序列化后的图片(二进制大目标),从现在的完结上说,在音讯中保存引证,将实在数据保存到到其他组件,消费时读取引证(比方文件名或许 uk)其实是一个更适宜的规划。
多条音讯的接连写
上文说到,不同 Topic 的音讯数据是直接混合追加数据到 CommitLog 中 (也便是上文说到的 MappedFileQueue),再交由其他后端线程做分发。其实我觉得 RocketMQ 这种 CommitLog 与元数据的分开办理的机制也有一些 PacificaA (微软提出的仿制结构) 的影子,然后以一种更简略的办法完结强一致。
这儿的强一致指的是在 Master Broker (对应于 PacificA 的 Primary) 对一切音讯的耐久化进行定序,再经过全序广播 (total order broadcast) 完结线性一致 (Linearizability)。这几种完结都会需求处理两个类似的问题,一是怎么完结单机下的次序写,二是怎么加快写入的速度。
假如是副本组是异步多写的(高功能中牢靠性),将日志非最新(水位最高)的备选为主,主备的数据日志或许会发生分叉。在 RocketMQ 5.0 中,主备会经过依据版别的洽谈机制,运用落后补齐,切断未提交数据等办法来确保数据的一致性。
顺便一提,RocketMQ 5.0 中完结了 logic queue 计划处理大局分区数改动的问题,这和 PacificaA 中经过 new-seal 新增副本组和分片 merge 给核算层读的一些优化战略有一些异曲同工之妙,具体能够参考这个规划计划。
- 独占锁完结次序写
怎么确保单机存储写 CommitLog 的次序性,直观的想法便是对写入动作加独占锁保护,即同一时刻只答应一个线程加锁成功,那么该选什么样的锁完结才适宜呢?RocketMQ 现在完结了两种办法。1. 依据 AQS 的 ReentrantLock 2. 依据 CAS 的 SpinLock。
那么什么时分选取 spinlock,什么时分选取 reentranlock?回忆下两种锁的完结,关于 ReentrantLock,底层 AQS 抢不到锁的话会休眠,可是 SpinLock 会一向抢锁,形成明显的 CPU 占用。SpinLock 在 trylock 失利时,能够预期持有锁的线程会很快退出临界区,死循环的忙等候很或许要比进程挂起等候更高效。这也是为什么在高并发下为了保持 CPU 平稳占用而选用办法一,单次恳求呼应时刻短的场景下选用办法二能够削减 CPU 开支。两种完结适用锁内操作时刻不同的场景,那线程拿到锁之后需求进行哪些动作呢?
- 预核算索引的方位,即 ConsumeQueueOffset,这个值也需求确保严厉递增
- 核算在 CommitLog 存储的方位,physicalOffset 物理偏移量,也便是大局文件的方位。
- 记载存储时刻戳 storeTimestamp,主要是为了确保音讯投递的时刻严厉保序
因而不少文章也会主张在同步耐久化的时分选用 ReentrantLock,异步耐久化的时分选用 SpinLock。那么这个地方还有没有优化的空间?现在能够考虑运用较新的 futex 替代 spinlock 机制。futex 保护了一个内核层的等候行列和许多个 SpinLock 链表。
当获得锁时,测验 cas 修改,假如成功则获得锁,否则就将当时线程 uaddr hash 放入到等候行列 (wait queue),涣散对等候行列的竞争,减小单个行列的长度。这听起来是不是也有一点点 concurrentHashMap 和 LongAddr 的味道,其实中心思想都是类似的,即涣散竞争。
- 成组提交与可见性
受限于磁盘 IO,块存储的呼应一般十分慢。要求一切恳求立即耐久化是不或许的,为了进步功能,大部分的体系总是将操作日志缓存到内存中,比方在满意”日志缓冲区中数据量超越必定巨细 / 距离前次刷入磁盘超越一守时刻” 的任一条件时,经过后台线程守时耐久化操作日志。
但这种成组提交的做法有一个很大的问题,存储体系意外毛病时,会丢掉终究一部分更新操作。例如数据库引擎总是要求先将操作日志刷入磁盘 (优先写入 redo log) 才能更新内存中的数据,这样断电重启则能够经过 undo log 进行事务回滚与丢掉。
在音讯体系的完结上有一些奇妙的不同,不同场景下对音讯的牢靠性要求不同,在金融云场景下或许要求主备都同步耐久化完结音讯才对下流可见,但日志场景希望尽或许低的推迟,一起答应毛病场景少数丢掉。此刻能够将 RocketMQ 装备为单主异步耐久化来进步功能,下降本钱。此刻宕机,存储层会损失终究一小段没保存的音讯,而下流的顾客实践上现已收到了。当下流的顾客重置位点到一个更早的时刻,回放至同样位点的时分,只能读取到了新写入的音讯,但读取不到之前消费过的音讯(相同位点的音讯不是同一条),这是一种 read uncommitted。
这样会有什么问题呢?关于一般音讯来说,由于这条音讯现已被下流处理,最坏的影响是重置位点时无法消费到。可是关于 Flink 这样的流核算结构,以 RocketMQ 作为 Source 的时分,经过回放最近一次 CheckPoint 到当时的数据的 offset 来完结高可用,不可重复读会形成核算体系没法做到准确的 excatly once 消费,核算的结果也就不正确了。相应的处理的计划之一是在副本组多数派承认的时分才构建被顾客可见的索引,这么做微观上的影响便是写入的推迟增加了,这也能够从另一个视点解读为阻隔等级的进步带来的价值。
关于权衡推迟和吞吐量这个问题,能够经过加快主备仿制速度,改动仿制的协议等手段来优化,这儿大家能够看下 SIGMOD 2022 关于 Kafka 运行在 RDMA 网络上显著下降推迟的论文《KafkaDirect: Zero-copy Data Access for Apache Kafka over RDMA Networks》
耐久化机制
关于这一块的讨论在社区里讨论是最多的,不少文章都把耐久化机制称为刷盘。我不喜欢这个词,由于它不准确。在 RocketMQ 中供给了三种办法来耐久化,对应了三个不同的线程完结,实践运用中只会挑选一个。
-
同步耐久化,运用 GroupCommitService。
-
异步耐久化且未敞开 TransientStorePool 缓存,运用 FlushRealTimeService。
-
异步耐久化且敞开 TransientStorePool 缓存,运用 CommitRealService。
-
耐久化
同步刷盘的落盘线程统一都是 GroupCommitService。写入线程仅仅担任唤醒落盘线程,将音讯转交给存储线程,而不会等候音讯存储完结之后就马上回来了。我个人对这个规划的了解是,音讯写入线程相对与存储线程来说也能够看作 IO 线程,而实在存储的线程需求攒批耐久化会堕入中止,所以才要大费周章的做转交。
从同步刷盘的完结看,落盘线程每隔 10 ms 会检查一次,假如有数据未耐久化,便将 page cache 中的数据刷入磁盘。此刻操作体系 crash 或许断电,那未落盘的数据丢掉会不会对生产者有影响呢?此刻生产者只需运用了牢靠发送 (指非 oneway 的 rpc 调用),这时关于发送者来说还没有收到成功的呼应,此刻客户端会进行重试,将音讯写入其他可用的节点。
异步耐久化对应的线程是 FlushRealTimeService,完结上又分为固定频率和非固定频率,中心差异是线程是否呼应中止。所谓的固定频率是指每次有新的音讯到来的时分不论,不呼应中止,每隔 500ms(可装备)flush 一次,假如发现未落盘数据缺乏(默许 16K),直接进入下一个循环,假如数据写入量很少,一向没有填充溢16K,就不会落盘了吗?这儿还有一个依据时刻的兜底计划,即线程发现距离前次写入现已很久了(默许 10 秒),也会履行一次 flush。
但事实上 FileChannel 仍是 MappedByteBuffer 的 force() 办法都不能准确操控写入的数据量,这儿的写行为也仅仅对内核的一种主张。关于非固定频率完结,即每次有新的音讯到来的时分,都会发送唤醒信号,当唤醒动作在数据量较大时,存在功能损耗,但音讯量较少且状况下实时性好,更省资源。在生产中,具体挑选哪种耐久化完结由具体的场景决议。是同步写仍是多副本异步写来确保数据存储的牢靠性,本质上是读写推迟和和本钱之间的权衡。
- 读写别离
广义上来说,读写别离这个名词有两个不同的含义:
- 像数据库一样主写从读,分摊读压力,献身推迟牢靠性更高,适用于音讯读写比十分高的场景。
- 存储写入将音讯暂存至 DirectByteBuffer,当数据成功写入后,再归还给缓冲池,写不运用 page cache 。
这儿主要来讨论第二点,当 Broker 装备异步耐久化且敞开缓冲池,启用的异步刷盘线程是 CommitRealTimeService。咱们知道操作体系自身一般是当 page cache 上积累了许多脏页后才会触发一次 flush 动作(由一些 vm 参数操控,比方 dirty_background_ratio 和 dirty_ratio)。
这儿有一个很有意思的说法是 CPU 的 cache 是由硬件保护一致性,而 page cache 需求由软件来保护,也被称为 syncable。这种异步的写入或许会形成刷脏页时磁盘压力较高,导致写入时呈现毛刺现象。为了处理这个问题,呈现了读写别离的完结。
RocketMQ 发动时会默许初始化 5 块(参数 transientStorePoolSize 决议)堆外内存(DirectByteBuffer)循环运用,由于复用堆外内存,这个小计划也被成为池化,池化的优点及弊端如下:
-
优点:数据写堆外后便很快回来,削减了用户态与内核态的切换开支。
-
弊端:数据牢靠性降为最低等级,进程重启就会丢数据(当然这儿一般合作多副本机制进行保障)。读取需求 load page cache,也会增加一些端到端的推迟。
-
宕机与毛病康复
宕机一般是由于底层的硬件问题导致,RocketMQ 宕机后假如磁盘没有永久毛病,一般只需求原地重启,Broker 首先会进行存储状态的康复,加载 CommitLog,ConsumeQueue 到内存,完结 HA 洽谈,终究初始化 Netty Server 供给服务。现在的完结是终究初始化对用户可见的网络层服务,实践上这儿也能够先初始化网络库,分批将 Topic 注册到 NameServer,这样正常晋级时能够对用户的影响更小。
在 recover 的进程中还有许多软件工程完结上的细节,比方从块设备加载的时分需求校验音讯的 crc 看是否发生过错,对终究一小段未承认的音讯进行 dispatch 等操作。默许从倒数第三个文件 recover CommitLog 加载音讯到 page cache (假设未耐久化的数据 < 3G),防止一上线由于客户端恳求的音讯不在内存,导致张狂的缺页中止堵塞线程。分布式场景下还需求对存储的数据保护一致性,这也就涉及到日志的切断,同步和回发等问题,后续我将在高可用篇再具体讨论这一点。
文件的生命周期
聊完了音讯的生产保存,再来讨论下音讯的生命周期,只需磁盘没有满,音讯能够长时刻保存。前面说到 RocketMQ 将音讯混合保存在 CommitLog,关于音讯和流这样近似 FIFO 的体系来说,越近期的音讯价值越高,所以默许以翻滚的办法早年向后删去最长远的音讯,而不会关注文件上的音讯是否悉数被消费。触发文件清除操作的是一个守时使命,默许每 10s 履行一次。在一次守时使命触发时,或许会有多个物理文件超越过期时刻可被删去,因而删去一个文件不但要判别这个文件是否还被运用,还需求间隔一守时刻(参数 deletePhysicFilesInterval)再删去别的一个文件,由于删去文件是一个十分消耗 IO 的操作,或许会引起存储颤动,导致新音讯写入和消费的推迟。所以又新增了一个守时删去的才能,运用 deleteWhen 装备操作时刻(默许是凌晨 4 点)。
咱们把由于磁盘空间缺乏导致的删去称为被动行为,由于高速介质一般比较贵(傲腾 ESSD等),出于本钱考虑,咱们还会异步的自动的将热数据转移到二级介质上。在一些特殊的场景下,删去的一起或许还需求对磁盘做安全擦除来防止数据康复。
防止存储颤动
- 快速失利
音讯被服务端 Netty 的 IO 线程读取后就会进入到堵塞行列中排队,而单个 Broker 节点有时会由于 GC,IO 颤动等要素形成短时存储写失利。假如恳求来不及处理,排队的恳求就会越积越多导致 OOM,客户端视角看从发送到收到服务端呼应的时刻大大延长,终究发送超时。RocketMQ 为了缓解这种颤动问题,引进了快速失利机制,即敞开一个扫描线程,不断的去检查行列中的第一个排队节点,假如该节点的排队时刻现已超越了 200ms,就会拿出这个恳求,立即向客户端回来失利,客户端会重试到其他副本组(客户端还有一些熔断与阻隔机制),完结全体服务的高可用。
存储体系不止是被动的感知一些下层原因导致的失利,RocketMQ 还规划了许多简略有用的算法来进行自动估算。例如音讯写入时 RocketMQ 想要判别操作体系的 page cache 是否繁忙,可是 JVM 自身没有供给这样的 Monitor 东西来评估 page cache 繁忙程度,于是运用体系的处理时刻来判别写入是否超越 1 秒,假如超时的话,让新恳求会快速失利。再比方客户端消费时会判别当时主的内存运用率比较高,大于物理内存的 40% 时,就会主张客户端从备机拉取音讯。
- 预分配与文件预热
为了在 CommitLog 写满之后快速的切换物理文件,后台运用一个后台线程异步创立新的文件并进行对进行内存确定,还大费周章的规划了一个额定文件预热开关(装备 warmMapedFileEnable),这么做主要有两个原因:
-
恳求分配内存并进行 mlock 体系调用后并不必定会为进程彻底确定这些物理内存,此刻的内存分页或许是写时仿制的。此刻需求向每个内存页中写入一些假的值,有些固态的主控或许会对数据紧缩,所以这儿不会写入 0。
-
调用 mmap 进行映射后,OS 仅仅树立虚拟内存地址至物理地址的映射表,而实践并没有加载任何文件至内存中。这儿或许会有许多缺页中止。RocketMQ 在做 mmap 内存映射的一起进行 madvise 调用,一起向 OS 表明 WILLNEED 的志愿。使 OS 做一次内存映射后对应的文件数据尽或许多的预加载至内存中,然后到达内存预热的效果。
当然,这么做也是有弊端的。预热后,写文件的耗时缩短了许多,但预热自身就会带来一些写扩大。全体来看,这么做能在必定程度上进步呼应时刻的安稳性,削减毛刺现象,但在 IO 自身压力很高的状况下则不主张敞开。
RocketMQ 是适用于 Topic 数量较多的事务音讯场景。所以 RocketMQ 选用了和 Kafka 不一样的零仿制计划,Kafka 选用的是堵塞式 IO 进行 sendfile,适用于体系日志音讯这种高吞吐量的大块文件。而 RocketMQ 挑选了 mmap + write 非堵塞式 IO (依据多路复用) 作为零仿制办法,这是由于 RocketMQ 定坐落事务级音讯这种小数据块/高频率的 IO 传输,当想要更低的推迟的时分挑选 mmap 更适宜。
当 kernal 把可用的内存分配后 free 的内存就不够了,假如进程一下发生许多的新分配需求或许缺页中止,还需求将经过淘汰算法进行内存回收,此刻或许会发生颤动,写入会有短时的毛刺现象。
- 冷数据读取
关于 RocketMQ 来说,读取冷数据或许有两种状况。
- 恳求来自于这个副本组的其他节点,进行副本组内的数据仿制,也或许是离线转储到其他体系。
- 恳求来自于客户端,是顾客来消费几个小时以前的数据,归于正常的事务诉求。
关于第一种状况,在 RocketMQ 低版别源码中,关于需求许多仿制 CommitLog 的状况(例如备磁盘毛病,或新上线一个备机),主默许运用 DMA 仿制的办法将数据直接经过网络仿制给备机,此刻由于许多的缺页中止堵塞了 io 线程,此刻会影响 Netty 处理新的恳求,在完结上让一些组件之间的内部通讯运用 fastRemoting 供给的第二个端口,处理这个问题的临时计划还包含先用事务线程将数据 load 回内存而不运用零仿制,但这个做法没有从本质上处理堵塞的问题。关于冷仿制的状况,能够运用 madvice 主张 os 读取防止影响主的音讯写入,也能够从其他备仿制数据。
关于第二种状况,对各个存储产品来说都是一个应战,客户端消费一条音讯时,热数据悉数存储在 page cache,关于冷数据会退化为随机读(体系会有一个对 page cache 接连读的预测机制)。需求消费超越几个小时之前的数据的场景下,顾客一般都是做数据剖析或许离线使命,此刻下流的目标都是吞吐量优先而非推迟。关于 RocketMQ 来说有两个比较好的处理计划,第一是同 redirect 的办法将读取恳求转发给备进行分摊读压力,或许是从转储后的二级介质读取。在数据转储后,RocketMQ 自身的数据存储格局会发生改动,详见后文。
索引数据办理
在数据写入 CommitLog 后,在服务端当 MessageStore 向 CommitLog 写入一些音讯后,有一个后端的 ReputMessageService 服务 (dispatch 线程) 会异步的构建多种索引,满意不同办法的读取诉求。
行列维度的有序索引 ConsumeQueue
在 RocketMQ 的模型下,音讯自身存在的逻辑行列称为 MessageQueue,而对应的物理索引文件称为 ConsumeQueue。从某种意义上说 MessageQueue = 多个接连 ConsumeQueue 索引 + CommitLog 文件。
ConsumeQueue 相对与 CommitLog 来说是一个愈加轻量。dispatch 线程会源源不断的将音讯从 CommitLog 取出,再拿出音讯在 CommitLog 中的物理偏移量 (相关于文件存储的 Index),音讯长度以及Tag Hash 作为单条音讯的索引,分发到对应的消费行列。偏移 + 长度构成了对 CommitLog 的引证 (Ref)。这种 Ref 机制关于单条音讯只需 20B,显著下降了索引存储开支。ConsumeQueue 实践写入的完结与 CommitLog 不同,CommitLog 有许多存储战略能够挑选且混合存储,一个 ConsumeQueue 只会保存一个 Topic 的一个分区的索引,耐久化默许运用 FileChannel,实践上这儿运用 mmap 的话对小数据量的恳求愈加友爱,不必堕入中止。
客户端的 pull 恳求到服务端履行了如下流程来查询音讯:
- 查询 ConsumeQueue 文件 -> 2. 依据 cq 拿到 physicOffset + size -> 3. 查询 CommitLog 获得音讯
RocketMQ中默许指定每个消费行列的文件存储 30 万条索引,而一个索引占用 20 个字节,这样每个文件的巨细是 300 * 1000 * 20 / 1024 / 1024 ≈ 5.72M。为什么消费行列文件存储音讯的个数要设置成 30 万呢?这个经验值合适音讯量比较大的场景,事实上这个值关于大部分场景来说是偏大的,有用数据的实在占用率很低,导致ConsumeQueue 空载率高。
先来看看假如过大或许过小会带来什么问题。由于音讯总是有失效期的,例如 3 天失效,假如消费行列的文件设置过大的话,有或许一个文件中包含了过去一个月的音讯索引,但这个时分原始的数据现已翻滚没了,白白浪费了许多空间。但也不宜太小,导致 ConsumeQueue 也有许多小文件,下降读写功能。下面给出一个非严谨的空载率推导进程:
假设此刻单机的 Topic = 5000,单节点单个 Topic 的行列数一般是 8,分区数量 = 4万。以 1T 音讯数据为例,每条音讯巨细是 4KB,索引数量 = 音讯数量 = 1024 1024 * 1024 / 4 = 2.68 亿。最少需求的 ConsumeQueue = 索引数量 / 30万 = 895 个,实践运用率 (有用数据量) 约等于 2.4%。跟着 ConsumeQueue Offset 的原子自增翻滚, cq 头部是无效数据导致占用的磁盘空间会变大。依据公有云线上的状况来看,非 0 数据约占 5%,实践有用数据只占 1%。关于 ConsumeQueue 这样的索引文件,咱们能够运用 RocksDB 或许傲腾这样的耐久化内存来存储,或许对 ConsumeQueue 独自完结一个用户态文件体系,几个计划都能够削减全体索引文件巨细,进步访问功能。这一点在后文关于存储机制的优化中,咱们再详聊。
由于 CommitLog – ConsumerQueue – Offset 的关系从音讯写入的那一刻开端就确定了,在 Topic 跨副本组搬迁,副本组要下线等需求切流的场景下,假如需求音讯可读,需求选用仿制数据的计划来完结 Topic 跨副本组搬迁,只能选用音讯等级的仿制,而不能简略的把一个分区从副本组 A 移动到副本组 B。有一些音讯产品在面临这个场景时,选用了数据按分区仿制的计划,这种计划或许会马上发生许多的数据传输(分区 rebalance),而 RocketMQ 的切流一般能够做到秒级收效。
音讯维度的随机索引IndexFile
RocketMQ 作为事务音讯的首选,上文中 ReputMessageService 线程除了构建消费行列的索引外,还一起为每条音讯依据 id, key 构建了索引到 IndexFile。这是方便快速快速定位目标音讯而发生的,当然这个构建随机索引的才能是能够降级的,IndexFile文件结构如下:
IndexFile 也是定长的,从单个文件的数据结构来说,这是完结了一种简略原生的哈希拉链机制。当一条新的音讯索引进来时,首先运用 hash 算法命中黄色部分 500w 个 slot 中的一个,假如存在抵触就运用拉链处理,将最新索引数据的 next 指向上一条索引方位。一起将音讯的索引数据 append 至文件尾部(绿色部分),这样便形成了一条当时 slot 依照时刻存入的倒序的链表。这儿其实也是一种 LSM compaction 在音讯模型下的改进,下降了写扩大。
存储机制的演进方向
RocketMQ 的存储规划是以简略牢靠行列模型作为中心来抽象的,也因而发生了一些缺点和对应的优化计划。
KV 模型与 Queue 模型结合
RocketMQ 完结了单条事务音讯的退避重试,在生产实践中,咱们发现部分用户在客户端消费限流时直接将音讯回来失利,在重试音讯量比较大的时分,由于原有完结下重试行列数有限,导致重试音讯无法很好的负载均衡到一切客户端。一起,音讯来回的在服务端和客户端之间传输,使得两侧的开支都增加了,用户侧正确的做法应该是消费限流时,让消费的线程等候一会儿。
从存储服务的视点上来说,这其实是一种行列模型的缺乏,让一条行列只能被一个顾客持有。RocketMQ 提出了 pop 消费这种全新的概念,让单条行列的音讯能够被多个客户端消费到,这涉及到服务端对单条音讯的加解锁,KV 模型就十分符合这个场景。从长远来看,像守时音讯事务音讯能够有一些依据 KV 的更原生的完结,这也是 RocketMQ 未来尽力的方向之一。
音讯的紧缩与归档存储
紧缩便是用时刻去换空间的经典 trade-off,希望以较小的 CPU 开支带来更少的磁盘占用或更少的网络 I/O 传输。现在 RocketMQ 客户端从推迟考虑仅单条大于 4K 的音讯进行单条紧缩存储的。服务端关于收到的音讯没有马上进行紧缩存储有多个原因,例如为了确保数据能够及时的写入磁盘,音讯稀少的时分攒批效果比较差等,所以 Body 没有紧缩存储。而关于大部分的事务 Topic 来说,其实 Body 一般都有很大程度上是相似的,能够紧缩到原来的几分之一到几十分之一。
存储一般有高速(高频)介质与低速介质,热数据存放在高频介质上(如傲腾,ESSD,SSD),冷数据存放在低频介质上(NAS,OSS),以此来满意低本钱保存更久的数据。从高频介质转到更低频的 NAS 或许 OSS 时,不可防止的发生了一次数据仿制。咱们能够在这个进程中异步的对数据进行规整(闲时资源富余)。
那么咱们为什么要做规整呢,直接零仿制仿制不香吗?
答案便是低频介质虽然便宜大碗,但一般 iops 和吞吐量更低。关于 RocketMQ 来说需求规整的数据便是索引和 CommitLog 中的音讯,也便是说在高频介质与低频介质上音讯的存储格局能够是彻底不同的。当热音讯降级到二级存储的时分,数据密集且异步,这儿便是一个十分适宜的机会进行紧缩和规整。业界也有一些依据 FPGA 来加快存储紧缩的事例,将来咱们也会继续的做这方面的测验。
存储层资源同享与争抢
- 磁盘 IO 的抢占
没错,这儿想谈谈的其实是硬盘的调度算法。在一个考虑性价比的场景下,由于 RocketMQ 的存储机制,咱们能够把索引文件存储在 SSD,音讯自身放在 HDD 里,由于热音讯总是在 PageCache 中的,所以在 IO 调度上优先满意写而饿死读。关于没有堆积的顾客来说,消费到的数据是从 page cache 仿制到 socket 再传输给用户,实时性现已很高了。而关于消费冷数据(几个小时,几天以前的数据)用户的诉求一般是赶快获取到音讯即可,此刻服务端能够挑选赶快满意用户的 Pull 恳求,由于许多的随机 IO,这样磁盘会发生严重的 rt 颤动。
仔细考虑,这儿其实用户想要的是尽或许大的吞吐量,假设访问冷数据需求 200 毫秒,假设在服务端把冷读的行为滞后,再加上推迟 500 毫秒再回来给用户数据,并没有显著的差异。而这儿的 500 毫秒,服务端内部就能够兼并许多的 IO 操作,咱们也能够运用 madvice 体系调用去主张内核读取。这儿的兼并带来的收益很高,能够显著的削减对热数据的写入的影响,大幅度进步功能。
- 用户态文件体系
仍是为了处理随机读功率低的问题,咱们能够规划一个用户态文件体系,让 IO 调用悉数 kernel-bypass。
主要有几个方向:
- 多点挂载。常用的 Ext4 等文件体系不支撑多点挂载,让存储能够支撑多个实例的对同一份数据的同享访问。
- 调整关于 IO 的兼并战略,IO优先级,polling 模式,行列深度等。
- 运用文件体系类似 O_DIRECT 的非缓存办法读写数据。
RocketMQ 的未来
RocketMQ 存储体系经过多年的开展,基本功能特性现已比较完善,经过一系列的立异技能处理了分布式存储体系中的难题,安稳的服务于阿里集团和海量的云上用户。RocketMQ 在云原生年代的演进中遇到了更多的有趣的场景和应战,这是一个需求全链路调优的杂乱工程。咱们会继续在规划,安稳性,多活容灾等企业级特性,本钱与弹性等方面发力,将 RocketMQ 打造为“音讯,事件,流”一体化的交融平台。一起,咱们也会将开源举动愈加可继续的开展下去,为社会创造价值。
参考文献
[1]. 深入了解 Linux 中的 page cache
www.jianshu.com/p/ae741edd6…
[2]. PacificA: Replication in Log-Based Distributed Storage Systems.
www.microsoft.com/en-us/resea…
[3]. J. DeBrabant, A. Pavlo, S. Tu, M. Stonebraker, and S. B. Zdonik. Anti-caching: A new approach to database management system architecture. PVLDB, 6(14):1942–1953, 2013.
[4]. 《RocketMQ 技能内情》
[5]. 一致性协议中的“鬼魂复现” zhuanlan.zhihu.com/p/47025699.
[6]. Calder B, Wang J, Ogus A, et al. Windows Azure Storage: a highly available cloud storage service with strong consistency[C]//Proceedings of the Twenty-Third ACM Symposium on Operating Systems Principles. ACM, 2011: 143-157.
[7]. Chen Z, Cong G, Aref W G. STAR: A distributed stream warehouse system for spatial data[C] 2020: 2761-2764.
[8]. design data-intensive application 《构建数据密集型应用》