简介:RocketMQ作为基于磁盘存储的中间件,具有无限积压能力,提供高吞吐量和低延迟的服务能力。RocketMQ的核心部分一定是其优雅的存储设计。RocketMQ作为一个基于磁盘存储的中间件,拥有无限的积压能力,提供高吞吐量和低延迟的服务能力。RocketMQ的核心部分一定是其优雅的存储设计。存储概述RocketMQ存储的文件主要包括Commitlog文件、ConsumeQueue文件和Index文件。RocketMQ将所有主题的消息存储在同一个文件中,保证发送消息时文件顺序写入,尽量保证消息发送的高可用和高吞吐。但是,消息中间件一般是基于主题的订阅和发布模型。消费消息时必须根据主题选择消息。显然,根据Commitlog文件中的主题过滤消息将变得极其低效。为了提高基于topic的消息检索效率,RocketMQ引入了ConsumeQueue文件,俗称消费队列文件。关系数据库可以根据字段属性进行记录检索。RocketMQ作为一款主要面向业务开发的消息中间件,也提供了基于消息属性的检索能力。底层的核心设计理念是为Commitlog文件创建一个哈希索引,并存储在Index文件中。RocketMQ中顺序写入Commitlog文件后,异步构建ConsumeQueue和Index文件,数据流图如下:存储文件组织方式RocketMQ在消息写入过程中追求极致的磁盘顺序写入。所有主题的消息都写入一个文件,即Commitlog文件。所有消息都按到达顺序附加到文件中。消息一旦写入,不支持修改。Commitlog文件的具体布局如下图所示:基于文件的编程和基于内存的编程有很大的区别。在基于内存的编程模式下,我们有现成的数据结构,比如List、HashMap等,非常方便读写数据,那么Commitlog文件中存入一条消息后如何查找呢?就像关系型数据为每条数据引入一个ID字段一样,在基于文件的编程模型中,也为一条消息引入了一个标识标志:消息的物理偏移量,即消息存储的起始位置文件。因为有物理偏移量的概念,Commitlog文件的命名也很取巧,使用文件中存储的第一条消息在整个Commitlog文件组中的偏移量来命名,比如第一个Commitlog文件为000000000000000000000,第二个文件是00000000001073741824,依此类推。这样做的好处是可以给出任意一个消息的物理偏移量,比如消息偏移量是73741824,可以通过二分法查找,在第一个文件中快速定位到这个文件,然后利用消息的物理偏移量的差值减去文件名得到的就是文件中的绝对地址。Commitlog文件的设计理念是追求极致的消息书写,但我们知道消息消费模型是一种基于主题的订阅机制,即一个消费组消费特定主题的消息。如果我们根据主题从commitlog文件中检索消息,我们会发现这绝不是一个好主意。它只能从文件中的第一条消息中逐条检索。其性能可想而知。因此,为了解决基于主题的消息检索问题,RocketMQ引入了consumequeue文件,consumequeue的结构如下图所示。ConsumeQueue文件是消息消费队列文件,是Commitlog文件的一个基于主题的索引文件。主要用于消费者根据主题消费消息。它的组织方式是/topic/queue,同一个队列中有多个文件。Consumequeue的设计非常巧妙,每个entry的长度都是固定的(8字节commitlog物理偏移量,4字节消息长度,8字节taghashcode)。我们选择存储哈希码,而不是存储标签的原始字符串。目的是保证每个条目的长度是固定的。可以通过访问相似数组下标的方式快速定位入口,大大提高ConsumeQueue文件的读取性能。试想一下,消息消费者可以根据主题和消息消费进度(consumeuqe逻辑偏移量),使用逻辑偏移量logicOffset*20访问消息找到入口,即第一个Consumeque入口。startoffset(consumequeue文件中的offset),然后在不遍历consumequeue文件的情况下读取offset的最后20个字节得到一个entry。与Kafka相比,RocketMQ有一个强大的优势,就是支持通过消息属性来获取消息。consumequeue文件的引入解决了基于主题的搜索问题,但是如果要根据消息的某个属性来查找消息,consumequeue文件就无能为力了。RocketMQ引入了Index索引文件来实现基于文件的哈希索引。IndexFile的文件存储结构如下图所示:IndexFile实现了基于物理磁盘文件的Hash索引。它的文件由一个40字节的文件头,500万个Hash槽,每个4字节,最后2000万个Indexentry组成,每个Indexentry由20个字节组成,每个是一个4字节的indexkeyhashcode,8-字节消息物理偏移量、4字节时间戳和4字节前索引条目(Hash冲突的链表结构)。即建立索引key的hashcode与物理偏移量的映射关系,首先快速定义key到commitlog文件中。顺序写是基于磁盘读写的。另一个提高其写入性能的设计原则是磁盘顺序写入。磁盘顺序写入广泛应用于基于文件的存储模型。大家不妨想想引入MySQLRedologs的目的。我们知道在MySQLInnoDB存储引擎中,会有一个内存池来缓存磁盘上的文件块。当update语句修改数据后,会先在内存中修改,然后将修改写入redo文件(flushtodisk),然后周期性的将InnoDB内存池中的数据flush到磁盘。为什么不在数据发生变化时直接更新到指定的数据文件呢?MySQLInnoDB的一个数据库中有几千张表,每张表的数据都会存储在一个单独的文件中。如果每个表的数据发生变化,都会写入磁盘,会出现大量的随机写入,性能无法提升,所以引入了一个redo文件,redo文件是顺序写入的。表面上多了一个刷盘的步骤,但是因为是顺序写入,相对于随机写入,性能提升非常显着。虽然基于磁盘顺序写入的内存映射机制可以大大提高IO的写入效率,但是如果基于文件的存储使用常规的JAVA文件操作API,如FileOutputStream,性能提升将非常有限。RocketMQ引入内存映射,将磁盘文件映射到内存,以操作内存的方式操作磁盘,性能又提升了一个档次。在JAVA中,可以通过FileChannel的map方法创建内存映射文件。该方法在Linux服务器中创建的文件使用的是操作系统的pagecache,即pagecache。Linux操作系统中的内存使用策略会尽可能的使用机器的物理内存,并保存在内存中,也就是所谓的pagecache。当操作系统的内存不够用时,会使用缓存置换算法,比如LRU来回收不常用的pagecache,也就是操作系统会自动管理这部分内存。如果RocketMQBroker进程异常退出,保存在pagecache中的数据不会丢失,操作系统会周期性的将pagecache中的数据持久化到磁盘,保证数据的安全性和可靠性。但是如果是机器掉电等异常情况,pagecache中保存的数据可能会丢失。灵活多变的刷机策略在顺序写入和内存映射的支持下,RocketMQ的写入性能得到了极大的保证,但凡事有利有弊。引入了内存映射和页面缓存机制,消息会写入Pagecache,此时消息实际上并没有持久化到磁盘。那么broker收到client的消息后,是存入pagecache后直接返回success,还是持久化到磁盘后返回success呢?这是一个“艰难”的选择,是性能和消息可靠性之间的权衡。为此,RocketMQ提供了多种策略:同步刷盘、异步刷盘。1、同步刷写同步刷写在RocketMQ的实现中是分组提交,并不是每条消息都必须刷写。它的设计思路如图:采用同步刷写,每个线程追数据到内存,向刷写线程提交请求,然后阻塞;flashing线程从任务队列中获取一个任务,然后触发flush,但是它并不仅仅flush请求相关的消息,而是直接把内存中所有要flush的消息批量批量flush,然后唤醒一组请求线程实现groupflush。2、异步刷写和同步刷写的优点是可以保证消息不会丢失,即给客户端返回成功就意味着消息已经持久化到磁盘,即消息非常可靠,但这是以写响应延迟性能为代价的,因为RocketMQ消息是先写入pagecache的,消息丢失的可能性很小。如果可以容忍一定概率的消息丢失,可以考虑使用异步刷新。异步刷盘是指broker将消息存入pagecache后立即返回success,然后启动一个异步线程定时执行FileChannel的force方法,定时将内存中的数据刷到磁盘中。默认间隔为500毫秒。内存级读写分离RocketMQ为了减轻pagecache的压力,引入了transientStorePoolEnable机制,即内存级读写分离机制。RocketMQ默认将消息写入pagecache,消费消息时从pagecache中读取。这样高并发时pagecache的压力会比较大,容易出现transientbrokerbusy。因此,RocketMQ还引入了transientStorePoolEnable,将消息先写入堆中。堆外内存并立即返回,然后将堆外内存中的数据异步提交到pagecache,再异步刷新到磁盘。它的工作机制如下图所示:当消息被读取和消费时,不会尝试从堆外内存中读取,而是从pagecache中读取,从而形成内存级别的读写分离,即就是,消息写入时,主要面对堆外内存,读取消息时主要面对pagecache。这种方案的好处是消息直接写入堆外内存,然后异步写入pagecache。与将每条消息直接添加到pagechae相比,它最大的优势在于将消息写入pagecache的操作批量化。这种方案的缺点是,如果Broker进程因为一些意外操作异常退出,堆外内存中保存的数据会丢失,但如果放在pagecache中,broker异常退出,不会丢失信息。原文链接本文为阿里云原创内容,未经许可不得转载。
