多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

libSQL 数据库在线体检与修复:sqlite3_checker 与 repair 扩展深度解析

libSQL 数据库在线体检与修复:sqlite3_checker 与 repair 扩展深度解析 libSQL 数据库在线体检与修复sqlite3_checker 与 repair 扩展深度解析【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql本指南围绕 libSQL 仓库中libsql-sqlite3/ext/repair/目录SQLite 上游继承的 repair 工具集讲解如何在不将应用下线的前提下对大型 SQLite/libSQL 数据库进行在线完整性检查、损坏定位与修复准备。读完本文你将掌握sqlite3_checker的构建方法、全部命令行选项、checkfreelist与incremental_index_check两个核心扩展的实现原理以及如何用官方测试套件验证工具行为。为什么需要在线数据库体检SQLite 的典型应用场景中数据库规模普遍不大PRAGMA integrity_check之类的全量扫描可以在毫秒到秒级完成。但随着 SQLite 被用于越来越大的数据库单库文件尺寸正进入 TB 量级。在如此规模下硬件故障乃至宇宙射线偶发翻转比特cosmic rays都会以不可忽视的概率损坏数据库文件而对一个 TB 级数据库执行完整的问题检测与修复耗时可能长达数小时甚至数天。要求依赖这些数据库的应用为此长时间下线是绝大多数生产环境无法接受的。libsql-sqlite3/ext/repair/README.md明确指出了这一背景本目录中的工具与扩展目标正是在数据库处于活跃使用状态时提供检测与修复大型数据库问题的机制。同时 README 也如实声明截至 2017-10-12 撰写时这些工具属于实验性质、处于积极开发中待其稳定后 README 会更新说明。repair 目录全景工具与扩展的组成整个目录包含两个层面的产物独立的分析工具以及可嵌入 SQL 的扩展模块。文件角色README.md目录定位说明本文主题文档sqlite3_checker.c.insqlite3_checker程序的 C 骨架经tool/mkccode.tcl生成最终源码sqlite3_checker.tclsqlite3_checker的 Tcl 主驱动脚本参数解析、检查调度、进度与结果输出checkfreelist.c空闲页链表freelist检查扩展提供 C API 与 SQL 函数checkindex.c增量式索引完整性检查虚表incremental_index_checktest/测试套件test.tcl驱动脚本与checkfreelist01.test、checkindex01.test两个测试模块从 sqlite3_checker.c.in 的编译指令可以看到程序的血统它把sqlite3.c、tclsqlite.c以及三个扩展ext/misc/btreeinfo.c、checkindex.c、checkfreelist.c全部静态编入一个可执行文件并通过sqlite3_auto_extension()在启动时自动注册sqlite3_btreeinfo_init、sqlite3_checkindex_init、sqlite3_checkfreelist_init三个扩展入口。此外它还内置了一个名为sqlite3_imposter的 Tcl 命令——这是测试用钩子允许以自定义表结构覆盖某个真实表的 rootpage从而在不破坏原数据的前提下模拟出索引与表内容不一致的损坏场景见 checkindex.c 的 imposter 机制。构建 sqlite3_checker根据 test/README.md构建只需一条命令make sqlite3_checker该目标定义在 main.mk 中先用tclsh tool/mkccode.tcl ext/repair/sqlite3_checker.c.in sqlite3_checker.c展开模板把INCLUDE $ROOT/...指令替换为各源码文件的实际内容BEGIN_STRING/END_STRING之间的 Tcl 脚本被内嵌为字符串常量再以$(TCCX) $(TCL_FLAGS)连同$(LIBTCL)链接出可执行文件。sqlite3_checker同时也是 main.mk 中TESTPROGS列表的一员这意味着它不仅是运维工具也是官方测试基础设施的一部分。编译产物sqlite3_checker本质上是一个内嵌 SQLite 与 Tcl 解释器的单文件程序它能直接打开数据库执行检查也能在--test模式下驱动 Tcl 测试脚本。若你的构建环境缺少 Tcl 开发库可先参考仓库根目录的构建说明如 libsql-sqlite3/README-SQLite.md准备好依赖。命令行选项详解sqlite3_checker的用法与选项定义在 sqlite3_checker.tcl 的usage过程中Usage: sqlite3_checker OPTIONS database-filename对指定的 SQLite 数据库文件执行完整性检查。核心选项如下选项含义--batchsize N每个事务中检查的行数默认 1000--freelist仅执行空闲页链表freelist检查--index NAME仅检查名为 NAME 的索引--summary输出数据库空间利用概况--table NAME检查表 NAME 上的所有索引--tclsh进入内置 Tcl 解释器调试用--trace仅调试输出扫描过程追踪信息--version打印 SQLite 版本号与源码标识--test FILENAME ARGS用 FILENAME 替换默认主脚本执行测试模式需要注意几个参数间的交互语义这直接来自 sqlite3_checker.tcl 的解析逻辑默认行为是全量索引检查变量bAll初始为 1。只有显式给出--freelist、--summary、--index、--table之一时bAll才被置 0。因此不带任何检查选项运行时工具会先做 freelist 检查再按行数升序ORDER BY nEntry即从小到大遍历sqlite_btreeinfo(main)中所有rootpage0的索引逐一检查。--batchsize的语义check_index过程以LIMIT $batchsize的方式分批调用incremental_index_check虚表每批结束后用返回的current_key作为下一次扫描的after_key续查。因为每一批查询都发生在独立的语句中数据库可以在批次之间继续接受其他连接的事务这正是在线检查的具体体现。默认 1000 行/批可通过--batchsize调大或调小以权衡吞吐与并发影响。进度输出每次扫描前先用sqlite_btreeinfo(main)查出索引总行数nEntry然后以\r回车符原地刷新 索引名: 已查 i 行 / 共 max 行 (百分比%) 的进度条结束时打印 索引名: N errors out of M entries。索引选择--index NAME指定单个索引--table NAME则从sqlite_master查出该表所有typeindex AND rootpage0的索引逐个检查。数据库文件可通过file:URI 形式传入脚本会剥离前缀校验实际文件是否存在、是否可读并以sqlite3 db $file_to_analyze打开若参数是.tcl结尾的脚本文件则直接当作 Tcl 脚本执行。一个典型的组合用法# 只查 freelist速度快适合日常巡检 ./sqlite3_checker --freelist app.db # 输出空间占用概况按索引大小排序 ./sqlite3_checker --summary app.db # 全量在线检查所有索引每批 500 行以降低对业务的影响 ./sqlite3_checker --batchsize 500 app.db # 只检查某个大表上的全部索引 ./sqlite3_checker --table orders app.db # 查看内置 SQLite 版本 ./sqlite3_checker --version app.db空闲页链表检查checkfreelist 的实现与原理freelist 是 SQLite 记录已删除、可复用页面的链表其头部指针记录在数据库第 1 页偏移 32 字节处。若 freelist 元数据被损坏例如删页操作中途崩溃、比特翻转会导致页面复用错误甚至数据覆盖因此它是完整性检查的第一道防线。checkfreelist.c 提供三种使用形态C 函数int sqlite3_check_freelist(sqlite3 *db, const char *zDb)检查zDbmain、temp 等的 freelist通过sqlite3_log()报告错误返回SQLITE_OK或错误码注意——即使 freelist 已损坏但未发生 IO/OOM 错误返回值也可能是SQLITE_OK错误细节在日志中。SQL 函数编译为可加载扩展后注册checkfreelist(database-name)把所有错误消息合并为单个文本值换行分隔返回无损坏时返回空字符串。在sqlite3_checker中由--freelist触发SELECT checkfreelist(main)并在外层包了BEGIN/END事务。其核心算法checkFreelist 函数值得拆解WITH freelist_trunk(i, d, n) AS ( SELECT 1, NULL, sqlite_readint32(data, 32) FROM sqlite_dbpage(:1) WHERE pgno1 UNION ALL SELECT n, data, sqlite_readint32(data) FROM freelist_trunk, sqlite_dbpage(:1) WHERE pgnon ) SELECT i, d FROM freelist_trunk WHERE i!1;这条递归 CTE 借助sqlite_dbpage虚表由SQLITE_ENABLE_DBPAGE_VTAB编译选项开启逐页读取原始数据从第 1 页的偏移 32 处取得第一个 trunk 页号再沿 trunk 页首 4 字节的下一 trunk 页号指针走完整个链表。对每个 trunk 页代码校验leaf 数量越界trunk 页偏移 4 处的 4 字节记录该页挂载的 leaf 页数量nLeaf若超过(nData/4)-2-6则报leaf count out of rangetrunk 页号越界若下一 trunk 页号大于PRAGMA page_count报trunk page N is out of rangeleaf 页号越界逐一检查每个 leaf 页号为 0 或超过总页数时报leaf page N is out of range (child i of trunk page T)总数不一致把链表实际统计的 free 页数与PRAGMA freelist_count头部记录对比不一致时报free-list count mismatch: actualX headerY。配套的sqlite_readint32(BLOB[, OFFSET])SQL 函数用于从页数据 blob 中解码大端序 32 位整数注册于 cflRegister。该扩展也可脱离sqlite3_checker单独编译为可加载扩展使用gcc -Os -fPIC -shared checkfreelist.c -o checkfreelist.so增量式索引检查incremental_index_check 虚表索引与表数据不一致是数据库损坏的常见形式而全量PRAGMA integrity_check在 TB 级数据库上会扫描全部内容树耗时过长。checkindex.c 通过注册名为incremental_index_check的只读虚表把索引检查改造成可分批推进的增量扫描——这正是本目录在线体检思想的核心实现。虚表结构定义于 cidxConnectCREATE TABLE xyz( errmsg TEXT, -- 错误消息无错误时为 NULL current_key TEXT, -- 当前扫描到的键值quote() 后的文本 index_name HIDDEN, -- IN要扫描的索引名 after_key HIDDEN, -- IN从该键之后继续扫描 scanner_sql HIDDEN -- 调试本次扫描实际执行的 SQL )errmsg列只产生两类错误文案见 cidxColumnrow missing索引中存在某条目但按主键/rowid 回表查不到对应数据行子查询返回非整数即无行row data mismatch索引条目与表中实际行的数据不一致子查询返回 0。查询方式即把约束写在 WHERE 中例如检查索引i1全部条目SELECT errmsg, current_key FROM incremental_index_check(i1);分批续查则利用after_key值来自上一批最后一条current_keyLIMIT控制每批行数——sqlite3_checker.tcl 的 check_index 过程 正是按此模式循环直到返回空集。cidxBestIndex通过给三种计划赋不同代价无参数 10^9、单参数 10^6、双参数 10^3引导查询规划器优先选择带after_key的续查计划见 checkindex.c。每次扫描的 SQL 由 cidxGenerateScanSql 动态生成过程为用sqlite_schema查出索引所属表与其CREATE INDEX语句用PRAGMA index_xinfo获取索引列、排序方向DESC、是否为 PK 键列及排序规则collation解析CREATE INDEX原文cidxParseSQL 手写了一个能识别 SQL 字符串、--与/* */注释的微型解析器提取表达式列、ASC/DESC 与可选的WHERE子句生成SELECT (子查询), quote(i0)||,||quote(i1)... FROM (SELECT 列 FROM 表 INDEXED BY 索引 ORDER BY ...) AS i形式的扫描语句内层强制按索引键序读取条目外层对每个条目回表比对。INDEXED BY保证读取顺序与索引完全一致。这套设计使虚表能正确处理各种复杂索引形态——测试 checkindex01.test 覆盖了普通索引、DESC索引、多列复合索引、WITHOUT ROWID表、非默认排序规则COLLATE nocase、表达式索引json_extract(y,$.x)、自动索引sqlite_autoindex_*、列定义中夹杂注释、以及带WHERE的部分索引。例如其中用t4cc ON t4(c1 COLLATE nocase, c2 COLLATE nocase)验证大小写不敏感排序规则下仍能正确比对测试 4.1。如何制造损坏sqlite3_imposter 测试钩子为了在不破坏测试库的前提下验证检测能力sqlite3_checker.c.in暴露了sqlite3_imposterTcl 命令用法如下# 安装 imposter用给定表结构覆盖真实表 t1 的 rootpage sqlite3_imposter db main $tblroot {CREATE TABLE xt1(a,b)} # 在 imposter 上制造不一致直接改页内容 db eval { UPDATE xt1 SET asix WHERE rowid3; DELETE FROM xt1 WHERE rowid 5; } # 卸载 imposter sqlite3_imposter db main其实现位于 sqlite3_checker.c.in调用 SQLite 内部测试接口SQLITE_TESTCTRL_IMPOSTER先以 imposter 模式覆盖 rootpagesqlite3_exec执行构造表语句后恢复。由于 imposter 表与真实表共享同一 B-tree 页修改它等价于直接篡改真实数据——checkindex01.test 的 1.5 节正是这样把rowid3的行改为six、删掉rowid5的行随后 1.6 节即看到{row missing} five,5与{row data mismatch} three,3的精确报错。而 freelist 检查的损坏注入更直接——checkfreelist01.test 通过UPDATE sqlite_dbpage SET data set_int(...)直接改写页内字节验证了四类典型错误的报错文案注入方式期望输出把 trunk 页头部 free 计数减 1free-list count mismatch: actual6725 header6726把 leaf 页号改为超出总页数的值leaf page 10092 is out of range (child 3 of trunk page 4860)把 leaf 页号清零leaf page 0 is out of range (child 3 of trunk page 4860)把 trunk 页的 leaf 数量字段改为 249leaf count out of range (249) on trunk page 5空间占用概况--summary 与 sqlite_btreeinfo--summary选项输出按索引占空间降序排列的清单数据源是sqlite_btreeinfo虚表来自 ext/misc/btreeinfo.cSELECT nPage*$pgsz AS sz, name, tbl_name FROM sqlite_btreeinfo WHERE typeindex ORDER BY 1 DESC, name其中nPage是该索引 B-tree 的页数乘以PRAGMA page_size即为其字节占用。脚本会自动选择单位总大小超过 10 MB 用 MB否则用 KB输出形如1234.5 MB index idx_orders_created of table orders 45.0 KB index sqlite_autoindex_users_1 of table users这为定位哪些索引值得优先检查占用越大、扫描代价越高提供了量化依据也能辅助排查冗余索引。运行官方测试套件测试驱动脚本与说明位于 test/README.md 与 test/test.tcl。构建后运行# 先构建工具 make sqlite3_checker # 运行 test 目录下全部 *.test 模块 ./sqlite3_checker --test $path/test.tcl # 可选只运行指定模块 ./sqlite3_checker --test $path/test.tcl $path/checkfreelist01.test $path/checkindex01.testtest.tcl 实现了与 SQLite 官方测试同风格的do_test/do_execsql_test框架不传模块时自动glob同目录下所有*.test每个模块运行前重建test.db结束后汇总输出N errors out of M tests。它依赖sqlite3_checker的--test特殊模式——sqlite3_checker.tcl 检测到首参数为--test时直接用第二个参数替换自身脚本源码执行因此sqlite3_checker同时充当了测试运行器。局限性与使用建议结合源码与 README 的声明使用前应明确以下边界实验状态README 明确标注这些工具当前为实验性、处于积极开发中2017-10-12 时点。升级 SQLite/libSQL 后建议重新验证行为关注 README.md 是否更新了稳定声明。检测为主、修复能力有限目录定位是检测问题并可能修复detect problems, and possibly fix them。从实现看checkfreelist与incremental_index_check目前只负责报告损坏位置错误文案、键值、页号并不自动改写数据。定位到row missing/row data mismatch的具体键后通常仍需配合REINDEX、VACUUM或从备份恢复等手段完成修复。在线是相对概念虽然检查按批推进、允许其他连接并发读写但每批内部仍是一个受限于事务边界的读扫描--batchsize越小对在线业务扰动越小但总耗时越长。TB 级场景下应结合--summary优先处理大索引。依赖编译特性sqlite3_checker需要 Tcl 开发库并启用了SQLITE_ENABLE_DBPAGE_VTAB等编译选项见 sqlite3_checker.c.in若要用checkfreelist的 SQL 函数形态需按上文命令单独编译.so并LOAD_EXTENSION。总而言之ext/repair是 libSQLSQLite 分支仓库中面向大规模部署的健康巡检工具箱sqlite3_checker以单文件工具形式集成了分批索引扫描、freelist 校验与空间统计checkfreelist和incremental_index_check既可作为内嵌能力也可作为独立可加载扩展复用。理解其按 key 续扫的分批模型与虚拟表实现是将其正确接入自身巡检体系的前提。【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表