起因
前一段時間,在給用戶升級 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 工具并沒有寫。