3.3 持久化恢复与检查点


3.3 持久化恢复与检查点

本节摘要:提交先进预写日志,检查点再把日志合并进库文件——这一对机制用顺序写的代价换来了崩溃安全。本节讲清两者的配合与节奏,回答"断电丢不丢数据""库文件为何变大"两个高频问题,并给出备份与恢复的实操守则。

为什么提交不直接写主文件

上一节的写入路径还留了半截:数据最终怎么安全落进库文件?最直觉的做法是直接改文件——但崩溃恰好发生在"改了一半"时,文件就烂了。数据库的通行解法是把"随机改"换成"顺序记":每个已提交事务的操作先追加进一份预写日志(WAL),主文件暂时不动。顺序追加对磁盘友好,写一个字节和写一页的开销接近,持久化的延迟被压到最低。

那主文件何时更新?在检查点(checkpoint)时刻。引擎把日志里积累的变更批量合并进库文件,随后日志清零。检查点是"把零散的欠账一次性还清",它批量处理、顺序重写,效率远高于逐笔小改。这也是为什么第2.3节说"长写事务会拖住检查点"——欠账没还清之前,日志文件一直膨胀。两套机制的分工还可以说得更直白:日志负责"安全"(每个提交都不丢),检查点负责"整洁"(主文件不积欠账),一个管下限、一个管体态,谁也替代不了谁。理解了这对分工,后面所有关于备份、恢复、文件体积的操作守则都能自己推出来。

崩溃恢复的完整逻辑

有了日志,恢复就成了确定性的重放过程。进程启动打开库文件时,先看日志有没有存货:没有,直接服务;有,把已提交事务按序重放进主文件,重建到最近的一致状态,然后才开始工作。整个过程不需要人工介入,也不存在"半笔数据"——原子性由日志的记录方式保证,要么整笔在,要么整笔无。恢复还有一个使用者视角的特征:重放的速度很快——日志里只有最近的增量变更,重放的是"几步欠账"而不是"全部历史",这正是大库重启也不必泡一上午的原因。持久化机制的性能账在最容易被担心的地方(崩溃后)恰恰是最宽裕的。

用主线案例演练一次。假设全年数据导入跑在一个大事务里,进程在半途被强制终止:库里不会有"半个季度"的数据——事务未提交,日志里没有它的提交记录,重启后重放会跳过未完成部分。你要做的只是重新执行那次导入。反过来,如果导入拆成四个季度分批提交,崩溃后已提交的季度安全在库,续跑剩下两个即可。事务边界的设计,直接决定崩溃后你从哪里重来——这是第2.3节"写事务要短"的另一半理由。把这句话再推一步:事务边界就是你的"恢复粒度",设计导入流程时先想清楚"崩了我能接受重跑多少",事务的切分自然就有了答案。

库文件为什么会悄悄变大

两个常见现象都有明确解释。现象一:明明没导多少数据,库文件和日志加起来远超预期——多半是检查点还没跑,变更都躺在日志里;等它触发,日志清零,文件回落。现象二:删了大量行,文件一点没小——删除在存储层是标记失效而非物理腾挪,空间留待复用;要立刻归还空间,需要在干净状态下重建表(建新表、复制、改名换位)。

-- 观察日志与主文件的体量分布 CALL pragma_database_size(); -- 手动触发检查点:日志合并进主文件 CHECKPOINT; -- 调整自动检查点的触发阈值(日志涨到该体量即触发) SET checkpoint_threshold = '512MB';

检查点的节奏有讲究:跑批期间频繁自动检查点会打断写入节奏,可以把阈值调大让它少打扰;跑完一批后手动执行一次,把欠账还清——这正好是第1.2节升级守则里"升级前先检查点"的原理所在。

备份与恢复的实操守则

单文件形态让备份朴素到极致,但仍有一条铁律:先检查点、再拷贝文件。日志里的存货不合并,拷出去的文件就缺最近一段变更;检查点之后,文件是自足的,拷走即完整备份。更稳妥的做法是连日志一起拷——即便拷贝瞬间有未合并变更,恢复逻辑也能用日志补齐。

-- 备份前的标准收尾:合并、校验、再拷贝 CHECKPOINT; SELECT count(*) AS 行数快照 FROM trades_clean; -- 记下这个数,恢复后核对

还有一类只读场景值得知道:多个进程可以同时以只读方式打开同一个库文件,互不干扰——这是"一个写者"原则的另一面。典型的组合拳是夜间跑批进程独占写入,白天多个分析进程只读共读。写进程执行检查点的时刻,注意避开只读方正在拷贝文件的窗口即可。这套组合的进阶版是"库文件当发布物":跑批进程每天产出一份检查点完毕的库文件,分析师人手一份只读副本——版本号即数据版本,出问题回滚就是换回昨天的文件,比任何备份工具都直观。持久化的全部机制走到这里,最终都收敛成同一个朴素的事实:文件是自足的,管理文件就是管理数据

检查点时刻发生了什么

把检查点内部摊开看一眼,很多行为就顺理成章了。触发时引擎做四件事:把日志里已提交的变更按表归并;将变更数据写进库文件的新位置(不是原地改——旧块还可能被正在运行的长查询引用);更新元数据指向新块;日志清零。三步里藏着两个使用层的推论。其一,检查点期间写入会顿一下——跑批高峰把阈值调大就是在给这一顿让路。其二,长查询横跨检查点也没关系——新块旧块并存,老查询按快照读旧块,这就是第2.3节多版本机制在存储层的配合动作。机制之间互相成全:事务层管版本可见性,存储层管新旧块共存,检查点才能放心地动主文件。

库文件体检速查表

两个"变大"现象之外,把常见体检项收拢成一张表,对着查:

症状 多半是 验证与处置
库文件远大于数据应有体积 大量更新留下的陈旧版本、删除未回收 重建表(建新、复制、换名)归还空间
日志文件持续增长不回落 检查点阈值太高,或长写事务拖着 手动 CHECKPOINT;拆短写事务
断电后启动变慢 日志存货多,恢复重放耗时 属正常过程;跑批后手动检查点减少存货
拷出去的库缺最近数据 拷贝前没做检查点 先 CHECKPOINT 再拷,或连日志一起拷

这张表和第7.3节的报错解码表是配套工具:报错是显式的求救,体检是隐式的望闻问切,两者都过一遍,持久化层的意外基本绝迹。

能不能关掉日志换性能

务实主义者迟早会问:日志这一层能不能省?答案分两半。分析场景九成的操作是批量导入与只读查询——批量导入走的就是顺序写,日志层的开销本来就被摊到极薄;只读操作与日志无关。真正"日志重"的场景只有高频小事务写,而那本来就不是这套引擎的定位(第1.4节反例清单第一条)。所以"关日志"在 DuckDB 里既不是常规选项也不是值得惦记的优化——你要是想省持久化开销,正确的问题不是"怎么关日志",而是"这个负载该不该用分析引擎"。反过来说,有真本事把写入做到纯追加、无随机改的负载(时序批量落盘这类),在 DuckDB 上就是日志友好的最佳公民,什么都不用调。

本节要点回顾

  • 顺序记、批量合:提交进日志保证崩溃安全,检查点批量还账保证主文件整洁。
  • 恢复是确定性重放:崩溃后从日志重建到最近一致状态,事务边界决定重来成本。
  • 文件变大有据可查:日志未合并、删除未回收是两大主因,各有明确解法。
  • 备份三步:先检查点、记行数快照、再拷文件;重要时刻连日志一起拷。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U