注:书本链接:GitHub - Vonng/ddia: 《Designing Data-Intensive Application》DDIA中文翻译
注:译者文本网站:简介 · ddia-cn
注:如有侵权,请联系删除
第五章:复制
- 使得数据与用户在地理上接近(从而减少延迟)
- 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
- 伸缩可以接受读请求的机器数量(从而提高读取吞吐量)
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的 变更(change),这就是本章所要讲的。我们将讨论三种流行的变更复制算法:单领导者(single leader,单主),多领导者(multi leader,多主) 和 无领导者(leaderless,无主)。几乎所有分布式数据库都使用这三种方法之一。
一、领导者与追随者
存储了数据库拷贝的每个节点被称为 副本(replica) 。
最常见的解决方案被称为 基于领导者的复制(leader-based replication) (也称 主动/被动(active/passive) 复制或 主/从(master/slave) 复制)。
1、同步复制与异步复制
复制系统的一个重要细节是:复制是 同步(synchronously) 发生的还是 异步(asynchronously) 发生的。(在关系型数据库中这通常是一个配置项,其他系统则通常硬编码为其中一个)。
将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中 一个 从库是同步的,而其他的从库则是异步的。如果该同步从库变得不可用或缓慢,则将一个异步从库改为同步运行。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。这种配置有时也被称为 半同步(semi-synchronous)【7】。
2、设置新从库
- 在某个时刻获取主库的一致性快照(如果可能,不必锁定整个数据库)。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如用于 MySQL 的 innobackupex【12】。
- 将快照复制到新的从库节点。
- 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称,例如 PostgreSQL 将其称为 日志序列号(log sequence number,LSN),MySQL 将其称为 二进制日志坐标(binlog coordinates)。
- 当从库处理完快照之后积累的数据变更,我们就说它 赶上(caught up) 了主库,现在它可以继续及时处理主库产生的数据变化了。
3、处理宕机节点
(1)从库失效:追赶恢复
(2)主库失效:故障切换
节点故障、不可靠的网络、对副本一致性、持久性、可用性和延迟的权衡,这些问题实际上是分布式系统中的基本问题。
4、复制日志的实现
(1)基于语句的复制
(2)传输预写式日志(WAL)
(3)逻辑日志复制(基于行)
(4)基于触发器的复制
5、复制延迟问题
如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 最终一致性(eventual consistency)
(1)读自己写入的数据
如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了。
在这种情况下,我们需要 写后读一致性(read-after-write consistency),也称为 读己之写一致性(read-your-writes consistency)
(2)单调读
用户首先从新副本读取,然后从旧副本读取。时间看上去回退了。(时光倒流(moving backward in time))
单调读(monotonic reads)【23】可以保证这种异常不会发生。这是一个比 强一致性(strong consistency) 更弱,但比 最终一致性(eventual consistency) 更强的保证。
(3)一致前缀读
一致前缀读(consistent prefix reads)【23】。这个保证的意思是说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
这是 分区(partitioned) 或 分片(sharded) 数据库中的一个特殊问题,我们将在 第六章 中讨论分区数据库。如果数据库总是以相同的顺序应用写入,而读取总是看到一致的前缀,那么这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在 全局的写入顺序:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些则处于较新的状态。
注:此处与日常使用的kafka保障消息消费顺序性观念相近
(4)复制延迟的解决方案
如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库 “做了正确的事情”,那该多好呀。这就是 事务(transaction) 存在的原因:数据库通过事务提供强大的保证,所以应用程序可以更加简单。
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务,声称事务在性能和可用性上的代价太高,并断言在可伸缩系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。我们将在 第七章 和 第九章 回到事务的话题,并将在 第三部分 讨论一些替代机制。
4、多主复制
(1)多主复制的应用场景
运维多个数据中心
需要离线操作的客户端
协同编辑
(2)处理写入冲突
同步与异步冲突检测
避免冲突
-
收敛至一致的状态
实现冲突合并解决有多种途径:
- 给每个写入一个唯一的 ID(例如时间戳、长随机数、UUID 或者键和值的哈希),挑选最高 ID 的写入作为胜利者,并丢弃其他写入。如果使用时间戳,这种技术被称为 最后写入胜利(LWW, last write wins)。虽然这种方法很流行,但是很容易造成数据丢失【35】。我们将在本章末尾的 检测并发写入 一节更详细地讨论 LWW。
- 为每个副本分配一个唯一的 ID,ID 编号更高的写入具有更高的优先级。这种方法也意味着数据丢失。
- 以某种方式将这些值合并在一起 - 例如,按字母顺序排序,然后连接它们(在 图 5-7 中,合并的标题可能类似于 “B/C”)。
- 用一种可保留所有信息的显式数据结构来记录冲突,并编写解决冲突的应用程序代码(也许通过提示用户的方式)。
自定义冲突解决逻辑
什么是冲突?
(3)多主复制拓扑
复制拓扑(replication topology)用来描述写入操作从一个节点传播到另一个节点的通信路径。
5、无主复制
(1)当节点故障时写入数据库
-
读修复和反熵
在 Dynamo 风格的数据存储中经常使用两种机制:
-
读修复(Read repair)
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在 图 5-10 中,用户 2345 获得了来自副本 3 的版本 6 值和来自副本 1 和 2 的版本 7 值。客户端发现副本 3 具有陈旧值,并将新值写回到该副本。这种方法适用于读频繁的值。
-
反熵过程(Anti-entropy process)
此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显著的延迟。
-
读写的法定人数
(2)法定人数一致性的局限性
- 监控陈旧度
(3)宽松的法定人数与提示移交
- 运维多个数据中心
(4)检测并发写入
- 最后写入胜利(丢弃并发写入)
- “此前发生”的关系和并发(happen-before原则)
- 捕获"此前发生"关系(happen-before)
- 合并并发写入的值
- 版本向量
本章小结
在本章中,我们考察了复制的问题。复制可以用于几个目的:
-
高可用性
即使在一台机器(或多台机器,或整个数据中心)停机的情况下也能保持系统正常运行
-
断开连接的操作
允许应用程序在网络中断时继续工作
-
延迟
将数据放置在地理上距离用户较近的地方,以便用户能够更快地与其交互
-
可伸缩性
通过在副本上读,能够处理比单机更大的读取量
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(这还不包括更隐蔽的故障,例如由于软件错误导致的静默数据损坏)。
我们讨论了复制的三种主要方法:
-
单主复制
客户端将所有写入操作发送到单个节点(主库),该节点将数据更改事件流发送到其他副本(从库)。读取可以在任何副本上执行,但从库的读取结果可能是陈旧的。
-
多主复制
客户端将每个写入发送到几个主库节点之一,其中任何一个主库都可以接受写入。主库将数据更改事件流发送给彼此以及任何从库节点。
-
无主复制
客户端将每个写入发送到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
每种方法都有优点和缺点。单主复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点、网络中断和延迟峰值的情况下,多主复制和无主复制可以更加健壮,其代价是难以推理并且仅提供非常弱的一致性保证。
复制可以是同步的,也可以是异步的,这在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是要弄清楚在复制延迟增加和服务器故障时会发生什么,这一点很重要。如果主库失败后你将一个异步更新的从库提升为新的主库,那么最近提交的数据可能会丢失。
我们研究了一些可能由复制延迟引起的奇怪效应,我们也讨论了一些有助于决定应用程序在复制延迟时的行为的一致性模型:
-
写后读一致性
用户应该总是能看到自己提交的数据。
-
单调读
用户在看到某个时间点的数据后,他们不应该再看到该数据在更早时间点的情况。
-
一致前缀读
用户应该看到数据处于一种具有因果意义的状态:例如,按正确的顺序看到一个问题和对应的回答。
最后,我们讨论了多主复制和无主复制方法所固有的并发问题:因为他们允许多个写入并发发生,这可能会导致冲突。我们研究了一个数据库可以使用的算法来确定一个操作是否发生在另一个操作之前,或者它们是否并发发生。我们还谈到了通过合并并发更新来解决冲突的方法。
在下一章中,我们将继续考察数据分布在多台机器间的另一种不同于 复制 的形式:将大数据集分割成 分区。