存储:消息数据和元数据的存储是如何设计的

204 阅读4分钟

消息队列中的数据一般分为元数据和消息数据。元数据是指 Topic、Group、User、ACL、Config 等集群维度的资源数据信息,消息数据指客户端写入的用户的业务数据。下面我们先来看元数据信息的存储。

元数据信息的存储

元数据信息的特点是数据量比较小,不会经常读写,但是需要保证数据的强一致和高可靠,不允许出现数据的丢失。

一般有两个思路。

  1. 基于第三方组件来实现元数据的存储。
  2. 在集群内部实现元数据的存储。 基于第三方组件来实现元数据的存储是目前业界的主流选择。

消息数据的存储

一般情况下,消息队列的存储主要是指消息数据的存储,分为存储结构、数据分段、数据存储格式、数据清理四个部分。

数据存储结构设计

Topic 和 Group 不承担数据存储功能,承担的是逻辑组织的功能,实际的数据存储是在在分区维度完成的。

数据的落盘存储也有两个思路。

  1. 每个分区单独一个存储“文件”。
  2. 每个节点上所有分区的数据都存储在同一个“文件”。

第一个思路,每个分区对应一个文件的形式去存储数据。具体实现时,每个分区上的数据顺序写到同一个磁盘文件中,数据的存储是连续的。因为消息队列在大部分情况下的读写是有序的,所以这种机制在读写性能上的表现是最高的。

但如果分区太多,会占用太多的系统 FD 资源,极端情况下有可能把节点的 FD 资源耗完,并且硬盘层面会出现大量的随机写情况,导致写入的性能下降很多,另外管理起来也相对复杂。Kafka 在存储数据的组织上用的就是这个思路。

第二种思路,每个节点上所有分区的数据都存储在同一个文件中,这种方案需要为每个分区维护一个对应的索引文件,索引文件里会记录每条消息在 File 里面的位置信息,以便快速定位到具体的消息内容。

因为所有文件都在一份文件上,管理简单,也不会占用过多的系统 FD 资源,单机上的数据写入都是顺序的,写入的性能会很高。缺点是同一个分区的数据一般会在文件中的不同位置,或者不同的文件段中,无法利用到顺序读的优势,读取的性能会受到影响,但是随着 SSD 技术的发展,随机读写的性能也越来越高。

目前 RocketMQ、RabbitMQ 和 Pulsar 的底层存储 BookKeeper 用的就是这个方案。

消息数据的分段实现

数据分段的规则一般是根据大小来进行的,比如默认 1G 一个文件,同时会支持配置项调整分段数据的大小。

image.png

消息数据存储格式

消息数据存储格式一般包含消息写入文件的格式和消息内容的格式两个方面。

消息写入文件的格式指消息是以什么格式写入到文件中的,比如 JSON 字符串或二进制。消息队列中的数据基本都是以二进制的格式写入到文件的。这部分二进制数据,我们不能直接用 vim/cat 等命令查看,需要用专门的工具读取,并解析对应的格式。

消息内容的格式是指写入到文件中的数据都包含哪些信息。在数据的存储格式设计方面,内容的格式需要尽量完整且不要有太多冗余。一般包含通用信息和业务信息两部分。通用信息主要包括时间戳、CRC、消息头、消息体、偏移量、长度、大小等信息,业务信息主要跟业务相关,包含事务、幂等、系统标记、数据来源、数据目标等信息。

image.png

消息数据清理机制

消息队列的数据过期机制一般有手动删除和自动删除两种形式,从实现上看主要有三种思路。

  1. 消费完成执行 ACK
  2. 删除数据根据时间和保留大小删除
  3. ACK 机制和过期机制相结合

延时删除,以段数据为单位清理,降低频繁修改文件内容和频繁随机读写文件的操作。只有该段里面的数据都允许删除后,才会把数据删除。而删除该段数据中的某条数据时,会先对数据进行标记删除,比如在内存或 Backlog 文件中记录待删除数据,然后在消费的时候感知这个标记,这样就不会重复消费这些数据。


此文章为11月Day5学习笔记,内容来源于极客时间《深入拆解消息队列 47 讲》