返回:SQLite—系列文章目录
上一篇:SQLiteC/C++接口详细介绍sqlite3_stmt类(十三)
下一篇:SQLite使用的临时文件(二)
概述
SQLite数据库具有很强的抗损坏能力。如果应用程序崩溃,或操作系统崩溃,甚至电源故障发生在事务中间,部分写入的事务应在下次自动回滚将访问数据库文件。恢复过程已完全完成自动,不需要用户采取任何行动或应用程序。尽管SQLite可以抵抗数据库损坏,但它也不能幸免。本文档介绍了SQLite数据库可能引起的各种损坏情况。
1.被恶意线程或进程覆盖文件
SQLite数据库文件是普通的磁盘文件。这意味着任何进程都可以打开文件和用垃圾覆盖它。没有什么是SQLite的图书馆可以做些什么来防御这种情况。
1.1.关闭文件描述符后继续使用文件描述符
我们已经看到多个案例,其中文件描述符在文件上打开,然后,该文件描述符被关闭并在SQLite数据库上重新打开。后来,其他一些线程继续写入旧文件描述符,没有意识到原始文件已关闭已经。但是由于文件描述符已被SQLite重新打开,打算进入原始文件的信息最终被覆盖SQLite数据库的某些部分,导致数据库。
其中一个例子发生在2013-08-30左右的规范存储库上用于化石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日的一篇博客文章中报告了描述符。
2019-07-11针对Fossil报告了此错误的另一个示例。文件描述符将打开以调试输出,但随后由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()
SQLite在unix平台上使用的默认锁定机制是POSIX咨询锁定。不幸的是,POSIX咨询锁定有设计怪癖使其容易被误用和失败。特别是,任何线程与保存POSIX的文件描述符在同一进程中建议锁可以使用不同的文件描述符覆盖该锁。一个特别有害的问题是close()系统呼叫将取消同一文件上的所有POSIX咨询锁进程中的线程和所有文件描述符。
因此,例如,假设一个多线程进程具有两个或多个线程,具有单独的SQLite数据库连接到相同的数据库文件。然后第三个线程出现并想要阅读同一数据库文件中的某些内容,而不使用SQLite库。第三个线程执行open()、read(),然后执行一个close()。人们会认为这是无害的。但是close()系统调用导致由所有其他线程在数据库上保留的锁将被丢弃。那些其他线程无法知道它们的锁刚刚被锁定trashed(POSIX不提供任何机制来确定这一点)等等他们在假设他们的锁仍然有效的情况下继续运行。这可能导致两个或多个线程或进程尝试写入同时,导致数据库损坏。
请注意,两个或多个线程访问使用SQLite库的相同SQLite数据库文件。unix驱动程序SQLite了解POSIX咨询锁定怪癖并解决它们。仅当线程尝试绕过SQLite库时,才会出现此问题并直接读取数据库文件。
2.2.1.链接到同一应用程序的多个SQLite副本
如上一段所述,SQLite采取措施来工作围绕POSIX咨询锁定的怪癖。该解决方法的一部分涉及保留打开的SQLite数据库文件的全局列表(mutexprotected)。但是,如果将SQLite的多个副本链接到同一个应用程序中,然后,此全局列表将有多个实例。使用SQLite库的一个副本打开的数据库连接将不知道使用另一个副本打开的数据库连接,并且无法解决POSIX建议锁定的怪癖。对一个连接的close()操作可能会在不知不觉中清除其他数据库连接上的锁,导致数据库腐败。
上面的场景听起来很牵强。但是SQLite开发人员知道至少发布一个商业产品正是这个错误。供应商来找SQLite开发人员寻求帮助追踪一些不常见的数据库损坏问题在Linux和Mac上看到。问题最终被追溯到事实上,应用程序链接到SQLite的两个独立副本。解决方案是更改要链接的应用程序构建过程只是一个SQLite副本,而不是两个。
2.3.使用不同锁定协议的两个进程
SQLite在unix平台上使用的默认锁定机制是POSIX咨询锁定,但还有其他选择。通过选择使用sqlite3_open_v2()接口的替代sqlite3_vfs,一个应用程序可以使用其他可能更适用于某些文件系统。例如,点文件锁定可能被选中用于必须在NFS文件系统上运行的应用程序不支持POSIX公告锁定。
与同一数据库文件的所有连接都必须使用相同的锁定协议。如果一个应用程序使用POSIX咨询锁,而另一个应用程序使用POSIX咨询锁正在使用点文件锁定,则两个应用程序将看不到每个其他人的锁,可能无法协调数据库访问导致数据库损坏。
2.4.使用时取消链接或重命名数据库文件
如果两个进程具有与同一数据库文件的开放连接,并且一个进程关闭其连接,取消文件链接,然后创建一个新的数据库文件,并重新打开新文件,然后,这两个进程将与不同的数据库文件进行通信同名。(请注意,这仅在Posix和类似Posix的Posix上才有可能允许在文件仍处于打开状态时取消链接的系统阅读和写作。Windows不允许发生这种情况。由于回滚日志和WAL文件基于数据库的名称文件,则两个不同的数据库文件将共享相同的回滚journal或WAL文件。其中一个数据库的回滚或恢复可能会使用其他数据库中的内容,从而导致损坏。如果数据库文件在重命名时重命名,则会出现类似的问题打开,然后使用旧名称创建一个新文件。
换言之,取消链接或重命名打开的数据库文件导致未定义且可能不受欢迎的行为。
从SQLite版本3.7.17(2013-05-20)开始,unixOS接口将如果数据库文件未链接,则向错误日志发送SQLITE_WARNING消息当它仍在使用时。
2.5.指向同一文件的多个链接
如果单个数据库文件具有多个链接(硬链接或软链接)那么这只是文件具有多个名称的另一种说法。如果两个或多个进程使用不同的名称打开数据库,则他们将使用不同的回滚日志和WAL文件。这意味着如果一个进程崩溃,另一个进程将无法恢复事务正在进行中,因为它将查找错误的位置对于适当的期刊。
换言之,打开并使用具有两个或名称越多,行为就越不明确,而且可能不受欢迎。
从SQLite版本3.7.17(2013-05-20)开始,unixOS接口将SQLITE_WARNING如果数据库文件有多个硬链接。
从SQLite版本3.10.0(2016-01-06)开始,unixOS接口将尝试解析符号链接并通过其规范名称。在版本3.10.0之前,打开数据库文件通过符号链接类似于打开数据库文件具有多个硬链接并导致未定义的行为。
2.6.在fork()上携带开放数据库连接
不要打开SQLite数据库连接,然后fork(),然后尝试在子进程中使用该数据库连接。各式各样将导致锁定问题,并且您很容易以损坏告终数据库。SQLite不是为支持这种行为而设计的。必须打开子进程中使用的任何数据库连接在子进程中,不是从父进程继承的。
甚至不要在数据库连接上调用sqlite3_close()子进程(如果连接已在父进程中打开)。这是安全的关闭基础文件描述符,但sqlite3_close()接口可能会调用清理活动,这些活动将删除内容从父级下,导致错误甚至数据库腐败。
3.同步失败
为了保证数据库文件始终保持一致,SQLite偶尔会要求操作系统将所有挂起的写入刷新到持久性存储,然后等待刷新完成。这是使用unix下的fsync()系统调用和Windows下的FlushFileBuffers()完成。我们称之为pending写入“sync”。
实际上,如果只关心原子和一致的写入和愿意放弃持久写入,同步操作不需要等待内容完全存储在持久性媒体上。相反同步操作可以看作是I/O屏障。只要所有在同步完成之前发生的写入在发生任何写入之前同步后,不会发生数据库损坏。如果同步以I/O屏障而不是真正的同步,然后是电源故障或系统崩溃可能会导致一个或多个以前提交的事务回滚(违反了“ACID”的“持久”属性),但数据库将在至少要保持一致,这是大多数人关心的。
3.1.不支持同步请求的磁盘驱动器
不幸的是,大多数消费级大容量存储设备都在撒谎同步。磁盘驱动器将报告内容安全地处于持久状态介质在到达轨道缓冲区后,在实际存在之前写到氧化物。这使得磁盘驱动器似乎运行得更快(这对制造商来说至关重要,这样他们才能展示贸易杂志上的良好基准数字)。公平地说,谎言通常不会造成伤害,只要没有断电或硬复位在轨道缓冲区实际写入氧化物之前。但是,如果确实会发生断电或硬重置,如果这导致内容是在同步达到氧化物之后写的,而内容是在之前写的同步仍在轨道缓冲区中,则可能会发生数据库损坏。
USB闪存棒似乎是特别有害的骗子关于同步请求。人们可以很容易地看到这一点,通过提交一个大的事务到USB记忆棒上的SQLite数据库。TheCOMMIT命令会相对较快地返回,表示记忆棒已经告诉操作系统,操作系统已经告诉SQLite所有内容都安全地保存在持久存储中,但末端的LED的记忆棒将继续闪烁几秒钟。在LED仍然闪烁时拔出记忆棒会经常导致数据库损坏。
请注意,无论操作系统和硬件如何,SQLite都必须相信告诉它同步请求的状态。SQLite没有办法检测其中任何一个都在撒谎,并且写入可能无序发生。但是,WAL模式下的SQLite要宽容得多无序写入比在默认回滚日志模式下。在WAL中模式,这是失败的同步操作可能导致数据库损坏的唯一时间在检查点操作期间。COMMIT期间的同步失败可能导致持久性损失,但不会导致数据库文件损坏。因此防止因同步操作失败而导致数据库损坏的一道防线是在WAL模式下使用SQLite,并尽可能少地进行检查点。
3.2.使用PRAGMA禁用同步
SQLite为帮助确保完整性而执行的同步操作可以在运行时使用同步编译指示禁用。通过设置PRAGMAsynchronous=OFF,则省略所有同步操作。这使得SQLite似乎运行得更快,但它也允许操作系统自由地对写入重新排序,如果电源故障,可能会导致数据库损坏或硬重置发生在所有内容到达持久存储之前。
为了实现最大的可靠性和防止数据库损坏的鲁棒性,SQLite应始终以其默认同步设置FULL运行。
4.磁盘驱动器和闪存故障
如果文件内容发生更改,SQLite数据库可能会损坏由于磁盘驱动器或闪存故障。这是非常罕见的,但是磁盘偶尔会在一个扇区的中间翻转一点。
4.1.非电源安全闪存控制器
我们被告知,在一些闪存控制器中,磨损均衡逻辑如果在写入过程中电源中断,可能会导致随机文件系统损坏。例如,这可能表现为文件中间的随机更改在断电时甚至没有打开。因此,例如,当发生断电,这可能导致SQLite数据库被损坏,即使数据库在功率损耗。
4.2.假容量U盘
有许多欺诈性的U盘在流通中报告有高容量(例如:8GB),但实际上只能存储很多较小的数量(例如:1GB)。尝试在这些设备上写入将通常会导致不相关的文件被覆盖。任何使用欺诈行为因此,闪存设备很容易导致数据库损坏。互联网搜索,如“假容量usb”,会出现很多有关此问题的令人不安的信息。
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实现使使用更新且正确的NPTL实现Pthreads。开始在SQLite版本3.7.0(2010-07-21)中,假定使用NPTL。没有检查䍬。因此,最新版本的SQLite会出现微妙的故障,并且可能损坏的数据库文件(如果在运行的多线程应用程序中使用)在使用LinuxThreads的旧Linux系统上。
6.2.mmap()在QNX上的失败
QNX上的mmap()存在一些微妙的问题,使得针对单个文件描述符的第二次mmap()调用可能会导致从第一次mmap()调用获取的内存要归零。SQLite开启UNIX使用mmap()为事务创建共享内存区域在WAL模式下进行协调,它会多次调用mmap()用于大额交易。QNXmmap()已被证明会损坏数据库文件。QNX工程师意识到了这个问题并正在研究解决方案;该问题可能已经通过以下方式解决你读这篇文章的时候。
在QNX上运行时,建议内存映射I/O永远不要被使用。此外,要使用WAL模式,建议应用程序采用独占锁定模式,以便使用不带共享内存的WAL。
6.3.文件系统损坏
由于SQLite数据库是普通的磁盘文件,因此文件系统可能会损坏数据库。现代操作系统中的文件系统非常可靠,但错误仍然会发生。例如,在2013-10-01保存Tcl/Tk的Wiki的SQLite数据库几天就损坏了将主机移动到(Linux)内核的狡猾版本后这在文件系统层中存在问题。在这种情况下,文件系统最终变得如此严重,以至于机器无法使用,但是最早出现问题的症状是损坏的SQLite数据库。
7.SQLite配置错误
SQLite具有许多针对数据库损坏的内置保护。但是,这些保护中的许多都可以通过配置选项禁用。如果禁用保护,则可能会发生数据库损坏。
以下是禁用内置保护的示例SQLite的机制:
设置PRAGMAsynchronous=OFF可能会导致数据库如果发生操作系统崩溃或电源故障,则损坏,尽管此设置不会因应用程序崩溃而损坏。
更改PRAGMAschema_version而其他数据库连接已打开。
使用PRAGMAjournal_mode=OFF或PRAGMAjournal_mode=MEMORY,并在写入事务过程中发生应用程序崩溃。
将PRAGMA设置为writable_schema=ON,然后更改使用DML语句的数据库架构可以完整地呈现数据库如果不仔细操作,则不可读。
8.SQLite中的错误
SQLite经过非常仔细的测试,以帮助确保它是尽可能没有错误。在进行的众多测试中每个SQLite版本都是模拟电源故障、I/O错误、和内存不足(OOM)错误,并验证是否未发生数据库损坏在上述任何事件中。SQLite也经过了现场验证,大约有20亿次活动部署,没有严重问题。
然而,没有任何软件是100%完美的。已经有一些SQLite中可能导致数据库损坏的历史错误(现已修复)。可能还有一些尚未被发现。因为SQLite的广泛测试和广泛使用,导致以下错误的数据库损坏往往非常模糊。可能性遇到SQLite错误的应用程序很小。为了说明这一点,下面给出了一个帐户在SQLite中发现的所有数据库损坏错误从2009-04-01到2013-04-15的四年时间。这个叙述应该让读者直观地了解SQLite中设法通过测试程序的Bug类型并制作成一个版本。
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模式,并在交换机之间运行VACUUM命令,在一个进程或线程,可能会导致另一个进程或线程具有数据库文件打开以错过数据库已更改的事实。第二个过程或者线程可能会尝试使用过时的缓存修改数据库,并且导致数据库损坏。
此问题是在内部测试期间发现的,并且从未被发现在野外观察到。该问题已在2011-01-27及版本中修复3.7.5.
8.3.获取锁时的I/O错误导致损坏
如果操作系统在尝试获取在WAL模式下对共享内存进行特定锁定,则SQLite可能会失败要重置其缓存,如果尝试进行后续写入,则可能导致数据库损坏。
请注意,仅当尝试获取锁时,才会发生此问题导致I/O错误。如果根本没有授予锁定(因为一些其他线程或进程已经持有冲突锁),则没有腐败将永远发生。我们不知道任何操作系统在尝试在共享上锁定文件时将失败并出现I/O错误记忆。所以这是一个理论问题,而不是一个真正的问题。毋庸置疑,这个问题从未在野外观察到。这在测试中对SQLite进行压力测试时发现问题模拟I/O错误的线束。
此问题已在2010-09-20的SQLite版本3.7.3中修复。
8.4.数据库页面从可用页面列表中泄漏
从SQLite数据库中删除内容时,没有使用时间较长的内容将添加到免费列表中,并重复用于保存内容由后续插入添加。SQLite中存在的一个bug版本3.6.16到3.7.2可能会导致页面丢失使用incremental_vacuum时的空闲列表。这不会导致数据丢失。但这会导致数据库文件大于必要。这将导致integrity_check编译指示报告免费列表中缺少页面。
此问题已在2010年8月23日针对SQLite版本3.7.2修复。
8.5.3.6和3.7交替写入后损坏
SQLite3.7.0版引入了许多新的增强功能SQLite数据库文件格式(例如但不限于WAL)。3.7.0版本是这些新功能的彻底版本。我们期望找到问题,并没有失望。
如果数据库最初是使用SQLite版本3.7.0创建的,然后由SQLite版本3.6.23.1编写,使得数据库的大小文件增加,然后由SQLite版本3.7.0再次写入数据库文件可能会损坏。
此问题已在2010-08-04的SQLite版本3.7.1中修复。
8.6.Windows系统恢复中的争用条件
SQLite版本3.7.16.2修复了锁定中的微妙争用条件Windows系统上的逻辑。当需要数据库文件时的恢复,因为上一个写入它的进程在在事务中间,两个或多个进程尝试打开该数据库,则争用条件可能会导致其中一个进程来获得恢复的错误指示已经完成,允许该过程继续使用数据库文件,而不先运行恢复。如果该进程写入到文件,则文件可能会损坏。此竞争条件显然存在于所有以前版本的SQLiteforWindows中时间回到2004年。但比赛非常激烈。实际上,你需要一台快速的多核机器,您可以在其中启动两个进程来运行同时在两个单独的内核上恢复。这个缺陷是仅在Windows系统上,不影响posixOS界面。
8.7.嵌套事务使用的二级日志中的边界值错误
当使用SAVEPOINT启动嵌套事务时,SQLite使用辅助回滚日志,用于跟踪嵌套的更改事务,以防内部事务需要回滚。二次期刊不参与保护数据库免受损坏程序崩溃或断电。二级期刊只进入回滚嵌套事务的内部事务时播放。
这些辅助日志既可以保存在内存中,也可以作为临时日志保存磁盘上的文件。默认行为是将它们存储在磁盘上。但是可以使用-DSQLITE_TEMP_STORE编译时选项进行更改,或在运行时使用PRAGMAtemp_store语句。错误只有当辅助日志保存在内存中时才会出现。
在SQLite版本3.35.0(2021-03-12)中,新的优化是添加,以便当SQLite在内存中保存辅助日志时,将使用更少的内存。不幸的是,边界检查新逻辑编码不正确。本应是“<”运算符的内容被编码为“<=”。这错误可能会导致辅助日志进入不一致状态如果它曾经回滚过。如果进行了其他更改,并且外部事务最终提交,数据库可能会留在状态不一致。
这个问题是由一位独立研究人员发现的,他试图使用模糊器在SQLite中查找错误。模糊器发现了一个在使用的assert()语句中失败以帮助验证辅助日志的内部状态。该错误是一个足够晦涩难懂的角落情况,以至于许多人可能没有注意到它年,如果不是在SQLite中大量使用assert()语句,安全研究人员的毅力和坚韧,以及他们定制的最先进的模糊器。
此问题已在版本3.37.2(2022-01-06)中修复。