编写数据库:第2部分-预写日志

与往常同样,请访问github.com/danchia/ddb…代码html

因此,您的数据不是很耐用...

第1部分中,我使用gRPC和Go编写了一个很是简单的服务器,该服务器用于服务GetPut请求内存中的映射。若是服务器退出,它将丢失全部数据,对于数据库,我必须认可这是很是糟糕的。 我实现了预写日志记录,容许在服务器从新启动时恢复内存中状态。尽管这个想法真的很简单,但实现起来倒是很困难的!最后,我看了 LevelDB , Cassandra 和 etcd 如何解决此问题。node

预写日志

预写日志(WAL)是数据库系统中一种经常使用的技术,用于保证写操做的原子性持久性。WAL背后的关键思想是,在咱们对数据库状态进行任何实际修改以前,咱们必须首先记录咱们但愿是原子性的和持久存储(例如磁盘)的完整操做集。git

经过在将更改应用于例如内存中表示以前,先将预期的改变写入WAL来提供持久性。经过首先写入WAL,若是数据库以后崩溃,咱们将可以恢复改变并在必要时从新应用。github

原子性更加微妙。假设一个改变须要改变ABC发生,可是咱们的应用没有办法一下应用全部的改变。咱们能够先记日志算法

intending to apply A
intending to apply B
intending to apply C
复制代码

而后才开始制做实际的应用程序。若是服务器中途崩溃,咱们能够查看日志并查看可能须要重作的操做。数据库

在DDB中,WAL是记录 append-only 的文件:缓存

record:
 length: uint32      // length of data section
 checksum: uint32    // CRC32 checksum of data
 data: byte[length]  // serialized ddb.internal.LogRecord proto 
复制代码

因为序列化原型不是自我描述的,所以咱们须要一个 length 字段来知道data有效载荷的大小。此外,为了防止各类形式的损坏(和错误!),咱们提供了数据的 CRC32 校验和。安全

性能相当重要,可是磁盘速度很慢

一般,WAL结束了全部变动操做的关键路径,由于咱们必须在进行变动以前执行预写日志的记录。bash

您可能会认为咱们会在File.Write调用返回后继续前进,可是因为操做系统缓存,一般状况并不是如此。服务器

我将在这里以Linux为例。 __buffer cache. 这些缓存有助于提升性能,由于应用程序常常读取它们最近写的内容,并且应用程序并不老是按顺序读取或写入。

Linux一般以写回模式(write-back)运行,在该模式下,缓冲区缓存仅按期(约30秒)刷新到磁盘。 File.Write``fsync()

写了一个快速的基准来判断个人WAL的性能。该测试重复记录100字节或1KB的记录,每n次调用一次fsync()。这些测试在装有本地SSD的Windows 10计算机上运行。

基准测试

DDB WAL Benchmarks
BenchmarkAppend100NoSync             529 ns/op         200.23 MB/s
BenchmarkAppend100NoBatch         879939 ns/op           0.12 MB/s
BenchmarkAppend100Batch10          88587 ns/op           1.20 MB/s
BenchmarkAppend100Batch100          9727 ns/op          10.90 MB/s
BenchmarkAppend1000NoSync           2213 ns/op         455.45 MB/s
BenchmarkAppend1000NoBatch        906057 ns/op           1.11 MB/s
BenchmarkAppend1000Batch10         94318 ns/op          10.69 MB/s
BenchmarkAppend1000Batch100        14384 ns/op          70.08 MB/s
复制代码

绝不奇怪,fsync()它很慢!100字节的日志条目没有同步须要529ns,同步须要880us。880us会将咱们限制在〜1.1k QPS。对于普通的HDD,可能会更糟,由于磁盘寻道可能要花费咱们10毫秒左右的时间。对于HDD来讲,仅将专用驱动器用于WAL以减小寻道时间并很多见。

为了理智地检查个人结果,我运行了etcd的WAL基准测试。

etcd WAL Benchmarks
BenchmarkWrite100EntryWithoutBatch    868817 ns/op           0.12 MB/s 
BenchmarkWrite100EntryBatch10         79937 ns/op           1.35 MB/s
BenchmarkWrite100EntryBatch100        9512 ns/op          11.35 MB/s
BenchmarkWrite1000EntryWithoutBatch   875304 ns/op           1.15 MB/s 
BenchmarkWrite1000EntryBatch10        84618 ns/op          11.92 MB/s
BenchmarkWrite1000EntryBatch100       12380 ns/op          81.50 MB/sk
复制代码

etcd的单个100字节写操做为869ns,因此我很是接近!他们的大批产品的性能要好一些,但这并不奇怪,由于他们的实现获得了更优化。我怀疑若是我要测量等待时间直方图,它们的性能可能会缩短尾部等待时间。

同步仍是不一样步?

鉴于同步是如此昂贵,其余数据库又是怎么作的呢?

  • LevelDB 实际上默认为不一样步。他们声称非同步写入一般能够安全地使用,而且用户应该在但愿进行同步时进行选择。
  • Cassandra 默认为每10秒进行一次按期同步。写入将被放置到OS文件缓冲区中后被确认。
  • etcd 对因而否同步有一些逻辑,但最好的办法是告诉用户写操做最终会致使同步。

我如今决定改正正确性并始终保持同步。我要寻找的一种潜在优化是尝试批量更新WAL,从而分摊同步成本。

其余的问题/我没有作的事情

大多数WAL实现将其日志记录在段中。段达到必定大小后,WAL将开始一个新段。一旦再也不须要日志的较早部分,这将很容易截断它们。 处理多个文件,或者其实是通常的文件系统,可能会很棘手。特别是,就像使用编译器和内存同样,操做系统一般能够自由地将操做从新排序到磁盘,而且许多文件操做不是原子的。诸如写入临时文件而后将其重命名为最终位置以进行原子文件写入之类的技术很常见。对此,你能够检查出GitHub上另外一个项目 issue 来了解 ACID 文件系统写入的难度。

下一次:共识

我但愿接下来经过共识算法(例如Raft)进行复制。

若是您喜欢阅读本文,请从新分享并经过任何推文(@DanielChiaJH)发给我!

译自:medium.com/@daniel.chi…

本文由博客一文多发平台 OpenWrite 发布!

相关文章
相关标签/搜索