内容简介:在预写日志(WAL)是数据库系统中一种常用的技术,用于保证写操作的通过在将更改应用于例如内存中表示之前,先将预期的改变写入WAL来提供持久性。通过首先写入WAL,如果数据库之后崩溃,我们将能够恢复改变并在必要时重新应用。
在 第1部分中
,我使用gRPC和 Go 编写了一个非常简单的服务器,该服务器用于服务 Get
和 Put
请求内存中的映射。如果服务器退出,它将丢失所有数据,对于数据库,我必须承认这是非常糟糕的。
我实现了预写日志记录,允许在服务器重新启动时恢复内存中状态。尽管这个想法真的很简单,但实现起来却是很困难的!最后,我看了 LevelDB
, Cassandra
和 etcd
如何解决此问题。
预写日志
预写日志(WAL)是数据库系统中一种常用的技术,用于保证写操作的 原子性 和 持久性 。WAL背后的关键思想是,在我们对数据库状态进行任何实际修改之前,我们必须首先记录我们希望是原子性的和持久存储(例如磁盘)的完整操作集。
通过在将更改应用于例如内存中表示之前,先将预期的改变写入WAL来提供持久性。通过首先写入WAL,如果数据库之后崩溃,我们将能够恢复改变并在必要时重新应用。
原子性更加微妙。假设一个改变需要改变 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结束了所有变更操作的关键路径,因为我们必须在进行变更之前执行预写日志的记录。
您可能会认为我们会在 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 文件系统写入的难度。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用 Clojure 编写 OpenWhisk 操作,第 1 部分: 使用 Lisp 方言为 OpenWhisk 编写简明的代码
- 如何使用 vue + typescript 编写页面 ( vuex装饰器部分 )
- [译] 被遗忘的面向对象编程史(软件编写)(第十六部分)
- .NET 编写一个可以异步等待循环中任何一个部分的 Awaiter
- 使用 Clojure 编写 OpenWhisk 操作,第 3 部分: 改进您的 OpenWhisk Clojure 应用程序
- 使用 Clojure 编写 OpenWhisk 操作,第 2 部分: 将 Clojure OpenWhisk 操作连接成有用的序列
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。