mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
update translations in ch5.md
This commit is contained in:
parent
aaf62c6ffb
commit
7650ae9ceb
118
ch5.md
118
ch5.md
@ -18,175 +18,175 @@
|
|||||||
|
|
||||||
本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在 [第六章](ch6.md) 中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
|
本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在 [第六章](ch6.md) 中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
|
||||||
|
|
||||||
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的 **变更(change)**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者(single leader)**,**多领导者(multi leader)** 和 **无领导者(leaderless)**。几乎所有分布式数据库都使用这三种方法之一。
|
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的 **变更(change)**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者(single leader,单主)**,**多领导者(multi leader,多主)** 和 **无领导者(leaderless,无主)**。几乎所有分布式数据库都使用这三种方法之一。
|
||||||
|
|
||||||
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
|
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
|
||||||
|
|
||||||
数据库的复制算得上是老生常谈了 ——70 年代研究得出的基本原则至今没有太大变化【1】,因为网络的基本约束仍保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手,因此对于诸如 **最终一致性(eventual consistency)** 等问题存在许多误解。在 “[复制延迟问题](#复制延迟问题)” 一节,我们将更加精确地了解最终的一致性,并讨论诸如 **读己之写(read-your-writes)** 和 **单调读(monotonic read)** 保证等内容。
|
数据库的复制算得上是老生常谈了 ——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)** 。当客户端要向数据库写入时,它必须将请求发送给 **领导者**,领导者会将新数据写入其本地存储。
|
1. 其中一个副本被指定为 **领导者(leader)**,也称为 **主库(master|primary)** 。当客户端要向数据库写入时,它必须将请求发送给该 **领导者**,其会将新数据写入其本地存储。
|
||||||
2. 其他副本被称为 **追随者(followers)**,亦称为 **只读副本(read replicas)**,**从库(slaves)**,**备库( secondaries)**,**热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为 **复制日志(replication log)** 记录或 **变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
|
2. 其他副本被称为 **追随者(followers)**,亦称为 **只读副本(read replicas)**、**从库(slaves)**、**备库( secondaries)** 或 **热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为 **复制日志(replication log)** 或 **变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照与领导者相同的处理顺序来进行所有写入。
|
||||||
3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
|
3. 当客户想要从数据库中读取数据时,它可以向领导者或任一追随者进行查询。但只有领导者才能接受写入操作(从客户端的角度来看从库都是只读的)。
|
||||||
|
|
||||||
[^i]: 不同的人对 **热(hot)**,**温(warm)**,**冷(cold)** 备份服务器有不同的定义。 例如在 PostgreSQL 中,**热备(hot standby)** 指的是能接受客户端读请求的副本。而 **温备(warm standby)** 只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
|
[^i]: 不同的人对 **热(hot)**、**温(warm)** 和 **冷(cold)** 备份服务器有不同的定义。例如在 PostgreSQL 中,**热备(hot standby)** 指的是能接受客户端读请求的副本。而 **温备(warm standby)** 只是追随领导者,但不处理客户端的任何查询。就本书而言,这些差异并不重要。
|
||||||
|
|
||||||
![](img/fig5-1.png)
|
![](img/fig5-1.png)
|
||||||
**图 5-1 基于领导者 (主 - 从) 的复制**
|
**图 5-1 基于领导者的(主/从)复制**
|
||||||
|
|
||||||
这种复制模式是许多关系数据库的内置功能,如 PostgreSQL(从 9.0 版本开始),MySQL,Oracle Data Guard 【2】和 SQL Server 的 AlwaysOn 可用性组【3】。 它也被用于一些非关系数据库,包括 MongoDB,RethinkDB 和 Espresso 【4】。 最后,基于领导者的复制并不仅限于数据库:像 Kafka 【5】和 RabbitMQ 高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统,例如 DRBD 这样的块复制设备也与之类似。
|
这种复制模式是许多关系数据库的内置功能,如 PostgreSQL(从 9.0 版本开始)、MySQL、Oracle Data Guard【2】和 SQL Server 的 AlwaysOn 可用性组【3】。 它也被用于一些非关系数据库,包括 MongoDB、RethinkDB 和 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)
|
![](img/fig5-2.png)
|
||||||
**图 5-2 基于领导者的复制:一个同步从库和一个异步从库**
|
**图 5-2 基于领导者的复制:一个同步从库和一个异步从库**
|
||||||
|
|
||||||
在 [图 5-2](img/fig5-2.png) 的示例中,从库 1 的复制是同步的:在向用户报告写入成功,并使结果对其他用户可见之前,主库需要等待从库 1 的确认,确保从库 1 已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者 2 的复制是异步的:主库发送消息,但不等待从库的响应。
|
在 [图 5-2](img/fig5-2.png) 的示例中,从库 1 的复制是同步的:在向用户报告写入成功并使结果对其他用户可见之前,主库需要等待从库 1 的确认,确保从库 1 已经收到写入操作。而从库 2 的复制是异步的:主库发送消息,但不等待该从库的响应。
|
||||||
|
|
||||||
在这幅图中,从库 2 处理消息前存在一个显著的延迟。通常情况下,复制的速度相当快:大多数数据库系统能在不到一秒内完成从库的同步,但它们不能提供复制用时的保证。有些情况下,从库可能落后主库几分钟或更久;例如:从库正在从故障中恢复,系统在最大容量附近运行,或者如果节点间存在网络问题。
|
在这幅图中,从库 2 处理消息前存在一个显著的延迟。通常情况下,复制的速度相当快:大多数数据库系统能在不到一秒内完成从库的同步,但它们不能提供复制用时的保证。有些情况下,从库可能落后主库几分钟或更久,例如:从库正在从故障中恢复,系统正在最大容量附近运行,或者当节点间存在网络问题时。
|
||||||
|
|
||||||
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
|
同步复制的优点是,从库能保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
|
||||||
|
|
||||||
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中 **一个** 跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为 **半同步(semi-synchronous)**【7】。
|
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中 **一个** 从库是同步的,而其他的从库则是异步的。如果该同步从库变得不可用或缓慢,则将一个异步从库改为同步运行。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为 **半同步(semi-synchronous)**【7】。
|
||||||
|
|
||||||
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证 **持久(Durable)** 。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
|
通常情况下,基于领导者的复制都配置为完全异步。在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。这意味着即使已经向客户端确认成功,写入也不能保证是 **持久(Durable)** 的。然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
|
||||||
|
|
||||||
弱化的持久性可能听起来像是一个坏的折衷,然而异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在 “[复制延迟问题](#复制延迟问题)” 中回到这个问题。
|
弱化的持久性可能听起来像是一个坏的折衷,但异步复制其实已经被广泛使用了,特别是在有很多从库的场景下,或者当从库在地理上分布很广的时候。我们将在讨论 “[复制延迟问题](#复制延迟问题)” 时回到这个问题。
|
||||||
|
|
||||||
> ### 关于复制的研究
|
> ### 关于复制的研究
|
||||||
>
|
>
|
||||||
> 对于异步复制系统而言,主库故障时有可能丢失数据。这可能是一个严重的问题,因此研究人员仍在研究不丢数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**【8,9】] 是同步复制的一种变体,已经在一些系统(如 Microsoft Azure 存储【10,11】)中成功实现。
|
> 对于异步复制系统而言,主库故障时会丢失数据可能是一个严重的问题,因此研究人员仍在研究不丢数据但仍能提供良好性能和可用性的复制方法。例如,**链式复制(chain replication)**【8,9】是同步复制的一种变体,已经在一些系统(如 Microsoft Azure Storage【10,11】)中成功实现。
|
||||||
>
|
>
|
||||||
> 复制的一致性与 **共识**(consensus,使几个节点就某个值达成一致)之间有着密切的联系,[第九章](ch9.md) 将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
|
> 复制的一致性与 **共识**(consensus,使几个节点就某个值达成一致)之间有着密切的联系,[第九章](ch9.md) 将详细地探讨这一领域的理论。本章主要讨论实践中的数据库常用的简单复制形式。
|
||||||
>
|
>
|
||||||
|
|
||||||
### 设置新从库
|
### 设置新从库
|
||||||
|
|
||||||
有时候需要设置一个新的从库:也许是为了增加副本的数量,或替换失败的节点。如何确保新的从库拥有主库数据的精确副本?
|
有时候需要设置一个新的从库:也许是为了增加副本的数量,或替换失败的节点。如何确保新的从库拥有主库数据的精确副本?
|
||||||
|
|
||||||
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。复制的结果可能没有任何意义。
|
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断地变化,标准的文件复制会看到数据库的不同部分在不同的时间点的内容,其结果可能没有任何意义。
|
||||||
|
|
||||||
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违背高可用的目标。幸运的是,拉起新的从库通常并不需要停机。从概念上讲,过程如下所示:
|
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违背高可用的目标。幸运的是,设置新从库通常并不需要停机。从概念上讲,其过程如下所示:
|
||||||
|
|
||||||
1. 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如 MySQL 的 innobackupex 【12】。
|
1. 在某个时刻获取主库的一致性快照(如果可能,不必锁定整个数据库)。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如用于 MySQL 的 innobackupex【12】。
|
||||||
2. 将快照复制到新的从库节点。
|
2. 将快照复制到新的从库节点。
|
||||||
3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称:例如,PostgreSQL 将其称为 **日志序列号(log sequence number, LSN)**,MySQL 将其称为 **二进制日志坐标(binlog coordinates)**。
|
3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称,例如 PostgreSQL 将其称为 **日志序列号(log sequence number,LSN)**,MySQL 将其称为 **二进制日志坐标(binlog coordinates)**。
|
||||||
4. 当从库处理完快照之后积压的数据变更,我们说它 **赶上(caught up)** 了主库。现在它可以继续处理主库产生的数据变化了。
|
4. 当从库处理完快照之后积累的数据变更,我们就说它 **赶上(caught up)** 了主库,现在它可以继续及时处理主库产生的数据变化了。
|
||||||
|
|
||||||
建立从库的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个需要由管理员手动执行的,有点神秘的多步骤工作流。
|
建立从库的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个需要由管理员手动执行的、有点神秘的多步骤工作流。
|
||||||
|
|
||||||
### 处理节点宕机
|
### 处理节点宕机
|
||||||
|
|
||||||
系统中的任何节点都可能宕机,可能因为意外的故障,也可能由于计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。
|
系统中的任何节点都可能宕机,可能因为意外的故障,也可能由于计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。
|
||||||
|
|
||||||
如何通过基于主库的复制实现高可用?
|
如何通过基于领导者的复制实现高可用?
|
||||||
|
|
||||||
#### 从库失效:追赶恢复
|
#### 从库失效:追赶恢复
|
||||||
|
|
||||||
在其本地磁盘上,每个从库记录从主库收到的数据变更。如果从库崩溃并重新启动,或者,如果主库和从库之间的网络暂时中断,则比较容易恢复:从库可以从日志中知道,在发生故障之前处理的最后一个事务。因此,从库可以连接到主库,并请求在从库断开期间发生的所有数据变更。当应用完所有这些变化后,它就赶上了主库,并可以像以前一样继续接收数据变更流。
|
在其本地磁盘上,每个从库记录从主库收到的数据变更。如果从库崩溃并重新启动,或者,如果主库和从库之间的网络暂时中断,则比较容易恢复:从库可以从日志中知道,在发生故障之前处理的最后一个事务。因此,从库可以连接到主库,并请求在从库断开期间发生的所有数据变更。当应用完所有这些变更后,它就赶上了主库,并可以像以前一样继续接收数据变更流。
|
||||||
|
|
||||||
#### 主库失效:故障切换
|
#### 主库失效:故障切换
|
||||||
|
|
||||||
主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为 **故障切换(failover)**。
|
主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为 **故障切换(failover)**。
|
||||||
|
|
||||||
故障切换可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障切换过程通常由以下步骤组成:
|
故障切换可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动的故障切换过程通常由以下步骤组成:
|
||||||
|
|
||||||
1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时(Timeout)** :节点频繁地相互来回传递消息,并且如果一个节点在一段时间内(例如 30 秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。
|
1. 确认主库失效。有很多事情可能会出错:崩溃、停电、网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时(Timeout)** :节点频繁地相互来回传递消息,如果一个节点在一段时间内(例如 30 秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。
|
||||||
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的 **控制器节点(controller node)** 来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个 **共识** 问题,将在 [第九章](ch9.md) 详细讨论。
|
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的 **控制器节点(controller node)** 来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(以最小化数据损失)。让所有的节点同意一个新的领导者,是一个 **共识** 问题,将在 [第九章](ch9.md) 详细讨论。
|
||||||
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在 “[请求路由](ch6.md#请求路由)” 中讨论这个问题)。如果旧主库恢复,可能仍然认为自己是主库,而没有意识到其他副本已经让它失去领导权了。系统需要确保旧主库意识到新主库的存在,并成为一个从库。
|
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在 “[请求路由](ch6.md#请求路由)” 中讨论这个问题)。如果旧主库恢复,可能仍然认为自己是主库,而没有意识到其他副本已经让它失去领导权了。系统需要确保旧主库意识到新主库的存在,并成为一个从库。
|
||||||
|
|
||||||
故障切换会出现很多大麻烦:
|
故障切换的过程中有很多地方可能出错:
|
||||||
|
|
||||||
* 如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作。在选出新主库后,如果老主库重新加入集群,新主库在此期间可能会收到冲突的写入,那这些写入该如何处理?最常见的解决方案是简单丢弃老主库未复制的写入,这很可能打破客户对于数据持久性的期望。
|
* 如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作。在选出新主库后,如果老主库重新加入集群,新主库在此期间可能会收到冲突的写入,那这些写入该如何处理?最常见的解决方案是简单丢弃老主库未复制的写入,这很可能打破客户对于数据持久性的期望。
|
||||||
|
|
||||||
* 如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。例如在 GitHub 【13】的一场事故中,一个过时的 MySQL 从库被提升为主库。数据库使用自增 ID 作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的 ID 作为主键。这些主键也在 Redis 中使用,主键重用使得 MySQL 和 Redis 中数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
|
* 如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。例如在 GitHub 【13】的一场事故中,一个过时的 MySQL 从库被提升为主库。数据库使用自增 ID 作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的 ID 作为主键。这些主键也在 Redis 中使用,主键重用使得 MySQL 和 Redis 中的数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
|
||||||
|
|
||||||
* 发生某些故障时(见 [第八章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂 (split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(请参阅 “[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点 [^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
|
* 发生某些故障时(见 [第八章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂 (split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(请参阅 “[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点 [^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
|
||||||
|
|
||||||
[^ii]: 这种机制称为 **屏蔽(fencing)**,充满感情的术语是:**爆彼之头(Shoot The Other Node In The Head, STONITH)**。
|
[^ii]: 这种机制称为 **屏障(fencing)**,或者更充满感情的术语是:**爆彼之头(Shoot The Other Node In The Head, STONITH)**。我们将在 “[领导者和锁](ch8.md#领导者和锁)” 中对屏障进行详细讨论。
|
||||||
|
|
||||||
* 主库被宣告死亡之前的正确超时应该怎么配置?在主库失效的情况下,超时时间越长,意味着恢复时间也越长。但是如果超时设置太短,又可能会出现不必要的故障切换。例如,临时负载峰值可能导致节点的响应时间超时,或网络故障可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。
|
* 主库被宣告死亡之前的正确超时应该怎么配置?在主库失效的情况下,超时时间越长意味着恢复时间也越长。但是如果超时设置太短,又可能会出现不必要的故障切换。例如,临时的负载峰值可能导致节点的响应时间增加到超出超时时间,或者网络故障也可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。
|
||||||
|
|
||||||
这些问题没有简单的解决方案。因此,即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障切换。
|
这些问题没有简单的解决方案。因此,即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障切换。
|
||||||
|
|
||||||
节点故障、不可靠的网络、对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第八章](ch8.md) 和 [第九章](ch9.md) 将更深入地讨论它们。
|
节点故障、不可靠的网络、对副本一致性、持久性、可用性和延迟的权衡,这些问题实际上是分布式系统中的基本问题。[第八章](ch8.md) 和 [第九章](ch9.md) 将更深入地讨论它们。
|
||||||
|
|
||||||
### 复制日志的实现
|
### 复制日志的实现
|
||||||
|
|
||||||
基于主库的复制底层是如何工作的?实践中有好几种不同的复制方式,所以先简要地看一下。
|
基于领导者的复制在底层是如何工作的?实践中有好几种不同的复制方式,所以先简要地看一下。
|
||||||
|
|
||||||
#### 基于语句的复制
|
#### 基于语句的复制
|
||||||
|
|
||||||
在最简单的情况下,主库记录下它执行的每个写入请求(**语句**,即 statement)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个 `INSERT`、`UPDATE` 或 `DELETE` 语句都被转发给每个从库,每个从库解析并执行该 SQL 语句,就像从客户端收到一样。
|
在最简单的情况下,主库记录下它执行的每个写入请求(**语句**,即 statement)并将该语句日志发送给从库。对于关系数据库来说,这意味着每个 `INSERT`、`UPDATE` 或 `DELETE` 语句都被转发给每个从库,每个从库解析并执行该 SQL 语句,就像直接从客户端收到一样。
|
||||||
|
|
||||||
虽然听上去很合理,但有很多问题会搞砸这种复制方式:
|
虽然听上去很合理,但有很多问题会搞砸这种复制方式:
|
||||||
|
|
||||||
* 任何调用 **非确定性函数(nondeterministic)** 的语句,可能会在每个副本上生成不同的值。例如,使用 `NOW()` 获取当前日期时间,或使用 `RAND()` 获取一个随机数。
|
* 任何调用 **非确定性函数(nondeterministic)** 的语句,可能会在每个副本上生成不同的值。例如,使用 `NOW()` 获取当前日期时间,或使用 `RAND()` 获取一个随机数。
|
||||||
* 如果语句使用了 **自增列(auto increment)**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
|
* 如果语句使用了 **自增列(auto increment)**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
|
||||||
* 有副作用的语句(例如:触发器、存储过程、用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
|
* 有副作用的语句(例如:触发器、存储过程、用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定性的。
|
||||||
|
|
||||||
的确有办法绕开这些问题 —— 例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
|
的确有办法绕开这些问题 —— 例如,当语句被记录时,主库可以用固定的返回值替换掉任何不确定的函数调用,以便所有从库都能获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
|
||||||
|
|
||||||
基于语句的复制在 5.1 版本前的 MySQL 中使用。因为它相当紧凑,现在有时候也还在用。但现在在默认情况下,如果语句中存在任何不确定性,MySQL 会切换到基于行的复制(稍后讨论)。 VoltDB 使用了基于语句的复制,但要求事务必须是确定性的,以此来保证安全【15】。
|
基于语句的复制在 5.1 版本前的 MySQL 中被使用到。因为它相当紧凑,现在有时候也还在用。但现在在默认情况下,如果语句中存在任何不确定性,MySQL 会切换到基于行的复制(稍后讨论)。 VoltDB 使用了基于语句的复制,但要求事务必须是确定性的,以此来保证安全【15】。
|
||||||
|
|
||||||
#### 传输预写式日志(WAL)
|
#### 传输预写式日志(WAL)
|
||||||
|
|
||||||
在 [第三章](ch3.md) 中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现,通常写操作都是追加到日志中:
|
在 [第三章](ch3.md) 中,我们讨论了存储引擎如何在磁盘上表示数据,我们也发现了通常会将写操作追加到日志中:
|
||||||
|
|
||||||
* 对于日志结构存储引擎(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
|
* 对于日志结构存储引擎(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
|
||||||
* 对于覆写单个磁盘块的 [B 树](ch3.md#B树),每次修改都会先写入 **预写式日志(Write Ahead Log, WAL)**,以便崩溃后索引可以恢复到一个一致的状态。
|
* 对于覆写单个磁盘块的 [B 树](ch3.md#B树),每次修改都会先写入 **预写式日志(Write Ahead Log, WAL)**,以便崩溃后索引可以恢复到一个一致的状态。
|
||||||
|
|
||||||
在任何一种情况下,日志都是包含所有数据库写入的仅追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主库还可以通过网络将其发送给其从库。
|
在任何一种情况下,该日志都是包含了所有数据库写入的仅追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主库还可以通过网络将其发送给从库。
|
||||||
|
|
||||||
当从库应用这个日志时,它会建立和主库一模一样数据结构的副本。
|
通过使用这个日志,从库可以构建一个与主库一模一样的数据结构拷贝。
|
||||||
|
|
||||||
PostgreSQL 和 Oracle 等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层:WAL 包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本,通常不可能在主库和从库上运行不同版本的数据库软件。
|
这种复制方法在 PostgreSQL 和 Oracle 等一些产品中被使用到【16】。其主要缺点是日志记录的数据非常底层:WAL 包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本,通常不可能在主库和从库上运行不同版本的数据库软件。
|
||||||
|
|
||||||
看上去这可能只是一个微小的实现细节,但却可能对运维产生巨大的影响。如果复制协议允许从库使用比主库更新的软件版本,则可以先升级从库,然后执行故障切换,使升级后的节点之一成为新的主库,从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配(传输 WAL 经常出现这种情况),则此类升级需要停机。
|
看上去这可能只是一个小的实现细节,但却可能对运维产生巨大的影响。如果复制协议允许从库使用比主库更新的软件版本,则可以先升级从库,然后执行故障切换,使升级后的节点之一成为新的主库,从而允许数据库软件的零停机升级。如果复制协议不允许版本不匹配(传输 WAL 经常出现这种情况),则此类升级需要停机。
|
||||||
|
|
||||||
#### 逻辑日志复制(基于行)
|
#### 逻辑日志复制(基于行)
|
||||||
|
|
||||||
另一种方法是,复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来。
|
另一种方法是对复制和存储引擎使用不同的日志格式,这样可以将复制日志从存储引擎的内部实现中解耦出来。这种复制日志被称为逻辑日志(logical log),以将其与存储引擎的(物理)数据表示区分开来。
|
||||||
|
|
||||||
关系数据库的逻辑日志通常是以行的粒度描述对数据库表的写入的记录序列:
|
关系数据库的逻辑日志通常是以行的粒度来描述对数据库表的写入记录的序列:
|
||||||
|
|
||||||
* 对于插入的行,日志包含所有列的新值。
|
* 对于插入的行,日志包含所有列的新值。
|
||||||
* 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值。
|
* 对于删除的行,日志包含足够的信息来唯一标识被删除的行,这通常是主键,但如果表上没有主键,则需要记录所有列的旧值。
|
||||||
* 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
|
* 对于更新的行,日志包含足够的信息来唯一标识被更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
|
||||||
|
|
||||||
修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL 的二进制日志(当配置为使用基于行的复制时)使用这种方法【17】。
|
修改多行的事务会生成多条这样的日志记录,后面跟着一条指明事务已经提交的记录。 MySQL 的二进制日志(当配置为使用基于行的复制时)使用了这种方法【17】。
|
||||||
|
|
||||||
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
|
由于逻辑日志与存储引擎的内部实现是解耦的,系统可以更容易地做到向后兼容,从而使主库和从库能够运行不同版本的数据库软件,或者甚至不同的存储引擎。
|
||||||
|
|
||||||
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统,这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获(change data capture)**,[第十一章](ch11.md) 将重新讲到它。
|
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存【18】,这一点会很有用。这种技术被称为 **数据变更捕获(change data capture)**,[第十一章](ch11.md) 将重新讲到它。
|
||||||
|
|
||||||
#### 基于触发器的复制
|
#### 基于触发器的复制
|
||||||
|
|
||||||
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果你只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果你需要冲突解决逻辑(请参阅 “[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
|
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果你只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果你需要冲突解决逻辑(请参阅 “[处理写入冲突](#处理写入冲突)”),则可能需要将复制操作上移到应用程序层。
|
||||||
|
|
||||||
一些工具,如 Oracle Golden Gate 【19】,可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
|
一些工具,如 Oracle Golden Gate【19】,可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
|
||||||
|
|
||||||
触发器允许你将数据更改(写入事务)发生时自动执行的自定义应用程序代码注册在数据库系统中。触发器有机会将更改记录到一个单独的表中,使用外部程序读取这个表,再加上一些必要的业务逻辑,就可以将数据变更复制到另一个系统去。例如,Databus for Oracle 【20】和 Bucardo for Postgres 【21】就是这样工作的。
|
触发器允许你将数据更改(写入事务)发生时自动执行的自定义应用程序代码注册在数据库系统中。触发器有机会将更改记录到一个单独的表中,使用外部程序读取这个表,再加上一些必要的业务逻辑,就可以将数据变更复制到另一个系统去。例如,Databus for Oracle【20】和 Bucardo for Postgres【21】就是这样工作的。
|
||||||
|
|
||||||
基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出错,也有很多限制。然而由于其灵活性,仍然是很有用的。
|
基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库内置的复制更容易出错,也有很多限制。然而由于其灵活性,它仍然是很有用的。
|
||||||
|
|
||||||
|
|
||||||
## 复制延迟问题
|
## 复制延迟问题
|
||||||
|
|
||||||
容忍节点故障只是需要复制的一个原因。正如在 [第二部分](part-ii.md) 的介绍中提到的,另一个原因是可伸缩性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
|
容忍节点故障只是需要复制的一个原因。正如在 [第二部分](part-ii.md) 的介绍中提到的,另一个原因是可伸缩性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
|
||||||
|
|
||||||
基于主库的复制要求所有写入都由单个节点处理,但只读查询可以由任何副本处理。所以对于读多写少的场景(Web 上的常见模式),一个有吸引力的选择是创建很多从库,并将读请求分散到所有的从库上去。这样能减小主库的负载,并允许向最近的副本发送读请求。
|
基于领导者的复制要求所有写入都由单个节点处理,但只读查询可以由任何副本处理。所以对于读多写少的场景(Web 上的常见模式),一个有吸引力的选择是创建很多从库,并将读请求分散到所有的从库上去。这样能减小主库的负载,并允许向最近的副本发送读请求。
|
||||||
|
|
||||||
在这种伸缩体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制 —— 如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
|
在这种伸缩体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制 —— 如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user