持久性数据库I/O
要理解持久性数据库的读写操作是如何执行的,首先了解缓存的工作原理是非常有帮助的。通常情况下,持久性数据库应用程序不会直接对持久性介质(如硬盘或固态硬盘)执行 I/O 操作。相反,读写操作是通过内存缓存来优化整体性能,而不是直接访问持久性介质。
SmartEDB 使用了一种称为页面池的数据库缓存机制,该机制可以通过多种配置选项进行优化。页面池保存了一定量的数据以实现快速访问。当需要时,它会通过操作系统文件系统将数据写入或从持久性介质读取。操作系统文件系统也管理着自己的缓存,在实际访问持久性介质之前缓冲内容。
这两种缓存类型及其相互作用将在接下来的章节中详细解释。
关于绕过文件系统缓存的说明
为什么我们需要文件系统缓存呢?使用 O_DIRECT 标志可以绕过文件系统缓存,从而避免所谓的“双缓冲”问题。所谓“双缓冲”,是指相同的页面同时被数据库缓存管理和操作系统文件系统缓存管理所缓存,这会导致额外的复制操作和内存浪费。有些人认为数据库管理系统应当完全掌控缓存,并将操作系统排除在这一过程之外。
然而,事实是只有操作系统最清楚它在某一特定时刻拥有的空闲资源量。许多实验证明,试图完全绕过操作系统的缓存实际上可能会导致性能下降。
Linux 系统中的大页支持
大多数现代 Linux 系统允许配置其虚拟内存空间的一部分使用大页。大页功能使 Linux 内核能够管理更大的内存页面,除了标准的 4KB(在 x86 和 x86_64 上)或 16KB(在 IA64 上)的页面大小。当系统需要访问虚拟内存位置时,它使用页表将虚拟地址转换为物理地址。使用大页意味着系统需要将更少的此类映射加载到转换后备缓冲区(TLB)中,这是 CPU 上的页表缓存,可加快虚拟地址到物理地址的转换。启用大页功能后,内核可以在 TLB 中使用指向大页的 hugetlb 条目。这些条目可以覆盖更大的地址空间,从而减少所需的 TLB 条目数量。对于运行 SmartEDB 数据库且内存超过 16GB 的系统,启用大页功能可以显著提高数据库性能。您可以通过在内存设备标志中指定 MAP_HUGETLB 标志,或者对于 SmartEDB 共享内存数据库指定 SHM_HUGETLB 标志,来启用大页功能。
文件系统缓存
操作系统(如 Linux、MacOS、Windows、VxWorks 等)会为文件系统管理分配一部分系统内存。文件系统努力防止应用程序遭受磁盘 I/O 延迟,例如通过使用 DRAM 缓冲写入操作,并对读取操作进行缓存和预取。对于数据库应用程序而言,重要的是其向文件系统发出请求时所经历的延迟。 如以下图表所示,读取和写入操作通常是从文件系统的主内存缓存中进行,而非从磁盘中进行:

如图中较宽的箭头所示,“缓存命中”(即通过内存访问)的 I/O 吞吐量比“缓存未命中”(需要操作系统访问持久性介质)快几个数量级。
有关 SmartEDB 与操作系统文件管理系统交互的更多详情,请参阅“持久性媒体 I/O”页面。我们希望这些信息能帮助您更好地理解系统的性能特点。
数据库缓存
为何我们需要单独的数据库缓存?
如前所述,文件系统的读写请求延迟会显著影响数据库应用程序的性能。当读取或写入请求导致文件系统“缓存未命中”时,访问持久性介质可能会严重拖慢应用程序的速度。由于多种原因,文件系统无法完全满足数据库应用程序的性能需求:
其他进程的干扰:除了数据库应用程序之外,其他进程也可能妨碍高效的缓存管理。例如,假设 SmartEDB 应用程序与其他应用程序一起运行,而另一个应用程序产生了大量的磁盘 I/O 操作。即使 SmartEDB 在 DRAM 中缓存效果很好,几乎不使用磁盘,但夜间备份进程遍历整个文件系统时,仍然会导致磁盘活动频繁。这可能会使 SmartEDB 经历非常缓慢的数据库查询,而这完全是由于一个不相关的进程引起的。
缺乏对数据库对象的理解:文件系统并不了解需要一起访问的特定数据库对象,例如索引或支持结构等。因此,它只能部分缓存所需的数据库数据,无法实现最佳性能。
数据库逻辑的影响:数据库逻辑可能会根据应用需求调整缓存内容,例如需要缓存特定类的对象。这种灵活性是文件系统无法提供的。
确保数据一致性:数据库必须确保写入的安全性,以保持数据的一致状态。文件系统的 _commit()(或 _sync())API 虽然可以将缓冲数据写入磁盘,但在系统故障的情况下,文件系统缓冲区可能会丢失,从而导致数据库损坏。
鉴于这些原因,SmartEDB 实现了一个专门的数据库缓存,以尽量减少持久性介质 I/O 对性能的影响,如下图所示:

数据库缓存被定义为一种内存设备,可以是常规的随机存取存储器(RAM)或共享内存。数据库事务会影响一个或多个“页”,这些“页”由数据库缓存(或“页池”)管理器进行管理,如下图所示:

缓存大小与调优
在配置数据库时,传递给数据库打开 API 的内存设备会指定缓存的内存地址和大小。内存可以是共享内存或本地内存(如果两个或多个进程需要共享数据库,则必须使用共享内存)。通常,较大的缓存有助于提高应用程序性能,但对持久介质的更新频率(即缓存页面的刷新)对性能的影响更为关键。数据库更新如何写入持久介质是由事务提交策略决定的。
由于缓存的容量有限,无法容纳应用程序可能需要的所有数据,因此在某个时刻必须从缓存中移除某些内容,以便为新数据腾出空间。我们的目标是保留那些很快可能再次被检索到的数据项。这需要一种合理的算法来选择从缓存中移除的内容。关于缓存替换策略或页面替换算法的详细信息,您可以参考这篇维基百科文章,其中介绍了多种处理这一问题的方法,非常值得一读。默认情况下,SmartEDB 使用一种名为“优先缓存”的 CLOCK 算法变体。更多实现细节,请参阅优先缓存页面。
当重新打开一个持久性数据库时,SmartEDB 通常通过运行常规的应用程序操作来填充缓存,因此要达到最佳性能需要一些时间。为了解决这个问题,SmartEDB 运行时提供了保存和加载缓存内容的 API。这样,在重新打开持久性数据库时,可以通过加载之前保存的缓存内容,显著提升其准备就绪的速度和性能。
数据库日志策略
日志策略决定了事务何时提交到持久存储。SmartEDB 支持三种不同的数据库日志策略。
请注意,不要将 SmartEDB 的事务日志记录(这是一组完全独立的 API)与 SmartEDB 持久数据库的常规运行时日志记录混淆。有关更多详细信息,请参阅事务日志记录页面。
以下是这三种策略的基本策略、优点和缺点总结:
预写日志(REDO_LOG 策略)
预写日志的核心概念是,在对数据进行更改之前,必须先将这些更改记录到日志中,然后再写入数据文件。
优点:
- 提交时磁盘写入次数显著减少(仅在提交时进行写入);
- 日志文件按顺序写入,因此同步日志的成本远低于刷新数据页的成本。
缺点:
- 缓存大小限制:当存在大量未提交的更新时可能会耗尽内存;
- 交易大小受页面池(缓存)大小的限制。
即时修改日志记录(UNDO_LOG 策略)
即时修改日志记录通过日志文件中的条目来撤销当前事务的更新。
优点:
- 该算法永远不会耗尽内存,并且提供简便高效的恢复功能。
缺点:
- 所有更新在提交到持久性介质时都必须刷新到数据库文件中;
- 所有“写入”操作都是针对数据库文件的,且是随机的,因此比针对日志文件的顺序写入要慢;
- 不仅在数据库提交时会执行“刷新”操作;几乎在从页池中丢弃脏页时也会执行“刷新”操作。
无日志记录策略(NO_LOG 策略)
如果选择此选项,则关闭事务日志记录,并且不会创建日志文件。
优点:
- 更新速度可以显著加快。
缺点:
- 应用程序在发生崩溃时将无法恢复数据库;
- 当使用 MURSIW 事务管理器时,事务回滚不可用。
预写日志(REDO_LOG 策略)
预写日志(WAL)是一种广泛采用的事务日志记录方法。如果您想了解更多关于这一概念的一般性描述,请参考此网页。
简单来说,WAL 的核心理念是:对数据的更改必须先记录到日志中,并且当日志记录已刷新到永久存储后,才能将这些更改写入数据文件。当更新页面(无论是数据页还是索引页)时,该页面会被“固定”在页面池(缓存)中,在整个事务期间不会被换出(即遵循“不偷窃”策略)。在事务提交时,所有更新的页面首先会写入日志,然后提交并刷新到永久存储。只有在此之后,更新的页面才会写入数据库(但不会立即刷新)。如果在提交过程中日志大小超过指定阈值,则会创建一个检查点:所有更新的页面会被写入磁盘,更新会被刷新到永久存储,并且日志会被截断。
REDO_LOG 策略的优点:
- 显著减少磁盘写入次数,因为事务提交时只需将日志文件刷新到磁盘。
- 日志文件是顺序写入的,因此同步日志的成本远低于刷新数据页的成本。
REDO_LOG 策略的缺点:
- 当存在大量未提交的更新时,算法可能会耗尽内存。
- 事务大小受限于页池(缓存)的大小。每当一个页面被标记为“脏页”(即页面上的任何内容被更改),它都必须保留在缓存中。
以下图表说明了预写式日志(WAL)的工作原理:

即时修改日志记录(UNDO_LOG 策略)
当使用 UNDO_LOG 策略时,日志文件包含允许撤销当前事务更新的条目。简单来说,SmartEDB 实现这一方法的过程如下:在更新期间,运行时会将包含的页面标记为“脏”并在已修改页面的位图中标记,然后将原始页面写入日志文件。无论在事务期间单个页面被更改多少次,原始页面图像仅写入日志文件一次。当事务提交时,所有已修改的页面会被写入并刷新到数据库文件,随后截断日志文件。恢复和回滚过程会从日志文件中读取所有保存的页面,恢复页面的原始图像,并清除页面的“脏”位。
使用 UNDO_LOG 策略的优点:
- 该算法永远不会耗尽内存,并且提供简单高效的恢复功能。
使用 UNDO_LOG 策略的缺点:
- 所有更新在提交到持久介质时都必须刷新到数据库文件。对数据库文件的写入通常是随机的,因此比顺序写入日志文件要慢。
- 在更新页面时,我们必须确保在将原始版本保存到日志之前,更新后的版本不会被存储到持久介质中。因此,不仅在数据库提交时会执行“刷新”操作,而且几乎在从页面池中丢弃脏页时也会执行。
以下图表说明了撤销日志记录的工作原理:

非事务模式(NO_LOG 选项)
如果选择 NO_LOG 选项,日志文件更新将被关闭,并且不会创建日志文件。这将显著提高更新性能,但请注意,在应用程序崩溃的情况下,数据库将无法恢复,事务回滚功能也不可用。因此,此模式仅适用于需要快速填充数据库文件的场景。在其他情况下,我们不建议使用此选项,以确保数据的安全性和完整性。
选择日志文件类型
日志策略的选择取决于日志文件类型:UNDO_LOG、REDO_LOG 或 NO_LOG。以下是根据应用程序特性选择适当日志策略的一些指南,帮助您做出最适合的选择:
选择 UNDO_LOG 的情况:
- 应用程序具有长期运行的事务;
- 提交时对性能要求不高;
- 运行在空间受限的事务环境中;
- 恢复时间至关重要。
选择 REDO_LOG 的情况:
- 应用程序通常具有短事务;
- 需要快速提交,尤其是对于吞吐量和延迟敏感的操作。
选择 NO_LOG 的情况:
- 如果数据安全性不是关键因素,请选择 NO_LOG。请注意,除非数据安全无关紧要,否则不建议在生产环境中使用此选项。
有关更多详细信息,请参阅“设置日志文件类型”页面。希望这些指南能帮助您更好地选择适合您应用的日志策略。
数据库事务
ACID(原子性、一致性、隔离性、持久性)模型是数据库理论中最古老且最重要的概念之一。它设定了每个数据库管理系统都必须努力实现的四个关键目标:原子性、一致性、隔离性和持久性。任何未能满足这四个目标中的任何一个的数据库,都无法被视为可靠的。
原子性
原子性规定数据库修改必须遵循“全有或全无”的原则。每个事务都被视为“原子的”,即不可分割的整体。如果事务的一部分失败,整个事务就会失败。无论出现任何数据库管理系统、操作系统或硬件故障,数据库管理系统都必须确保事务的原子性。这一点至关重要,以保证数据的完整性和可靠性。
一致性
一致性确保只有有效数据才会被写入数据库。如果某个事务违反了数据库的一致性规则,整个事务将被回滚,数据库将恢复到符合这些规则的状态。另一方面,如果事务成功执行,它会将数据库从一个符合规则的状态转换到另一个也符合规则的状态。这样可以确保数据库始终处于一致的状态。
隔离性
隔离性要求同时发生的多个事务不会相互影响。例如,如果乔和玛丽在同一时间对数据库发出不同的事务,这两个事务应以隔离的方式操作。数据库要么先完成乔的整个事务,然后再处理玛丽的事务,或者反之亦然。这可以防止乔的事务读取玛丽事务的部分执行所产生的中间数据,而这些数据最终不会提交到数据库。请注意,隔离属性并不决定哪个事务会先执行,而是确保它们不会相互干扰。
持久性
持久性确保提交到数据库中的任何事务都不会丢失。通过使用数据库备份和事务日志,即使在后续出现任何软件或硬件故障的情况下,也能恢复已提交的事务。这为数据提供了强有力的保障,确保事务一旦提交就不会因任何原因而丢失。
事务提交策略
当事务提交后将控制权交还给应用程序时,如果在应用程序或系统出现故障的情况下,事务数据仍可恢复(假设数据库和/或事务日志所在的介质未受损或未被破坏),则称该事务具有持久性。
为了确保事务的持久性,数据库系统必须强制将更新从数据库缓存和文件系统缓存写入物理介质(无论是固态存储还是传统硬盘)。这种刷新操作虽然在性能上代价较高,但却是确保持久性的唯一途径。
为什么某些应用程序可能会选择放宽事务的持久性属性呢?因为持久性事务的成本较高。例如,有些应用程序不需要持久性事务,如可以从其他来源重新加载数据的商品/股票交易系统,以及可以在必要时重复执行批量加载操作的应用程序。
SmartEDB 提供了四种事务提交策略。这些策略在打开数据库时最初设定,但在程序执行期间也可以进行调整。
请注意,事务提交策略仅影响持久化数据库。
以下是这四种策略的基本策略、优点和缺点总结:
缓冲同步(MCO_COMMIT_BUFFERED 策略)
在使用 MCO_COMMIT_BUFFERED 策略时,数据库缓存不会在事务提交时刷新到磁盘。
### 好处
- 显著减少了 I/O 操作的数量,提升了性能。
- 即使应用程序出现故障,也不会导致数据库损坏。
- 应用程序可以根据需要明确地将缓存强制刷新到介质。例如,可以定期将所有数据持久化到磁盘并截断日志文件。
### 缺点
- 如果应用程序失败,数据库缓存将被销毁,所有在设置策略后提交的缓冲事务(即这些事务所做的更改)将会丢失。
无同步(MCO_COMMIT_NO_SYNC 策略)
在使用 MCO_COMMIT_NO_SYNC 策略时,数据库运行时不会显式地将文件系统缓冲区与文件系统介质同步。实际何时将数据写入介质由文件系统决定。
### 好处
- 与完全同步模式相比,此模式提供了一些性能优势。
### 缺点
- 在系统崩溃的情况下,如果已提交的事务仍在文件系统缓存中,可能会丢失事务数据。此外,数据库和日志文件可能会处于不一致的状态,从而导致数据库内容损坏。
延迟同步(MCO_COMMIT_DELAYED 策略)
在使用 MCO_COMMIT_DELAYED 策略时,修改过的页面会保留在页面池中,类似于 MCO_COMMIT_BUFFERED 策略。当所有保留页面的总大小达到阈值或延迟提交的次数超过某个值时,所有延迟的事务会一次性提交到持久存储中。
### 好处
- 此模式在性能方面优于全同步(MCO_COMMIT_SYNC_FLUSH)模式。
- 不会损坏数据库。
### 缺点
- 如果系统出现故障,缓存中尚未处理的多个事务将会丢失。
完全同步(MCO_COMMIT_SYNC_FLUSH 策略)
在使用 MCO_COMMIT_SYNC_FLUSH 策略时,数据库提交时会刷新缓存,并同步数据库文件和日志文件的文件系统缓冲区。(请注意,这是默认的事务策略。)
### 好处
- 此策略确保了事务的持久性,提供了最高的数据可靠性。
### 缺点
- 相比其他策略,这种模式的速度可能会稍慢一些。
缓冲同步(MCO_COMMIT_BUFFERED 策略)
此策略规定,在事务提交时,数据库缓存不会立即刷新到磁盘,而是在应用程序调用缓存刷新 API 时进行刷新。当前事务所修改的页面会保留在数据库缓存中,这包括数据库页面和日志文件页面。通过这种方式,该策略显著减少了 I/O 操作的数量:运行时仅在正常交换期间将脏页写入磁盘。
如果应用程序出现故障,数据库缓存将被销毁,所有由缓冲事务(即在设置此策略后提交的事务)所做的更改都将丢失。特别是在使用 NO_LOG 日志记录策略的情况下,如果应用程序异常终止,数据库可能会损坏。
然而,请放心,只要硬件和操作系统正常运行,应用程序故障不会导致数据库损坏。因为在缓存刷新操作期间,日志文件会被写入持久存储,从而确保数据库文件的一致性。不过,由于并非每次提交都会触发写入持久介质的操作,因此缓冲的提交可能会丢失。当应用程序重新启动时,系统会从日志文件中恢复数据库到一致状态。
以下图表说明了缓冲同步方法:

请注意,虚线箭头表示缓存(缓冲事务)中的数据不会自动刷新到持久性介质。当应用程序调用缓存刷新 API 时,才会发生持久性更新。
无同步(MCO_COMMIT_NO_SYNC 策略)
此策略规定,数据库运行时不会显式地将文件系统缓冲区与文件系统介质同步。在事务提交时,事务所做的所有更改会从应用程序空间转移到操作系统空间,并且日志文件会被截断(仅在使用 UNDO_LOG 日志策略时)。文件系统会决定何时将数据实际写入介质。这种模式相比完全同步模式提供了一些性能优势,但在系统崩溃的情况下也存在丢失事务数据的风险(尽管已提交的事务仍在文件系统缓存中)。
请注意,在出现故障的情况下,数据库和日志文件可能会处于不一致的状态,从而导致数据库内容损坏。
不过,使用 MCO_COMMIT_NO_SYNC 模式时,应用程序可以明确强制将缓存刷新到介质。当与 MCO_COMMIT_NO_SYNC 事务策略结合使用时,缓存刷新 API 允许应用程序根据需要定期将所有数据持久化到磁盘并截断日志文件,以确保数据的安全性和一致性。
以下图表说明了“无同步”方法:

请注意,实线箭头表示在提交时数据库缓存中的数据会刷新到文件系统,而虚线箭头表示文件系统缓存(缓冲事务)中的数据会在文件系统的控制下刷新到持久介质。
延迟同步(MCO_COMMIT_DELAYED 策略)
此提交策略仅在日志策略设置为 REDO_LOG 时可用。与 MCO_COMMIT_BUFFERED 模式类似,在此模式下,数据库提交时事务不会立即写入持久介质。相反,数据库缓存管理器会将事务暂时保留在内存中,直到达到指定的阈值。有两个阈值标准:1)已用空间量(默认为总空间的 1/3),和 2)事务数量(默认关闭)。因此,当所有保留页面的总大小达到已用空间阈值或延迟提交的事务数量超过设定值时,所有延迟的事务将一次性提交到持久存储。
如果应用程序出现故障,一些最近的事务可能会丢失,因为无法确定对持久性介质的提交实际发生的时间以及哪些事务实际上已被写入。
以下图表说明了延迟同步方法:

请注意,虚线箭头表示缓存(缓冲事务)中的数据不会自动刷新到持久性介质。当达到指定阈值时,才会进行持久更新。
完全同步(MCO_COMMIT_SYNC_FLUSH 策略)
此策略确保在数据库提交时,缓存会被刷新,并且会同步数据库和日志文件的文件系统缓冲区。如果使用 UNDO_LOG 日志记录策略,还会截断日志文件。通过这种方式,此策略提供了持久性的事务处理。只有在存储数据库和日志文件的物理介质受损的情况下,数据库才可能被损坏。
以下图表说明了完全同步的方法:

请注意,实线箭头表示当事务提交时,数据库缓存中的数据会自动刷新到文件系统缓存以及持久介质中。