Small. Fast. Reliable.
Choose any three.
如何损坏SQLite数据库文件

概述

SQLite数据库具有极高的抗破坏能力。如果在事务中间发生应用程序崩溃,操作系统崩溃甚至断电,则下次访问数据库文件时,应自动回滚部分写入的事务。恢复过程是全自动的,不需要用户或应用程序执行任何操作。

尽管SQLite可以抵抗数据库损坏,但它并非无法幸免。本文档介绍了SQLite数据库可能损坏的各种方式。

1.文件被恶意线程或进程覆盖

SQLite数据库文件是普通磁盘文件。这意味着任何进程都可以打开文件并用垃圾覆盖它。SQLite库无法采取任何措施来防御这种情况。

1.1。关闭文件描述符后继续使用

我们已经看到了多种情况,其中在文件上打开文件描述符,然后关闭该文件描述符,然后在SQLite数据库上重新打开该文件描述符。后来,其他一些线程继续写入旧文件描述符,但没有意识到原始文件已被关闭。但是,由于文件描述符已被SQLite重新打开,因此原本打算放入原始文件中的信息最终覆盖了SQLite数据库的某些部分,从而导致数据库损坏。

其中一个示例发生在2013年8月30日左右的Fossil DVCS规范存储库中。在那种情况下,文件描述符2(标准错误)在sqlite3_open_v2()之前被错误地关闭(我们怀疑 是 stunnel ),因此用于存储库数据库文件的文件描述符为2。后来,一个应用程序错误导致assert( )语句通过调用write(2,...)发出错误消息。但是由于文件描述符2现在已连接到数据库文件,因此错误消息覆盖了数据库的一部分。为了防止此类问题,SQLite版本3.8.1(2013-10-17)及更高版本拒绝为数据库文件使用低编号的文件描述符。(请参见SQLITE_MINIMUM_FILE_DESCRIPTOR 有关其他信息。)

Facebook工程师在2014年8月12日的博客文章中报道了使用封闭文件描述符导致的另一个损坏示例 。

Fossil于2019-07-11报告了此错误的另一个示例 。将打开一个文件描述符以调试输出,但随后由SQLite关闭并重新打开。但是调试逻辑继续写入原始文件描述符。有关 错误报告,请参见 论坛讨论,并提供此修复程序的链接。

1.2。事务处于活动状态时进行备份或还原

在后台运行自动备份的系统可能会尝试在事务中间创建SQLite数据库文件的备份副本。然后,备份副本可能包含一些旧内容和一些新内容,因此已损坏。

制作可靠的SQLite数据库备份副本的最佳方法是利用SQLite库中的备份API。失败的话,只要没有任何进程在进行事务处理,就可以安全地复制SQLite数据库文件。如果先前的事务失败,则将任何回滚日志(* -journal文件)或写日志(* -wal文件)与数据库文件本身一起复制是很重要的。

1.3。删除热门日记

SQLite通常将所有内容存储在单个磁盘文件中。但是,在执行事务时,在崩溃或电源故障后恢复数据库所需的信息存储在辅助日志文件中。这样的日志文件被描述为“热门”。日志文件具有与原始数据库文件相同的名称,并带有-journal-wal后缀。

SQLite必须查看日志文件才能从崩溃或电源故障中恢复。如果在崩溃或电源故障后移动,删除或重命名热日志文件,则自动恢复将不起作用,数据库可能会损坏。

此问题的另一种表现是 由8 + 3文件名的不一致使用引起的数据库损坏

1.4。数据库文件和热日志配对不正确

前面的示例是一个更普遍的问题的特定情况:SQLite数据库的状态由数据库文件和日记文件控制。在静态状态下,日记文件不存在,只有数据库文件很重要。但是,如果日志文件确实存在,则必须将其与数据库保存在一起以避免损坏。以下操作都可能导致腐败:

2.文件锁定问题

SQLite在数据库文件以及预写日志WAL文件上使用文件锁 来协调并发进程之间的访问。没有协调,两个线程或进程可能会尝试同时对数据库文件进行不兼容的更改,从而导致数据库损坏。

2.1。具有损坏的或丢失的锁实现的文件系统

正如文档所说,SQLite依赖底层文件系统进行锁定。但是某些文件系统在其锁定逻辑中包含错误,因此这些锁定并不总是像所宣传的那样运行。对于网络文件系统尤其是NFS尤其如此。如果在锁定原语包含错误的文件系统上使用SQLite,并且如果两个或多个线程或进程尝试同时访问同一数据库,则可能导致数据库损坏。

2.2。Posix咨询锁由执行close()的单独线程取消

Unix平台上SQLite使用的默认锁定机制是POSIX咨询锁定。不幸的是,POSIX咨询锁定具有一些设计上的怪癖,使其很容易被滥用和失败。特别是,在同一进程中具有文件描述符且持有POSIX咨询锁的任何线程都可以使用其他文件描述符覆盖该锁。一个特别有害的问题是close()系统调用将取消该进程中所有线程和所有文件描述符在同一文件上的所有POSIX咨询锁。

因此,例如,假设一个多线程进程具有两个或多个线程,这些线程具有到同一数据库文件的单独SQLite数据库连接。然后,出现第三个线程,并且希望自己从同一数据库文件中读取某些内容,而不使用SQLite库。第三个线程执行open()read()然后执行close()。有人会认为这将是无害的。但是close()系统调用导致所有其他线程保留在数据库上的锁被删除。这些其他线程无法知道它们的锁刚刚被破坏了(POSIX不提供任何确定此锁的机制),因此它们在假定它们的锁仍然有效的情况下继续运行。这可能导致两个或多个线程或进程尝试同时写入数据库,从而导致数据库损坏。

请注意,两个或多个线程使用SQLite库访问同一SQLite数据库文件是绝对安全的。SQLite的unix驱动程序了解POSIX咨询锁定怪癖并解决它们。仅当线程尝试绕过SQLite库并直接读取数据库文件时,才会出现此问题。

2.2.1。链接到同一应用程序的多个SQLite副本

如前一段所述,SQLite采取了一些步骤来解决POSIX咨询锁定的怪癖。解决方法的一部分涉及保持打开的SQLite数据库文件的全局列表(互斥量受保护)。但是,如果将SQLite的多个副本链接到同一应用程序中,则此全局列表将有多个实例。使用SQLite库的一个副本打开的数据库连接将不知道使用另一副本打开的数据库连接,并且将无法解决POSIX咨询锁定怪癖。一个close()方法的一个连接操作可能在不知不觉中清除不同的数据库连接上的锁,导致数据库损坏。

上面的场景听起来牵强。但是SQLite开发人员知道至少有一个商业产品正是与此错误一起发布的。该供应商来找SQLite开发人员寻求帮助,以跟踪他们在Linux和Mac上遇到的一些罕见的数据库损坏问题。问题最终归结为以下事实:应用程序链接到两个单独的SQLite副本。解决方案是将应用程序构建过程更改为仅链接到一个SQLite副本而不是两个副本。

2.3。两个进程使用不同的锁定协议

Unix平台上SQLite使用的默认锁定机制是POSIX咨询锁定,但是还有其他选择。通过使用sqlite3_open_v2()接口选择备用的sqlite3_vfs,应用程序可以使用可能更适合某些文件系统的其他锁定协议。例如,可能选择了点文件锁定,以用于必须在不支持POSIX咨询锁定的NFS文件系统上运行的应用程序中。

重要的是,到同一数据库文件的所有连接都使用相同的锁定协议。如果一个应用程序正在使用POSIX咨询锁,而另一个应用程序正在使用点文件锁,则这两个应用程序将看不到彼此的锁并且将无法协调数据库访问,可能导致数据库损坏。

2.4。在使用时取消链接或重命名数据库文件

如果两个进程具有到同一数据库文件的打开的连接,并且一个进程关闭其连接,取消链接,然后以相同的名称在其位置创建一个新的数据库文件并重新打开新文件,则这两个进程将进行不同的交谈具有相同名称的数据库文件。(请注意,这仅在Posix和类似Posix的系统上可行,该系统允许在文件仍处于打开状态以进行读取和写入时取消链接文件。Windows不允许这种情况发生。)由于回滚日志和WAL文件基于数据库文件名,两个不同的数据库文件将共享相同的回滚日志或WAL文件。其中一个数据库的回滚或恢复可能会使用另一个数据库中的内容,从而导致损坏。

换句话说,取消链接或重命名打开的数据库文件会导致行为未定义并且可能是不希望的。

从SQLite版本3.7.17(2013-05-20)开始,如果在数据库文件仍处于使用状态时取消链接,则unix OS界面会将SQLITE_WARNING消息发送到错误日志

如果单个数据库文件具有多个链接(硬链接或软链接),那就是说文件具有多个名称的另一种方式。如果两个或多个进程使用不同的名称打开数据库,则它们将使用不同的回滚日志和WAL文件。这意味着,如果一个进程崩溃,则另一个进程将无法恢复进行中的事务,因为它将在错误的位置查找适当的日记。

换句话说,打开和使用具有两个或更多个名称的数据库文件会导致行为未定义,并且可能是不希望的。

从SQLite版本3.7.17(2013-05-20)开始,如果数据库文件具有多个硬链接,则UNIX操作系统界面将向错误日志发送SQLITE_WARNING消息。

从SQLite版本3.10.0(2016-01-06)开始,unix OS界面将尝试解析符号链接并按规范名称打开数据库文件。在3.10.0之前的版本中,通过符号链接打开数据库文件类似于打开具有多个硬链接并导致未定义行为的数据库文件。

2.6。跨fork()进行开放式数据库连接

不要打开SQLite数据库连接,然后打开fork(),然后尝试在子进程中使用该数据库连接。将导致各种锁定问题,并且您很容易以损坏的数据库告终。SQLite并非旨在支持这种行为。子进程中使用的任何数据库连接都必须在子进程中打开,而不是从父进程继承。

如果在父进程中打开了数据库连接,则甚至不要在子进程的数据库连接上调用sqlite3_close()。关闭基础文件描述符是安全的,但是sqlite3_close() 接口可能会调用清除活动,这些活动将从父目录下删除内容,从而导致错误甚至数据库损坏。

3.无法同步

为了确保数据库文件始终保持一致,SQLite有时会要求操作系统将所有未完成的写入刷新到持久性存储中,然后等待该刷新完成。这是通过在Windows下使用unix下的fsync()系统调用和在Windows下使用 FlushFileBuffers()来完成的。我们称此未完成写入为“同步”。

实际上,如果只关心原子和一致的写入,并且愿意放弃持久写入,则同步操作不需要等到内容完全存储在持久媒体上。相反,可以将同步操作视为I / O障碍。只要在同步之前发生的所有写入都在同步之后发生的任何写入之前完成,就不会发生数据库损坏。如果同步是作为I / O屏障而不是真正的同步运行,则电源故障或系统崩溃可能导致一个或多个先前提交的事务回滚(违反了“ ACID”的“持久”属性),但是数据库至少将继续保持一致,而这正是大多数人关心的。

3.1。不接受同步请求的磁盘驱动器

不幸的是,大多数消费者级大容量存储设备都在于同步。磁盘驱动器将在内容到达跟踪缓冲区后且实际上未写入氧化物之前报告内容已安全地存在于持久性介质上。这使磁盘驱动器的运行速度似乎更快(这对制造商而言至关重要,因此它们可以在贸易杂志上显示出良好的基准数字)。公平地说,只要在实际将磁道缓冲区写入氧化物之前没有任何功率损耗或硬重置,该谎言通常不会造成任何伤害。但是,如果确实发生断电或硬重置,并且导致同步后写入的内容达到氧化物,而同步之前写入的内容仍在轨道缓冲区中,则可能会发生数据库损坏。

对于同步请求,USB闪存棒似乎是特别有害的骗子。通过将大量事务提交到USB记忆棒上的SQLite数据库,可以轻松地看到这一点。COMMIT命令将相对快速地返回,表明记忆棒已告知操作系统,并且操作系统已告知SQLite所有内容均已安全地存储在持久性存储中,但记忆棒末端的LED将继续闪烁数次。再过几秒钟。当LED仍在闪烁时拔出存储棒通常会导致数据库损坏。

请注意,无论操作系统和硬件如何告知SQLite,SQLite都必须相信同步请求的状态。SQLite无法检测到两者都在说谎,并且写操作可能是乱序发生的。但是,与默认回滚日志模式相比,WAL模式下的SQLite更能容忍无序写入。在WAL模式下,失败的同步操作可能导致数据库损坏的唯一时间是在检查点操作期间。COMMIT期间的同步失败可能会导致持久性损失,但不会导致数据库文件损坏。因此,防止由于同步操作失败而导致数据库损坏的一道防线是在WAL模式下使用SQLite并尽可能不频繁地检查点。

3.2。使用PRAGMA禁用同步

SQLite为帮助确保完整性而执行的同步操作可以在运行时使用同步编译指示禁用。通过将PRAGMA同步设置为OFF,将忽略所有同步操作。这使得SQLite看起来运行得更快,但是它也允许操作系统自由地对写操作进行重新排序,如果在所有内容到达持久性存储之前发生电源故障或硬重置,则可能导致数据库损坏。

为了获得最大的可靠性和鲁棒性以防止数据库损坏,SQLite应该始终以其默认同步设置FULL运行。

4.磁盘驱动器和闪存故障

如果文件内容由于磁盘驱动器或闪存故障而更改,则SQLite数据库可能会损坏。这种情况很少见,但是磁盘有时会在一个扇区的中间翻转一点。

4.1。非电源安全闪存控制器

我们被告知,在某些闪存控制器中,如果在写操作期间断电,损耗均衡逻辑可能会导致随机的文件系统损坏。例如,这可能表现为文件中间的随机更改,而这些文件在断电时甚至没有打开。因此,例如,发生断电时,设备会将内容写入闪存中的MP3文件中,这可能会导致SQLite数据库损坏,即使断电时甚至没有使用该数据库也是如此。

4.2。假容量USB随身碟

流通中有许多欺诈性USB记忆棒,报告容量很高(例如:8GB),但实际上只能存储的容量小得多(例如:1GB)。尝试在这些设备上进行写操作通常会导致不相关的文件被覆盖。因此,任何对欺诈性闪存设备的使用都可能轻易导致数据库损坏。诸如“假容量USB”之类的Internet搜索将出现许多有关此问题的令人不安的信息。

5.内存损坏

SQLite是一个C库,在与其所服务的应用程序相同的地址空间中运行。这意味着应用程序中的杂散指针,缓冲区溢出,堆损坏或其他故障可能损坏内部SQLite数据结构,并最终导致损坏的数据库文件。通常,这些问题在任何数据库损坏发生之前都以段错误的形式表现出来,但是在某些情况下,应用程序代码错误已导致SQLite发生细微故障,从而损坏了数据库文件,而不是惊慌失措。

使用内存映射的I / O时,内存损坏问题变得更加严重。当数据库文件的全部或部分映射到应用程序的地址空间时,覆盖该映射空间的任何部分的杂散指针将立即破坏数据库文件,而无需应用程序执行后续的write()系统调用。

6.其他操作系统问题

有时,操作系统会表现出非标准的行为,这可能会导致问题。有时,这种非标准行为是故意的,有时是实现中的错误。但是无论如何,如果操作的执行方式与SQLite期望的方式不同,则存在数据库损坏的可能性。

6.1。Linux线程

一些旧版本的Linux使用LinuxThreads库提供线程支持。LinuxThreads与Pthreads类似,但是在处理POSIX咨询锁方面有细微的不同。SQLite版本2.2.3到3.6.23意识到运行时正在使用LinuxThreads,并采取了适当的措施来解决LinuxThreads的非标准行为。但是,大多数现代Linux实现都使用Pthreads的更新且正确的NPTL实现。从SQLite版本3.7.0(2010-07-21)开始,假定使用NPTL。不进行检查。因此,如果在使用LinuxThreads的较旧linux系统上运行的多线程应用程序中使用SQLite,则最新版本的SQLite可能会引起某些故障,并可能损坏数据库文件。

6.2。QNX上的mmap()失败

QNX上的mmap()存在一些细微的问题,例如,对单个文件描述符进行第二次mmap()调用会导致从第一次mmap()调用获得的内存为零。Unix上的SQLite在WAL模式下使用mmap()创建一个用于事务协调的共享内存区域,它将对大型事务多次调用mmap()。在这种情况下,QNX mmap()已被证明损坏了数据库文件。QNX工程师已经意识到了这个问题,并且正在研究解决方案。在您阅读本文时,该问题可能已经解决。

当QNX运行,建议内存映射I / O永远不会被使用。此外,要使用WAL模式,建议应用程序使用排他锁定模式,以便在没有共享内存的情况下使用WAL

6.3。文件系统损坏

由于SQLite数据库是普通磁盘文件,因此文件系统中的任何故障都可能损坏数据库。现代操作系统中的文件系统非常可靠,但是仍然会发生错误。例如,2013年10月1日,在将主机移至文件系统层中存在问题的(linux)内核的狡猾版本之后几天,保存Tcl / Tk Wiki的SQLite数据库 损坏。在那种情况下,文件系统最终会严重损坏,以至于机器无法使用,但最早的故障症状是损坏的SQLite数据库。

7. SQLite配置错误

SQLite具有许多内置的保护措施,可以防止数据库损坏。但是,许多保护可以通过配置选项禁用。如果禁用保护,则可能会发生数据库损坏。

以下是禁用SQLite的内置保护机制的示例:

8. SQLite中的错误

对SQLite进行了非常仔细的测试,以确保SQLite尽可能没有错误。在针对每个SQLite版本执行的众多测试中,有一些模拟电源故障,I / O错误和内存不足(OOM)错误,并验证在任何这些事件期间都不会发生数据库损坏的测试。SQLite还经过了大约20亿个活动部署的现场验证,没有出现严重问题。

但是,没有软件是100%完美的。SQLite中有一些历史错误(现已修复),可能会导致数据库损坏。而且可能还有更多未发现的东西。由于对SQLite进行了广泛的测试和广泛使用,导致数据库损坏的错误往往非常模糊。应用程序遇到SQLite错误的可能性很小。为了说明这一点,下面提供了从2009-04-01到2013-04-15的四年期间在SQLite中发现的所有数据库损坏错误的说明。该帐户应该使读者直观地了解SQLite中的各种错误,这些错误设法通过了测试过程并将其发布。

8.1。由于数据库缩小而产生的虚假损坏报告

如果数据库是由SQLite版本3.7.0或更高版本编写的,然后又由SQLite版本3.6.23或更早版本编写的,其方式是减小数据库文件的大小,则下一次该SQLite版本3.7.0访问数据库文件,它可能会报告该数据库文件已损坏。但是,数据库文件并没有真正损坏。3.7.0版在检测到损坏时过于热心。

该问题已于2011-02-20修复。该修复程序首先出现在SQLite版本3.7.6(2011-04-12)中。

8.2。在回滚和WAL模式之间切换后损坏

在一个进程或线程中,反复将SQLite数据库切换为WAL模式或退出WAL模式, 并在两个交换机之间运行VACUUM命令,可能会导致打开数据库文件的另一个进程或线程丢失数据库已更改的事实。然后,第二个进程或线程可能会尝试使用陈旧的缓存来修改数据库,并导致数据库损坏。

此问题是在内部测试期间发现的,从未在野外观察到。该问题已在2011-01-27和版本3.7.5中修复。

8.3。获取锁定时发生I / O错误会导致损坏

如果操作系统在WAL模式下尝试获取共享内存上的某个锁定时返回I / O错误,则SQLite可能无法重置其缓存,如果尝试后续写入,则可能导致数据库损坏。

请注意,仅当尝试获取锁导致I / O错误时,才会出现此问题。如果根本不授予该锁(因为某些其他线程或进程已经持有冲突的锁),则不会发生损坏。我们不知道任何尝试在共享内存上获取文件锁定时会因I / O错误而失败的操作系统。因此,这是一个理论问题,而不是一个实际问题。不用说,从未在野外观察到此问题。在模拟I / O错误的测试工具中对SQLite进行压力测试时发现了该问题。

对于SQLite版本3.7.3,此问题已于2010-09-20修复。

8.4。数据库页面从可用页面列表中泄漏

从SQLite数据库中删除内容后,不再使用的页面将被添加到空闲列表中,并被重复使用以容纳后续插入添加的内容。在使用3.6.16到3.7.2版本的SQLite中存在一个错误,可能会导致在使用incremental_vacuum时页面从可用列表中丢失。这不会导致数据丢失。但这将导致数据库文件大于必需的文件。这将导致完整性检查杂物报告空闲列表中缺少的页面。

对于SQLite版本3.7.2,此问题已于2010-08-23修复。

8.5。从3.6和3.7交替写入后出现损坏。

SQLite版本3.7.0对SQLite数据库文件格式(例如但不限于WAL)引入了许多新的增强功能。3.7.0版本是这些新功能的淘汰版本。我们希望能发现问题,并且不会感到失望。

如果数据库最初是使用SQLite版本3.7.0创建的,然后由SQLite版本3.6.23.1编写,以使数据库文件的大小增加,然后由SQLite版本3.7.0再次编写,则数据库文件可能会损坏。

对于SQLite版本3.7.1,此问题已于2010-08-04修复。

8.6。Windows系统恢复中的竞争状况。

SQLite版本3.7.16.2修复了Windows系统上锁定逻辑中的细微竞争条件。当数据库文件需要恢复时,由于先前写入该文件的进程在事务中间崩溃,并且两个或多个进程尝试同时打开该数据库,那么竞争条件可能导致这些进程之一错误地指示恢复已完成,从而使该过程可以继续使用数据库文件而无需先运行恢复。如果该进程写入文件,则文件可能会损坏。早在2004年,用于Windows的所有SQLite早期版本中就已经存在这种竞争条件。但是竞争非常紧张。实际上,您需要一台快速的多核计算机,在其中启动两个进程以在两个不同的内核上同时运行恢复。此缺陷仅在Windows系统上,不影响posix OS界面。