起因
前一段时间,在给用户升级 TiKV 并重启的时候,突然爆出了大量 “Sst file size mismatch” 的错误,也就是硬盘上面的 SST 文件跟实际 MANIFEST 里面的文件大小不一致。通常遇到这个问题,表明 RocksDB 的文件已经有问题了。
因为是大批量的 SST 都报了这样的错误,所以我们首先怀疑跟 disk 有关,看是否是硬件损坏。但通过观察系统日志,发现那段时间并没有系统异常,而且 disk 做了 raid0,照理也不应该出现如此大规模的文件损坏,所以我们决定排除。剩下的就是怀疑 RocksDB 自己的 bug 了。
于是将这个问题放在了 RocksDB 的群里面,得知在某些新版本内核的 XFS 文件系统上面,fallocate
函数是有 bug 的,这个就会导致使用 fallocate
分配的 SST 文件 size 跟实际 manifest 里面的不一致。虽然是一个 kernel 的 bug,但 RocksDB 也需要绕过去,修复在这个 https://github.com/facebook/rocksdb/pull/2038。
但上面出现不一致错误的 SST 文件并没有损坏,只是文件末尾多了一些 hole,只要我们从 MANIFEST 文件里面得到这个 SST 实际的 size,手动 truncate,就可以正常使用了。为了修复这些文件,我决定研究一下 manifest 文件。
MANIFEST
MANIFEST 记录着 RocksDB 一些状态变化的信息,用来在重启的时候能让 RocksDB 还原到最近的一致状态上面去。
为什么需要 MANIFEST 呢?RocksDB 是一个 key-value storage,但它实际的数据文件还是会存放到操作系统的文件系统上面。有些时候,文件系统的操作并不是原子的,可能因为一些系统的问题导致出现数据不一致的状态,即使文件系统有 journal log,也不是绝对安全的。所以 RocksDB 并不会将自己的一些 meta 信息存放到自己的 key-value 系统里面,而是使用了单独的一个 MANIFEST 文件。
MANIFEST 包括一系列的 manifest 文件,以及标识最后最新的一个 manifest 文件的 CURRENT 文件。Manifest 文件名的格式类似 MANIFEST-<seq no>
,sequence number 会一直递增,最新的 manifest 文件一定有最大的 sequence number。
我们可以认为 MANIFEST 是一个 transaction log,只要 RocksDB 的状态变化,就会记录一下。当一个 manifest 文件超过了配置的最大值的时候,一个包含当前 RocksDB 状态信息的新的 manifest 文件就会创建,CURRENT 文件会记录最新的 manifest 文件信息。当所有的更改都 sync 到文件系统之后,之前老的 manifest 文件就会被清除。
MANIFEST = { CURRENT, MANIFEST-<seq-no>* }
CURRENT = 标识最新的一个 manifest 文件
MANIFEST-<seq no> = 某个 snapshot 的 RocksDB 状态以及后续的更新操作
这里需要注意,一定要设置 max_manifest_file_size
,不然 RocksDB recover 的时间会非常的长。
Version Edit
RocksDB 使用 version 来表示任意时间的一个特定状态(其实就是 snapshot),任何对 version 的改动会被认为是一次 version edit。一个 version 通过合并一系列的 version edits 来构造。也就是一个 manifest 文件其实就是包含着一系列 version edits record。每一个 record 都会有一个唯一的 edit number 来标识。
Record Format
Version Edit 里面的类型都采用了特定的编码方式,对于整形,通常是 Var 和 Fixed 两种,譬如 Var32 就是对 int32 整数的可变长度编码。
对于 string 类型,使用 size(n) + content
的方式,size 就是整个 string 的实际长度,用 Var32 方式编码。
对于一个 Version edit 记录来说,由 record ID 加上可变长度的 bytes 组成,record ID 使用 Var32 编码,而后面实际的 record 数据则是需要根据不同的类型来实际进行解析。
Record Type
Record 有多重类型,包括 Comparator,Log Number,Previous Manifest File Number 等,譬如对于 Comparator 来说,格式就是
+-------------+----------------+
| kComparator | data |
+-------------+----------------+
<-- Var32 --->|<-- String -->|
具体不同 Type 的解析,可以参考 RocksDB 源码 VersionEdit::DecodeFrom
函数,因为比较简单,所以这里不再做说明。
这里我们重点关注 record 为 New File 的类型,因为它会记录实际的 SST 的信息,譬如 New File Format 4 的格式就是:
+--------------+-------------+--------------+------------+----------------+--------------+----------------+----------------+
| kNewFile4 | level | file number | file size | smallest_key | largest_key | smallest_seqno | largest_seq_no |
+--------------+-------------+--------------+------------+----------------+--------------+----------------+----------------+
|<-- var32 -->|<-- var32 -->|<-- var64 -->|<- var64 ->|<-- String -->|<-- String -->|<-- var64 -->|<-- var64 -->|
+-----------+---------------+-------+------------------+-------+--------------+
|kPathID ---| Path size(n) | path | kNeedCompaction | 1 | value (0/1) |
+-----------+---------------+-------+------------------+-------+--------------+
<- var32 ->|<-- var32 -->|<- n ->|<-- var32 -->|<- 1 ->|<-- 1 -->|
具体到我们之前出现的问题,如果要修复 SST,就需要在 manifest 文件里面读取到 New File record,然后解析出它实际的 path 以及对应的 file size,然后将其做 truncate。
后记
因为一个 kernel 的 bug,我们得以研究了一下 MANIFEST。当然因为 RocksDB 已经提交了 PR 去 fix 这个问题,加上现阶段用户那边除了一台机器有这个 kernel 的 bug,其他机器都是没问题的,所以相关 truncate 工具并没有写。