数据库和文件系统几乎普遍使用预写日志 (WAL) 来确保原子性操作和数据持久性。它的基本概念非常简单:在应用到数据文件之前,数据修改会被保存到一个专用的日志区域。在一般情况中,数据修改首先在内存WAL缓冲区中作为日志记录累积,然后刷新到存储WAL文件中。为了最大限度地提高数据持久性,系统会在每次事务提交时执行memory-to-storage flush(使用fsync或fdatasync等命令)。为了减少日志引起的存储成本,传统做法总是将日志记录紧密地打包到内存中的WAL缓冲区中,从而将存储中的WAL文件打包。因此,多个连续的WAL缓冲区刷新可能会写入存储设备上的同一个4KB LBA block,尤其是当事务记录明显小于4KB和/或工作负载并发性不高时。这可以在图1中说明:假设三个事务TRX-1、TRX-2和TRX-3(具有日志记录 L1、L2 和 L3)分别在时间t1、t2 和 t3提交。如图1所示,在时间 t1,4K 数据 [L1, 0] 从内存中的WAL缓冲区刷新到存储设备上的LBA 0x0001,其中0表示全零向量(all-zero vector)。稍后,日志记录L2被附加到WAL缓冲区中,并且在时间 t2,4KB数据[L1, L2, 0]被刷新到存储设备上相同的LBA 0x0001。类似地,在时间t3,4KB数据 [L1, L2, L3, 0] 被刷新到存储设备上相同的LBA 0x0001。如图1所示,相同的日志记录(例如,L1 和 L2)被多次写入NAND闪存,导致写放大。众所周知,写放大直接导致更短的闪存寿命和更低的存储IOPS性能。可以通过简单地禁用每次事务提交刷新(例如,每分钟调用memory-to-storage flush)来减少WAL引起的写放大,但这不再能够保证最近提交事务的数据持久性。
利用存储中透明压缩启用的几乎可变大小的block I/O,我们可以应用一种称为Sparse Logging的技术,在不牺牲数据持久性的情况下,来显着减少WAL引起的写放大。它的基本思想很简单:在每次事务提交时,我们总是将零填充到内存中的WAL缓冲区中,使其内容按4KB对齐。因此,下一条日志记录将写入WAL缓冲区中新的4KB空间。因此,每条日志记录只会写入一次NAND闪存,与传统做法相比,这样可以显著减少写入放大。这可以在图2中进一步说明:假设与上面图1所示的场景相同,事务TRX-1在时间t1提交后,我们将零填充到WAL缓冲区并刷新4K数据[L1, 0 ]到存储设备上的LBA 0x0001。随后,我们将下一条日志记录L2放在WAL缓冲区中新的4KB空间中。在时间t2,4KB数据[L2, 0]被刷新到存储设备上新的LBA 0x0002。类似地,在时间t3,4KB数据[L3, 0]被刷新到存储设备上的另一个新的LBA 0x0003。显然,每个日志记录只写入一次NAND闪存。同时,每个sparse 4KB block中的所有零都被存储中透明压缩给压缩掉。与传统做法相比,这会导致较小的WAL引起的写入放大。
为了阐释的更为清楚,我们实践了Sparse Logging的方法,并将它的写入放大与RocksDB和Wiredtiger(MongoDB 的默认存储引擎)中由WAL引起的写入放大进行了比较。所有实验都是在我们实现内置透明压缩的CSD 2000驱动器上进行的。我们将刷新策略设置为per-transaction-commit(即,在每次事务提交时将脏内存WAL缓冲区数据刷新到存储WAL文件中)。图3显示了测量的WAL引起的写入放大,其中我们考虑了不同的日志记录大小(即 128B、32B 和 16B)和不同数量的并发写入线程。结果表明,Sparse Logging方法可以将WAL引起的写入放大降低10倍以上。例如,在16B记录大小和单个客户端线程下,RocksDB和WiredTiger中WAL引起的写放大为138和128,而Sparse Logging的方法可以使WAL引起的写放大减少到仅仅只有4,意味着超过30倍的下降。