LevelDB dumpfile:读懂数据库内部三种文件的完整方法
【免费下载链接】leveldbLevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.项目地址: https://gitcode.com/GitHub_Trending/leveldb4/leveldb
凌晨,应用报无法打开 LevelDB 数据库,错误信息只指向一个 WAL 文件。打开数据目录,里面是若干带六位数字前缀的二进制文件:000007.ldb、000013.log、MANIFEST-000012,hexdump 只有一串看不懂的字节,连"哪些 key 还活着"都无法确认。LevelDB 自带的 dumpfile 模块就是为这种处境准备的:它把这些存储文件变成可逐行阅读的文字。
先 dump 一个 SSTable 试试
命令行入口是 db/leveldbutil.cc,它只注册了一个dump子命令,行为是调用 include/leveldb/dumpfile.h 声明的DumpFile,把结果写到 stdout。编译运行路径:
git clone https://gitcode.com/GitHub_Trending/leveldb4/leveldb cd leveldb && mkdir build && cd build cmake .. && make leveldbutil ./leveldbutil dump ../mydb/000007.ldb一个小 .ldb 文件的输出大致长这样:
'user:1001' @ 42 : val => '{"name":"alice","role":"ops"}' 'user:1002' @ 47 : del => '' 'session:9f2c' @ 53 : val => '{"token":"s_8a3d","expire":1756915200}'user:1001:用户键,即应用 Put/Get 传入的 key42:序列号,越大表示写入越晚val/del:记录类型,值写入或墓碑删除标记=> '...':实际存储的值,del 记录为空
这一行的每个字段都不是直接从文件里解出来的,而是内部键拆分后拼装的结果,机制在下一节。
文件类型靠文件名识别
LevelDB 数据目录里可能有几十种文件,但 dumpfile 只处理其中三类,判断完全由文件名完成。DumpFile是调度中心:先用GuessType从文件名识别类型,再按类型路由到DumpLog、DumpTable、DumpDescriptor。真正干活的是 db/filename.cc 的ParseFileName,匹配规则非常严格:
CURRENT、LOCK、LOG等固定名 → 各自对应一种固定文件类型MANIFEST-<数字>→ 描述符文件 kDescriptorFile<数字>.log→ 日志文件;<数字>.ldb或.sst→ SSTable- 其余 → 识别失败,返回 "unknown file type"
文件名就是类型声明:只有上表三类有解析处理器,能识别但没有解析器的文件(如 CURRENT、LOCK)会返回 "not a dump-able file type"。
键值输出逐字段拆解
内部键:用户键加两个字段
SSTable 里 dump 出的 key 并不是你在代码里写的那个。db/dbformat.h 中ParsedInternalKey由 user_key、sequence、type 三部分构成:磁盘上把序列号和操作类型(占低 8 位)与用户键打包成一个内部键,因此同一个业务 key 可以在同一张表里留下多条不同序列号的记录。
db/dumpfile.cc 的DumpTable先Table::Open打开整张表,再从SeekToFirst遍历到末尾,逐条用ParseInternalKey拆出内部键,才拼出'key' @ seq : val/del => 'value'这一行。解析不了的记录会输出为badkey行而不是中断,迭代出错则追加一行iterator error——这是它对损坏表的容错设计。
还原日志文件里的写入历史
.log 文件(WAL,预写日志)存的是 WriteBatch 记录,每条对应一次应用写入。DumpLog用 log::Reader 逐条读取记录,每条记录经WriteBatchPrinter展开:
--- offset 32; sequence 101 put 'user:1001' '{"name":"alice","role":"ops"}' del 'session:9f2c'offset 32:这条 WriteBatch 在日志文件中起始的字节偏移sequence 101:本批起始序列号,批内操作依次取 101、102……put/del:批内操作,键和值转义后输出
与 SSTable 输出的差别在于视角:SSTable 回答"每个 key 最终存了哪个版本",日志回答"写入按什么顺序进来"。排查"某次写入到底落盘没有"时,以日志的 offset 与 sequence 为锚点。
从 MANIFEST 追溯版本轨迹
MANIFEST 是 LevelDB 分层结构的持久化记录,每次 compaction 增删文件都先写入它。DumpDescriptor把每条记录解码成 VersionEdit,再用DebugString(格式定义见 db/version_edit.cc)输出:
--- offset 41; VersionEdit { LogNumber: 12 NextFile: 21 LastSeq: 104 AddFile: 2 20 4096 user:1000 .. user:1199 }AddFile: 2 20 4096 ...:向第 2 层加入编号 20 的文件,大小 4096,键范围 user:1000 到 user:1199RemoveFile: L N:从第 L 层移除编号 N 的文件LastSeq:本次变更时的序列号水位线
按 offset 顺序读 MANIFEST,就得到数据库层级结构的完整演化时间线,是追查"某个大文件是哪次 compaction 产物"的第一站。
从损坏日志提取有效记录
回到开头场景:WAL 损坏、数据库打不开。关键在于 dumpfile 遇到损坏不会停——log::Reader 检测到校验和失败时交给注册的 reporter,db/dumpfile.cc 里的CorruptionReporter只把一行 "corruption: N bytes; <原因>" 写进输出流,随后继续读取后面的记录:
./leveldbutil dump mydb/000013.log > recovered.txt grep "put '" recovered.txt # 筛出有效 put 行损坏段被压缩成一行 corruption 报错,其前后完整的 WriteBatch 照常 dump 出来,据此可以把真正生效的操作重建为修复脚本。三种文件格式本身在 doc/table_format.md(SSTable 块布局)和 doc/log_format.md(日志记录分帧与校验和)里有完整说明,想弄清 dumpfile 凭什么能读出这些文件,先读这两份文档再回看 db/dumpfile.cc 会顺畅很多。
【免费下载链接】leveldbLevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.项目地址: https://gitcode.com/GitHub_Trending/leveldb4/leveldb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考