TSDB 不需要 WAL

Write-Ahead Log(WAL)是现代 TSDB 中的常见机制:

让我们来看看 WAL 理论。
WAL 用于防止在断电时丢失最近添加的数据。所有写入的数据必须在返回success给客户端之前写入预写日志。这保证了在断电后可以从WAL文件中恢复数据。理论上看起来简单而且很棒!那么在实践中呢?

Page Cache 和 WAL

数据库开发人员都知道操作系统(OS)不会在每次 write syscall 时将数据写入磁盘。数据只是驻留在 page cache,除非使用 direct IO。因此,成功写入WAL文件的数据在断电时可能会消失。如何处理这个问题?要么使用 繁琐的 direct IO,要么通过 fsync syscall 明确告诉操作系统将最近写入的数据从 page cache 刷新到磁盘。但是fsync有一个致命的缺点,就是在 SSD 上非常慢(1K-10K rps),在 HDD 上更慢(100 rps)。例如,1M/s写入速度可能会因使用fsync-after-each-inserted-row直接降成100/s

数据库开发人员如何处理慢速的 fsync?他们通过各种方式放宽数据安全保证:

  • Prometheus 仅在大量数据(即segment)写入WAL后才调用 fsync,因此在断电前所有segment数据可能会丢失/损坏。如果操作系统将写入数据的几个页面刷新到磁盘,但没有刷新剩余的页面,数据可能会损坏。Prometheus 默认每2小时对segment进行fsync,因此在硬件故障时可能会损坏大量数据。
  • Cassandra 默认每10秒才对WAL调用一次 fsync,因此在断电时最后10秒的数据可能会丢失/损坏。可能在这种情况下,多副本可以有所帮助。
  • InfluxDB 默认在每个写请求上调用fsync,因此 InfluxDB 建议一次写请求包含5K-10K个数据点,以缓解fsync的拖垮性能。它建议对于高写入量的场景或使用了读写速度慢的 HDD,将wal-fsync-delay设置为非零值,因此在断电时数据可能会丢失。
  • TimescaleDB 依赖于 PostgreSQL 的WAL机制,该机制将数据放入 RAM 中的 WAL 缓冲区并定期将其刷新到 WAL 文件。这意味着在断电或进程崩溃时,未刷新的WAL缓冲区中的数据将丢失。

因此,现代 TSDB 提供了“宽松的数据安全保证”,即最近插入的数据都可能会在断电时丢失。由此产生以下问题:

  • 这些放宽措施是否违背了预写日志的主要目的?恕我直言,这个问题的答案是“是的”。
  • 是否存在具有类似数据安全保证的更好方法?是的,那就是 SSTable

SSTable 代替 WAL?

这个想法很简单,只需将数据缓存在内存中,并以原子方式将其刷新到磁盘上。数据存储在类似 SSTable 的数据结构中,无需 WAL。

刷新可以通过超时(即每N秒)或达到最大缓冲区大小来触发。这提供了与上面WAL近似的数据安全保证,即最近插入的数据可能会在断电/进程崩溃时丢失。

细心的读者可能会注意到区别,WAL 可能导致数据损坏,而“直接写入 SSTable”方法容易受到进程崩溃的影响。恕我直言,进程崩溃时最近写入的数据丢失的严重性远低于数据损坏。数据库优雅关闭实现得好可以明显降低数据丢失的风险。优雅关闭程序非常简单,停止接收新数据,然后将内存缓冲区刷新到磁盘,然后退出。

以下数据库倾向于直接写入 SSTable 而不是 WAL:

  • ClickHouse。默认情况下,它将传入数据直接写入类似 SSTable 的磁盘。它通过 Buffer 表支持内存缓冲。
  • VictoriaMetrics。它将传入数据缓存在 RAM 中,并定期将其刷新到磁盘上的类似 SSTable 的数据结构中。刷新间隔硬编码为一秒。

结论

WAL 的使用在现代 TSDB 中看起来是有问题的。它不能保证在断电时最近写入数据的安全性。而且 WAL 还有两个额外的缺点:

  • 预写日志往往会消耗大量的磁盘IO。由于这个缺点,建议将WAL放在一个单独的物理磁盘上。直接写入 SSTable 方法消耗更少的磁盘 IO,因此数据库可以在没有 WAL 的情况下处理更高的数据写入量。
  • WAL 可能会由于缓慢的恢复行为减慢数据库启动时间,甚至可能导致 OOM 和崩溃循环

Prometheus、InfluxDB 和 Cassandra 已经使用了类似 LSM 的数据结构和 SSTables,因此它们可以快速切换到新方法。目前尚不清楚 TimescaleDB 是否可以使用新方法,因为它不使用 LSM。