ch5 修正

This commit is contained in:
Vonng 2018-03-13 14:46:08 +08:00
parent a6df7e9fb6
commit e520097e7b
4 changed files with 84 additions and 84 deletions

View File

@ -15,6 +15,10 @@
## 译序
> 不懂数据库的全栈工程师不是好架构师
@ -92,18 +96,18 @@
| 章节 | 进度 |
| :--------------------------------: | :------: |
| 序言 | 初翻 |
| 第一部分:数据系统基础 ——概览 | **精翻** |
| 第一章:可靠性、可扩展性、可维护性 | **精翻** |
| 第一部分:数据系统基础 ——概览 | 精翻 |
| 第一章:可靠性、可扩展性、可维护性 | 精翻 |
| 第二章:数据模型与查询语言 | 初翻 |
| 第三章:存储与检索 | 初翻 |
| 第四章:编码与演化 | 初翻 |
| 第二部分:分布式数据——概览 | 精翻 |
| 第五章:复制 | 初翻 |
| 第五章:复制 | 精翻 30% |
| 第六章:分片 | 初翻 |
| 第七章:事务 | 初翻 |
| 第七章:事务 | 精翻 60% |
| 第八章:分布式系统中的问题 | 初翻 |
| 第九章:一致性与共识 | 机翻 |
| 第三部分:前言 | **精翻** |
| 第九章:一致性与共识 | 初翻30% |
| 第三部分:前言 | 精翻 |
| 第十章:批处理 | 机翻 |
| 第十一章:流处理 | 机翻 |
| 第十二章:数据系统的未来 | 机翻 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -10,105 +10,101 @@
[TOC]
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在第二部分的介绍中所讨论的那样,有几种想要复制数据的原因:
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在[第二部分简介](part-ii.md)中所讨论的那样,我们希望能复制数据,可能出于各种各样的原因:
* 保持数据在地理位置上接近用户(从而减少延迟)
* 即使部分零件出现故障,系统也能继续工作(从而提高可用性)
* 扩可以接受读请求的机器数量(从而提高读取吞吐量)
* 使得数据与用户在地理上接近(从而减少延迟)
* 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
* 扩可以接受读请求的机器数量(从而提高读取吞吐量)
本章将假设的数据集非常小,每台机器都可以保存整个数据集的副本。在[第6章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
本章将假设的数据集非常小,每台机器都可以保存整个数据集的副本。在[第6章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
如果正在复制的数据不会随时间而改变,那么复制太简单了:将数据复制到每个节点一次就完事了。复制的困难之处在于应对复制数据的**变化**,这就是本章的内容。我们将讨论三种流行的变化复制算法:**单主single leader****多主multi leader**和**无主leaderless**。几乎所有分布式数据库都使用这三种方法之一。
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?。这些通常是数据库中的配置项,细节因数据库而异,但原理在许多不同的实现中都是相似的。本章将讨论这些决策的后果。
数据库的复制是一个老话题 ——自1970年代研究以来这些原则并没有太大的改变【1】因为网络的基本约束保持不变。然而在研究之外许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近的事。由于许多程序员都是这方面的新手对**最终一致性eventual consistency**等问题存在诸多误解。在“复制滞后问题”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写read-your-writes**和**单调读monotonic read**保证等内容。
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的**变更change**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者single leader****多领导者multi leader**和**无领导者leaderless**。几乎所有分布式数据库都使用这三种方法之一。
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
数据库的复制算得上是老生常谈了 ——70年代研究得出的基本原则至今没有太大变化【1】因为网络的基本约束仍保持不变。然而在研究之外许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手因此对于诸如**最终一致性eventual consistency**等问题存在许多误解。在“[复制延迟问题](#复制延迟问题)”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写read-your-writes**和**单调读monotonic read**保证等内容。
## 领导者与追随者
存储数据库副本的每个节点称为**副本replica**。当有多个副本时就会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
存储数据库副本的每个节点称为**副本replica**。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
每一次向数据库的写入都需要传播到所有副本上,否则副本包含的数据就会不一样。最常见的解决方案称为**基于领导者的复制leader-based replication**(也称**主动/被动 active/passive** 或 **主/从 master/slave**复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案称为**基于领导者的复制leader-based replication**(也称**主动/被动active/passive** 或 **主/从master/slave**复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
1. 副本之一被指定为**领导者leader**,也称为 **主master** **首要primary**。当客户端要写入数据库时,它们必须将请求发送给**领导者**,首先将新数据写入领导者的本地存储。
2. 其他副本被称为**追随者followers**,亦称为**只读副本read replicas****从slaves****次要( sencondaries****热备hot-standby**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志replication log**记录或**变更流change stream**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
1. 副本之一被指定为**领导者leader**,也称为 **主master** **首要primary**。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
2. 其他副本被称为**追随者followers**,亦称为**只读副本read replicas****从slaves****次要( sencondaries****热备hot-standby**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志replication log**记录或**变更流change stream**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
[^i]: 不同的人对**热hot****温warn****冷cold** 备份服务器有不同的定义。 例如在PostgreSQL中**热备hot standby**指的是能接受客户端读请求的副本。而**温备warm standby**只是追随领导者,但不处理客户端的任何查询。 就本书而言差异并不重要。
[^i]: 不同的人对**热hot****温warn****冷cold** 备份服务器有不同的定义。 例如在PostgreSQL中**热备hot standby**指的是能接受客户端读请求的副本。而**温备warm standby**只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
![](img/fig5-1.png)
**图5-1 基于领导者(主-从)的复制**
这种复制模式是许多关系数据库的内置功能如PostgreSQL从9.0版本开始MySQLOracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也用于一些非关系数据库包括MongoDBRethinkDB和Espresso [4]。 最后基于领导者的复制并不仅限于数据库像Kafka [5]和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统例如DRBD这样的块复制设备也与之类似。
这种复制模式是许多关系数据库的内置功能如PostgreSQL从9.0版本开始MySQLOracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也被用于一些非关系数据库包括MongoDBRethinkDB和Espresso 【4】。 最后基于领导者的复制并不仅限于数据库像Kafka 【5】和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统例如DRBD这样的块复制设备也与之类似。
### 同步复制与异步复制
复制系统的一个重要细节是复制是**同步synchronously**发生还是**异步asynchronously**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
复制系统的一个重要细节是复制是**同步synchronously**发生还是**异步asynchronously**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
想[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人资料图像。在某个时间点,客户发送更新请求给领导者;不久之后领导者就收到了。在某个时候,领导者将数据转换转发给追随者。最后,领导通知客户更新成功。
象[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时刻,主库又会将数据变更转发给自己的从库。最后,主库通知客户更新成功。
[图5-2](img/fig5-2.png)显示了系统各个组件之间的通信:用户的客户端,领导者和两个关注者。时间从左到右流动。请求或响应消息显示为粗箭头
[图5-2](img/fig5-2.png)显示了系统各个组件之间的通信:用户客户端,主库和两个从库。时间从左到右流动。请求或响应消息用粗箭头表示
![](img/fig5-2.png)
**图5-2 基于领导者的复制:一个同步从库和一个异步从库**
在[图5-2]()的示例中,跟随者1的复制是同步的领导者等待直到跟随者1确认在向用户报告成功之前以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的领导者发送消息,但不等待跟随者的响应。
在[图5-2]()的示例中,从库1的复制是同步的在向用户报告写入成功并使结果对其他用户可见之前主库需要等待从库1的确认确保从库1已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的主库发送消息,但不等待从库的响应。
该图表明在跟随者2处理消息之前存在显着的延迟。通常情况下复制速度非常快大多数数据库系统在不到一秒的时间内对关注者进行更改。但是不能保证需要多长时间。有些情况下追随者可能落后于领导者几分钟或更长时间例如如果跟随者正在从故障中恢复如果系统正在接近最大容量或者如果节点之间存在网络问题。
在这幅图中从库2处理消息前存在一个显着的延迟。通常情况下复制的速度相当快大多数数据库系统能在一秒向从库应用变更但它们不能提供复制用时的保证。有些情况下从库可能落后主库几分钟或更久例如从库正在从故障中恢复系统在最大容量附近运行或者如果节点间存在网络问题。
同步复制的优点是,跟随者保证有与领导者一致的最新数据副本。如果领导者突然失败,我们可以肯定的是,这些数据仍然可以在追随者身上找到。缺点是如果同步跟随器没有响应(因为它已经崩溃,或者出现网络故障,或者出于任何其他原因),写入不能被处理。领导者必须阻止所有写入,并等待同步副本再次可用。
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为**半同步semi-synchronous**【7】。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为**半同步semi-synchronous**【7】。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果领导失败并且不可恢复,则任何尚未复制给追随者的写入都将丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久Durable**。 然而,一个完全异步的配置也有优点:即使所有的追随者都落后了,领导者也可以继续处理写入。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久Durable**。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
弱化的持久性可能听起来像是一个坏的折衷,无论如何,异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“复制延迟”中回到这个问题。
弱化的持久性可能听起来像是一个坏的折衷,无论如何,异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“[复制延迟问题](#复制延迟问题)”中回到这个问题。
> ### 关于复制的研究
>
> 如果主库故障,那么异步复制系统会丢失数据可能是一个严重的问题,因此研究人员继续研究不丢失数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**[8,9]是同步复制的一种变体已经在一些系统如Microsoft Azure存储【10,11】中成功实现。
> 对于异步复制系统而言,主库故障时有可能丢失数据。这可能是一个严重的问题,因此研究人员仍在研究不丢数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**【8,9】]是同步复制的一种变体已经在一些系统如Microsoft Azure存储【10,11】中成功实现。
>
> 复制的一致性与**共识consensus**使几个节点就某个值达成一致之间有着密切的联系第9章将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
> 复制的一致性与**共识consensus**(使几个节点就某个值达成一致)之间有着密切的联系,[第9章](ch9.md)将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
>
### 设置新追随者
### 设置新从库
有时候需要设置一个新的追随者:也许是为了增加副本的数量,或替换失败的节点。如何确保新的追随者有领导者数据的准确副本?
有时候需要设置一个新的从库:也许是为了增加副本的数量,或替换失败的节点。如何确保新的从库拥有主库数据的精确副本?
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。结果可能没有任何意义。
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。复制的结果可能没有任何意义。
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违反高可用的目标。幸运的是,拉起一个从库通常不需要停机。从概念上讲,过程如下所示:
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违背高可用的目标。幸运的是,拉起新的从库通常并不需要停机。从概念上讲,过程如下所示:
1. 在某个时间点对领导者的数据库取一个一致的快照(如果可能的话),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。在某些情况下需要第三方工具如MySQL的*innobackupex* 【12】。
2. 将快照复制到新的跟随者节点。
3. 追随者连接到领导者,并请求快照后发生的所有数据更改。这要求快照与领导者的复制日志中的精确位置关联。该位置有不同的名称例如PostgreSQL将其称为**日志序列号lsn, log sequence number**MySQL将其称为**二进制日志坐标binlog coordinates**。
4. 当追随者处理完快照之后积压的数据变化,我们说它**赶上caught up**了主库。现在它可以继续处理领导者产生的数据变化了。
1. 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景可能需要第三方工具例如MySQL的innobackupex 【12】。
2. 将快照复制到新的从库节点。
3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称例如PostgreSQL将其称为**日志序列号log sequence number, LSN**MySQL将其称为**二进制日志坐标binlog coordinates**。
4. 当从库处理完快照之后积压的数据变更,我们说它**赶上caught up**了主库。现在它可以继续处理主库产生的数据变化了。
建立跟随者的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个有点神秘的多步骤工作流程,需要由管理员手动执行
建立从库的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个需要由管理员手动执行的,有点神秘的多步骤工作流。
### 处理节点宕机
系统中的任何节点都可能停机,也许因为意外的故障,也可能是计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。
系统中的任何节点都可能宕机,可能因为意外的故障,也可能由于计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。
如何通过基于领导者的复制实现高可用?
如何通过基于主库的复制实现高可用?
#### 从库失效:追赶恢复
在其本地磁盘上,每个追随者记录从领导者收到的数据变更。如果跟随者崩溃并重新启动,或者如果领导者和追随者之间的网络暂时中断,则追随者可以很容易地恢复:从日志中知道在发生故障之前处理的最后一个事务。因此,追随者可以连接到领导者并请求在追随者断开连接时发生的所有数据改变。当它应用了这些变化后,它已经赶上了领导者,并可以像以前一样继续接收数据流的变化
在其本地磁盘上,每个从库记录从主库收到的数据变更。如果从库崩溃并重新启动,或者,如果主库和从库之间的网络暂时中断,则比较容易恢复:从库可以从日志中知道,在发生故障之前处理的最后一个事务。因此,从库可以连接到主库,并请求在从库断开连接时发生的所有数据变更。当应用完所有这些变化后,它就赶上了主库,并可以像以前一样继续接收数据变更流
#### 主库失效:故障转移
处理领导者的失败相当棘手:其中一个追随者需要被提升为新的领导者,需要重新配置客户端,以将它们的写操作发送给新的领导者,其他追随者需要开始拉取来自新领导者的数据变更。这个过程被称为**故障转移failover**。
主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为**故障转移failover**。
故障转移可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的领导者)或自动进行。自动故障转移过程通常由以下步骤组成:
故障转移可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障转移过程通常由以下步骤组成:
1. 确认领导者失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是使用**超时Timeout**:节点相互之间频繁地来回传递消息并且如果一个节点在一段时间内例如30秒没有响应就认为它挂了计划内故意关闭领导者不算)
2. 选择一个新的领导者。这可以通过选举过程(领导者由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点controller node**来指定新的领导者。领导者的最佳人选通常是拥有旧领导者最新数据副本的追随者(最小化数据损失)。让所有的节点同意一个新的领导是一个**共识**问题,在[第9章](ch9.md)详细讨论。
3. 重新配置系统以使用新的领导者。客户端现在需要将他们的写请求发送给新领导者(将在“请求路由”中讨论这个问题)。如果老领导回来,可能仍然认为自己是领导者,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个追随者
1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用**超时Timeout**:节点频繁地相互来回传递消息并且如果一个节点在一段时间内例如30秒没有响应就认为它挂了因为计划内维护而故意关闭主库不算)。
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点controller node**来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,在[第9章](ch9.md)详细讨论。
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在“[请求路由](ch6.md#请求路由)”中讨论这个问题)。如果老领导回来,可能仍然认为自己是主库,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个从库
故障转移会出现很多大麻烦:
@ -116,48 +112,48 @@
* 如果数据库需要和其他外部存储相协调那么丢弃写入内容是极其危险的操作。例如在GitHub 【13】的一场事故中一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键因为新主库的计数器落后于老主库的计数器所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用主键重用使得MySQL和Redis中数据产生不一致最后导致一些私有数据泄漏到错误的用户手中。
* 发生某些故障时(见[第8章](ch8.md))可能会出现两个节点都以为自己是领导者的情况。这种情况称为**脑裂(split brain)**非常危险如果两个领导者都可以接受写操作却没有冲突解决机制参见第168页的“多主复制”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个领导节点同时存在时会关闭其中一个节点[^ii]但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
* 发生某些故障时(见[第8章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为**脑裂(split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(参见“[多领导者复制](#多领导者复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点[^ii]但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
[^ii]: 这种机制称为**击剑fencing**,充满感情的术语是:***爆彼之头STONITHShoot The Other Node In The Head***。
[^ii]: 这种机制称为**屏蔽fencing**,充满感情的术语是:**爆彼之头Shoot The Other Node In The Head, STONITH**。
* 领导者被宣布死亡之前的正确超时应该怎么设?在领导者失效的情况下,超时时间越长,意味着恢复时间也越长。但是如果超时太短,又可能会出现不必要的故障转移。例如,临时负载峰值可能导致节点的响应时间超时,或网络故障可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。
* 主库被宣告死亡之前的正确超时应该怎么配置?在主库失效的情况下,超时时间越长,意味着恢复时间也越长。但是如果超时设置太短,又可能会出现不必要的故障转移。例如,临时负载峰值可能导致节点的响应时间超时,或网络故障可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。
这些问题没有简单的解决方案。因此即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障转移。
这些问题没有简单的解决方案。因此即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障转移。
节点故障;不可靠的网络;对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第8章](ch8.md)和[第9章](ch9.md)将更深入地讨论它们。
节点故障、不可靠的网络、对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第8章](ch8.md)和[第9章](ch9.md)将更深入地讨论它们。
### 复制日志的实现
基于领导者的复制底层是如何工作的?实践中有好几种不同的复制方式,所以先简要地看一下。
基于主库的复制底层是如何工作的?实践中有好几种不同的复制方式,所以先简要地看一下。
#### 基于语句的复制
在最简单的情况下,领导者记录它执行的每个写入请求(**语句statement**)并将该语句日志发送给其追随者。对于关系数据库来说,这意味着每个`INSERT``UPDATE`或`DELETE`语句都被转发给关注者和每个关注者。跟随者解析并执行该SQL语句就像从客户端收到一样。
在最简单的情况下,主库记录下它执行的每个写入请求(**语句statement**)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个`INSERT``UPDATE`或`DELETE`语句都被转发给每个从库每个从库解析并执行该SQL语句就像从客户端收到一样。
虽然听上去很合理,但有很多问题会打破这种复制方式:
虽然听上去很合理,但有很多问题会搞砸这种复制方式:
* 任何调用**非确定性函数nondeterministic**的语句(如`NOW()`获取当前日期和时间或`RAND()`获取一个随机数)可能会在每个副本上生成不同的值
* 如果语句使用**自增列auto increment**,或者它们依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会有不同的效果。当有多个同时执行的事务时,这可能产生很大的限制。
* 任何调用**非确定性函数nondeterministic**的语句,可能会在每个副本上生成不同的值。例如,使用`NOW()`获取当前日期时间,或使用`RAND()`获取一个随机数
* 如果语句使用**自增列auto increment**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
* 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
有办法绕开问题 ——例如,当语句被记录时,领导者可以用固定的返回值替换任何不确定的函数调用,以便追随者获得相同的值。但是由于边缘情况如此之多,所以现在通常选择其他的复制方法。
的确有办法绕开这些问题 ——例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
基于语句的复制在5.1版本前的MySQL中使用。现在也还在一些地方用因为它非常紧凑。但默认情况下如果语句中有任何不确定性MySQL现在会切换到基于行的复制稍后讨论。 VoltDB使用基于语句的复制通过要求事务是确定性来保证安全[15]
基于语句的复制在5.1版本前的MySQL中使用。因为它相当紧凑现在有时候也还在用。但现在在默认情况下如果语句中存在任何不确定性MySQL会切换到基于行的复制稍后讨论。 VoltDB使用了基于语句的复制但要求事务必须是确定性的以此来保证安全【15】
#### 传输预写式日志WAL
在[第3章](ch3.md)中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现通常每个写操作都附加到日志中:
在[第3章](ch3.md)中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现,通常写操作都是追加到日志中:
* 对于日志结构存储引擎(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
* 对于覆写单个磁盘块的B树请参阅第79页的“B树”,每次修改都会先写入**预写式日志WAL, Write Ahead Log**,以便崩溃后索引可以恢复到一个一致的状态。
* 对于覆写单个磁盘块的[B树](ch3.md#B树),每次修改都会先写入**预写式日志Write Ahead Log, WAL**,以便崩溃后索引可以恢复到一个一致的状态。
在任何一种情况下,日志都是包含所有数据库写入的追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,领导者还可以通过网络将其发送给其追随者
在任何一种情况下,日志都是包含所有数据库写入的追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主库还可以通过网络将其发送给其从库
追随者应用这个日志时,它会建立和领导者一模一样数据结构的副本。
从库应用这个日志时,它会建立和主库一模一样数据结构的副本。
PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本通常不可能在领导者和追随者上运行不同版本的数据库软件。
PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本通常不可能在主库和从库上运行不同版本的数据库软件。
看上去这可能只是一个微小的实现细节,但却可能对运维产生巨大的影响。如果复制协议允许跟随者使用比领导者更新的软件版本,则可以先执行升级跟随者,然后执行故障转移,使升级后的节点之一成为新的领导者从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配传输WAL经常出现这种情况则此类升级需要停机。
看上去这可能只是一个微小的实现细节,但却可能对运维产生巨大的影响。如果复制协议允许从库使用比主库更新的软件版本,则可以先升级从库,然后执行故障转移,使升级后的节点之一成为新的主库从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配传输WAL经常出现这种情况则此类升级需要停机。
#### 逻辑日志复制(基于行)
@ -179,7 +175,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(参阅“[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
一些工具如Oracle GoldenGate 【19】可以通过读取数据库日志使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能触发器和存储过程。
一些工具如Oracle Golden Gate 【19】可以通过读取数据库日志使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能触发器和存储过程。
触发器允许您注册在数据库系统中发生数据更改写入事务时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中使用外部程序读取这个表再加上任何业务逻辑处理会后将数据变更复制到另一个系统去。例如Databus for Oracle 【20】和Bucardo for Postgres 【21】就是这样工作的。
@ -189,17 +185,17 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
## 复制延迟问题
容忍节点故障只是需要复制的一个原因。正如在第二部分的介绍中提到的,另一个原因是可扩展性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更近用户)。
容忍节点故障只是需要复制的一个原因。正如在[第二部分](part-ii.md)的介绍中提到的,另一个原因是可扩展性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更近用户)。
基于领导者的复制要求所有写入都由一个节点处理但只读查询可以由任何副本处理。所以对于读多写少Web上的常见模式的场景一个有吸引力的选择是创建很多追随者并将读取请求分散到所有的从库上去。这样能减小主库的负载,并允许向最近的副本发送读请求。
基于主库的复制要求所有写入都由单个节点处理但只读查询可以由任何副本处理。所以对于读多写少的场景Web上的常见模式一个有吸引力的选择是创建很多从库并将读请求分散到所有的从库上去。这样能减小主库的负载,并允许向最近的副本发送读请求。
在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制 - 如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制——如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
不幸的是,当应用程序从异步追随者读取时,如果追随者落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对领导者和跟随者执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在跟随者中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,追随者最终会赶上并与领导者保持一致。出于这个原因,这种效应被称为**最终一致性eventually consistency**[^iii]【22,23】
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为**最终一致性eventually consistency**[^iii]【22,23】
[^iii]: 道格拉斯·特里Douglas Terry等人创造了术语最终一致性。 【24】 并经由Werner Vogels 【22】推广成为许多NoSQL项目的战吼。 然而不只有NoSQL数据库是最终一致的关系型数据库中的异步复制追随者也有相同的特性。
“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟replication lag**,即写入主库到反映至追随者之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟replication lag**,即写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
因为滞后时间太长引入的不一致性,可不仅是一个理论问题,更是应用设计中会遇到的真实问题。本节将重点介绍三个由复制延迟问题的例子,并简述解决这些问题的一些方法。
@ -213,7 +209,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
**图5-3 用户写入后从旧副本中读取数据。需要写后读(read-after-write)的一致性来防止这种异常**
在这种情况下,我们需要**读写一致性read-after-write consistency**,也称为**读己之写一致性read-your-writes consistency**[24]。这是一个保证,如果用户重新加载页面,他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺:其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
在这种情况下,我们需要**读写一致性read-after-write consistency**,也称为**读己之写一致性read-your-writes consistency**【24】。这是一个保证,如果用户重新加载页面,他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺:其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
如何在基于领导者的复制系统中实现读后一致性?有各种可能的技术,这里说一些:
@ -505,7 +501,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
在Dynamo风格的数据存储中经常使用两种机制
* ***读修复Read repair***
***读修复Read repair***
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在[图5-10](img/fig5-10.png)中用户2345获得了来自Replica 3的版本6值和来自副本1和2的版本7值。客户端发现副本3具有陈旧值并将新值写回复制品。这种方法适用于频繁阅读的值。
@ -563,7 +559,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
* 如果写操作在某些副本上成功而在其他节点上失败例如因为某些节点上的磁盘已满在小于w个副本上写入成功。所以整体判定写入失败但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败后续的读取仍然可能会读取这次失败写入的值【47】。
* 如果携带新值的节点失败需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复则存储新值的副本数可能会低于w从而打破法定人数条件。
* 即使一切工作正常,有时也会不幸地出现关于**时序timing**的边缘情况在这种情况下您可能会感到不安因为我们将在第334页上的“[线性化和法定人数]()”中看到。
* 即使一切工作正常,有时也会不幸地出现关于**时序timing**的边缘情况在这种情况下您可能会感到不安因为我们将在第334页上的“[线性化和法定人数](ch9.md#线性化和法定人数)”中看到。
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率但把它们当成绝对的保证是不明智的。
@ -630,9 +626,9 @@ Dynamo风格的数据库允许多个客户端同时写入相同的Key这意
#### 最后写入为准(丢弃并发写入)
实现最终融合的一种方法是声明每个副本只需要存储最***“最近”***的值,并允许***“更旧”***的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
实现最终融合的一种方法是声明每个副本只需要存储最**“最近”**的值,并允许**“更旧”**的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
正如**“最近”**的引号所表明的这个想法其实颇具误导性。在图5-12的例子中当客户端向数据库节点发送写入请求时客户端都不知道另一个客户端因此不清楚哪一个先发生了。事实上说“发生”是没有意义的我们说写入是**并发concurrent**的,所以它们的顺序是不确定的。
正如**“最近”**的引号所表明的,这个想法其实颇具误导性。在[图5-12](img/fig5-12.png)的例子中,当客户端向数据库节点发送写入请求时,客户端都不知道另一个客户端,因此不清楚哪一个先发生了。事实上,说“发生”是没有意义的:我们说写入是**并发concurrent**的,所以它们的顺序是不确定的。
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最**“最近”**的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为**最后写入为准LWW, last write wins**是Cassandra 【53】唯一支持的冲突解决方法也是Riak 【35】中的一个可选特征。
@ -640,7 +636,7 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
有一些情况如缓存其中丢失的写入可能是可以接受的。如果丢失数据不可接受LWW是解决冲突的一个很烂的选择。
与LWW一起使用数据库的唯一安全方法是确保一个Key只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如推荐使用Cassandra的方法是使用UUID作为键从而为每个写操作提供一个唯一的键【53】。
与LWW一起使用数据库的唯一安全方法是确保一个只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如推荐使用Cassandra的方法是使用UUID作为键从而为每个写操作提供一个唯一的键【53】。
#### “此前发生”的关系和并发
@ -722,7 +718,7 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
> #### 版本向量和向量时钟
>
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节【576061】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
>
## 本章小结

View File

@ -28,7 +28,7 @@
## 章节概述
我们将从[第十章](ch10.md)开始研究例如MapReduce这样**面向批处理batch-oriented**的数据流系统。对于建设大规模数据系统,我们将看到,它们提供了优秀的工具和思想。[第十一章](ch11.md)将把这些思想应用到**流式数据data streams**中,使我们能用更低的延迟完成同样的任务。[第十二章](ch12.md)将对本书进行总结,探讨如何使用这些工具来构建可靠,可扩展和可维护的应用。
我们将从[第十章](ch10.md)开始研究例如MapReduce这样**面向批处理batch-oriented**的数据流系统。对于建设大规模数据系统,我们将看到,它们提供了优秀的工具和思想。[第十一章](ch11.md)将把这些思想应用到**流式数据data streams**中,使我们能用更低的延迟完成同样的任务。[第十二章](ch12.md)将对本书进行总结,探讨如何使用这些工具来构建可靠,可扩展和可维护的应用。
## 索引