与往常同样,请访问github.com/danchia/ddb…代码html
在第1部分中,我使用gRPC和Go编写了一个很是简单的服务器,该服务器用于服务Get
和Put
请求内存中的映射。若是服务器退出,它将丢失全部数据,对于数据库,我必须认可这是很是糟糕的。 我实现了预写日志记录,容许在服务器从新启动时恢复内存中状态。尽管这个想法真的很简单,但实现起来倒是很困难的!最后,我看了 LevelDB
, Cassandra
和 etcd
如何解决此问题。node
预写日志(WAL)是数据库系统中一种经常使用的技术,用于保证写操做的原子性和持久性。WAL背后的关键思想是,在咱们对数据库状态进行任何实际修改以前,咱们必须首先记录咱们但愿是原子性的和持久存储(例如磁盘)的完整操做集。git
经过在将更改应用于例如内存中表示以前,先将预期的改变写入WAL来提供持久性。经过首先写入WAL,若是数据库以后崩溃,咱们将可以恢复改变并在必要时从新应用。github
原子性更加微妙。假设一个改变须要改变A
,B
而C
发生,可是咱们的应用没有办法一下应用全部的改变。咱们能够先记日志算法
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)发给我!
本文由博客一文多发平台 OpenWrite 发布!