针对冷热读写场景的RocketMQ存储系统设计
简单描述
实现功能点
- 实现了一个单机的消息队列存储引擎,实现了顺序写入和随机读取的基本功能,并保证机器断电数据不丢失的持久化特性。
- 设计底层数据文件的存储结构,并基于topic和queueId实现文件分区。设置内存写缓冲区,实现多客户端写入聚合批量刷盘,并保证写入磁盘4KB对齐。
- 设计内存索引结构,并基于堆外内存、傲腾持久化内存(Aep)以及SSD设计多级缓存结构,以及相应的数据读取查询过程。基于Aep设计了实现了环形缓冲区,当写入数据量溢出时则覆盖旧数据。
项目描述
此项目是我之前参加的阿里巴巴云原生竞赛的项目,其主要内容是设计一个单机消息存储队列,实现基于topic和queueid的消息的的写入与读取功能,并且要保证机器断电数据不丢失的正确性。所以消息写入返回时,数据必须落入磁盘。同时阿里云还提供了有限的傲腾持久化内存给我们使用,所以基于多级内存、持久化内存以及固态硬盘多级存储架构,设计了索引结构以及读取过程。
技术要点
存储和索引结构
- 磁盘存储结构:将所有队列分组,写入5个数据文件,每个数据文件保存该组所有队列的写入数据(每条记录包括主题、队列id、和数据)。
- 内存合并写入缓存:每个数据文件对应一个内存写入缓冲区,该写入缓存需要加锁,因为会有多线程并发写入。该缓存区存储每条消息的主题、队列id、和数据。该写入缓存并不用于读取作用,4kb对齐刷写。
- AEP存储结构和AEP写入缓存:aep存储为环形存储,即设置固定的大小,当写入数据超过大小时,则从头覆盖写入。每个工作线程对应一个AEP存储和相应的AEP写入缓存(ThreadLocal),这样的好处在于避免并发问题,所有对于aep和缓存的读写操作都是顺序执行,不会有并发问题。由于是AEP环形存储结构以及固定大小的写入缓存,所以需要记录其有效数据的startoffset。当缓存没命中时,要到下一层中进行读取。
- 内存索引:由于有三层存储结构(内存、AEP、磁盘),所以需要设置索引结构辅助读取操作。由于数据断电不丢失,索引可以根据数据文件进行恢复,所以索引结构不需要持久化。在内存中按照topic和queueid分别存储每条消息的aepWriteBufferOffset、aepOffset、ssdOffset。
写入过程
- 根据topicid和queueid进行hash,获取分组编号groupid;
- 将消息写入当前线程的aep写入缓存,如果aep写入缓存已满,则将其刷写到aep数据文件中,再写入aep写入缓存。并添加该消息的aep写入缓存索引和aep数据文件索引。
- 将消息写入内存合并写入缓冲区(加锁),若缓冲区已满,则刷写到SSD(根据groupid获取对应数据文件)中并保持4K对齐,添加该消息的SSD数据文件索引,并唤醒写入阻塞的线程。否则,直接写入缓冲区,并阻塞当前线程直至数据被刷写到磁盘。
读取过程
- 根据topicid和queueid进行hash,获取分组编号groupid;
- 根据topic和queueid以及offset获取消息的对应索引(aepWriteBufferOffset、aepOffset、ssdOffset)。
- 读取当前线程的AEP写入缓存,如果不包含对应数据,则读取当前线程的AEP缓存,如果不包含对应数据,则读取groupid对应的SSD数据文件。
总结
- 每条消息共写入4次:AEP写入缓存、AEP缓存、内存合并写入缓冲区和SSD。其中AEP写入缓存、AEP缓存写入不需要加锁,应该其是线程独有(安全)的。
- 分组合并写入的优点是:1.多个线程的写入合并刷盘,减少了IO的次数,提高了写入效率,减少写入线程的阻塞时间。2.多个线程的写入快速积累数据到4kb倍数,提高flush效率。
- 数据读取按照三级缓存:AEP写入缓存、AEP缓存和SSD。
个人感想
之前一直对消息队列的存储设计并不了解,通过此次比赛,让我受益匪浅。对于一个消息队列,为了提高其并发性,需要减少加锁阻塞的消耗。所以设置工作线程池,并将所有队列进行hash分配线程,使得对于一个指定的queue,其读写操作一定是由同一个线程进行顺序操作的,这对于设置内存缓冲有极大好处,即不需要加锁。在此赛题中也是如此规定的,即一个线程固定绑定多个queue。所以对于每一个工作线程可以设置线程独有的缓冲区,在写入和读取指定queue时,对其缓冲区进行操作则不需要进行加锁。