重新理解 RocketMQ Commit Log 存储协议

最近突然感觉:很多软件、硬件在设计上是有 root reason 的,不是 by desgin 如此,而是解决了那时、那个场景的那个需求。一旦了解后,就会感觉在和设计者对话,了解他们的思路,学习他们的方法,思维同屏:活到老学到老。

问题思考

1、Consumer Queue Offset 是连续的吗, 为什么?

2、Commit Log Offset 是连续的吗, 为什么?

3、Java 写的文件,默认是大端序还是小端序,为什么?

Commit Log 真实分布

在大家思考之际, 我们回想下 commit log 是怎么分布的呢?

在 Broker 配置的存储根目录下,通过查看 Broker 实际生成的 commit log 文件可以看到类似下面的数据文件分布:

可以看到,真实的存储文件有多个, 每一个都是以一串类似数字的字符串作为文件名的,并且大小 1G。

我们结合源码可以知道,实际的抽象模型如下:

由上图得知:

Commit Log 是一类文件的称呼,实际上 Commit Log 文件有很多个, 每一个都可以称为 Commit Log 文件。如图中表示了总共有 T 个 Commit Log 文件,他们按照由过去到现在的创建时间排列。

每个 Commit Log 文件都保存消息, 并且是按照消息的写入顺序保存的,并且总是在写创建时间最大的文件,并且同一个时刻只能有一个线程在写。如图中第 1 个文件,1,2,3,4... 表示这个文件的第几个消息,可以看到第 1234 个消息是第 1 个 Commit Log 文件的最后一个消息,第 1235 个消息是第 2 个 Commit Log 的第 1 个消息。

说明 1:每个 Commit Log 文件里的全部消息实际占用的存储空间大小 <=1G。这个问题大家自行思考下原因。

说明 2:每次写 Commit Log 时, RocketMQ 都会加锁,代码片段见 https://github.com/apache/rocketmq/blob/7676cd9366a3297925deabcf27bb590e34648645/store/src/main/java/org/apache/rocketmq/store/CommitLog.java#L676-L722

我们看到 Commit Log 文件中有很多个消息,按照既定的协议存储的,那具体协议是什么呢, 你是怎么知道的呢?

Commit Log 存储协议

关于 Commit Log 存储协议,我们问了下 ChatGPT, 它是这么回复我的,虽然不对,但是这个回复格式和说明已经非常接近答案了。

我们翻看源码,具体说明下:https://github.com/apache/rocketmq/blob/rocketmq-all-4.9.3/store/src/main/java/org/apache/rocketmq/store/CommitLog.java#L1547-L1587

我整理后, 如下图:

说明 1:我整理后的消息协议编号和代码中不是一致的,代码中只是标明了顺序, 真实物理文件中的存储协议会更详细。说明 2:在我写的《RocketMQ 分布式消息中间件:核心原理与最佳实践》中,这个图缺少了 Body 内容,这里加了,也更详细的补充了其他数据。 这里有几个问题需要说明下:

1、二进制协议存在字节序,也就是常说的大端、小端。大小端这里不详细说明感兴趣的同学自己 google 或者问题 ChatGPT,回答肯定比我说的好。

2、在 java 中, 一个 byte 占用 1 个字节,1 个 int 占用 4 个字节,1 个 short 占用 2 个字节,1 个 long 占用 8 个字节。

3、Host 的编码并不是简单的把 IP:Port 作为字符串直接转化为 byte 数组,而是每个数字当作 byte 依次编码。在下一节的 Golang 代码中会说明。

4、扩展信息的编码中,使用了不可见字符作为分割,所以扩展字段 key-value 中不能包含那 2 个不可见字符。具体是哪 2 个,大家找找?

我们看到这个协议后,如何证明你的物理文件就是按照这个协议写的呢?

用 Golang 解开 RocketMQ Commit Log

RocketMQ 是用 java 写的,根据上文描述的存储协议,我用 Golang 编写了一个工具,可以解开 Commit Log 和 Cosumer Queue,代码地址:https://github.com/rmq-plus-plus/rocketmq-decoder。

这个工具目前支持 2 个功能:

1、指定 Commit Log 位点,直接解析 Commit Log 中的消息,并且打印。

2、指定消费位点,先解析 Consumer Queue,得到 Commit Log Offset 后,再根据 Commit Log Offset 直接解析 Commit Log,并且打印。

在 Golang 中没有依赖 RocketMQ 的任何代码,纯粹是依靠协议解码。

这里贴了一段 golang 中解析 Commit Log Offset 的例子:在 java 中这个 offset 是一个 long 类型,占用 8 个字节。

在 golang 中,读取 8 个字节长度的数据,并且按照大端序解码为 int64,就可以得到正常的 Commit Log Offset。

 我跑了一个 demo 结果,大家参考:

回答最初的问题

以下为个人见解,大家参考:

1、Consumer Queue Offset 是连续的吗, 为什么?

是连续的。

consumer queue offset,是指每个 queue 中索引消息的下标,下标当然是连续的。消费者也是利用了这个连续性,避免消费位点提交空洞的。

每个索引消息占用相同空间,都是 20 字节,结构如下:

这里物理位点也就是 Commit Log Offset。

2、Commit Log Offset 是连续的吗, 为什么?

不是连续的。

Commit Log Offset 是指的每个消息在全部 Commit Log 文件中的字节偏移量, 每个消息的大小是不确定的,所以 Commit Log Offset,也即是字节偏移量肯定是不一样的。

并且可以知道,每两个偏移量的差的绝对值就是前一个消息的消息字节数总长度。

并且上文中图 “Commit Log 存储文件分布抽象” 中的有误解,每个小方格的大小其实是不一样的。

3、Java 写的文件,默认是大端序还是小端序,为什么?

大端序。大端序其实有字节存储顺序和网络传输顺序,java 中默认用的大端序,保持和网络传输一样,这样方便编解码。

每段网络传输层的数据报文最前面的字节是表达后面的数据是用什么协议传输的,这样数据接收者在接受数据时, 按照字节顺序,先解析协议,再根据协议解码后面的字节序列,符合人类思考和解决问题的方式。

讨论说明:由于 RocketMQ 一些版本可能有差异,本文在 4.9.3 版本下讨论,大家可以参考这个方法,解开 5.0 甚至其他版本,其他数据文件的存储协议格式。

你可能感兴趣的:(rocketmq,java)