mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
fix format
This commit is contained in:
parent
d05529b73c
commit
9672e6f332
@ -83,8 +83,3 @@ PostgreSQL是世界上最先进的开源关系型数据库!
|
||||
|
||||
* [Introduction to PostGIS](http://workshops.boundlessgeo.com/postgis-intro/index.html)
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
闷声发大财才是坠吼地。
|
101
ddia/README.md
101
ddia/README.md
@ -3,6 +3,9 @@
|
||||
* 作者: Martin Kleppmann
|
||||
* 原书名称:《Designing Data-Intensive Application》
|
||||
* 译者:冯若航 (fengruohang@outlook.com , http://vonng.com/about)
|
||||
* 建议本地使用[Typora](https://www.typora.io)以获取最佳阅读体验。
|
||||
|
||||
|
||||
|
||||
|
||||
## 前言
|
||||
@ -19,79 +22,35 @@
|
||||
|
||||
## 目录
|
||||
|
||||
### [序言](preface.md) [机翻]
|
||||
### [序言](preface.md)
|
||||
|
||||
### 第一部分: [数据系统的基石](part-i.md)
|
||||
### [数据系统的基石](part-i.md)
|
||||
|
||||
1. [可靠性、可扩展性、可维护性](ch1.md) [初翻30%]
|
||||
* 关于数据系统的思考
|
||||
* 可靠性
|
||||
* 可扩展性
|
||||
* 可维护性
|
||||
2. [数据模型与查询语言](ch2.md) [初翻]
|
||||
* 关系模型与文档模型
|
||||
* 数据查询语言
|
||||
* 图数据模型
|
||||
3. [存储与检索](ch3.md) [初翻]
|
||||
* 支撑数据库的数据结构
|
||||
* 分析还是事务处理?
|
||||
* 列存储
|
||||
4. [编码与演化](ch4.md) [初翻]
|
||||
* 编码数据的格式
|
||||
* 数据流的模型
|
||||
1. [可靠性、可扩展性、可维护性](ch1.md)
|
||||
2. [数据模型与查询语言](ch2.md)
|
||||
3. [存储与检索](ch3.md)
|
||||
4. [编码与演化](ch4.md)
|
||||
|
||||
### 第二部分: [分布式数据](part-ii.md)
|
||||
### [分布式数据](part-ii.md)
|
||||
|
||||
5. [复制](ch5.md) [机翻]
|
||||
* 主从
|
||||
* 复制延迟带来的问题
|
||||
* 多主复制
|
||||
* 无主复制
|
||||
6. [分片](ch6.md) [机翻]
|
||||
* 分片与复制
|
||||
* 键值对数据的分片
|
||||
* 分片与次级索引
|
||||
* 分片再平衡
|
||||
* 请求路由
|
||||
7. [事务](ch7.md) [机翻]
|
||||
* 事务的棘手概念
|
||||
* ACID的含义
|
||||
* 弱隔离级别
|
||||
* 序列化能力
|
||||
8. [分布式系统的麻烦](ch8.md) [待翻]
|
||||
* 故障与部分失效
|
||||
* 不可靠的网络
|
||||
* 不可靠的时钟
|
||||
* 知识、真相与谎言
|
||||
9. [一致性与共识](ch9.md) [待翻]
|
||||
* 一致性保证
|
||||
* 线性一致性
|
||||
* 顺序保证
|
||||
* 分布式事务与共识
|
||||
5. [复制](ch5.md)
|
||||
6. [分片](ch6.md)
|
||||
7. [事务](ch7.md)
|
||||
8. [分布式系统的麻烦](ch8.md)
|
||||
9. [一致性与共识](ch9.md)
|
||||
|
||||
### 第三部分:[衍生数据](part-iii.md)
|
||||
### [衍生数据](part-iii.md)
|
||||
|
||||
10. [批处理](ch10.md) [未翻]
|
||||
* 使用Unix工具的批处理
|
||||
* MapReduce和分布式文件系统
|
||||
* 后MapReduce时代
|
||||
11. [流处理](ch11.md) [未翻]
|
||||
* 传递事件流
|
||||
* 流与数据库
|
||||
* 流处理
|
||||
12. [数据系统的未来](ch12.md) [未翻]
|
||||
* 数据集成
|
||||
* 拆分数据库
|
||||
* 目标是正确性
|
||||
* 做正确的事情
|
||||
10. [批处理](ch10.md)
|
||||
11. [流处理](ch11.md)
|
||||
12. [数据系统的未来](ch12.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 翻译计划
|
||||
|
||||
机翻:程序翻译,基本保留原书的组织结构,也是阅读原文,学习本书的过程。
|
||||
机翻:股沟翻译,整理图片、引用、备注与基本的排版,不对照着原文基本没法看。
|
||||
|
||||
初翻:人工修复格式错误和显著翻译错误,重新组织语言,保障基本的可读性。
|
||||
|
||||
@ -99,7 +58,25 @@
|
||||
|
||||
翻译完全看心情,但通常每周至少会完成一章的初翻。
|
||||
|
||||
阅读建议使用Typora,可以获得最好的阅览体验。
|
||||
| 章节 | 文件 | 计划 | 进度 |
|
||||
| ------ | ------ | ---- | ---- |
|
||||
| 序 | [preface.md](preface.md) | | 机翻 |
|
||||
| 第一部分:数据系统基础 ——概览 | [part-i.md](part-i.md) | | 精翻 20% |
|
||||
| 第一章:可靠性、可扩展性、可维护性 | [ch1.md](ch1.md) | | 初翻 |
|
||||
| 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | | 初翻 |
|
||||
| 第三章:存储与检索 | [ch3.md](ch3.md) | | 初翻 |
|
||||
| 第四章:编码与演化 | [ch4.md](ch4.md) | | 初翻 |
|
||||
| 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | | 初翻 |
|
||||
| 第五章:复制 | [ch5.md](ch5.md) | | 初翻 60% |
|
||||
| 第六章:分片 | [ch6.md](ch6.md) | | 机翻 |
|
||||
| 第七章:事务 | [ch7.md](ch7.md) | | 机翻 |
|
||||
| 第八章:分布式系统的麻烦 | [ch8.md](ch8.md) | | - |
|
||||
| 第九章:一致性与共识 | [ch9.md](ch9.md) | | - |
|
||||
| 第三部分:前言 | [part-iii.md](part-iii.md) | | 机翻 |
|
||||
| 第十章:批处理 | [ch10.md](ch10.md) | | - |
|
||||
| 第十一章:流处理 | [ch11.md](ch11.md) | | - |
|
||||
| 第十二章:数据系统的未来 | [ch12.md](ch12.md) | | - |
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
![](img/ch10.png)
|
||||
|
||||
带有太强个人色彩的系统无法成功。当第一版健壮的设计完成时,真正的考验才开始:许多不同观点的人会以他们自己的方式来测试。
|
||||
|
||||
——高德纳
|
||||
> 带有太强个人色彩的系统无法成功。当第一版健壮的设计完成时,不同的人们以自己的方式来测试,真正的考验才开始。
|
||||
>
|
||||
> ——高德纳
|
||||
|
||||
---------------
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
![](img/ch11.png)
|
||||
|
||||
> 有效的复杂系统总是从简单的系统演化而来。 反之亦然:从零开始设计的复杂系统没一个能有效工作的。
|
||||
> 有效的复杂系统总是从简单的系统演化而来。 反之亦然:从零设计的复杂系统没一个能有效工作的。
|
||||
>
|
||||
> ——约翰·加尔,Systemantics(1975)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
![](img/ch12.png)
|
||||
|
||||
> 如果船长的最高目标是保护他的船,他应该永远待在港口。
|
||||
> 如果船长的终极目标是保护船只,他应该永远待在港口。
|
||||
>
|
||||
> ——圣托马斯·阿奎那《神学大全》(1265-1274)
|
||||
|
||||
|
16
ddia/ch2.md
16
ddia/ch2.md
@ -48,7 +48,7 @@
|
||||
|
||||
### NoSQL的诞生
|
||||
|
||||
现在,在2010年代,NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字是不幸的,因为它实际上并没有涉及到任何特定的技术,它最初只是作为一个吸引人的Twitter标签在2009年的开源,分布式,非关系数据库上聚会。无论如何,这个术语触动了一个神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与#NoSQL#标签相关联,并被追溯性地重新解释为不仅是SQL [4]。
|
||||
现在,在2010年代,NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字是不幸的,因为它实际上并没有涉及到任何特定的技术,它最初只是作为一个吸引人的Twitter标签在2009年的开源,分布式,非关系数据库上聚会。无论如何,这个术语触动了一个神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与#NoSQL#标签相关联,并被追溯性地重新解释为不仅是SQL [4]。
|
||||
|
||||
采用NoSQL数据库有几个驱动力,其中包括:
|
||||
|
||||
@ -57,11 +57,13 @@
|
||||
* 关系模型不能很好地支持一些特殊的查询操作
|
||||
* 对关系模型限制性感到受挫,对更多动态性与表现力的渴望
|
||||
|
||||
不同的应用程序有不同的要求,一个用例的最佳技术选择可能不同于另一个用例的最佳选择。因此,在可预见的未来,关系数据库似乎可能会继续与各种非关系数据库一起使用 - 这种想法有时也被称为混合持久化(Polyglot Persistences)
|
||||
不同的应用程序有不同的要求,一个用例的最佳技术选择可能不同于另一个用例的最佳选择。因此,在可预见的未来,关系数据库似乎可能会继续与各种非关系数据库一起使用 - 这种想法有时也被称为**混合持久化(Polyglot Persistences)**
|
||||
|
||||
### 对象关系不匹配
|
||||
|
||||
现在大多数应用程序开发都是在面向对象的编程语言中完成的,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么应用程序代码中的对象之间需要一个笨拙的转换层,表,行和列的数据库模型。模型之间的不连贯有时被称为阻抗不匹配(impedance mismatch 从电子学借用一个术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配可能导致信号反射和其他问题。)
|
||||
现在大多数应用程序开发都是在面向对象的编程语言中完成的,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么应用程序代码中的对象之间需要一个笨拙的转换层,表,行和列的数据库模型。模型之间的不连贯有时被称为**阻抗不匹配(impedance mismatch)**[^i]。
|
||||
|
||||
[^i]: 从电子学借用一个术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配可能导致信号反射和其他问题
|
||||
|
||||
像ActiveRecord和Hibernate这样的对象关系映射(ORM)框架减少了这个翻译层需要的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
|
||||
|
||||
@ -194,7 +196,9 @@ CODASYL中的查询是通过遍历记录列表和访问路径后,通过在数
|
||||
|
||||
#### 关系模型
|
||||
|
||||
相比之下,关系模型做的就是将所有的数据放在open中:一个关系(table)只是一个元组(行)的集合,就是这样。没有迷宫似的嵌套结构,如果你想看看数据,没有复杂的访问路径。您可以读取表中的任何或所有行,选择符合任意条件的行。您可以通过指定某些列作为关键字并匹配这些关键字来读取特定行。您可以在任何表中插入一个新的行,而不必担心与其他表的外键关系(外键约束允许对修改做限制,对于关系模型这并不是必选项。 即使有约束,在查询时执行外键连接,而在CODASYL中,连接在插入时高效完成。)
|
||||
相比之下,关系模型做的就是将所有的数据放在open中:一个关系(table)只是一个元组(行)的集合,就是这样。没有迷宫似的嵌套结构,如果你想看看数据,没有复杂的访问路径。您可以读取表中的任何或所有行,选择符合任意条件的行。您可以通过指定某些列作为关键字并匹配这些关键字来读取特定行。您可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iii]。
|
||||
|
||||
[^iii]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。 即使有约束,在查询时执行外键连接,而在CODASYL中,连接在插入时高效完成。
|
||||
|
||||
在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。这些选择实际上是“访问路径”,但最大的区别在于它们是由查询优化器自动生成的,而不是由程序员生成,所以我们很少需要考虑它们。
|
||||
|
||||
@ -524,7 +528,7 @@ db.observations.aggregate([
|
||||
|
||||
![](img/fig2-5.png)
|
||||
|
||||
##### 图2-5 图数据结构示例(框代表顶点,箭头代表边)
|
||||
**图2-5 图数据结构示例(框代表顶点,箭头代表边)**
|
||||
|
||||
有几种不同但相关的方法来构建和查询图表中的数据。 在本节中,我们将讨论属性图模型(由Neo4j,Titan和InfiniteGraph实现)和三存储(triple-store)模型(由Datomic,AllegroGraph等实现)。 我们将看图的三种声明性查询语言:Cypher,SPARQL和Datalog。 除此之外,还有像Gremlin [36]这样的图形查询语言和像Pregel这样的图形处理框架(见第10章)。
|
||||
|
||||
@ -866,7 +870,7 @@ Cypher和SPARQL使用SELECT立即跳转,但是Datalog一次只需要一小步
|
||||
|
||||
![](img/fig2-6.png)
|
||||
|
||||
图2-6. 使用示例2-11中的Datalog规则确定爱达荷州在北美。
|
||||
**图2-6 使用示例2-11中的Datalog规则确定爱达荷州在北美。**
|
||||
|
||||
现在规则3可以找到出生在某个地方BornIn的人,并住在某个地方LivingIn。通过查询BornIn ='United States'和LivingIn ='Europe',并将此人作为变量Who,我们要求Datalog系统找出变量Who可以出现哪些值。因此,最后我们得到了与早先的Cypher和SPARQL查询相同的答案。
|
||||
|
||||
|
330
ddia/ch5.md
330
ddia/ch5.md
@ -2,7 +2,7 @@
|
||||
|
||||
![](img/ch5.png)
|
||||
|
||||
> 天作孽,尤可治,自作孽,不可活。
|
||||
> 与可能出错的东西比,'不可能'出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。
|
||||
>
|
||||
> ——道格拉斯·亚当斯(1992)
|
||||
|
||||
@ -10,276 +10,295 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在第二部分的介绍中所讨论的那样,有几种想要复制数据的原因:
|
||||
|
||||
|
||||
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在第二部分的介绍中所讨论的那样,您可能想要复制数据的原因有几个:
|
||||
|
||||
* 为了保持数据在地理位置上接近用户(从而减少延迟)
|
||||
* 保持数据在地理位置上接近用户(从而减少延迟)
|
||||
* 即使部分零件出现故障,系统也能继续工作(从而提高可用性)
|
||||
* 为了扩大可以为读取查询服务的机器数量(从而提高读取吞吐量)
|
||||
* 扩充可以接受读请求的机器数量(从而提高读取吞吐量)
|
||||
|
||||
在本章中,我们将假设您的数据集非常小,以至于每台机器都可以保存整个数据集的副本。在第6章中,我们将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
|
||||
本章将假设您的数据集非常小,每台机器都可以保存整个数据集的副本。在[第6章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
|
||||
|
||||
如果您正在复制的数据不会随时间而改变,那么复制很容易:您只需将数据复制到每个节点一次,即可完成。复制中的所有困难都在于处理复制数据的变化,这就是本章的内容。我们将讨论三种流行的复制节点间变化的算法:单主,多主和无主。几乎所有分布式数据库都使用这三种方法之一。
|
||||
如果正在复制的数据不会随时间而改变,那么复制太简单了:将数据复制到每个节点一次就完事了。复制的困难之处在于应对复制数据的**变化**,这就是本章的内容。我们将讨论三种流行的变化复制算法:**单主(single leader)**,**多主(multi leader)**和**无主(leaderless)**。几乎所有分布式数据库都使用这三种方法之一。
|
||||
|
||||
他们都有各种各样的优点和缺点,我们将详细审查。在复制时需要考虑许多折衷:例如,是否使用同步或异步复制,以及如何处理失败的副本。这些通常是数据库中的配置选项,虽然细节因数据库而异,但一般原则在许多不同的实现中是相似的。我们将在本章讨论这种选择的后果。
|
||||
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?。这些通常是数据库中的配置项,细节因数据库而异,但原理在许多不同的实现中都是相似的。本章将讨论这些决策的后果。
|
||||
|
||||
数据库的复制是一个老话题 - 自1970年代研究以来,这些原则并没有太大的改变[1],因为网络的基本约束保持不变。然而,除了研究之外,许多开发人员仍然假定一个数据库只有一个节点。主流使用分布式数据库是最近的事情。由于许多应用程序开发人员都是这方面的新手,对最终一致性等问题存在诸多误解。在第161页的“复制滞后问题”中,我们将更加精确地了解最终的一致性,并讨论诸如读写和单调读取保证等内容。
|
||||
数据库的复制是一个老话题 ——自1970年代研究以来,这些原则并没有太大的改变[1],因为网络的基本约束保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近的事。由于许多程序员都是这方面的新手,对**最终一致性(eventual consistency)**等问题存在诸多误解。在“复制滞后问题”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写(read-your-writes)**和**单调读(monotonic read)**保证等内容。
|
||||
|
||||
## 主从
|
||||
|
||||
存储数据库副本的每个节点称为*副本(replica)*。有了多个副本,一个问题不可避免地会出现:我们如何确保所有数据在所有副本上都结束?
|
||||
|
||||
每次写入数据库都需要处理每个副本;否则,副本将不再包含相同的数据。最常见的解决方案称为基于领导者的复制(也称为主动/被动或主从复制),如图5-1所示。它的工作原理如下:
|
||||
## 领导者与追随者
|
||||
|
||||
1. 副本之一被指定为领导者(也称为 **主master** 或**首要的primary**)。当客户想要写入数据库时,他们必须将他们的请求发送给领导,首先将新数据写入其本地存储。
|
||||
2. 其他副本被称为追随者(只读副本,从属副本,热备份副本)(不同的人对热hot,温warn,冷cold 备份服务器有不同的定义。 例如,在PostgreSQL中,热备用来引用从客户端接受读取的副本,而温备紧跟领袖,但不处理来自客户端的任何查询。 就本书而言,差异并不重要。)每当领导者将新数据写入本地存储时,它也会将数据更改发送给其所有追随者作为复制的一部分记录或更改流。每个跟随者从领导者处获取日志,并相应地更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
|
||||
3. 当客户想要从数据库中读取数据时,它可以查询领导或任何追随者。 然而,只有领导才能接受写操作(从客户端的角度来看,从库都是只读的)。
|
||||
存储数据库副本的每个节点称为**副本(replica)**。当有多个副本时就会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
|
||||
|
||||
每一次向数据库的写入都需要传播到所有副本上,否则副本包含的数据就会不一样。最常见的解决方案称为:**基于领导者的复制(leader-based replication)**(也称**主动/被动 active/passive** 或 **主/从 master/slave**复制),如[图5-1](img/fig5-1.png)所示。它的工作原理如下:
|
||||
|
||||
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)**只是追随领导者,但不处理客户端的任何查询。 就本书而言差异并不重要。
|
||||
|
||||
![](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这样的块复制设备也与之类似。
|
||||
|
||||
|
||||
|
||||
### 同步复制与异步复制
|
||||
|
||||
复制系统的一个重要细节是复制是同步发生还是异步发生。 (在关系型数据库中,这通常是一个可配置的选项;其他系统通常被硬编码为一个或另一个)。
|
||||
复制系统的一个重要细节是复制是**同步(synchronously)**发生还是**异步(asynchronously)**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
|
||||
|
||||
想想图5-1中发生的情况,网站的用户更新他们的个人资料图像。在某个时间点,客户发送更新请求给领导;不久之后,领导就收到了。在某个时候,领导者将数据转换转发给追随者。最后,领导通知客户更新成功。
|
||||
想想[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人资料图像。在某个时间点,客户发送更新请求给领导者;不久之后领导者就收到了。在某个时候,领导者将数据转换转发给追随者。最后,领导通知客户更新成功。
|
||||
|
||||
图5-2显示了系统各个组件之间的通信:用户的客户端,领导者和两个关注者。时间从左到右流动。请求或响应消息显示为粗箭头。
|
||||
[图5-2](img/fig5-2.png)显示了系统各个组件之间的通信:用户的客户端,领导者和两个关注者。时间从左到右流动。请求或响应消息显示为粗箭头。
|
||||
|
||||
![](img/fig5-2.png)
|
||||
**图5-2 基于领导者的复制有一个同步和一个异步的从库。**
|
||||
**图5-2 基于领导者的复制:一个同步从库和一个异步从库**
|
||||
|
||||
在图5-2的示例中,跟随者1的复制是同步的:领导者等待直到跟随者1确认在向用户报告成功之前以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的:领导者发送消息,但不等待跟随者的响应。
|
||||
在[图5-2]()的示例中,跟随者1的复制是同步的:领导者等待直到跟随者1确认在向用户报告成功之前以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的:领导者发送消息,但不等待跟随者的响应。
|
||||
|
||||
该图表明,在跟随者2处理消息之前存在显着的延迟。通常情况下,复制速度非常快:大多数数据库系统在不到一秒的时间内对关注者进行更改。但是,不能保证需要多长时间。有些情况下,追随者可能落后于领导者几分钟或更长时间;例如,如果跟随者正在从故障中恢复,如果系统正在接近最大容量,或者如果节点之间存在网络问题。
|
||||
该图表明,在跟随者2处理消息之前存在显着的延迟。通常情况下,复制速度非常快:大多数数据库系统在不到一秒的时间内对关注者进行更改。但是,不能保证需要多长时间。有些情况下,追随者可能落后于领导者几分钟或更长时间;例如,如果跟随者正在从故障中恢复,如果系统正在接近最大容量,或者如果节点之间存在网络问题。
|
||||
|
||||
同步复制的优点是,跟随者保证有与领导者一致的最新数据副本。如果领导者突然失败,我们可以肯定的是,这些数据仍然可以在追随者身上找到。缺点是如果同步跟随器没有响应(因为它已经崩溃,或者出现网络故障,或者出于任何其他原因),写入不能被处理。领导者必须阻止所有写入,并等待同步副本再次可用。
|
||||
|
||||
因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中一个跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为半同步[7]。
|
||||
因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为**半同步(semi-synchronous)**[7]。
|
||||
|
||||
通常,基于领导者的复制配置为完全异步。 在这种情况下,如果领导失败并且不可恢复,则任何尚未复制给追随者的写入都将丢失。 这意味着写入不能保证持久,即使已经被客户确认。 然而,一个完全异步的配置具有领导者可以继续处理写入的优点,即使其所有追随者都落后了。
|
||||
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果领导失败并且不可恢复,则任何尚未复制给追随者的写入都将丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久(Durable)**。 然而,一个完全异步的配置也有优点:即使所有的追随者都落后了,领导者也可以继续处理写入。
|
||||
|
||||
弱化的耐久性可能听起来像是一个坏的折衷,但异步复制却被广泛使用,特别是如果有很多追随者或者是地理分布的话。 我们将在第161页的“复制滞后问题”中回到这个问题。
|
||||
弱化的持久性可能听起来像是一个坏的折衷,无论如何,异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“复制延迟”中回到这个问题。
|
||||
|
||||
#### *关于复制的研究*
|
||||
> ### 关于复制的研究
|
||||
>
|
||||
> 如果主库故障,那么异步复制系统会丢失数据可能是一个严重的问题,因此研究人员继续研究不丢失数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**[8,9]是同步复制的一种变体,已经在一些系统(如Microsoft Azure存储[10,11])中成功实现。
|
||||
>
|
||||
> 复制的一致性与**共识(consensus)**(使几个节点就某个值达成一致)之间有着密切的联系,第9章将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
|
||||
>
|
||||
|
||||
如果主库故障,那么异步复制系统会丢失数据可能是一个严重的问题,因此研究人员继续研究不丢失数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**[8,9]是同步复制的一种变体,已经在一些系统(如Microsoft Azure存储[10,11])中成功实现。
|
||||
### 设置新追随者
|
||||
|
||||
复制的一致性与共识(得到几个节点以达成一个价值)之间有着密切的联系,我们将在第9章更详细地探讨这一领域的理论。在本章中,我们将集中讨论更简单的形式 在实践中最常用于数据库的复制。
|
||||
有时候需要设置一个新的追随者:也许是为了增加副本的数量,或替换失败的节点。如何确保新的追随者有领导者数据的准确副本?
|
||||
|
||||
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。结果可能没有任何意义。
|
||||
|
||||
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违反高可用的目标。幸运的是,拉起一个从库通常不需要停机。从概念上讲,过程如下所示:
|
||||
|
||||
### 设置新从
|
||||
|
||||
有时候,你需要建立新的追随者 - 也许增加副本的数量,或者替换失败的节点。你如何确保新的追随者有一个领导者的数据的准确副本?
|
||||
|
||||
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是处于不断变化的状态,因此标准文件副本会在不同的时间点看到数据库的不同部分。结果可能没有任何意义。
|
||||
|
||||
您可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违反我们的高可用性目标。幸运的是,建立一个跟随者通常可以在没有停机的情况下完成。从概念上讲,过程如下所示:
|
||||
|
||||
1. 在某个时间点对领导者的数据库进行一致的快照(如果可能的话),而不必锁定整个数据库。大多数数据库都具有这种功能,因为它也是备份所必需的。在某些情况下,需要第三方工具,如MySQL的innobackupex [12]。
|
||||
1. 在某个时间点对领导者的数据库取一个一致的快照(如果可能的话),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。在某些情况下,需要第三方工具,如MySQL的*innobackupex* [12]。
|
||||
2. 将快照复制到新的跟随者节点。
|
||||
3. 跟随者连接到领导并请求拍摄快照后发生的所有数据更改。这要求快照与领导者的复制日志中的确切位置相关联。该位置有不同的名称:例如,PostgreSQL将其称为*日志序列号(log sequence number, lsn)*,MySQL将其称为*二进制日志坐标(binlog coordinates)*。
|
||||
4. 从追随者处理了自快照以来的数据变化积压,我们说它已经赶上了。现在,它可以继续处理领导者发生的数据变化。
|
||||
3. 追随者连接到领导者,并请求快照后发生的所有数据更改。这要求快照与领导者的复制日志中的精确位置关联。该位置有不同的名称:例如,PostgreSQL将其称为**日志序列号(lsn, log sequence number)**,MySQL将其称为**二进制日志坐标(binlog coordinates)**。
|
||||
4. 当追随者处理完快照之后积压的数据变化,我们说它**赶上(caught up)**了主库。现在它可以继续处理领导者产生的数据变化了。
|
||||
|
||||
建立跟随者的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个有点神秘的多步骤工作流程,需要由管理员手动执行。
|
||||
|
||||
### 处理节点停机
|
||||
|
||||
系统中的任何节点都可能停机,也许因为意外的故障,也可能是计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。
|
||||
|
||||
### 处理节点失控
|
||||
如何通过基于领导者的复制实现高可用?
|
||||
|
||||
系统中的任何节点都可能因故障而意外停机,但可能由于计划内的维护(例如,重新引导机器以安装内核安全修补程序)。能够在不停机的情况下重新启动单个节点是操作和维护的一大优势。因此,我们的目标是保持整个系统的运行,尽管个别节点失效,并尽可能保持节点外部的影响。
|
||||
#### 从库失效:追赶恢复
|
||||
|
||||
如何通过基于领导者的复制实现高可用性?
|
||||
在其本地磁盘上,每个追随者记录从领导者收到的数据变更。如果跟随者崩溃并重新启动,或者如果领导者和追随者之间的网络暂时中断,则追随者可以很容易地恢复:从日志中知道在发生故障之前处理的最后一个事务。因此,追随者可以连接到领导者并请求在追随者断开连接时发生的所有数据改变。当它应用了这些变化后,它已经赶上了领导者,并可以像以前一样继续接收数据流的变化。
|
||||
|
||||
#### 从库故障:追赶恢复
|
||||
#### 主库失效:故障转移
|
||||
|
||||
在其本地磁盘上,每个追随者记录从领导者收到的数据变更。如果跟随者崩溃并重新启动,或者如果领导者和跟随者之间的网络暂时中断,则跟随者可以很容易地恢复:从日志中知道在发生故障之前处理的最后一个事务。因此,跟随者可以连接到领导者并请求在跟随者断开连接时发生的所有数据改变。当它应用了这些变化后,它已经赶上了领导者,并可以像以前一样继续接收数据流的变化。
|
||||
处理领导者的失败相当棘手:其中一个追随者需要被提升为新的领导者,需要重新配置客户端,以将它们的写操作发送给新的领导者,其他追随者需要开始拉取来自新领导者的数据变更。这个过程被称为**故障转移(failover)**。
|
||||
|
||||
#### 领导失败:故障转移
|
||||
故障转移可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的领导者)或自动进行。自动故障转移过程通常由以下步骤组成:
|
||||
|
||||
处理领导者的失败是棘手的:其中一个追随者需要被提升为新的领导者,客户端需要重新配置以将他们的写入发送给新的领导者,其他追随者需要开始消费来自新的领导。这个过程被称为故障转移。
|
||||
|
||||
故障转移可以手动进行(通知管理员领导已经失败,并采取必要的步骤来创建新的领导者)或自动进行。自动故障转移过程通常由以下步骤组成:
|
||||
|
||||
1. 确定领导失败。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是使用一个超时:节点频繁地在相互之间来回弹跳消息,并且如果一个节点在一段时间内(例如30秒)没有响应,它被认为是死的。 (如果领导被故意拆除进行计划维护,这不适用。)
|
||||
2. 选择一个新的领导者。这可以通过选举过程(其中领导者由大多数剩余复制品选择)来完成,或者可以由之前选定的控制器节点来指定新的领导者。领导者的最佳人选通常是来自旧领导者的最新数据变更的副本(以最小化任何数据丢失)。让所有的节点同意一个新的领导是一个共识问题,在第9章详细讨论。
|
||||
3. 重新配置系统以使用新的领导者。客户端现在需要将他们的写请求发送给新领导(我们将在“请求路由”在本页中讨论这个问题)。如果老领导人回来了,可能仍然认为这是领导者,没有意识到其他副本迫使他下台。系统需要确保老领导成为追随者,并认可新领导。
|
||||
1. 确认领导者失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是使用**超时(Timeout)**:节点相互之间频繁地来回传递消息,并且如果一个节点在一段时间内(例如30秒)没有响应,就认为它挂了(计划内故意关闭领导者不算)
|
||||
2. 选择一个新的领导者。这可以通过选举过程(领导者由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点(controller node)**来指定新的领导者。领导者的最佳人选通常是拥有旧领导者最新数据副本的追随者(最小化数据损失)。让所有的节点同意一个新的领导是一个**共识**问题,在第9章详细讨论。
|
||||
3. 重新配置系统以使用新的领导者。客户端现在需要将他们的写请求发送给新领导者(将在“请求路由”中讨论这个问题)。如果老领导回来,可能仍然认为自己是领导者,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个追随者。
|
||||
|
||||
故障转移会出现很多大麻烦:
|
||||
|
||||
* 如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作。在选出新主库后,如果老主库重新加入集群,新主库在此期间可能会收到冲突的写入,那这些写入该如何处理?最常见的解决方案是简单丢弃老主库未复制的写入,这很可能打破客户对于数据持久性的期望。
|
||||
|
||||
* 如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。例如在GitHub [13]的一场事故中,一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用,主键重用使得MySQL和Redis中数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
|
||||
* 发生某些故障时(见第8章)可能会出现两个节点都以为自己是领导者的情况。这种情况称为**脑裂(split brain)**,非常危险:如果两个领导者都可以接受写操作,却没有冲突解决机制(参见第168页的“多主复制”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个领导节点同时存在时会关闭其中一个节点。这种机制称为**击剑(fencing)**,更夸张的术语是:***爆其他节点的头(Shoot The Other Node In The Head , STONITH)***。但设计粗糙的机制可能最后会导致两个节点都被关闭[14]。
|
||||
* 领导被宣布死亡之前的正确超时是什么?在领导失败的情况下,超时时间越长意味着恢复的时间越长。但是,如果超时太短,可能会出现不必要的故障转移。例如,临时负载峰值可能导致节点的响应时间超过超时,或者网络故障可能导致延迟的数据包。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会使情况变得更糟,而不是更好。
|
||||
|
||||
这些问题没有简单的解决方案。因此,有些操作团队更愿意手动执行故障切换,即使软件支持自动故障切换。
|
||||
* 发生某些故障时(见第8章)可能会出现两个节点都以为自己是领导者的情况。这种情况称为**脑裂(split brain)**,非常危险:如果两个领导者都可以接受写操作,却没有冲突解决机制(参见第168页的“多主复制”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个领导节点同时存在时会关闭其中一个节点[^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭[14]。
|
||||
|
||||
这些问题 - 节点故障;不可靠的网络;并且在副本一致性,耐用性,可用性和延迟方面进行权衡 - 实际上是分布式系统中的基本问题。在第8章和第9章中,我们将更深入地讨论它们。
|
||||
[^ii]: 这种机制称为**击剑(fencing)**,充满感情的术语是:***爆彼之头(STONITH,Shoot The Other Node In The Head)***。
|
||||
|
||||
* 领导者被宣布死亡之前的正确超时应该怎么设?在领导者失效的情况下,超时时间越长,意味着恢复时间也越长。但是如果超时太短,又可能会出现不必要的故障转移。例如,临时负载峰值可能导致节点的响应时间超时,或网络故障可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。
|
||||
|
||||
这些问题没有简单的解决方案。因此即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障转移。
|
||||
|
||||
节点故障;不可靠的网络;对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。第8章和第9章将更深入地讨论它们。
|
||||
|
||||
### 复制日志的实现
|
||||
|
||||
基于领导者的复制如何在底层工作?在实践中使用了几种不同的复制方法,所以让我们简单地看一下。
|
||||
基于领导者的复制底层是如何工作的?实践中有好几种不同的复制方式,所以先简要地看一下。
|
||||
|
||||
#### 基于语句的复制
|
||||
|
||||
在最简单的情况下,领导者记录它执行的每个写入请求(语句)并将该语句日志发送给其追随者。对于关系数据库来说,这意味着每个INSERT,UPDATE或DELETE语句都被转发给关注者和每个关注者。跟随者解析并执行该SQL语句就像从客户端收到一样。
|
||||
在最简单的情况下,领导者记录它执行的每个写入请求(**语句(statement)**)并将该语句日志发送给其追随者。对于关系数据库来说,这意味着每个`INSERT`,`UPDATE`或`DELETE`语句都被转发给关注者和每个关注者。跟随者解析并执行该SQL语句就像从客户端收到一样。
|
||||
|
||||
虽然这听起来很合理,但这种复制方式可能有多种方式可以打破:
|
||||
虽然听上去很合理,但有很多问题会打破这种复制方式:
|
||||
|
||||
* 任何调用非确定性函数的语句(如`NOW()`获取当前日期和时间或`RAND()`获取一个随机数)可能会在每个副本上生成不同的值。
|
||||
* 如果语句使用自动增量列,或者它们依赖于数据库中的现有数据(例如,UPDATE ... WHERE <某些条件>),则必须按照每个副本上的完全相同的顺序执行它们,否则它们可能会有不同的效果。当有多个同时执行的事务时,这可能是限制性的。
|
||||
* 任何调用**非确定性函数(nondeterministic)**的语句(如`NOW()`获取当前日期和时间或`RAND()`获取一个随机数)可能会在每个副本上生成不同的值。
|
||||
* 如果语句使用**自增列(auto increment)**,或者它们依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会有不同的效果。当有多个同时执行的事务时,这可能产生很大的限制。
|
||||
* 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
|
||||
|
||||
有可能解决这些问题 - 例如,当语句被记录时,领导者可以用固定的返回值替换任何不确定的函数调用,以便追随者获得相同的值。但是,由于存在如此多的边缘情况,所以现在通常优选其他复制方法。
|
||||
有办法绕开问题 ——例如,当语句被记录时,领导者可以用固定的返回值替换任何不确定的函数调用,以便追随者获得相同的值。但是由于边缘情况如此之多,所以现在通常选择其他的复制方法。
|
||||
|
||||
基于语句的复制在5.1版本之前在MySQL中使用。今天仍然有一些使用,因为它非常紧凑,但是默认情况下,如果在语句中有任何不确定性,MySQL现在会切换到基于行的复制(稍后讨论)。 VoltDB使用基于语句的复制,并通过要求事务是确定性来保证安全[15]。
|
||||
基于语句的复制在5.1版本前的MySQL中使用。现在也还在一些地方用,因为它非常紧凑。但默认情况下,如果语句中有任何不确定性,MySQL现在会切换到基于行的复制(稍后讨论)。 VoltDB使用基于语句的复制,通过要求事务是确定性来保证安全[15]。
|
||||
|
||||
#### 传输预写式日志(WAL)
|
||||
|
||||
在第3章中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现通常每个写操作都附加到日志中:
|
||||
|
||||
* 对于日志结构存储引擎(请参阅第70页的“SSTables和LSM-tree”),此日志是存储的主要位置。日志段在后台压缩并垃圾回收。
|
||||
* 对于覆盖单个磁盘块的B树(请参阅第79页的“B树”),每次修改都会先写入预写日志,以便索引可以恢复到一致状态一个崩溃。
|
||||
* 对于日志结构存储引擎(请参阅“SSTables和LSM-tree”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
|
||||
* 对于覆写单个磁盘块的B树(请参阅第79页的“B树”),每次修改都会先写入**预写式日志(WAL, Write Ahead Log)**,以便崩溃后索引可以恢复到一个一致的状态。
|
||||
|
||||
在任何一种情况下,日志都是包含对数据库所有写入的字节序列。我们可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,领导者还可以通过网络将其发送给其追随者。
|
||||
在任何一种情况下,日志都是包含所有数据库写入的只追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,领导者还可以通过网络将其发送给其追随者。
|
||||
|
||||
当追随者处理这个日志时,它会建立一个和领导者一模一样的数据结构的副本。
|
||||
当追随者应用这个日志时,它会建立和领导者一模一样数据结构的副本。
|
||||
|
||||
PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志描述的数据很低:WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制紧密地耦合到存储引擎。如果数据库将其存储格式从一个版本更改为另一个版本,则通常不可能在领导者和追随者上运行不同版本的数据库软件。
|
||||
PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录的数据非常底层:WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本,通常不可能在领导者和追随者上运行不同版本的数据库软件。
|
||||
|
||||
这看起来可能只是一个小实施细节,但可能会产生巨大的运营影响。如果复制协议允许跟随者使用比领导者更新的软件版本,则可以先执行升级跟随者,然后执行故障转移,使升级后的节点之一成为新的领导者,从而执行数据库软件的零停机升级。如果复制协议不允许此版本不匹配(WAL运输经常出现这种情况),则此类升级需要停机。
|
||||
看上去这可能只是一个微小的实现细节,但却可能对运维产生巨大的影响。如果复制协议允许跟随者使用比领导者更新的软件版本,则可以先执行升级跟随者,然后执行故障转移,使升级后的节点之一成为新的领导者,从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配(传输WAL经常出现这种情况),则此类升级需要停机。
|
||||
|
||||
#### 逻辑(基于行)日志复制
|
||||
#### 逻辑日志复制(基于行)
|
||||
|
||||
另一种方法是对复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎(物理)数据表示区分开来。
|
||||
另一种方法是,复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来。
|
||||
|
||||
关系数据库的逻辑日志通常是以行的粒度描述对数据库表的写入的记录序列:
|
||||
|
||||
* 对于插入的行,日志包含所有列的新值。
|
||||
* 对于已删除的行,日志包含足够的信息来唯一标识已删除的行。通常这将是主键,但是如果表上没有主键,则需要记录所有列的旧值。
|
||||
* 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值。
|
||||
* 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
|
||||
|
||||
修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL的二进制日志(当配置为使用基于行的复制时)使用这种方法[17]。
|
||||
|
||||
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
|
||||
|
||||
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统(如数据),这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存[18]。 这种技术被称为更改数据捕获,我们将在第11章中回到它。
|
||||
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统(如数据),这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存[18]。 这种技术被称为**捕获数据变更(change data capture)**,第11章将重新讲到它。
|
||||
|
||||
#### 基于触发器的复制
|
||||
|
||||
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的 - 但是在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(请参阅第171页的“处理写入冲突”),则可能需要将复制移动到应用程序层。
|
||||
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(请参阅第171页的“处理写入冲突”),则可能需要将复制移动到应用程序层。
|
||||
|
||||
一些工具,如Oracle GoldenGate [19],可以通过读取数据库日志来使应用程序可用数据。另一种方法是使用许多关系数据库中可用的功能:触发器和存储过程。
|
||||
一些工具,如Oracle GoldenGate [19],可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
|
||||
|
||||
触发器允许您注册在数据库系统中发生数据更改(写入事务)时自动执行的自定义应用程序代码。触发器有机会将此更改记录到一个单独的表中,通过这个表可以通过外部过程读取它。然后,外部进程可以应用任何必要的应用程序逻辑,并将数据更改复制到另一个系统。例如,Databus for Oracle [20]和Bucardo for Postgres [21]就是这样工作的。
|
||||
触发器允许您注册在数据库系统中发生数据更改(写入事务)时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中,使用外部程序读取这个表,再加上任何业务逻辑处理,会后将数据变更复制到另一个系统去。例如,Databus for Oracle [20]和Bucardo for Postgres [21]就是这样工作的。
|
||||
|
||||
基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出现错误和限制。然而,由于其灵活性,它仍然是有用的。
|
||||
基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出错,也有很多限制。然而由于其灵活性,仍然是很有用的。
|
||||
|
||||
|
||||
|
||||
## 复制延迟问题
|
||||
|
||||
能够容忍节点故障只是需要复制的一个原因。正如在第二部分的介绍中提到的,其他原因是可扩展性(处理比单个机器可处理更多的请求)和延迟(将副本地理位置更靠近用户)。
|
||||
容忍节点故障只是需要复制的一个原因。正如在第二部分的介绍中提到的,另一个原因是可扩展性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更靠近用户)。
|
||||
|
||||
基于领导者的复制需要所有写入都通过单个节点,但只读查询可以转到任何副本。对于由大部分读取组成的工作负载以及只有很小比例的写入(Web上的常见模式),有一个有吸引力的选择:创建许多追随者,并将读取请求分布到以下各个位置。这将从领导者中移除负载,并允许读取请求由附近的副本服务。
|
||||
基于领导者的复制要求所有写入都由一个节点处理,但只读查询可以由任何副本处理。所以对于读多写少(Web上的常见模式)的场景,一个有吸引力的选择是创建很多追随者,并将读取请求分散到所有的从库上去。这样能减小主库的负载,并允许向最近的副本发送读请求。
|
||||
|
||||
在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制 - 如果您尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置将是非常不可靠的。
|
||||
|
||||
不幸的是,如果应用程序从异步跟随者读取,如果跟随者落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:如果同时对领导者和跟随者执行相同的查询,则可能得到不同的结果,因为并非所有的写入都反映在跟随者中。这种不一致只是一个暂时的状态 - 如果你不再写数据库并等待一段时间,追随者最终会赶上并与领导者保持一致。出于这个原因,这种效应被称为最终一致性[22,23] (道格拉斯·特里(Douglas Terry)等人创造了最终的一致性。 [24],由Werner Vogels [22]推广,成为许多NoSQL项目的战斗口号。 但是,不仅NoSQL数据库最终是一致的:异步复制关系数据库中的追随者具有相同的特征。)
|
||||
不幸的是,当应用程序从异步追随者读取时,如果追随者落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对领导者和跟随者执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在跟随者中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,追随者最终会赶上并与领导者保持一致。出于这个原因,这种效应被称为**最终一致性(eventually consistency)**[^iii][22,23]
|
||||
|
||||
“最终”一词故意含糊不清:总的来说,复制品落后的程度是没有限制的。在正常的操作中,在领导者身上发生的写作和在追随者身上反映的延迟 - 复制滞后 - 可能仅仅是一秒的一部分,在实践中并不明显。但是,如果系统在接近容量的情况下运行,或者如果网络中存在问题,则滞后可以容易地增加到几秒甚至几分钟。
|
||||
[^iii]: 道格拉斯·特里(Douglas Terry)等人创造了术语最终一致性。 [24] 并经由Werner Vogels [22]推广,成为许多NoSQL项目的战吼。 然而,不只有NoSQL数据库是最终一致的:关系型数据库中的异步复制追随者也有相同的特性。
|
||||
|
||||
当滞后时间太长时,引入的不一致性不仅仅是一个理论问题,而且是一个真正的应用问题。在本节中,我们将重点介绍三个复制滞后时可能出现的问题,并概述解决这些问题的一些方法。
|
||||
“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟(replication lag)**,即写入主库到反映至追随者之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
|
||||
|
||||
因为滞后时间太长引入的不一致性,可不仅是一个理论问题,更是应用设计中会遇到的真实问题。本节将重点介绍三个由复制延迟问题的例子,并简述解决这些问题的一些方法。
|
||||
|
||||
### 读已之写
|
||||
|
||||
许多应用程序让用户提交一些数据,然后查看他们提交的内容。这可能是客户数据库中的记录,也可能是对讨论主题的评论,或其他类似的内容。提交新数据时,必须将其发送给领导者,但是当用户查看数据时,可以从追随者读取数据。如果数据经常被查看,但只是偶尔写入,这是特别适合的。
|
||||
许多应用程序让用户提交一些数据,然后查看他们提交的内容。可能是用户数据库中的记录,也可能是对讨论主题的评论,或其他类似的内容。提交新数据时,必须将其发送给领导者,但是当用户查看数据时,可以从追随者读取。如果数据经常被查看,但只是偶尔写入,这是非常合适的。
|
||||
|
||||
对于异步复制,存在一个问题,如图5-3所示:如果用户在写入后不久查看数据,则新数据可能尚未到达副本。对用户来说,他们看起来好像是提交的数据丢失了,所以他们可以理解的不高兴
|
||||
但对于异步复制,问题就来了。如[图5-3](fig5-3.png)所示:如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了,用户会不高兴,可以理解。
|
||||
|
||||
![](img/fig5-3.png)
|
||||
|
||||
**图5-3 用户进行写入,然后从旧副本中读取数据。 为了防止这种异常,我们需要写后读的一致性**
|
||||
**图5-3 用户写入后从旧副本中读取数据。需要写后读(read-after-write)的一致性来防止这种异常**
|
||||
|
||||
在这种情况下,我们需要读后写一致性,也称为读己之写一致性[24]。这是一个保证,如果用户重新加载页面,他们总是会看到他们自己提交的任何更新。它不会对其他用户做出承诺:其他用户的更新可能会在稍后才能看到。但是,它保证用户自己的输入已被正确保存。
|
||||
在这种情况下,我们需要**写后读一致性(read-after-write consistency)**,也称为**读己之写一致性(read-your-writes consistency)**[24]。这是一个保证,如果用户重新加载页面,他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺:其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
|
||||
|
||||
我们如何在基于领导者的复制的系统中实现读后一致性?有各种可能的技术。提一些:
|
||||
如何在基于领导者的复制系统中实现读后一致性?有各种可能的技术,这里说一些:
|
||||
|
||||
* 阅读用户可能已经修改过的内容时,请阅读领导者;否则,从追随者读取它。这就要求你有一些方法可以知道是否修改了某些东西,而不需要实际查询它。例如,社交网络上的用户个人资料信息通常只能由个人资料的所有者编辑,而不能由其他人编辑。因此,一个简单的规则是:总是从领导读取用户自己的个人资料,从跟随者读取其他用户的个人资料。
|
||||
* 如果应用程序中的大部分内容都可能被用户编辑,那么这种方法将不会有效,因为大部分内容都必须从领导读取(否定读取缩放的好处)。在这种情况下,可以使用其他标准来决定是否从领导读取。例如,您可以跟踪上次更新的时间,并且在上次更新后的一分钟内,从领导者进行所有读取。您还可以监控追随者的复制滞后,并防止任何超过领导者一分多钟的追随者查询。
|
||||
* 客户端可以记住最近一次写入的时间戳,然后系统可以确保为该用户提供任何读取的副本反映更新,至少在该时间戳之前。如果复制副本不够充足,则可以由另一副本处理读取,或者查询可以等到复制副本赶上。时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;请参阅“不可靠的时钟”(第269页))。
|
||||
* 如果您的副本分布在多个数据中心(与用户的地理接近度或可用性),则会增加复杂性。任何需要由领导者提供服务的请求都必须路由到包含领导者的数据中心。
|
||||
* 读用户**可能已经修改过**的内容时,都从主库读;这就要求有一些方法,不用实际查询就可以知道用户是否修改了某些东西。举个例子,社交网络上的用户个人资料信息通常只能由用户本人编辑,而不能由其他人编辑。因此一个简单的规则是:从主库读取用户自己的档案,在从库读取其他用户的档案。
|
||||
|
||||
如果同一用户从多个设备访问您的服务,例如桌面Web浏览器和移动应用程序,则会出现另一个复杂情况。在这种情况下,您可能希望提供跨设备读写后一致性:如果用户在某个设备上输入了一些信息,然后在另一个设备上查看,则应该看到他们刚输入的信息。
|
||||
* 如果应用中的大部分内容都可能被用户编辑,那这种方法就没用了,因为大部分内容都必须从主库读取(扩容读就没效果了)。在这种情况下可以使用其他标准来决定是否从主库读取。例如可以跟踪上次更新的时间,在上次更新后的一分钟内,从主库读。还可以监控从库的复制延迟,防止任向任何滞后超过一分钟到底从库发出查询。
|
||||
|
||||
* 客户端可以记住最近一次写入的时间戳,系统需要确保从库为该用户提供任何查询时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另一个从库读,或者等待从库追赶上来。
|
||||
|
||||
时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;参阅“不可靠的时钟”)。
|
||||
|
||||
* 如果您的副本分布在多个数据中心(出于可用性目的与用户尽量在地理上接近),则会增加复杂性。任何需要由领导者提供服务的请求都必须路由到包含主库的数据中心。
|
||||
|
||||
另一种复杂的情况是:如果同一个用户从多个设备请求服务,例如桌面浏览器和移动APP。这种情况下可能就需要提供跨设备的写后读一致性:如果用户在某个设备上输入了一些信息,然后在另一个设备上查看,则应该看到他们刚输入的信息。
|
||||
|
||||
在这种情况下,还有一些需要考虑的问题:
|
||||
|
||||
* 需要记住用户上次更新时间戳的方法变得更加困难,因为在一台设备上运行的代码不知道在另一台设备上发生了什么更新。这个元数据将需要集中。
|
||||
* 如果副本分布在不同的数据中心,则不能保证来自不同设备的连接将路由到同一数据中心。 (例如,如果用户的台式计算机使用家庭宽带连接,并且他们的移动设备使用蜂窝数据网络,则设备的网络路线可能完全不同)。如果您的方法需要领导者阅读,您可能首先需要将来自所有用户设备的请求路由到同一个数据中心。
|
||||
* 记住用户上次更新时间戳的方法变得更加困难,因为一台设备上运行的程序不知道另一台设备上发生了什么。元数据需要一个中心存储。
|
||||
* 如果副本分布在不同的数据中心,很难保证来自不同设备的连接会路由到同一数据中心。 (例如,用户的台式计算机使用家庭宽带连接,而移动设备使用蜂窝数据网络,则设备的网络路线可能完全不同)。如果你的方法需要读主库,可能首先需要把来自同一用户的请求路由到同一个数据中心。
|
||||
|
||||
|
||||
|
||||
### 单调读
|
||||
|
||||
我们从异步从库读取异常的第二个例子是,用户可能会感受到时光倒流。
|
||||
从异步从库读取第二个异常例子是,用户可能会遇到**时光倒流(moving backward in time)**。
|
||||
|
||||
如果用户从不同副本进行多次读取,则可能发生这种情况。例如,图5-4显示了用户2345两次进行相同的查询,首先是一个滞后很少的追随者,然后是一个滞后较大的追随者。 (如果用户刷新一个网页,并且每个请求被路由到一个随机的服务器,这种情况是相当可能的。)第一个查询返回最近由用户1234添加的评论,但是第二个查询不返回任何东西,因为滞后的追随者还没有拿起写。实际上,第二个查询是在比第一个查询更早的时间点观察系统。如果第一个查询没有返回任何内容,这将不会那么糟糕,因为用户2345可能不知道用户1234最近添加了评论。然而,如果用户2345第一次看见用户1234的评论,然后看到它再次消失,那么对于用户2345来说,这是非常混乱的。
|
||||
如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图5-4](img/fig5-4.png)显示了用户2345两次进行相同的查询,首先查询了一个延迟很小的从库,然后是一个延迟较大的从库。 (如果用户刷新网页,而每个请求被路由到一个随机的服务器,这种情况是很有可能的。)第一个查询返回最近由用户1234添加的评论,但是第二个查询不返回任何东西,因为滞后的从库还没有拉取写入内容。在效果上相比第一个查询,第二个查询是在更早的时间点来观察系统。如果第一个查询没有返回任何内容,那问题并不大,因为用户2345可能不知道用户1234最近添加了评论。但如果用户2345先看见用户1234的评论,然后又看到它消失,那么对于用户2345,就很让人头大了。
|
||||
|
||||
![](img/fig5-4.png)
|
||||
|
||||
**图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
|
||||
|
||||
单调读取[23]是这种异常不会发生的保证。这是一个比强有力的一致性更小的保证,但比最终的一致性更强有力的保证。当您读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,在先前读取较新的数据之后,他们将不读取较旧的数据。
|
||||
**单调读(Monotonic reads)**[23]是这种异常不会发生的保证。这是一个比**强一致性(strong consistency)**更弱,但比**最终一致性(eventually consistency)**更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
|
||||
|
||||
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取(不同的用户可以从不同的副本读取)。例如,可以基于用户ID的散列来选择副本,而不是随机选择副本。但是,如果该副本失败,用户的查询将需要重新路由到另一个副本。
|
||||
|
||||
|
||||
|
||||
### 一致前缀读
|
||||
|
||||
我们的第三个复制例子滞后于反常因果关系。 想象一下Poons先生和Cake夫人之间的以下简短对话:
|
||||
第三个复制延迟例子违反了因果律。 想象一下Poons先生和Cake夫人之间的以下简短对话:
|
||||
|
||||
*Poons先生*
|
||||
蛋糕夫人,你能看到未来有多远?
|
||||
*蛋糕夫人*
|
||||
通常约十秒钟,庞斯先生。
|
||||
这两句话之间有因果关系:蛋糕夫人听到了庞斯先生的问题并回答了这个问题。
|
||||
现在,想象第三个人正在通过追随者来听这个对话。 Cake夫人所说的事情经历了一个很少滞后的追随者,但Poons先生所说的事情有更长的复制滞后时间(见图5-5)。 这个观察者会听到以下内容:
|
||||
蛋糕夫人
|
||||
通常约十秒钟,庞斯先生。
|
||||
Poons先生
|
||||
蛋糕夫人,你能看到未来有多远?
|
||||
对于观察者来说,看起来好像克莱尔太太正在回答这个问题。
|
||||
Poons甚至问过它。 这种精神力量是令人印象深刻的,但非常混乱[25]。
|
||||
> *Mr. Poons*
|
||||
> Mrs. Cake,你能看到未来有多远?
|
||||
> *Mrs. Cake*
|
||||
> 通常约十秒钟,Mr. Poons.
|
||||
|
||||
这两句话之间有因果关系:Cake夫人听到了Poons先生的问题并回答了这个问题。
|
||||
|
||||
现在,想象第三个人正在通过从库来听这个对话。 Cake夫人说的内容是从一个延迟很低的从库读取的,但Poons先生所说的内容,从库的延迟要大的多(见图5-5)。 于是,这个观察者会听到以下内容:
|
||||
|
||||
> *Mrs. Cake*
|
||||
> 通常约十秒钟,Mr. Poons.
|
||||
>
|
||||
> *Mr. Poons*
|
||||
> Mrs. Cake,你能看到未来有多远?
|
||||
|
||||
对于观察者来说,看起来好像Cake夫人在Poons先生发问前就回答了这个问题。
|
||||
这种超能力让人印象深刻,但也会把人搞糊涂。[25]。
|
||||
|
||||
![](img/fig5-5.png)
|
||||
|
||||
**图5-5 如果某些分区的复制速度慢于其他分区,那么观察者在看到问题之前可能会看到答案。**
|
||||
|
||||
防止这种异常需要另一种类型的保证:一致的前缀读取[23]。 这个保证说,如果一系列的写入按照某个顺序发生,那么读取这些写入的任何人都会看到它们以相同的顺序出现。
|
||||
|
||||
这是分区(分片)数据库中的一个特殊问题,我们将在第6章中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
一种解决方案是确保任何因果关系的写入都写入相同的分区,但在某些无法高效完成的应用程序中。还有一些算法明确地跟踪因果依赖关系,我们将在“关系和并发”一节第186页中的“发生”中返回一个主题。
|
||||
防止这种异常,需要另一种类型的保证:**一致前缀读(consistent prefix reads)**[23]。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
|
||||
|
||||
这是**分区(partitioned)**(**分片(sharded)**)数据库中的一个特殊问题,将在第6章中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
|
||||
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“关系与并发”一节中返回这个主题。
|
||||
|
||||
### 复制延迟的解决方案
|
||||
|
||||
在使用最终一致的系统时,如果复制延迟增加到几分钟甚至几小时,则应该考虑应用程序的行为。如果答案是“没问题”,那很好。但是,如果结果对于用户来说是一个不好的经验,那么设计系统来提供更强的保证是很重要的,例如写后读。假设事实上它是异步的复制是同步的,这是一个问题。
|
||||
在使用最终一致的系统时,如果复制延迟增加到几分钟甚至几小时,则应该考虑应用程序的行为。如果答案是“没问题”,那很好。但如果结果对于用户来说是不好体验,那么设计系统来提供更强的保证是很重要的,例如**写后读**。明明是异步复制却假设复制是同步的,这是很多麻烦的根源。
|
||||
|
||||
如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过对领导者进行某种读取。但是,在应用程序代码中处理这些问题是复杂的,容易出错。
|
||||
|
||||
如果应用程序开发人员不必担心细微的复制问题,并且可以相信他们的数据库“做正确的事情”,那将会更好。这就是事务存在的原因:它们是数据库提供更强保障的一种方式这样应用程序可以更简单。
|
||||
|
||||
单节点事务已经存在很长时间了。然而,在向分布式(复制和分区)数据库转移时,许多系统放弃了这些数据库,声称交易在性能和可用性方面过于昂贵,并断言在可扩展系统中最终的一致性是不可避免的。这个陈述中有一些事实,但它过于简单,我们将在本书其余部分的过程中形成一个更细致的观点。我们将回到第七章和第九章的交易话题,我们将在第三部分讨论一些替代机制。
|
||||
如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。
|
||||
|
||||
如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是**事务(transaction)**存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更假简单。
|
||||
|
||||
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可扩展系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。
|
||||
|
||||
## 多主复制
|
||||
|
||||
本章到目前为止,我们只考虑使用单个领导的复制架构。 虽然这是一种常见的方法,但也有一些有趣的选择。
|
||||
|
||||
基于领导者的复制有一个主要的缺点:只有一个领导者,所有的写作都必须通过它.(如果数据库被分区(见第6章),每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。)如果由于任何原因(例如由于你和领导之间的网络中断)而无法连接到领导者, 你不能写入数据库。
|
||||
基于领导者的复制有一个主要的缺点:只有一个领导者,所有的写入都必须通过它.(如果数据库被分区(见第6章),每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。)如果由于任何原因(例如由于你和领导之间的网络中断)而无法连接到领导者, 你不能写入数据库。
|
||||
|
||||
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 我们称之为多领导配置(也称为主 - 主或主动/主动复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
|
||||
|
||||
@ -289,10 +308,9 @@ Poons甚至问过它。 这种精神力量是令人印象深刻的,但非常
|
||||
|
||||
#### 运维多数据中心
|
||||
|
||||
想象一下,你有一个数据库在几个不同的数据中心(也许这样你可以容忍整个数据中心的故障,或者为了更接近你的用户)复制品。 使用常规的基于领导者的复制设置,领导者必须位于其中一个数据中心,并且所有写入都必须经过该数据中心。
|
||||
在多领导配置中,您可以在每个数据中心都有领导。 图5-6显示了这个架构的外观。 在每个数据中心内,使用常规的领导者跟随者复制; 在数据中心之间,每个数据中心的负责人都会将其更改复制到其他数据中心的领导。
|
||||
想象一下,你有一个数据库在几个不同的数据中心(也许这样你可以容忍整个数据中心的故障,或者为了更接近你的用户)有副本。 使用常规的基于领导者的复制设置,领导者必须位于其中一个数据中心,并且所有写入都必须经过该数据中心。
|
||||
|
||||
#####
|
||||
在多领导配置中,您可以在每个数据中心都有领导。 图5-6显示了这个架构的外观。 在每个数据中心内,使用常规的领导者跟随者复制; 在数据中心之间,每个数据中心的负责人都会将其更改复制到其他数据中心的领导。
|
||||
|
||||
![](img/fig5-6.png)
|
||||
|
||||
@ -300,17 +318,17 @@ Poons甚至问过它。 这种精神力量是令人印象深刻的,但非常
|
||||
|
||||
我们来比较一下多数据中心部署中的单引擎和多引导器配置:
|
||||
|
||||
* *性能*
|
||||
***性能***
|
||||
|
||||
在单领导配置中,每个写作都必须通过互联网与领导者一起进入数据中心。这可能会增加写入时间,并可能违反了首先有多个数据中心的目的。在多领导配置中,每个写操作都可以在本地数据中心进行处理,并与其他数据中心异步复制。因此,数据中心之间的网络延迟对用户来说是隐藏的,这意味着感知的性能可能会更好。
|
||||
在单领导配置中,每个写作都必须通过互联网与领导者一起进入数据中心。这可能会增加写入时间,并可能违反了首先有多个数据中心的目的。在多领导配置中,每个写操作都可以在本地数据中心进行处理,并与其他数据中心异步复制。因此,数据中心之间的网络延迟对用户来说是隐藏的,这意味着感知的性能可能会更好。
|
||||
|
||||
* *数据中心的中断容忍*
|
||||
***数据中心的中断容忍***
|
||||
|
||||
在单引导者配置中,如果引导者的数据中心发生故障,故障转移可以促使另一个数据中心的追随者成为领导者。在多领导者配置中,每个数据中心可以独立于其他数据中心继续运行,并且当发生故障的数据中心恢复联机时,复制将迎头赶上。
|
||||
在单引导者配置中,如果引导者的数据中心发生故障,故障转移可以促使另一个数据中心的追随者成为领导者。在多领导者配置中,每个数据中心可以独立于其他数据中心继续运行,并且当发生故障的数据中心恢复联机时,复制将迎头赶上。
|
||||
|
||||
* *容忍网络问题*
|
||||
***容忍网络问题***
|
||||
|
||||
数据中心之间的通信通常通过公共互联网,这可能不如数据中心内的本地网络可靠。单引号配置对这个数据中心链接中的问题非常敏感,因为通过这个链接进行写操作是同步的。具有异步复制功能的多领导者配置通常可以更好地承受网络问题:暂时的网络中断不会妨碍正在处理的写入。
|
||||
数据中心之间的通信通常通过公共互联网,这可能不如数据中心内的本地网络可靠。单引号配置对这个数据中心链接中的问题非常敏感,因为通过这个链接进行写操作是同步的。具有异步复制功能的多领导者配置通常可以更好地承受网络问题:暂时的网络中断不会妨碍正在处理的写入。
|
||||
|
||||
有些数据库默认情况下支持多领导配置,但通常也使用外部工具实现,例如用于MySQL的Tungsten Replicator [26],用于PostgreSQL的BDR [27]以及用于Oracle的GoldenGate [19]。
|
||||
|
||||
@ -337,11 +355,10 @@ Poons甚至问过它。 这种精神力量是令人印象深刻的,但非常
|
||||
|
||||
但是,为了加速协作,您可能希望将更改的单位设置得非常小(例如,一个按键),并避免锁定。这种方法允许多个用户同时进行编辑,但同时也带来了多领导者复制的所有挑战,包括需要解决冲突[32]。
|
||||
|
||||
|
||||
|
||||
### 处理写冲突
|
||||
|
||||
多领导者复制的最大问题是可能发生写冲突,这意味着需要解决冲突。
|
||||
|
||||
例如,考虑一个由两个用户同时编辑的维基页面,如图5-7所示。用户1将页面的标题从A更改为B,并且用户2同时将标题从A更改为C.每个用户的更改已成功应用到其本地领导。但是,当异步复制时,会发现冲突[33]。单引导数据库中不会出现此问题。
|
||||
|
||||
![](img/fig5-7.png)
|
||||
@ -395,17 +412,18 @@ Poons甚至问过它。 这种精神力量是令人印象深刻的,但非常
|
||||
|
||||
|
||||
|
||||
#### *题外话:自动冲突解决*
|
||||
|
||||
冲突解决规则可能很快变得复杂,并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子,由于冲突解决处理程序令人惊讶的效果:一段时间以来,购物车上的冲突解决逻辑将保留添加到购物车的物品,但不包括从购物车中移除的物品。因此,顾客有时会看到物品重新出现在他们的购物车中,即使他们之前已经被移走[37]。
|
||||
|
||||
已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几行研究值得一提:
|
||||
|
||||
* 无冲突的复制数据类型(CRDT)[32,38]是可以由多个用户同时编辑的集合,映射,有序列表,计数器等的一系列数据结构,它们以合理的方式自动解决冲突。一些CRDT已经在Riak 2.0中实现[39,40]。
|
||||
* 可合并的持久数据结构[41]显式跟踪历史记录,类似于Git版本控制系统,并使用三向合并功能(而CRDT使用双向合并)。
|
||||
* 可进行的转换[42]是Etherpad [30]和Google Docs [31]等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。
|
||||
|
||||
这些算法在数据库中的实现还很年轻,但很可能将来它们将被集成到更多的复制数据系统中。自动冲突解决方案可以使应用程序处理多领导者数据同步更为简单。
|
||||
> #### 题外话:自动冲突解决
|
||||
>
|
||||
> 冲突解决规则可能很快变得复杂,并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子,由于冲突解决处理程序令人惊讶的效果:一段时间以来,购物车上的冲突解决逻辑将保留添加到购物车的物品,但不包括从购物车中移除的物品。因此,顾客有时会看到物品重新出现在他们的购物车中,即使他们之前已经被移走[37]。
|
||||
>
|
||||
> 已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几行研究值得一提:
|
||||
>
|
||||
> * 无冲突的复制数据类型(CRDT)[32,38]是可以由多个用户同时编辑的集合,映射,有序列表,计数器等的一系列数据结构,它们以合理的方式自动解决冲突。一些CRDT已经在Riak 2.0中实现[39,40]。
|
||||
> * 可合并的持久数据结构[41]显式跟踪历史记录,类似于Git版本控制系统,并使用三向合并功能(而CRDT使用双向合并)。
|
||||
> * 可进行的转换[42]是Etherpad [30]和Google Docs [31]等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。
|
||||
>
|
||||
> 这些算法在数据库中的实现还很年轻,但很可能将来它们将被集成到更多的复制数据系统中。自动冲突解决方案可以使应用程序处理多领导者数据同步更为简单。
|
||||
>
|
||||
|
||||
|
||||
|
||||
@ -449,8 +467,6 @@ Poons甚至问过它。 这种精神力量是令人印象深刻的,但非常
|
||||
|
||||
如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。
|
||||
|
||||
#####
|
||||
|
||||
## 无主复制
|
||||
|
||||
我们在本章到目前为止所讨论的复制方法 - 单引导者和多引导者复制 - 是基于客户端向一个节点(领导者)发送写请求的想法,数据库系统负责复制写入其他副本。领导决定了写入的顺序,而跟随者按相同的顺序应用领导的写入。
|
||||
@ -625,13 +641,15 @@ LWW实现了最终收敛的目标,但是以持久性为代价:如果同一
|
||||
|
||||
|
||||
|
||||
#### *并发性,时间和相对性*
|
||||
> #### 并发性,时间和相对性
|
||||
>
|
||||
> 如果两个操作“同时”发生,似乎应该称为并发 - 但事实上,它们是否在时间上重叠并不重要。由于分布式系统中的时钟问题,实际上很难判断两个事件是否同时发生,这个问题我们将在第8章中详细讨论。
|
||||
>
|
||||
> 为了定义并发性,确切的时间并不重要:如果两个操作都不知道对方,我们只需调用两个并发操作,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来[54],它引入了信息不能比光速更快的思想。因此,如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。
|
||||
>
|
||||
> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作可能是并行的。例如,如果网络缓慢或中断,两个操作可能会发生一段时间,并且仍然是并发的,因为网络问题阻止一个操作能够知道另一个操作。
|
||||
|
||||
如果两个操作“同时”发生,似乎应该称为并发 - 但事实上,它们是否在时间上重叠并不重要。由于分布式系统中的时钟问题,实际上很难判断两个事件是否同时发生,这个问题我们将在第8章中详细讨论。
|
||||
|
||||
为了定义并发性,确切的时间并不重要:如果两个操作都不知道对方,我们只需调用两个并发操作,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来[54],它引入了信息不能比光速更快的思想。因此,如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。
|
||||
|
||||
在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作可能是并行的。例如,如果网络缓慢或中断,两个操作可能会发生一段时间,并且仍然是并发的,因为网络问题阻止一个操作能够知道另一个操作。
|
||||
|
||||
#### 捕捉此前发生关系
|
||||
|
||||
@ -717,6 +735,8 @@ LWW实现了最终收敛的目标,但是以持久性为代价:如果同一
|
||||
|
||||
能够处理比单个机器更高的读取量可以通过对副本进行读取来处理
|
||||
|
||||
|
||||
|
||||
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(甚至不考虑更隐蔽的故障,例如由于软件错误导致的无提示数据损坏)。
|
||||
|
||||
我们讨论了复制的三种主要方法:
|
||||
@ -734,19 +754,21 @@ LWW实现了最终收敛的目标,但是以持久性为代价:如果同一
|
||||
客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
|
||||
每种方法都有优点和缺点。单引导复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点,网络中断和延迟峰值的情况下,多领导者和无领导者复制可以更加稳健,但代价很难推理,只能提供非常弱的一致性保证。
|
||||
|
||||
|
||||
|
||||
复制可以是同步的,也可以是异步的,在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是在复制滞后增加和服务器故障时要弄清楚会发生什么,这一点很重要。如果一个领导者失败了,并且你推动一个异步更新的追随者成为新的领导者,那么最近承诺的数据可能会丢失。
|
||||
|
||||
我们研究了一些可能由复制滞后引起的奇怪效应,我们讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:
|
||||
|
||||
***读写后一致性***
|
||||
***写后读***
|
||||
|
||||
用户应该总是看到自己提交的数据。
|
||||
|
||||
***单调读取***
|
||||
***单调读***
|
||||
|
||||
当用户在某个时间点看到数据后,他们不应该在较早的时间点看到数据。
|
||||
|
||||
***一致的前缀读取***
|
||||
***一致前缀读***
|
||||
|
||||
用户应该将数据视为具有因果意义的状态:例如,按照正确的顺序查看问题及其答复。
|
||||
|
||||
|
12
ddia/ch6.md
12
ddia/ch6.md
@ -2,7 +2,7 @@
|
||||
|
||||
![](img/ch6.png)
|
||||
|
||||
> 显而易见,我们必须从电脑指令序列与限制约束中跳出来。 我们应当叙述定义、元数据、关系,而不是过程。
|
||||
> 我们必须跳出电脑指令序列的窠臼。 叙述定义、描述元数据、梳理关系,而不是编写过程。
|
||||
>
|
||||
> —— Grace Murray Hopper,未来的计算机及其管理(1962)
|
||||
>
|
||||
@ -15,15 +15,11 @@
|
||||
|
||||
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与网络分区(net splits)无关,这是节点之间网络中的一种故障类型。我们将在第8章讨论这些错误。
|
||||
|
||||
|
||||
|
||||
> ##### 术语澄清
|
||||
>
|
||||
> 我们在这里称之为分区`partition`的东西,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**;在HBase中称之为**区域(Region)**,Bigtable中的 `tablet`,Cassandra和Riak中的`vnode`以及Couchbase中的`vBucket`。但是,分区(partition)是最为重要的术语,所以我们坚持使用它。
|
||||
> 我们在这里称之为**分区(partition)**的东西,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**;在HBase中称之为**区域(Region)**,Bigtable中的 **表块(tablet)**,Cassandra和Riak中**虚节点(vnode)**以及Couchbase中的**虚桶(vBucket)**。但是**分区(partition)**是最重要的术语,所以这里坚持使用它。
|
||||
>
|
||||
|
||||
|
||||
|
||||
通常情况下,分区是这样定义的,即每条数据(每条记录,每行或每个文档)只属于一个分区。有很多方法可以实现这一点,本章将深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时触及多个分区的操作。
|
||||
|
||||
要分区数据的主要原因是可扩展性。不同的分区可以放在不共享的集群中的不同节点上(请参阅[第二部分](part-ii.md)关于无共享架构的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
|
||||
@ -34,16 +30,12 @@
|
||||
|
||||
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察数据索引如何与分区交互。然后,我们将讨论重新平衡,如果您想要添加或删除群集中的节点,则必须进行重新平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
|
||||
|
||||
|
||||
|
||||
## 分片与复制
|
||||
|
||||
![](img/fig6-1.png)
|
||||
|
||||
**图6-1 组合使用复制和分区:每个节点充当某些分区的领导者,其他分区充当追随者。**
|
||||
|
||||
|
||||
|
||||
## 键值数据的分片
|
||||
|
||||
假设你有大量的数据,你想分割它。你如何决定在哪些节点上存储哪些记录?
|
||||
|
61
ddia/ch9.md
61
ddia/ch9.md
@ -7,16 +7,48 @@
|
||||
|
||||
---------------
|
||||
|
||||
[TOC]
|
||||
|
||||
|
||||
|
||||
## 一致性保证
|
||||
|
||||
## 线性一致性
|
||||
|
||||
### 什么使得系统线性一致?
|
||||
|
||||
### 依赖线性一致性
|
||||
|
||||
### 实现线性一致的系统
|
||||
|
||||
### 线性一致性的代价
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 顺序保证
|
||||
|
||||
### 顺序与因果
|
||||
|
||||
### 序列号顺序
|
||||
|
||||
### 全局序列广播
|
||||
|
||||
|
||||
|
||||
## 分布式事务与共识
|
||||
|
||||
### 原子提交与二阶段提交
|
||||
|
||||
### 实践中的分布式事务
|
||||
|
||||
### 容错的共识
|
||||
|
||||
### 成员与协调服务
|
||||
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
|
||||
@ -476,31 +508,40 @@
|
||||
|
||||
![](img/fig9-1.png)
|
||||
|
||||
##### 图9-1
|
||||
图9-1
|
||||
|
||||
![](img/fig9-2.png)
|
||||
##### 图9-2
|
||||
|
||||
图9-2
|
||||
|
||||
![](img/fig9-3.png)
|
||||
##### 图9-3
|
||||
|
||||
图9-3
|
||||
|
||||
![](img/fig9-4.png)
|
||||
##### 图9-4
|
||||
|
||||
图9-4
|
||||
|
||||
![](img/fig9-5.png)
|
||||
##### 图9-5
|
||||
|
||||
图9-5
|
||||
|
||||
![](img/fig9-6.png)
|
||||
##### 图9-6
|
||||
|
||||
图9-6
|
||||
|
||||
![](img/fig9-7.png)
|
||||
##### 图9-7
|
||||
|
||||
图9-7
|
||||
|
||||
![](img/fig9-8.png)
|
||||
##### 图9-8
|
||||
|
||||
图9-8
|
||||
|
||||
![](img/fig9-9.png)
|
||||
##### 图9-9
|
||||
|
||||
图9-9
|
||||
|
||||
![](img/fig9-10.png)
|
||||
##### 图9-10
|
||||
|
||||
图9-10
|
Loading…
Reference in New Issue
Block a user