fix: refactor text format

This commit is contained in:
ZhengLin Li
2023-01-28 13:44:02 +08:00
committed by qtmuniao
parent 25480f9a9f
commit 33a7a994c7
11 changed files with 517 additions and 678 deletions

View File

@@ -5,59 +5,60 @@
## 目录
### [序](preface.md)
### 第一部分:数据系统基础
* [第一章:可靠、可扩展、可维护](ch01.md) [[b站](https://www.bilibili.com/video/BV1bY411L7HA) [Youtube](https://www.youtube.com/watch?v=geVsm3YGF4A&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=1)]
* [本书为什么以数据系统为主题](ch01.md#本书为什么以数据系统为主题)
* [可靠性](ch01.md#可靠性)
* [可伸缩性](ch01.md#可伸缩性)
* [可维护性](ch01.md#可维护性)
* [第二章:数据模型和查询语言](ch02.md) [b([上](https://www.bilibili.com/video/BV19a411C7UN) [](https://www.bilibili.com/video/BV1BZ4y1r79M)) Youtube ([](https://www.youtube.com/watch?v=1TVdmZ_sJCM&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=2) [](https://www.youtube.com/watch?v=GdihZOSMuuI&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=3))]
* [概要](ch02.md#概要)
* [关系模型 vs 文档模型](ch02.md#关系模型与文档模型)
* [数据查询语言](ch02.md#数据查询语言)
* [图模型](ch02.md#图模型)
* [第三章:存储与查询](ch03.md) [b([上](https://www.bilibili.com/video/BV1mL411P72H/) [](https://www.bilibili.com/video/BV1bL411A7ga)) Youtube([](https://www.youtube.com/watch?v=tI1BmIZpK-c&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=4) [](https://www.youtube.com/watch?v=8wiJSECzADc&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=5))]
* [底层数据结构](ch03.md#驱动数据库的底层数据结构)
* [事务型还是分析型](ch03.md#事务型还是分析型)
* [列式存储](ch03.md#列存)
* [第四章:编码和演进](ch04.md) [[b站](https://www.bilibili.com/video/BV1Aa411q7u9) [Youtube](https://www.youtube.com/watch?v=DqddHDVTkps&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=6)]
* [数据编码的格式](ch04.md#数据编码的格式)
* [几种数据流模型](ch04.md#几种数据流模型)
- [第一章:可靠、可扩展、可维护](ch01.md) [[b ](https://www.bilibili.com/video/BV1bY411L7HA) [Youtube](https://www.youtube.com/watch?v=geVsm3YGF4A&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=1)]
- [本书为什么以数据系统为主题](ch01.md#本书为什么以数据系统为主题)
- [可靠性](ch01.md#可靠性)
- [可伸缩性](ch01.md#可伸缩性)
- [可维护性](ch01.md#可维护性)
- [第二章:数据模型和查询语言](ch02.md) [b([上](https://www.bilibili.com/video/BV19a411C7UN) [](https://www.bilibili.com/video/BV1BZ4y1r79M)) Youtube ([](https://www.youtube.com/watch?v=1TVdmZ_sJCM&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=2) [](https://www.youtube.com/watch?v=GdihZOSMuuI&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=3))]
- [概要](ch02.md#概要)
- [关系模型 vs 文档模型](ch02.md#关系模型与文档模型)
- [数据查询语言](ch02.md#数据查询语言)
- [图模型](ch02.md#图模型)
- [第三章:存储与查询](ch03.md) [b([上](https://www.bilibili.com/video/BV1mL411P72H/) [](https://www.bilibili.com/video/BV1bL411A7ga)) Youtube([](https://www.youtube.com/watch?v=tI1BmIZpK-c&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=4) [](https://www.youtube.com/watch?v=8wiJSECzADc&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=5))]
- [底层数据结构](ch03.md#驱动数据库的底层数据结构)
- [事务型还是分析型](ch03.md#事务型还是分析型)
- [列式存储](ch03.md#列存)
- [第四章:编码和演进](ch04.md) [[b ](https://www.bilibili.com/video/BV1Aa411q7u9) [Youtube](https://www.youtube.com/watch?v=DqddHDVTkps&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=6)]
- [数据编码的格式](ch04.md#数据编码的格式)
- [几种数据流模型](ch04.md#几种数据流模型)
### 第二部分:分布式数据
* [第五章:冗余](ch05.md) [ b站 ([上](https://www.bilibili.com/video/BV1VR4y1K7eK) [](https://www.bilibili.com/video/BV1ou4116779)) Youtube([](https://www.youtube.com/watch?v=pbURsaoKiYc&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=7) [](https://www.youtube.com/watch?v=y23oqgIG7Vw&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=8))]
* [领导者与跟随者](ch05.md#领导者与跟随者)
* [复制滞后问题](ch05.md#复制滞后问题)
* [多主模型](ch05.md#多主模型)
* [无主模型](ch05.md#无主模型)
* [第六章:分区](ch06.md) [b站 ([上](https://www.bilibili.com/video/BV1tY4y157Np) [](https://www.bilibili.com/video/BV1AA4y1f7Hi)) Youtue ([](https://www.youtube.com/watch?v=7vvycyhJn1s&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=9) [](https://www.youtube.com/watch?v=aPeHdQgBmi4&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=10))]
* [分片与复制](ch06.md#分片与复制)
* [键值对集的分片](ch06.md#键值对集的分片)
* [分片和次级索引](ch06.md#分片和次级索引)
* [分片均衡](ch06.md#分片均衡)
* [请求路由](ch06.md#请求路由)
* [第七章:事务](ch07.md) [ b站 ([上](https://www.bilibili.com/video/BV1d94y117pW) [](https://www.bilibili.com/video/BV1u3411w765) [](https://www.bilibili.com/video/BV1Qr4y1M7Zm)) Youtube ([](https://www.youtube.com/watch?v=gbExnxslpCs&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=11) [](https://www.youtube.com/watch?v=sDKKhGFyUmk&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=12) [](https://www.youtube.com/watch?v=Lhs6H6IgFvY&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=13))]
* [棘手的概念](ch07.md#棘手的概念)
* [几种弱隔离级别](ch07.md#几种弱隔离级别)
* [可串行化](ch07.md#可串行化)
* [第八章:分布式系统中的麻烦事](ch08.md) [b([上](https://www.bilibili.com/video/BV1Ad4y1D7Yy) [](https://www.bilibili.com/video/BV1114y1L7wU) [](https://www.bilibili.com/video/BV1uG411A7GK)) Youtube ([](https://www.youtube.com/watch?v=-q-yY_0aCsg&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=13) [](https://www.youtube.com/watch?v=mk-QfuBV_NQ&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=14) [](https://www.youtube.com/watch?v=xhk-X-rkLU4&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=15))]
* [故障和部分失败](ch08.md#故障和部分失败)
* [不可靠的网络](ch08.md#不可靠的网络)
* [不可靠的时钟](ch08.md#不可靠的时钟)
* [知识、真相和谎言](ch08.md#知识、真相和谎言)
* [第九章:一致性和共识协议](ch09.md) [b([上](https://www.bilibili.com/video/BV1eK411o73Q) [](https://www.bilibili.com/video/BV1Y14y1P7xG) [](https://www.bilibili.com/video/BV1r3411S7J2)) Youtube ([](https://www.youtube.com/watch?v=Hq2gWib5n_I&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=17) [](https://www.youtube.com/watch?v=Nds2xpnyS8A&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=18) [](https://youtu.be/5ZCXSDMcerg))]
* [一致性保证](ch09.md#一致性保证)
* [线性一致性](ch09.md#线性一致性)
* [顺序保证](ch09.md#顺序保证)
* [分布式事务和共识协议](ch09.md#分布式事务和共识协议)
- [第五章:冗余](ch05.md) [ b 站 ([上](https://www.bilibili.com/video/BV1VR4y1K7eK) [](https://www.bilibili.com/video/BV1ou4116779)) Youtube([](https://www.youtube.com/watch?v=pbURsaoKiYc&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=7) [](https://www.youtube.com/watch?v=y23oqgIG7Vw&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=8))]
- [领导者与跟随者](ch05.md#领导者与跟随者)
- [复制滞后问题](ch05.md#复制滞后问题)
- [多主模型](ch05.md#多主模型)
- [无主模型](ch05.md#无主模型)
- [第六章:分区](ch06.md) [b 站 ([上](https://www.bilibili.com/video/BV1tY4y157Np) [](https://www.bilibili.com/video/BV1AA4y1f7Hi)) Youtue ([](https://www.youtube.com/watch?v=7vvycyhJn1s&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=9) [](https://www.youtube.com/watch?v=aPeHdQgBmi4&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=10))]
- [分片与复制](ch06.md#分片与复制)
- [键值对集的分片](ch06.md#键值对集的分片)
- [分片和次级索引](ch06.md#分片和次级索引)
- [分片均衡](ch06.md#分片均衡)
- [请求路由](ch06.md#请求路由)
- [第七章:事务](ch07.md) [ b 站 ([上](https://www.bilibili.com/video/BV1d94y117pW) [](https://www.bilibili.com/video/BV1u3411w765) [](https://www.bilibili.com/video/BV1Qr4y1M7Zm)) Youtube ([](https://www.youtube.com/watch?v=gbExnxslpCs&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=11) [](https://www.youtube.com/watch?v=sDKKhGFyUmk&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=12) [](https://www.youtube.com/watch?v=Lhs6H6IgFvY&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=13))]
- [棘手的概念](ch07.md#棘手的概念)
- [几种弱隔离级别](ch07.md#几种弱隔离级别)
- [可串行化](ch07.md#可串行化)
- [第八章:分布式系统中的麻烦事](ch08.md) [b([上](https://www.bilibili.com/video/BV1Ad4y1D7Yy) [](https://www.bilibili.com/video/BV1114y1L7wU) [](https://www.bilibili.com/video/BV1uG411A7GK)) Youtube ([](https://www.youtube.com/watch?v=-q-yY_0aCsg&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=13) [](https://www.youtube.com/watch?v=mk-QfuBV_NQ&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=14) [](https://www.youtube.com/watch?v=xhk-X-rkLU4&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=15))]
- [故障和部分失败](ch08.md#故障和部分失败)
- [不可靠的网络](ch08.md#不可靠的网络)
- [不可靠的时钟](ch08.md#不可靠的时钟)
- [知识、真相和谎言](ch08.md#知识、真相和谎言)
- [第九章:一致性和共识协议](ch09.md) [b([上](https://www.bilibili.com/video/BV1eK411o73Q) [](https://www.bilibili.com/video/BV1Y14y1P7xG) [](https://www.bilibili.com/video/BV1r3411S7J2)) Youtube ([](https://www.youtube.com/watch?v=Hq2gWib5n_I&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=17) [](https://www.youtube.com/watch?v=Nds2xpnyS8A&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=18) [](https://youtu.be/5ZCXSDMcerg))]
- [一致性保证](ch09.md#一致性保证)
- [线性一致性](ch09.md#线性一致性)
- [顺序保证](ch09.md#顺序保证)
- [分布式事务和共识协议](ch09.md#分布式事务和共识协议)
# DDIA 读书会
DDIA 读书分享会会逐章进行分享结合我在工业界分布式存储和数据库的一些经验补充一些细节。每两周左右分享一次欢迎加入Schedule 在[这里](https://docs.qq.com/sheet/DWHFzdk5lUWx4UWJq)。我们有个对应的分布式&数据库讨论群每次分享前会在群里通知。如想加入可以加我的微信号qtmuniao简单自我介绍下并注明分布式系统群。另外我的公众号“木鸟杂记”有更多的分布式系统、存储和数据库相关的文章欢迎关注。
读书会安排和往期录屏: [https://docs.qq.com/sheet/DWHFzdk5lUWx4UWJq](https://docs.qq.com/sheet/DWHFzdk5lUWx4UWJq)
读书会安排和往期录屏[https://docs.qq.com/sheet/DWHFzdk5lUWx4UWJq](https://docs.qq.com/sheet/DWHFzdk5lUWx4UWJq)
# 关于我

36
ch01.md
View File

@@ -8,7 +8,6 @@
因此,作为 IT 从业人员,有必要系统性的了解一下现代的、分布式的数据系统。学习本书,能够学习到数据系统的背后的原理、了解其常见的实践、进而将其应用到我们工作的系统设计中。
## 常见的数据系统有哪些
- 存储数据,以便之后再次使用——**数据库**
@@ -52,14 +51,10 @@
1. 正常情况下,应用行为满足 API 给出的行为
2. 在用户误输入/误操作时,能够正常处理
- **性能上**
在给定硬件和数据量下,能够满足承诺的性能指标。
- **安全上**
能够阻止未授权、恶意破坏。
可用性也是可靠性的一个侧面,云服务通常以多少个 9 来衡量可用性。
---
@@ -83,10 +78,8 @@
数据系统中常见的需要考虑的硬件指标:
- **MTTF mean time to failure**
单块盘 平均故障时间 5 ~10 年,如果你有 1w+ 硬盘,则均匀期望下,每天都有坏盘出现。当然事实是硬盘会一波一波坏。
解决办法,增加冗余度:
机房多路供电,双网络等等。
@@ -125,10 +118,8 @@
3. 报警机制
4. 问题预案
- **针对组织**
科学的培训和管理
## 可靠性有多重要?
事关用户数据安全,事关企业声誉,企业存活和做大的基石。
@@ -148,7 +139,7 @@
- 数据库中的读写比率
- 聊天室中同时活跃的用户数量
书中以 Twitter 201211 披露的信息为例进行了说明:
书中以 Twitter 201211 披露的信息为例进行了说明:
1. 识别主营业务:发布推文、首页 Feed 流。
2. 确定其请求量级:发布推文(平均 4.6k 请求/秒,峰值超过 12k 请求/秒查看其他人推文300k 请求/秒)
@@ -188,14 +179,10 @@
负载伸缩的两种方式:
- **自动**
如果负载不好预测且多变,则自动较好。坏处在于不易跟踪负载,容易抖动,造成资源浪费。
- **手动**
如果负载容易预测且不长变化,最好手动。设计简单,且不容易出错。
针对不同应用场景:
首先,如果规模很小,尽量还是用性能好一点的机器,可以省去很多麻烦。
@@ -207,14 +194,10 @@
两种服务类型:
- **无状态服务**
比较简单,多台机器,外层罩一个 gateway 就行。
- **有状态服务**
根据需求场景,如读写负载、存储量级、数据复杂度、响应时间、访问模式,来进行取舍,设计合乎需求的架构。
**不可能啥都要,没有万金油架构**!但同时:万变不离其宗,组成不同架构的原子设计模式是有限的,这也是本书稍后要论述的重点。
# 可维护性
@@ -223,19 +206,13 @@
但大部分人都喜欢挖坑,不喜欢填坑。因此有必要,在刚开就把坑开的足够好。有三个原则:
- ***可维护性Operability***
- **_可维护性Operability_**
便于运维团队无痛接手。
- ***简洁性Simplicity***
- **_简洁性Simplicity_**
便于新手开发平滑上手:这需要一个合理的抽象,并尽量消除各种复杂度。如,层次化抽象。
- ***可演化性Evolvability***
- **_可演化性Evolvability_**
便于后面需求快速适配:避免耦合过紧,将代码绑定到某种实现上。也称为**可扩展性extensibility****可修改性modifiability**  或**可塑性plasticity**。
## **可运维性Operability人生苦短关爱运维**
有效的运维绝对是个高技术活:
@@ -271,7 +248,7 @@
4. 为了提升性能的 hack。
5. 随处可见的补丁workaround
需求很简单,但不妨碍你实现的很复杂 😉:过多的引入了**额外复杂度***accidental* complexity
需求很简单,但不妨碍你实现的很复杂 😉:过多的引入了**额外复杂度**_accidental_ complexity
)——非问题本身决定的,而由实现所引入的复杂度。
通常是问题理解的不够本质,写出了“**流水账**”(没有任何**抽象abstraction**)式的代码。
@@ -307,9 +284,6 @@
应对之道:
- 项目管理上
敏捷开发
- 系统设计上
依赖前两点。合理抽象,合理封装,对修改关闭,对扩展开放。

93
ch02.md
View File

@@ -17,9 +17,8 @@
## 数据模型
> A **data model** is an [abstract model](https://en.wikipedia.org/wiki/Abstract_model) that organizes elements of [data](https://en.wikipedia.org/wiki/Data) and standardizes how they relate to one another and to the properties of real-world entities.
—[https://en.wikipedia.org/wiki/Data_model](https://en.wikipedia.org/wiki/Data_model)
>
> A **data model** is an [abstract model](https://en.wikipedia.org/wiki/Abstract_model) that organizes elements of [data](https://en.wikipedia.org/wiki/Data) and standardizes how they relate to one another and to the properties of real-world entities.
> —[https://en.wikipedia.org/wiki/Data_model](https://en.wikipedia.org/wiki/Data_model)
**数据模型**:如何组织数据,如何标准化关系,如何关联现实。
@@ -39,7 +38,6 @@
4. 作为**硬件工程师**,你需要将字节流表示为二极管的电位(内存)、磁场中的磁极(磁盘)、光纤中的光信号(网络)。
> 在每一层,通过对外暴露简洁的**数据模型**,我们**隔离**和**分解**了现实世界的**复杂度**。
>
这也反过来说明了,好的数据模型需有两个特点:
@@ -48,7 +46,6 @@
第二章首先探讨了关系模型、文档模型及其对比,其次是相关查询语言,最后探讨了图模型。
# 关系模型与文档模型
## 关系模型
@@ -101,8 +98,6 @@ NoSQL最初表示Non-SQL后来有人转解为Not only SQL是对不
2. **更好的局部性**:一个人的所有属性被集中访问的同时,也被集中存储。
3. **结构表达语义**:简历与联系信息、教育经历、职业信息等隐含一对多的树状关系可以被 JSON 的树状结构明确表达出来。
## 多对一和多对多
是一个对比各种数据模型的切入角度。
@@ -139,7 +134,6 @@ region 在存储时为什么不直接存储纯字符串“Greater Seattle
20 世纪 70 年代IBM 的信息管理系统 IMS。
> A **hierarchical database model** is a [data model](https://en.wikipedia.org/wiki/Data_model) in which the data are organized into a [tree](https://en.wikipedia.org/wiki/Tree_data_structure)-like structure. The data are stored as **records** which are connected to one another through **links.** A record is a collection of fields, with each field containing only one value. The **type** of a record defines which fields the record contains. — wikipedia
>
几个要点:
@@ -167,16 +161,11 @@ network model 是 hierarchical model 的一种扩展:允许一个节点有多
在关系模型中,数据被组织成**元组tuples**,进而集合成**关系relations**;在 SQL 中分别对应行rows和表tables
- 不知道大家好奇过没,明明看起来更像表模型,为什叫**关系模型**
表只是一种实现。
关系relation的说法来自集合论指的是几个集合的笛卡尔积的子集。
R ⊆ D1×D2×D3 ··· ×Dn
(关系用符号 R 表示,属性用符号 Ai 表示,属性的定义域用符号 Di 表示)
其主要目的和贡献在于提供了一种**声明式**的描述数据和构建查询的方法。
即,相比网络模型,关系模型的查询语句和执行路径相解耦,**查询优化器**Query Optimizer 自动决定执行顺序、要使用的索引),即将逻辑和实现解耦。
@@ -188,14 +177,13 @@ network model 是 hierarchical model 的一种扩展:允许一个节点有多
根据数据类型来选择数据模型
| | 文档型 | 关系型 |
| --- | --- | --- |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| 对应关系 | 数据有天然的一对多、树形嵌套关系,如简历。 | 通过外键 + Join 可以处理 多对一,多对多关系 |
| 代码简化 | 数据具有文档结构,则文档模型天然合适,用关系模型会使得建模繁琐、访问复杂。但不宜嵌套太深,因为只能手动指定访问路径,或者范围遍历 | 主键,索引,条件过滤 |
| Join 支持 | 对 Join 支持的不太好 | 支持的还可以,但 Join 的实现会有很多难点 |
| 模式灵活性 | 弱 schema支持动态增加字段 | 强 schema修改 schema 代价很大 |
| 访问局部性 | 1. 一次性访问整个文档,较优 <br/>2. 只访问文档一部分,较差 | 分散在多个表中 |
对于高度关联的数据集,使用文档型表达比较奇怪,使用关系型可以接受,使用图模型最自然。
### 文档模型中 Schema 的灵活性
@@ -203,7 +191,7 @@ network model 是 hierarchical model 的一种扩展:允许一个节点有多
说文档型数据库是 schemaless 不太准确,更贴切的应该是 **schema-on-read。**
| 数据模型 | | 编程语言 | | 性能 & 空间 |
| --- | --- | --- | --- | --- |
| --------------- | -------------------------------------- | -------- | ------------------ | ---------------------------------------------------------------------- |
| schema-on-read | 写入时不校验,而在读取时进行动态解析。 | 弱类型 | 动态,在运行时解析 | 读取时动态解析,性能较差。写入时无法确定类型,无法对齐空间利用率较差。 |
| schema-on-write | 写入时校验,数据对齐到 schema | 强类型 | 静态,编译时确定 | 性能和空间使用都较优。 |
@@ -227,13 +215,9 @@ network model 是 hierarchical model 的一种扩展:允许一个节点有多
### 关系型和文档型的融合
- MySQL 和 PostgreSQL 开始支持 JSON
原生支持 JSON 可以理解为MySQL 可以理解 JSON 格式。如 Date 格式一样,可以把某个字段作为 JSON 格式,可以修改其中的某个字段,可以在其中某个字段建立索引。
- RethinkDB 在查询中支持 relational-link Joins
科德Codd**nonsimple domains**,记录中的值除了简单类型(数字、字符串),还可以一个嵌套关系(表)。这很像 SQL 对 XML、JSON 的支持。
# 数据查询语言
@@ -241,9 +225,10 @@ network model 是 hierarchical model 的一种扩展:允许一个节点有多
获取动物表中所有鲨鱼类动物。
```jsx
function getSharks() { var sharks = [];
function getSharks() {
var sharks = [];
for (var i = 0; i < animals.length; i++) {
if (animals[i].family === "Sharks") {
if (animals[i].family === 'Sharks') {
sharks.push(animals[i]);
}
}
@@ -256,15 +241,16 @@ SELECT * FROM animals WHERE family = 'Sharks';
```
| | 声明式declarative语言 | 命令式imperative语言 |
| --- | --- | --- |
| -------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- |
| 概念 | 描述控制逻辑而非执行流程 | 描述命令的执行过程,用一系列语句来不断改变状态 |
| 举例 | SQLCSSXSL | IMSCODASYL通用语言如 CC++JS |
| 抽象程度 | 高 | 低 |
| 解耦程度 | 与实现解耦。 <br/>可以持续优化查询引擎性能; | 与实现耦合较深。
| 解耦程度 | 与实现解耦。 <br/>可以持续优化查询引擎性能; | 与实现耦合较深。 |
| 解析执行 | 词法分析 → 语法分析 → 语义分析 <br/>生成执行计划 → 执行计划优化 | 词法分析 → 语法分析 → 语义分析 <br/>中间代码生成 → 代码优化 → 目标代码生成 |
| 多核并行 | 声明式更具多核潜力,给了更多运行时优化空间 | 命令式由于指定了代码执行顺序,编译时优化空间较小。 |
> Q相对声明式语言命令式语言有什么优点
>
> 1. 当描述的目标变得复杂时,声明式表达能力不够。
> 2. 实现命令式的语言往往不会和声明式那么泾渭分明,通过合理抽象,通过一些编程范式(函数式),可以让代码兼顾表达力和清晰性。
@@ -314,14 +300,14 @@ li.selected > p {
但如果使用 JavaScript而不借助上述 selector 库):
```jsx
var liElements = document.getElementsByTagName("li");
var liElements = document.getElementsByTagName('li');
for (var i = 0; i < liElements.length; i++) {
if (liElements[i].className === "selected") {
if (liElements[i].className === 'selected') {
var children = liElements[i].childNodes;
for (var j = 0; j < children.length; j++) {
var child = children[j];
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "P") {
child.setAttribute("style", "background-color: blue");
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === 'P') {
child.setAttribute('style', 'background-color: blue');
}
}
}
@@ -364,17 +350,19 @@ WHERE family = 'Sharks' GROUP BY observation_month;
```jsx
db.observations.mapReduce(
function map() { // 2. 对所有符合条件 doc 执行 map
function map() {
// 2. 对所有符合条件 doc 执行 map
var year = this.observationTimestamp.getFullYear();
var month = this.observationTimestamp.getMonth() + 1;
emit(year + "-" + month, this.numAnimals); // 3. 输出一个 kv pair
emit(year + '-' + month, this.numAnimals); // 3. 输出一个 kv pair
},
function reduce(key, values) { // 4. 按 key 聚集
function reduce(key, values) {
// 4. 按 key 聚集
return Array.sum(values); // 5. 相同 key 加和
},
{
query: { family: "Sharks" }, // 1. 筛选
out: "monthlySharkReport" // 6. reduce 结果集
query: { family: 'Sharks' }, // 1. 筛选
out: 'monthlySharkReport', // 6. reduce 结果集
}
);
```
@@ -392,31 +380,30 @@ MapReduce 一些特点:
2. 不是只有 MapReduce 才允许嵌入通用语言(如 js模块。
3. MapReduce 是有一定**理解成本**的,需要熟悉其执行逻辑才能让两个函数紧密配合。
MongoDB 2.2+ 进化版,*aggregation pipeline:*
MongoDB 2.2+ 进化版,_aggregation pipeline:_
```jsx
db.observations.aggregate([
{ $match: { family: "Sharks" } },
{ $group: {
{ $match: { family: 'Sharks' } },
{
$group: {
_id: {
year: { $year: "$observationTimestamp" },
month: { $month: "$observationTimestamp" }
year: { $year: '$observationTimestamp' },
month: { $month: '$observationTimestamp' },
},
totalAnimals: { $sum: '$numAnimals' },
},
},
totalAnimals: { $sum: "$numAnimals" } }}
]);
```
# 图模型
- 文档模型的适用场景?
你的数据集中存在着大量**一对多**one-to-many的关系。
- 图模型的适用场景?
你的数据集中存在大量的**多对多**many-to-many的关系。
## 基本概念
图数据模型的基本概念一般有三个:**点****边**和附着于两者之上的**属性**。
@@ -424,18 +411,16 @@ db.observations.aggregate([
常见的可以用图建模的场景:
| 例子 | 建模 | 应用 |
| --- | --- | --- |
| -------- | --------------------------- | ---------------------- |
| 社交图谱 | 人是点follow 关系是边 | 六度分隔,信息流推荐 |
| 互联网 | 网页是点,链接关系是边 | PageRank |
| 路网 | 交通枢纽是点,铁路/公路是边 | 路径规划,导航最短路径 |
| 洗钱 | 账户是点,转账关系是边 | 判断是否有环 |
| 知识图谱 | 概念时点,关联关系是边 | 启发式问答 |
- 同构(*homogeneous*)数据和异构数据
- 同构(_homogeneous_)数据和异构数据
图中的点可以都具有相同类型,但是,也可以具有不同类型,并且更为强大。
本节都会以下图为例,它表示了一对夫妇,来自美国爱达荷州的 Lucy 和来自法国 的 Alain。他们已婚住在伦敦。
![example](img/ch02-fig05.png)
@@ -448,7 +433,7 @@ db.observations.aggregate([
## 属性图PGProperty Graphs
| 点 (vertices, nodes, entities) | 边 (edges, relations, arcs) |
| --- | --- |
| ------------------------------ | --------------------------- |
| 全局唯一 ID | 全局唯一 ID |
| 出边集合 | 起始点 |
| 入边集合 | 终止点 |
@@ -456,10 +441,8 @@ db.observations.aggregate([
| 表示点类型的 type | 表示边类型的 label |
- Q有一个疑惑点为什么书中对于 PG 点的定义中没有 Type
如果数据是异构的,应该有才对;莫非是通过不同的属性来标记不同的类型?
如果感觉不直观,可以使用我们熟悉的 SQL 语义来构建一个图模型如下图。Facebook TAO 论文中的单机存储引擎便是 MySQL
```sql
@@ -577,7 +560,7 @@ CREATE PROPERTY GRAPH bank_transfers
其含义如下:
| Subject | 对应图中的一个点 |
| --- | --- |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Object | 1. 一个原子数据,如 string 或者 number。<br/>2. 另一个 Subject。 |
| Predicate | 1. 如果 Object 是原子数据,则 <Predicate, Object> 对应点附带的 KV 对。<br/>2. 如果 Object 是另一个 Object则 Predicate 对应图中的边。 |
@@ -623,7 +606,7 @@ _:namerica a :Location; :name "North America"; :type "continent".
![ddia2-rdf.png](img/ch02-semantic-web-stack.png)
其中 **RDF** *ResourceDescription Framework资源描述框架*)提供了一种结构化网络中数据的标准。使发布到网络中的任何资源(文字、图片、视频、网页),都能以统一的形式被计算机理解。即,不需要让资源使用方深度学习抽取资源的语义,而是靠资源提供方通过 RDF 主动提供其资源语义。
其中 **RDF** _ResourceDescription Framework资源描述框架_)提供了一种结构化网络中数据的标准。使发布到网络中的任何资源(文字、图片、视频、网页),都能以统一的形式被计算机理解。即,不需要让资源使用方深度学习抽取资源的语义,而是靠资源提供方通过 RDF 主动提供其资源语义。
感觉有点理想主义,但互联网、开源社区都是靠这种理想主义、分享精神发展起来的!
@@ -703,7 +686,7 @@ SELECT ?personName WHERE {
否,他们在很多重要的方面都不一样。
| 模型 | 图模型Graph Model | 网络模型Network Model |
| --- | --- | --- |
| -------- | --------------------------------------------------------- | ------------------------------------------------------ |
| 连接方式 | 任意两个点之间都有可以有边 | 指定了嵌套约束 |
| 记录查找 | 1. 使用全局 ID <br/>2. 使用属性索引。<br/>3. 使用图遍历。 | 只能使用路径查询 |
| 有序性 | 点和边都是无序的 | 记录的孩子们是有序集合,在插入时需要考虑维持有序的开销 |
@@ -711,7 +694,7 @@ SELECT ?personName WHERE {
## 查询语言前驱Datalog
有点像 triple-store但是变了下次序(*subject*, *predicate*, *object*) → *predicate*(*subject*, *object*).
有点像 triple-store但是变了下次序(_subject_, _predicate_, _object_) → _predicate_(_subject_, _object_).
之前数据用 Datalog 表示为:
```
@@ -745,7 +728,7 @@ migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */
?- migrated(Who, 'United States', 'Europe'). /* Who = 'Lucy'. */
```
1. 代码中以大写字母开头的元素是**变量**,字符串、数字或以小写字母开头的元素是**常量**。下划线_被称为匿名变量
1. 代码中以大写字母开头的元素是**变量**,字符串、数字或以小写字母开头的元素是**常量**。下划线(\_被称为匿名变量
2. 可以使用基本 Predicate 自定义 Predicate类似于使用基本函数自定义函数。
3. 逗号连接的多个谓词表达式为且的关系。

40
ch03.md
View File

@@ -2,19 +2,20 @@
第二章讲了上层抽象:数据模型和查询语言。
本章下沉一些,聚焦数据库底层如何处理查询和存储。这其中,有个**逻辑链条**
> 使用场景 → 查询类型 → 存储格式。
查询类型主要分为两大类:
| 引擎类型 | 请求数量 | 数据量 | 瓶颈 | 存储格式 | 用户 | 场景举例 | 产品举例 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| -------- | ---------------------- | ------------------------ | -------------- | ------------ | ---------------------------- | -------- | ---------- |
| OLTP | 相对频繁,侧重在线交易 | 总体和单次查询都相对较小 | Disk Seek | 多用行存 | 比较普遍,一般应用用的比较多 | 银行交易 | MySQL |
| OLAP | 相对较少,侧重离线分析 | 总体和单次查询都相对巨大 | Disk Bandwidth | 列存逐渐流行 | 多为商业用户 | 商业分析 | ClickHouse |
其中OLTP 侧,常用的存储引擎又有两种流派:
| 流派 | 主要特点 | 基本思想 | 代表 |
| --- | --- | --- | --- |
| ------------------ | ---------------------------------------------------- | ---------------- | ------------------------------------------------ |
| log-structured 流 | 只允许追加,所有修改都表现为文件的追加和文件整体增删 | 变随机写为顺序写 | Bitcask、LevelDB、RocksDB、Cassandra、Lucene |
| update-in-place 流 | 以页page为粒度对磁盘数据进行修改 | 面向页、查找树 | B 族树,所有主流关系型数据库和一些非关系型数据库 |
@@ -75,7 +76,7 @@ $ db_get 42
![ddia-3-1-hash-map-csv.png](img/ch03-fig01.png)
看来很简单,但这正是 [Bitcask](https://docs.riak.com/riak/kv/2.2.3/setup/planning/backend/bitcask/index.html "Bitcask") 的基本设计,但关键是,他 Work在小数据量时即所有 key 都能存到内存中时):能提供很高的读写性能:
看来很简单,但这正是 [Bitcask](https://docs.riak.com/riak/kv/2.2.3/setup/planning/backend/bitcask/index.html 'Bitcask') 的基本设计,但关键是,他 Work在小数据量时即所有 key 都能存到内存中时):能提供很高的读写性能:
1. 写:文件追加写。
2. 读:一次内存查询,一次磁盘 seek如果数据已经被缓存则 seek 也可以省掉。
@@ -111,8 +112,6 @@ $ db_get 42
- 想想,会如何进行规避?
## SSTables 和 LSM-Trees
这一节层层递进,步步做引,从 SSTables 格式出发,牵出 LSM-Trees 全貌。
@@ -132,7 +131,6 @@ $ db_get 42
![ddia-3-4-merge-sst.png](img/ch03-fig04.png)
**不需要在内存中保存所有数据的索引**。仅需要记录下每个文件界限(以区间表示:[startKey, endKey],当然实际会记录的更细)即可。查找某个 Key 时,去所有包含该 Key 的区间对应的文件二分查找即可。
![ddia-3-5-sst-index.png](https://s2.loli.net/2022/04/16/j8tM6IUk1QrJXuw.png)
@@ -168,7 +166,7 @@ SSTables 格式听起来很美好,但须知数据是乱序的来的,我们
![ddia-3-leveldb-architecture.png](img/ch03-fig05.png)
这种数据结构是 Patrick ONeil 等人,在 1996 年提出的:[The Log-Structured Merge-Tree](https://www.cs.umb.edu/~poneil/lsmtree.pdf "The Log-Structured Merge-Tree")。
这种数据结构是 Patrick ONeil 等人,在 1996 年提出的:[The Log-Structured Merge-Tree](https://www.cs.umb.edu/~poneil/lsmtree.pdf 'The Log-Structured Merge-Tree')。
Elasticsearch 和 Solr 的索引引擎 Lucene也使用类似 LSM-Tree 存储结构。但其数据模型不是 KV但类似word → document list。
@@ -182,7 +180,7 @@ Elasticsearch 和 Solr 的索引引擎 Lucene也使用类似 LSM-Tree 存储
![ddia-sized-tierd-compact.png](img/ch03-sized-tiered.png)
对于 RocksDB 来说,工程上的优化和使用上的优化就更多了。在其 [Wiki](https://github.com/facebook/rocksdb/wiki "rocksdb wiki") 上随便摘录几点:
对于 RocksDB 来说,工程上的优化和使用上的优化就更多了。在其 [Wiki](https://github.com/facebook/rocksdb/wiki 'rocksdb wiki') 上随便摘录几点:
1. Column Family
2. 前缀压缩和过滤
@@ -194,7 +192,7 @@ Elasticsearch 和 Solr 的索引引擎 Lucene也使用类似 LSM-Tree 存储
虽然先讲的 LSM-Tree但是它要比 B+ 树新的多。
B 树于 1970 年被 R. Bayer and E. McCreight [提出](https://dl.acm.org/doi/10.1145/1734663.1734671 "b tree paper")后,便迅速流行了起来。现在几乎所有的关系型数据中,它都是数据索引标准一般的实现。
B 树于 1970 年被 R. Bayer and E. McCreight [提出](https://dl.acm.org/doi/10.1145/1734663.1734671 'b tree paper')后,便迅速流行了起来。现在几乎所有的关系型数据中,它都是数据索引标准一般的实现。
与 LSM-Tree 一样,它也支持高效的**点查**和**范围查**。但却使用了完全不同的组织方式。
@@ -214,10 +212,8 @@ B 树于 1970 年被 R. Bayer and E. McCreight [提出](https://dl.acm.org/doi/1
**分裂 or 合并**。级联分裂和合并。
- 一个记录大于一个 page 怎么办?
树的节点是逻辑概念page or block 是物理概念。一个逻辑节点可以对应多个物理 page。
### 让 B 树更可靠
B 树不像 LSM-Tree会在原地修改数据文件。
@@ -240,7 +236,7 @@ B 树出来了这么久,因此有很多优化:
## B-Trees 和 LSM-Trees 对比
| 存储引擎 | B-Tree | LSM-Tree | 备注 |
| --- | --- | --- | --- |
| -------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| 优势 | 读取更快 | 写入更快 | |
| 写放大 | 1. 数据和 WAL<br/>2. 更改数据时多次覆盖整个 Page | 1. 数据和 WAL<br/>2. Compaction | SSD 不能过多擦除。因此 SSD 内部的固件中也多用日志结构来减少随机小写。 |
| 写吞吐 | 相对较低:<br/>1. 大量随机写。 | 相对较高:<br/>1. 较低的写放大(取决于数据和配置)<br/>2. 顺序写入。<br/>3. 更为紧凑。 | |
@@ -282,11 +278,11 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
前述索引只提供全字段的精确匹配,而不提供类似搜索引擎的功能。比如,按字符串中包含的单词查询,针对笔误的单词查询。
在工程中常用 [Apace Lucene](https://lucene.apache.org/ "Apace Lucene") 库,和其包装出来的服务:[Elasticsearch](https://www.elastic.co/cn/ "Elasticsearch")。他也使用类似 LSM-tree 的日志存储结构,但其索引是一个有限状态自动机,在行为上类似 Trie 树。
在工程中常用 [Apace Lucene](https://lucene.apache.org/ 'Apace Lucene') 库,和其包装出来的服务:[Elasticsearch](https://www.elastic.co/cn/ 'Elasticsearch')。他也使用类似 LSM-tree 的日志存储结构,但其索引是一个有限状态自动机,在行为上类似 Trie 树。
### 全内存数据结构
随着单位内存成本下降,甚至支持持久化(*non-volatile memory*NVM如 Intel 的 [傲腾](https://www.intel.cn/content/www/cn/zh/products/details/memory-storage/optane-dc-persistent-memory.html "傲腾")),全内存数据库也逐渐开始流行。
随着单位内存成本下降,甚至支持持久化(_non-volatile memory_NVM如 Intel 的 [傲腾](https://www.intel.cn/content/www/cn/zh/products/details/memory-storage/optane-dc-persistent-memory.html '傲腾')),全内存数据库也逐渐开始流行。
根据是否需要持久化,内存数据大概可以分为两类:
@@ -294,7 +290,6 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
2. **需要持久化**。通过 WAL、定期 snapshot、远程备份等等来对数据进行持久化。但使用内存处理全部读写因此仍是内存数据库。
> VoltDB, MemSQL, and Oracle TimesTen 是提供关系模型的内存数据库。RAMCloud 是提供持久化保证的 KV 数据库。Redis and Couchbase 仅提供弱持久化保证。
>
内存数据库存在优势的原因不仅在于不需要读取磁盘,而在更于不需要对数据结构进行**序列化、编码**后以适应磁盘所带来的**额外开销**。
@@ -318,7 +313,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
下表是一个对比:
| 属性 | OLTP | OLAP |
| --- | --- | --- |
| ------------ | ------------------------------- | -------------------------------------- |
| 主要读取模式 | 小数据量的随机读,通过 key 查询 | 大数据量的聚合max,min,sum, avg查询 |
| 主要写入模式 | 随机访问,低延迟写入 | 批量导入ETL或者流式写入 |
| 主要应用场景 | 通过 web 方式使用的最终用户 | 互联网分析,为了辅助决策 |
@@ -351,7 +346,7 @@ AP 中的处理模型相对较少,比较常用的有**星状模型**,也称
![ddia3-9-star-schema.png](img/ch03-fig08.png)
如上图所示,星状模型通常包含一张**事件表(*fact table*** 和多张**维度表(*dimension tables*******。事件表以事件流的方式将数据组织起来,然后通过外键指向不同的维度。
如上图所示,星状模型通常包含一张**事件表(_fact table_** 和多张**维度表(\*dimension tables\*\*\***\*\*。事件表以事件流的方式将数据组织起来,然后通过外键指向不同的维度。
星状模型的一个变种是雪花模型,可以类比雪花(❄️)图案,其特点是在维度表中会进一步进行二次细分,讲一个维度分解为几个子维度。比如品牌和产品类别可能有单独的表格。星状模型更简单,雪花模型更精细,具体应用中会做不同取舍。
@@ -390,7 +385,7 @@ GROUP BY
将所有数据分列存储在一块,带来了一个意外的好处,由于同一属性的数据相似度高,因此更易压缩。
如果每一列中值阈相比行数要小的多,可以用**位图编码( *[bitmap encoding](https://en.wikipedia.org/wiki/Bitmap_index "bitmap encoding")* **。举个例子,零售商可能有数十亿的销售交易,但只有 100,000 个不同的产品。
如果每一列中值阈相比行数要小的多,可以用**位图编码(_[bitmap encoding](https://en.wikipedia.org/wiki/Bitmap_index 'bitmap encoding')_**。举个例子,零售商可能有数十亿的销售交易,但只有 100,000 个不同的产品。
![ddia-3-11-compress.png](img/ch03-fig11.png)
@@ -400,7 +395,7 @@ GROUP BY
2. 值为 0 则表示该下标没有出现该值
3. 值为 1 则表示该下标出现了该值
如果 bit array 是稀疏的,即大量的都是 0只要少量的 1。其实还可以使用 **[游程编码](https://zh.wikipedia.org/zh/%E6%B8%B8%E7%A8%8B%E7%BC%96%E7%A0%81 "游程编码")RLE Run-length encoding** 进一步压缩:
如果 bit array 是稀疏的,即大量的都是 0只要少量的 1。其实还可以使用 **[游程编码](https://zh.wikipedia.org/zh/%E6%B8%B8%E7%A8%8B%E7%BC%96%E7%A0%81 '游程编码')RLERun-length encoding** 进一步压缩:
1. 将连续的 0 和 1改写成 `数量+值`,比如 `product_sk = 29``9 个 01 个 18 个 0`
2. 使用一个小技巧,将信息进一步压缩。比如将同值项合并后,肯定是 0 1 交错出现,固定第一个值为 0则交错出现的 0 和 1 的值也不用写了。则 `product_sk = 29` 编码变成 `918`
@@ -422,7 +417,7 @@ WHERE product_sk = 31 AND store_sk = 3
### 列族
书中特别提到**列族column families**。它是 Cassandra 和 HBase 中的的概念,他们都起源于自谷歌的 [BigTable](https://en.wikipedia.org/wiki/Bigtable "BigTable") 。注意到他们和**列式column-oriented存储**有相似之处,但绝不完全相同:
书中特别提到**列族column families**。它是 Cassandra 和 HBase 中的的概念,他们都起源于自谷歌的 [BigTable](https://en.wikipedia.org/wiki/Bigtable 'BigTable') 。注意到他们和**列式column-oriented存储**有相似之处,但绝不完全相同:
1. 同一个列族中多个列是一块存储的并且内嵌行键row key
2. 并且列不压缩(存疑?)
@@ -434,7 +429,7 @@ WHERE product_sk = 31 AND store_sk = 3
数仓的超大规模数据量带来了以下瓶颈:
1. 内存处理带宽
2. CPU 分支预测错误和[流水线停顿](https://zh.wikipedia.org/wiki/%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%81%9C%E9%A1%BF "流水线停顿")
2. CPU 分支预测错误和[流水线停顿](https://zh.wikipedia.org/wiki/%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%81%9C%E9%A1%BF '流水线停顿')
关于内存的瓶颈可已通过前述的数据压缩来缓解。对于 CPU 的瓶颈可以使用:
@@ -446,7 +441,6 @@ WHERE product_sk = 31 AND store_sk = 3
由于数仓查询多集中于聚合算子(比如 sumavgminmax列式存储中的存储顺序相对不重要。但也免不了需要对某些列利用条件进行筛选为此我们可以如 LSM-Tree 一样,对所有行按某一列进行排序后存储。
> 注意,不可能同时对多列进行排序。因为我们需要维护多列间的下标间的对应关系,才可能按行取数据。
>
同时,排序后的那一列,压缩效果会更好。
@@ -461,7 +455,6 @@ WHERE product_sk = 31 AND store_sk = 3
上述针对数仓的优化(列式存储、数据压缩和按列排序)都是为了解决数仓中常见的读写负载,读多写少,且读取都是超大规模的数据。
> 我们针对读做了优化,就让写入变得相对困难。
>
比如 B 树的**原地更新流**是不太行的。举个例子,要在中间某行插入一个数据,**纵向**来说,会影响所有的列文件(如果不做 segment 的话);为了保证多列间按下标对应,**横向**来说,又得更新该行不同列的所有列文件。
@@ -479,7 +472,6 @@ WHERE product_sk = 31 AND store_sk = 3
其中一个值得一提的是**物化聚合materialized aggregates或者物化汇总**。
> 物化,可以简单理解为持久化。本质上是一种空间换时间的 tradeoff。
>
数据仓库查询通常涉及聚合函数,如 SQL 中的 COUNT、SUM、AVG、MIN 或 MAX。如果这些函数被多次用到每次都即时计算显然存在巨大浪费。因此一个想法就是能不能将其缓存起来。

28
ch04.md
View File

@@ -4,7 +4,6 @@
所有涉及跨进程通信的地方,都需要对数据进行**编码****Encoding**),或者说**序列化****Serialization**)。因为持久化存储和网络传输都是面向字节流的。序列化本质上是一种“**降维**”操作,将内存中高维的数据结构降维成单维的字节流,于是底层硬件和相关协议,只需要处理一维信息即可。
编码主要涉及两方面问题:
1. 如何编码能够节省空间、提高性能。
@@ -16,7 +15,6 @@
2. 向前兼容 (forward compatibility):当前代码可以读取新版本代码写入的数据。
> 翻译成中文后,很容易混淆,主要原因在于“后”的歧义性,到底指**身后**(过去),还是指**之后**(将来),私以为还不如翻译为,*兼容过去*和*兼容将来*。但为了习惯,后面行文仍然用向后/前兼容。
>
其中,向后兼容比较常见,因为时间总是向前流逝,版本总是升级,那么升级之后的代码总要处理历史积压的数据,自然会产生向后兼容的问题。向前兼容比较少见,书中给出的例子是多实例滚动升级,但其持续时间也很短。
@@ -29,18 +27,12 @@
编码Encoding有多种称谓如**序列化serialization** 或  **编组marshalling**。对应的解码Decoding也有多种别称**解析Parsing****反序列化deserialization****反编组 (unmarshalling。**
- 为什么内存中数据和外存、网络中的会有如此不同呢?
在内存中,借助编译器,我们可以将内存解释为各种数据结构;但在文件系统和网络中,我们只能通过 seek\read 等几个有限的操作来流式的读取字节流。那 mmap 呢?
- 编码和序列化撞车了?
在事务中,也有序列化相关的术语,所以这里专用编码,以避免歧义。
- 编码encoding和加密**encryption**
研究的范畴不太一样,编码是为了持久化或者传输,着重点在格式和演化;而加密是为了安全,着重点在于安全、防破解。
## 编程语言内置
很多编程语言内置了一些缺省的编码方法:
@@ -49,7 +41,7 @@
2. Ruby 有  `Marshal`
3. Python 有  `pickle`
如果你确定你的数据只会被某种特定的语言所读取,那么这种内置的编码方法很好用。比如深度学习研究员因为基本都用 Python所以常会把数据以 [pickle](https://docs.python.org/zh-cn/3/library/pickle.html "pickle 官方文档") 的格式传来传去。
如果你确定你的数据只会被某种特定的语言所读取,那么这种内置的编码方法很好用。比如深度学习研究员因为基本都用 Python所以常会把数据以 [pickle](https://docs.python.org/zh-cn/3/library/pickle.html 'pickle 官方文档') 的格式传来传去。
但这些编程语言内置的编码格式有以下缺点:
@@ -156,15 +148,12 @@ ProtoBuf 与 Thrift Compact Protocol 编码方式很类似,也用了变长编
1. **向后兼容**:新的代码,在处理新的增量数据格式的同时,也得处理旧的存量数据。
2. **向前兼容**:旧的代码,如果遇到新的数据格式,不能 crash。
- ProtoBuf 和 Thrift 是怎么解决这两个问题的呢?
**字段标号** + **限定符**optional、required
向后兼容:新加的字段需为 optional。这样在解析旧数据时才不会出现字段缺失的情况。
向前兼容:字段标号不能修改,只能追加。这样旧代码在看到不认识的标号时,省略即可。
### 数据类型和模式演变
修改数据类型比较麻烦:只能够在相容类型中进行修改。
@@ -202,10 +191,8 @@ record Person {
可以看到 Avro 没有使用字段标号。
- 仍是编码之前例子Avro 只用了 32 个字节,为什么呢?
他没有编入类型。
![ddia4-avro-enc.png](img/ch04-fig05.png)
因此Avro 必须配合模式定义来解析,如 Client-Server 在通信的握手阶段会先交换数据模式。
@@ -213,10 +200,8 @@ record Person {
### 写入模式和读取模式
- 没有字段标号Avro 如何支持模式演进呢?
答案是**显式的**使用两种模式。
即,在对数据进行编码(写入文件或者进行传输)时,使用模式 A称为**写入模式**writer schema在对数据进行解码从文件或者网络读取使用模式 B称为**读取模式**reader schema而两者不必相同只需兼容。
也就是说,只要模式在演进时,是**兼容**的,那么 Avro 就能够处理向后兼容和向前兼容。
@@ -256,20 +241,13 @@ union {null, long, string} field;
这取决于不同的应用场景。
- **所有数据条目同构的大文件**
典型的就是 Hadoop 生态中。如果一个大文件所有记录都使用相同模式编码,则在文件头包含一次写入模式即可。
- **支持模式变更的数据库表**
由于数据库表允许模式修改,其中的行可能写入于不同模式阶段。对于这种情况,可以在编码时额外记录一个模式版本号(比如自增),然后在某个地方存储所有的模式版本。
解码时,通过版本去查询对应的写入模式即可。
- **网络中发送数据**
在两个进程通信的握手阶段,交换写入模式。比如在一个 session 开始时交换模式,然后在整个 session 生命周期内都用此模式。
### 动态生成数据中的模式
Avro 没有使用字段标号的一个好处是,不需要手动维护字段标号到字段名的映射,这对于动态生成的数据模式很友好。
@@ -337,7 +315,7 @@ Thrift 和 Protobuf 会依据语言无关的 IDL 定义的模式,生成给定
有时候需要对数据库做备份到外存。在做备份(或者说快照)时,虽然会有不同时间点生成的数据,但通常会将各种版本数据转化、对齐到最新版本。毕竟,总是要全盘拷贝数据,那就顺便做下转换好了。
之前也提到了,对于这种场景,生成的是一次性的不可变的备份或者快照数据,使用 Avro 比较合适。此时也是一个很好地契机,可以将数据按需要的格式输出,比如面向分析的按列存储格式:[Parquet](https://parquet.apache.org/docs/file-format/ "Parquet")。
之前也提到了,对于这种场景,生成的是一次性的不可变的备份或者快照数据,使用 Avro 比较合适。此时也是一个很好地契机,可以将数据按需要的格式输出,比如面向分析的按列存储格式:[Parquet](https://parquet.apache.org/docs/file-format/ 'Parquet')。
## 经由服务的数据流REST 和 RPC

15
ch05.md
View File

@@ -9,15 +9,12 @@
3. **提高读吞吐**:平滑扩展可用于查询的机器。
> 本章假设我们的数据系统中所有数据能够存放到一台机器中,则本章只需考虑多机冗余的问题。如果数据超过单机尺度该怎么办?那是下一章要解决的事情。
>
如果数据是**只读**的,则冗余很好做,直接复制到多机即可。我们有时可以利用这个特性,使用分治策略,将数据分为只读部分和读写部分,则只读部分的冗余就会容易处理的多,甚至可以用 EC 方式做冗余,减小存储放大的同时,还提高了可用性。
- 想想 EC 牺牲了什么?
以计算换存储。
但难点就在于,数据允许数据变更时,如何维护多机冗余且一致。常用的冗余控制算法有:
1. 单领导者single leader
@@ -72,10 +69,8 @@
3. **全异步**:所有的从副本都异步写入。网络环境比较好的话,可以这么配置。
> 异步复制可能会造成副本丢失等严重问题,为了能兼顾一致性和性能,学术界也在不断研究新的复制方法。如,**链式复制chain-replication**。
>
> 多副本的一致性和共识性有诸多联系,本书后面章节会讨论。
>
## 新增副本
@@ -217,10 +212,9 @@
如果此时不再有写入,经过一段时间后,多副本最终会达到一致:**最终一致性**。
在实际中,网络通常比较快,**副本滞后replication lag**不太久,也即这个*最终***通常**不会太久,比如 ms 级别,最多 s 级别。但是,对于分布式系统,谁都不敢打包票,由于网络分区、机器高负载等等软硬件问题,在极端情况下,这个*最终*可能会非常久。
在实际中,网络通常比较快,**副本滞后replication lag**不太久,也即这个\*最终**\*通常**不会太久,比如 ms 级别,最多 s 级别。但是,对于分布式系统,谁都不敢打包票,由于网络分区、机器高负载等等软硬件问题,在极端情况下,这个*最终*可能会非常久。
> 总之,**最终**是一个非常不精确的限定词。
>
对于这种最终一致的系统,在工程中,要考虑到由于副本滞后所带来的一致性问题。
@@ -254,10 +248,8 @@
于是,我们再引入一种一致性保证:**单调读Monotonic reads**。
- 读写一致性和单调读有什么区别?
写后读保证的是写后读顺序,单调读保证的是**多次读**之间的顺序。
如何实现单调读?
1. 只从一个副本读数据。
@@ -320,7 +312,7 @@
单主和多主,在多数据中心场景下的对比:
| 对比项 | 单主模型 | 多主模型 |
| --- | --- | --- |
| ------ | ------------------------------------------ | ------------------------------------ |
| 性能 | 所有写入都要路由到一个数据中心 | 写入可以就近 |
| 可用性 | 主副本所在数据中心故障,需要有个切主的过程 | 每个数据中心可以独立工作 |
| 网络 | 跨数据中心,写入对网络抖动更敏感 | 数据中心间异步复制,对公网容错性更高 |
@@ -463,7 +455,6 @@ TODO自动冲突解决
在关系数据库时代,无主模型已经快被忘却。从 Amazon 的 Dynamo 论文开始无主模型又重新大放异彩RiakCassandra 和 Voldemort 都受其启发,可以统称为 **Dynamo 流Dynamo-style**
> 奇特的是Amazon 的一款数据库产品 DynamoDB和 Dynamo 并不是一个东西。
>
通常来说,在无主模型中,写入时可以:
@@ -614,10 +605,8 @@ A 和 B 并发 < === > A 不 happens-before B && B 不 happens-before A
如果两个操作可以定序,则 last write win如果两个操作并发则需要进行冲突解决。
> 并发、时间和相对性
>
> Lamport 时钟相关论文中有详细推导相关概念关系。为了定义并发,事件发生的绝对时间先后并不重要,只要两个事件都意识不到对方的存在,则称两个操作“并发”。从狭义相对论上来说,只要两个事件发生的时间差,小于光在两者距离传播所用时间,则两个事件不可能互相影响。推广到计算机网络中,只要由于网络问题导致,在事件发生时间差内,两者不能互相意识到,则称其是并发的。
>
### 确定 Happens-Before 关系

View File

@@ -6,7 +6,6 @@
2. **复制Replication**):系统机器一多,单机故障概率便增大,为了防止数据丢失以及服务高可用,需要做多副本。
> 分片Partition有很多别称。通用的有 Shard具体到实际系统HBase 中叫 RegionBigtable 中叫 tablet等等。本质上是对数据集的一种逻辑划分后面行文分片和分区可能混用且有时为名词有时为动词。
>
通常来说,数据系统在分布式系统中会有三级划分:数据集(如 Database、Bucket——分片Partition——数据条目Row、KV。通常每个分片只属于一个数据集每个数据条目只属于一个分片。单个分片就像一个小点的数据库。但是跨分区的操作的就要复杂的多。
@@ -119,7 +118,6 @@
2. 全局索引global index书中又称 term-based index
> 注:书中给的 document-based、term-based 两个名词(包括 document 和 term是从搜索中来的。由于搜索中都是 term→ document id list 的映射document-based 是指按 document id 进行分区,每个分区存的索引都是本地的 document ids而不管其他分区因此是本地索引查询时需要发到所有分区逐个查询。term-based 是指按 term 进行分区,则每个倒排索引都是存的全局的 document id list因此查询的时候只需要去 term 所在分区查询即可。
>
## 本地索引
@@ -213,15 +211,13 @@
但他们的分区数量都和集群节点数量没有直接关系。而另一种均衡策略,则是保持**总分区数量**和节点数量成正比,也即,保持每个节点分区数量不变。
假设集群有 m 个节点,每个节点有 n 个分区,在此种均衡策略下,当有新节点加入时,会从 m*n 个分区中随机选择 n 个分区,将其一分为二,一半由新节点分走,另一半留在原机器上。
假设集群有 m 个节点,每个节点有 n 个分区,在此种均衡策略下,当有新节点加入时,会从 m\*n 个分区中随机选择 n 个分区,将其一分为二,一半由新节点分走,另一半留在原机器上。
随机选择,很容易产生有倾斜的分割。但如果 n 比较大,如 Cassandra 默认是 256则新节点会比较容易均摊负载。
- 为什么?
是因为可以从每个节点选同样数量的分区吗?比如说 n = 256m = 16则可以从每个节点选 16 分区吗?
随机选择分区,要求使用基于哈希的分区策略,这也是最接近原始一致性哈希的定义的方法。(同样存疑。
## 运维:自动均衡还是手动均衡

11
ch07.md
View File

@@ -271,10 +271,8 @@ MVCC 的基本要点为:
使用 delete by 进行标记删除的原因在于可能还有正在进行的事务txid < deleted by可能会访问该对象。之后会有专门进行 GC 进程对这些数据进行真正的回收,当然删除时需要确认所有正在进行的事务 txid > deleted by。
- 个人认为不使用 delete by 也能达到标记删除的效果?
新的版本数据存在后,自动就使得老版本不可见。之后,只要确定没有事务正在访问老版本数据,即可进行 gc。通过 min(current tx) > latest version 即可判定没有事务访问了。
### 可见性规则
在事务中进行读取时,对于每个对象来说,需要**控制**其版本对事务的可见性,以保证该事务能够看到一致性的视图。
@@ -610,7 +608,6 @@ TDOO2PL 并非拿到所有的锁,才开始进行读写操作?而是按需
为了和书中保持一致,下面仍然称 2PL。
> 2PL 和 2PC 听起来很像,但它们不是一个东西,只是恰好都有两个阶段而已。
>
在防止脏写一节,提到了锁。但 2PL 中的锁会严格一些:
@@ -690,9 +687,9 @@ WHERE room_id = 123 AND
### 乐悲观并发控制
2PL 是一种**悲观***pessimistic*)的并发控制机制,就像多线程编程中的**互斥锁**mutual exclusion。其背后哲学是当可能有不好的事情如并发发生时先悲观的等待到条件好转其他事务释放锁再进行执行。而物理上的串行执行是将这种悲观哲学提升到了极致等价于每个事务在执行时都持有了整个数据库级别的互斥锁。为了弥补这种悲观带来的性能损失需要保证每个事务执行足够快。
2PL 是一种**悲观**_pessimistic_)的并发控制机制,就像多线程编程中的**互斥锁**mutual exclusion。其背后哲学是当可能有不好的事情如并发发生时先悲观的等待到条件好转其他事务释放锁再进行执行。而物理上的串行执行是将这种悲观哲学提升到了极致等价于每个事务在执行时都持有了整个数据库级别的互斥锁。为了弥补这种悲观带来的性能损失需要保证每个事务执行足够快。
SSI 是一种**乐观***optimistic*)的并发控制机制,类比多线程编程中的乐观锁。其相应哲学是,当存在潜在危险时,仍然不做任何检查去大胆的执行。当事务提交时,再进行冲突检测,如果存在冲突,则回退重试。将乐观发展到极致,则是不上任何锁,但为了给这种乐观进行兜底,需要在执行完后进行检查。
SSI 是一种**乐观**_optimistic_)的并发控制机制,类比多线程编程中的乐观锁。其相应哲学是,当存在潜在危险时,仍然不做任何检查去大胆的执行。当事务提交时,再进行冲突检测,如果存在冲突,则回退重试。将乐观发展到极致,则是不上任何锁,但为了给这种乐观进行兜底,需要在执行完后进行检查。
乐观并发控制并不是一种新思想,其优缺点被充分的讨论过:
@@ -709,7 +706,7 @@ SSI顾名思义基于快照隔离。即在 SSI 隔离级别中,所有的
2. 决策:考察读到的数据,做出某种决策。
3. 写入:将对应决策造成结果写回数据库。
即,这里面存在一个因果关系,读为因,写为果。如果在提交时,发现决策的**前提***premise*,如:“今天有两名医生排到了值班”)不再满足,则后面写入失去意义。因此为了提供可串行化的隔离级别,需要识别这种因果关系,并且能够在提交时检测前提是否失效,以决定是否中止事务。
即,这里面存在一个因果关系,读为因,写为果。如果在提交时,发现决策的**前提**_premise_,如:“今天有两名医生排到了值班”)不再满足,则后面写入失去意义。因此为了提供可串行化的隔离级别,需要识别这种因果关系,并且能够在提交时检测前提是否失效,以决定是否中止事务。
那如何检测前提是否失效呢?
@@ -755,7 +752,7 @@ SSI顾名思义基于快照隔离。即在 SSI 隔离级别中,所有的
1. 如果细粒度跟踪,虽然能精确的检测到真正的冲突,减少重试,但会有显著的记录开销。
2. 如果粗粒度的跟踪,虽然性能会好,但会导致更多的冲突和重试。
在某些情况下,即使一个事务读到的信息被另外一个事务的写入覆盖,仍然能保证可串行化的隔离级别。这取决于事务读到这些信息后,用来做了什么,*PostgreSQL* 便根据这个原则来减少不必要的重试。
在某些情况下,即使一个事务读到的信息被另外一个事务的写入覆盖,仍然能保证可串行化的隔离级别。这取决于事务读到这些信息后,用来做了什么,_PostgreSQL_ 便根据这个原则来减少不必要的重试。
和 2PL 相比SSI 的最大优点是不会通过锁来阻塞有依赖关系的事务并发执行。SSI 就想运行在快照隔离级别一样,读不阻塞写,写不阻塞读。只是追踪记录,在提交时决定是否提交或重试。这种设计是的查询延迟更可预测。尤其是,只读事务可以工作在一致性快照上,而不受影响,这对读负载很重的场景很有吸引力。

61
ch08.md
View File

@@ -1,6 +1,5 @@
# DDIA 逐章精读(八): 分布式系统中的麻烦事The Trouble with Distributed Systems
之前几章都在谈系统如何处理出错:副本故障切换、副本数据滞后、事务的并发控制。但前几张考虑到的情况:机器宕机、网络延迟都相对较**理想**。在实际大型分布式系统中,情况会更为悲观,可能会出错组件的一定会出错,而且出错的方式会更为复杂。任何大型系统的运维人员想必对此都有深有体会。
构建分布式系统和单机软件完全不同。在分布式系统中,系统有一千种奇妙的出错方法,本章将会探讨其中的一部分。我们会发现,在单机中我们以为是无比自然的假设,在分布式系统中,都可能不成立。作为工程师,我们总期望能构建能够处理任何可能故障的系统,但在实践中,一切都是**权衡**。不过,我们首先需要知道,可能会遇到哪些问题,才能进而选择:**是否要在目标场景下解决这些问题、还是为了降低系统复杂度忽略这些问题**。
@@ -22,7 +21,7 @@
在构建大型计算系统的选择上一个光谱:
1. 在光谱一侧,是**高性能计算**HPC*high-performance computing*)。使用上千个 CPU 构建的超级计算机,用于计算密集型工作,如天气预报、分子动力学模拟。
1. 在光谱一侧,是**高性能计算**HPC_high-performance computing_)。使用上千个 CPU 构建的超级计算机,用于计算密集型工作,如天气预报、分子动力学模拟。
2. 在光谱另一侧,是**云计算**cloud computing。云计算不是一个严谨的术语而是一个偏口语化的形象指代。通常指将通用的廉价的计算资源通过计算机网络收集起来进行池化然后按需分配给多租户并按实际用量进行计费。
3. 传统的企业**自建的数据中心**位于光谱中间。
@@ -31,8 +30,8 @@
但在本章,我们将重点放到以网络连接的多机系统中,这样的系统与单机应用与诸多不同之处:
1. **在线离线**。互联网应用多为**在线**online服务需要给用户提供随时可用、低延迟服务。在这种场景下重启以恢复任务或者服务是不可接受的。但在离线任务中如天气状况模拟。
2. **专用通用**。超算多用**专用硬件***specialized hardware*)构建而成。组件本身很可靠,组件间通信也很稳定——多通过共享内存或 RDMA 的方式。与之相反,云服务多由通用机器组网而成,通过堆数量达到与超算相当的性能,经济但故障率高。
3. **组网方式**。大型数据中心的网络通常基于 IP 和以太网,通常按 Clos 拓扑组网,以提供比较高的**对分带宽**bisection bandwidth。超算常用专用的网络拓扑如多维网格、环面拓扑*toruses*),能够为已知的 HPC 负载提供更好的性能。
2. **专用通用**。超算多用**专用硬件**_specialized hardware_)构建而成。组件本身很可靠,组件间通信也很稳定——多通过共享内存或 RDMA 的方式。与之相反,云服务多由通用机器组网而成,通过堆数量达到与超算相当的性能,经济但故障率高。
3. **组网方式**。大型数据中心的网络通常基于 IP 和以太网,通常按 Clos 拓扑组网,以提供比较高的**对分带宽**bisection bandwidth。超算常用专用的网络拓扑如多维网格、环面拓扑_toruses_),能够为已知的 HPC 负载提供更好的性能。
4. **故障常态化**。系统越是庞大,系统中有组件出错的概率便越高。在上千个节点组成的系统中,可以认为任何时刻,总有组件存在故障。在遇到故障时,如果在整个系统层面,仅简单选择放弃重试的策略,则系统可能不是在重试,就是在重试的路上,花在有效的工作时间少之又少。
5. **容错**。当部分节点故障时,如果系统仍能作为一个整体而正常工作,将会对运维十分友好。如,对于滚动升级,虽然单个经历了重启,但是多个节点组成的系统渐次重启时,整体仍然能对外正常工作。在云上,如果某个虚拟机有点慢,我们可以销毁它,再拉起一台(如果故障节点是少数,期望会更快)。
6. **本地异地**。多地部署的大型系统,多通过互联网通信(虽然也有专用网络),但总体来说,相对局域网更慢且易出错。相对的,我们对于超算有个基本预期——其多个节点都靠的很近。
@@ -42,7 +41,6 @@
因此,**面向容错进行设计**是对分布式系统软件的基本要求,为此,我们首先要了解**分布式系统中的常见问题**,并依此设计、编写、**测试**你的系统。
> 但在实践中任何设计都是取舍tradeoff容错是有**代价**(昂贵、损失性能、系统复杂度提升等等)的。因此,充分了解你的系统应用场景,才能做出合理的容错实现,过犹不及。
>
## 基于不可靠组件构建可靠系统
@@ -140,13 +138,10 @@
此外TCP 中存在**超时重传**机制,虽然重传本身对应用层不可见,但是超时重传带来的延迟却是无法掩盖的。
> **TCP 和 UDP**
>
> 一些对延迟敏感的场景,如视频会议和 IP 语音,常使用 UDP。由于 UDP 不需要提供额外保证,因此不需要做超时重传和流量控制,因此可以避免 TCP 很多排队造成的延迟。
>
> 当然,如果用户仍然需要某种程度的可靠性,可以基于 UDP 在应用层有针对性的做一些优化,比如在视频会议中,如果网络不好,可以主动问下:能再说一遍嘛?这是一种常用的思想,通用场景,可以使用屏蔽底层复杂度的协议;特化场景,可以使用相对底层、粗糙的协议,自己在应用层做有针对性的封装。是一个实现复杂度和效率的 tradeoff。
>
所有上述因素,都能造成网络延迟变化,且一个基本现象是:**网络流量越满,单个请求延迟抖动越大**。
@@ -160,11 +155,11 @@
如果我们的底层网络传输数据包时能够保证延迟上界、且不会丢包,那么基于此构建分布式系统将会容易的多。那为什么不在硬件层面解决相关问题让网络更可靠,从而让分布式软件免于关心这些复杂的细节呢?
为了回答这个问题,我们先来看一种历史产物——**固定电话网***fixed-line telephone network* ,非 VOIP、非蜂窝网络。在固话线路中高延迟音频帧和意外断线都是非常罕见的。固话网会为每一次通话预留**稳定低延迟**和**充足的带宽**链路以传输语音。如果计算机网络中也采用类似的技术,生活不会很美好吗?
为了回答这个问题,我们先来看一种历史产物——**固定电话网**_fixed-line telephone network_,非 VOIP、非蜂窝网络。在固话线路中高延迟音频帧和意外断线都是非常罕见的。固话网会为每一次通话预留**稳定低延迟**和**充足的带宽**链路以传输语音。如果计算机网络中也采用类似的技术,生活不会很美好吗?
当你在固网内拨打电话时,会建立一条贯穿贯穿全链路的保证足量带宽的固定链路,我们称之为**电路**circuit该电路会保持到通话结束才释放。以 ISDN 网络为例,其每秒能容纳 4000 帧语音信号,当发起通话时,它会在每个方向为每帧数据分配 16 比特空间。因此,在整个通话期间,两端各自允许每 250 微秒250us * 4000 = 1s发送 16 比特语音数据。
当你在固网内拨打电话时,会建立一条贯穿贯穿全链路的保证足量带宽的固定链路,我们称之为**电路**circuit该电路会保持到通话结束才释放。以 ISDN 网络为例,其每秒能容纳 4000 帧语音信号,当发起通话时,它会在每个方向为每帧数据分配 16 比特空间。因此,在整个通话期间,两端各自允许每 250 微秒250us \* 4000 = 1s发送 16 比特语音数据。
这种网络是**同步***synchronous*)的:尽管数据也会通过多个路由节点,但由于通信所需的资源(如上述 16 bit 空间)已经在下一跳中被提前预留出来了,因此这些数据帧不会面临排队问题。由于不存在排队,则端到端的最大延迟是固定的。我们也称此种网络为**有界网络**bounded network
这种网络是**同步**_synchronous_)的:尽管数据也会通过多个路由节点,但由于通信所需的资源(如上述 16 bit 空间)已经在下一跳中被提前预留出来了,因此这些数据帧不会面临排队问题。由于不存在排队,则端到端的最大延迟是固定的。我们也称此种网络为**有界网络**bounded network
### 计算机网络为什么不能同样稳定?
@@ -175,9 +170,9 @@
应用层给到 TCP 的任意大小的数据,都会在尽可能短的时间内被发送给对端。如果一个 TCP 连接暂时空闲,则他不会占用任何网络带宽。相比之下,在打电话时即使不说话,电路所占带宽也得一直被预留。
如果数据中心和互联网使用**电路交换***circuit-switched*)网络,他们应该能够建立一条保证稳定最大延迟的数据链路。但是事实上,由于以太网和 IP 网采用**封包交换**协议(*packet-switched protocols* ,常翻译为**分组交换**,但我老感觉它不太直观),没有电路的概念,只能在数据包传送的时候对其进行排队,也不得不忍受由此带来的无界延迟。
如果数据中心和互联网使用**电路交换**_circuit-switched_)网络,他们应该能够建立一条保证稳定最大延迟的数据链路。但是事实上,由于以太网和 IP 网采用**封包交换**协议(_packet-switched protocols_,常翻译为**分组交换**,但我老感觉它不太直观),没有电路的概念,只能在数据包传送的时候对其进行排队,也不得不忍受由此带来的无界延迟。
那为什么数据中心网络和互联网要使用封包交换协议呢?答曰,为了应对互联网中无处不在的**突发流量***bursty traffic*)。在电话电路中,音频传输所需带宽是固定的;但在互联网中,各种多媒体数据(如电子邮件、网页、文件)所需带宽却是差异极大且动态变化的,我们对他们的唯一要求就是传地尽可能快。
那为什么数据中心网络和互联网要使用封包交换协议呢?答曰,为了应对互联网中无处不在的**突发流量**_bursty traffic_)。在电话电路中,音频传输所需带宽是固定的;但在互联网中,各种多媒体数据(如电子邮件、网页、文件)所需带宽却是差异极大且动态变化的,我们对他们的唯一要求就是传地尽可能快。
设想你使用电路网络传输一个网页,你需要为它预留带宽,如果你预留过低,则传输速度会很慢;如果你预留过高,则可能电路都没法建立(带宽余量不够,就没法建立连接),如果建立了,也会浪费带宽。互联网数据的**丰富性**和**异构性**,让使用电路网络不太可能。
@@ -195,8 +190,7 @@
>
> 类似的情形还发生在 CPU 的分时复用里。如果再多个线程间动态的共享每个 CPU则一个线程使用 CPU 时,其他线程必须排队等待,且排队时间不确定。这种使用 CPU 的方式,比分配给每个线程固定的时间片要高效。类似的,使用虚拟化的方式共享同一台物理机,也会有更好的硬件利用率。
>
> 在资源静态分配的环境中,如专用的硬件、互斥的带宽分配,有界延迟能够被保证。但是,这种方式是以降低资源利用率为代价的,换句话说,更贵。反之,通过多租户方式动态的共享资源,更便宜,但代价是**不稳定的延迟***variable delays*)。**不稳定的延迟并非什么不可变的自然法则,而仅是一种代价和收益权衡的结果罢了**。
>
> 在资源静态分配的环境中,如专用的硬件、互斥的带宽分配,有界延迟能够被保证。但是,这种方式是以降低资源利用率为代价的,换句话说,更贵。反之,通过多租户方式动态的共享资源,更便宜,但代价是**不稳定的延迟**_variable delays_)。**不稳定的延迟并非什么不可变的自然法则,而仅是一种代价和收益权衡的结果罢了**。
# 不可靠的时钟
@@ -247,7 +241,7 @@
- 如果计算机时钟和 NTP 服务器相差太多,该计算机会拒绝同步或强制同步。如果强制同步,应用层在会在同步前后看到一个时钟的**跳变**。
- 如果一个节点通过偶然的设置把 **NTP 服务器给墙**了,并且没有留意到这个问题,则可能会造成这段时间内时钟不同步,实践中确实发生过类似问题。
- NTP 同步受限于网络的延迟,因此在延迟不稳定的**拥塞网络**中,其精度会受到影响。有实验表明通过互联网进行时钟同步,可以实现最小 35ms 的时钟误差。尽管网络中偶有将近 1s 延迟尖刺,但可以通过合理配置来忽略尖刺对应的同步。
- 润秒的存在会导致一分钟可能有 59s 或者 61s如果系统在做设计时没有考虑这种特殊情况就有可能在运行时遇到问题很多大型系统都因此而宕机。处理闰秒最好的办法是让 NTP 服务器在一天中逐渐调整,**摊平闰秒**(也称为:**拖尾***smearing*),不过在实际中, NTP 服务器处理闰秒的行为不尽相同。
- 润秒的存在会导致一分钟可能有 59s 或者 61s如果系统在做设计时没有考虑这种特殊情况就有可能在运行时遇到问题很多大型系统都因此而宕机。处理闰秒最好的办法是让 NTP 服务器在一天中逐渐调整,**摊平闰秒**(也称为:**拖尾**_smearing_不过在实际中NTP 服务器处理闰秒的行为不尽相同。
- 在虚拟机中,其**物理时钟是虚拟化**出来的,从而给运行其上并依赖精确计时的应用带来额外挑战。由于一个 CPU 内核是被多个 VM 所共享的,当一个 VM 运行时,其他 VM 就得让出内核几十毫秒。在 VM 恢复运行后,从应用代码的视角,其时钟就是毫无征兆的突然往前跳变了一段。
- 如果你的软件将会运行在**不受控的设备**上,如智能手机或者嵌入式设备,则你不能完全相信设备系统时钟。因为用户可能会由于一些原因(比如绕开游戏时间限制),故意将其硬件时钟设置成一个错误的日期和时间,从而引起系统时钟的跳变。
@@ -311,7 +305,7 @@
但不幸,大多数服务器的时钟系统 API 在给出时间点时,并不会一并给出对应的不确定区间。例如,你使用 `clock_gettime()` 系统调用获取时间戳时,返回值并不包括其置信区间,因此你无法知道这个时间点的误差是 5 毫秒还是 5 年。
一个有趣的反例是谷歌在 Spanner 系统中使用的 *TrueTime* API会显式的给出置信区间。当你向 TrueTime 系统询问当前时钟时,会得到两个值,或者说一个区间:`[earliest, latest]`,前者是最早可能的时间戳。后者是最迟可能的时间错。通过该不确定预估,我们可以确定准确时间点就在该时钟范围内。此时,区间的大小取决于,上一次同步过后本地石英钟的漂移多少。
一个有趣的反例是谷歌在 Spanner 系统中使用的 _TrueTime_ API会显式的给出置信区间。当你向 TrueTime 系统询问当前时钟时,会得到两个值,或者说一个区间:`[earliest, latest]`,前者是最早可能的时间戳。后者是最迟可能的时间错。通过该不确定预估,我们可以确定准确时间点就在该时钟范围内。此时,区间的大小取决于,上一次同步过后本地石英钟的漂移多少。
### 用于快照的时钟同步
@@ -323,7 +317,7 @@
那么,我们可以用机器的挂历时钟的时间戳作为事物的 ID 吗?如果我们能让系统中的多台机器时钟保持严格同步,则其可以满足要求:**后面的事务会具有较大的时间戳,即较大的事务 ID**。但现实中,由于时钟同步的不确定性,用这种方法产生事务 ID 是不太靠谱的。
但 Spanner 就使用了物理时钟实现了快照隔离它是如何做到可用的呢Spanner 在设计 TrueTime 的 API 时,让其返回一个**置信区间**,而非一个时间点,来代表一个**时间戳**。假如现在你有两个时间戳 A 和 B*A* = [*Aearliest*, *Alatest*] and *B* = [*Bearliest*, *Blatest*]),且这两个时间戳对应的区间没有交集(例如,*Aearliest* < *Alatest* < *Bearliest* < *Blatest*),则我们可以确信时间戳 B 发生于 A 之后。但如果两个区间有交集,我们则不能确定 A 和 B 的相对顺序。
但 Spanner 就使用了物理时钟实现了快照隔离它是如何做到可用的呢Spanner 在设计 TrueTime 的 API 时,让其返回一个**置信区间**,而非一个时间点,来代表一个**时间戳**。假如现在你有两个时间戳 A 和 B_A_ = [*Aearliest*, *Alatest*] and _B_ = [*Bearliest*, *Blatest*]),且这两个时间戳对应的区间没有交集(例如,_Aearliest_ < _Alatest_ < _Bearliest_ < _Blatest_),则我们可以确信时间戳 B 发生于 A 之后。但如果两个区间有交集,我们则不能确定 A 和 B 的相对顺序。
为了保证这种时间戳能够用作事务 ID相邻生成的两个时间戳最好要间隔一个置信区间以保证其没有交集。为此Spanner 在索要时间戳时(比如提交事务),会等待一个置信区间。因此置信区间越小,这种方案的性能也就越好。为此,谷歌在每个数据中心使用了专门的硬件做为时钟源,比如原子钟和 GPS 接收器,以保证时钟的置信区间不超过 7 ms。
@@ -365,11 +359,11 @@ while (true) {
- 一些编程语言的运行时(如 JVM都会有垃圾回收器GC。垃圾回收器有时候会暂停所有运行中的线程以进行垃圾回收这个**全暂停**stop-the-world时间有时甚至能到达数分钟即便号称可以并发 GC 的最新 GC 算法(如 Hotpot JVM 的 CMS 算法),也不能真正的和用户线程并行,仍会时不时的暂停,只不过这个时间缩短了很多。另外,我们也可通过修改内存分配模式和进行 GC 参数调优来进一步降低 GC 影响。但即便如此,如果我们想真正的提供足够鲁棒的程序,就不能对 GC 所造成的停顿时间做最坏假设。
- 在**虚拟化环境**中,一个虚拟机可能会在任意时间点被**挂起**suspended暂停所有正在运行的进程并将其上下文从内存中保存到磁盘和**恢复**resumed将上下文恢复到内存中并且继续执行暂停的进程挂起到恢复的间隔可能持续任意长时间。有时该功能也被称为虚机的**在线迁移**live migration此时虚机暂停的时间取决于上下文的迁移速率。
- 哪怕像在笔记本这样的用户终端上,程序的运行也有可能被随时挂起和恢复。如,用户合上笔记本。
- 当操作系统做上下文切换,将线程切走时;当**管控程序**hypervisor切到一个新的虚拟机时当前正在执行的线程可能会停在代码中的任意位置。在虚拟机环境中其他虚拟机占用 CPU 的时间也被称为**被窃时间***steal time*)。在物理机负载很重时,比如调度队列中有大量线程在等待时间片,某个被暂停的线程可能要好久才能重新执行。
- 当操作系统做上下文切换,将线程切走时;当**管控程序**hypervisor切到一个新的虚拟机时当前正在执行的线程可能会停在代码中的任意位置。在虚拟机环境中其他虚拟机占用 CPU 的时间也被称为**被窃时间**_steal time_)。在物理机负载很重时,比如调度队列中有大量线程在等待时间片,某个被暂停的线程可能要好久才能重新执行。
- 如果操作系统配置了允许**换页**swapping to disk, paging则有时候一个简单的内存访问也可能引起缺页错误这时我们就需要从磁盘中加载一个页到内存。在进行此 IO 时,线程多会挂起,让出 CPU。如果内存吃紧缺页换页可能会非常频繁。在极端情况下操作系统可能会将大部分时间都浪费在换页上而非正经工作上也被称为**颠簸**thrashing。为了避免此问题服务器上的允许换页的配置项一般不打开。当然你也可以点杀一部分进程来释放内存避免换页这就是 trade off 了。
- 在 Unix 操作系统中,可以通过向进程发送 **SIGSTOP** 信号来让其暂停。如,用户对执行的进程在 Shell 中按下 Ctrl-Z。该信号会阻止进程再获取 CPU 的时间片,直到我们使用 SIGCONT 将其再度唤起。你自己的环境中可能不怎么使用 SIGSTOP但是运维人员偶尔还是会用的。
所有上述情景都会在任意时刻**中断***preempt*)正在运行的线程,并在之后某个时刻将其重新唤醒,而线程本身对这个过程是不感知的。类似的情形还有单机多线程编程:你不能对多个线程代码的**相对执行顺序**有任何假设,因为上下文切换和并发执行可能会在任何时间以任何形式发生。
所有上述情景都会在任意时刻**中断**_preempt_)正在运行的线程,并在之后某个时刻将其重新唤醒,而线程本身对这个过程是不感知的。类似的情形还有单机多线程编程:你不能对多个线程代码的**相对执行顺序**有任何假设,因为上下文切换和并发执行可能会在任何时间以任何形式发生。
在单机我们有很多手段可以对多线程的执行进行协调,使之线程安全。如锁、信号量、原子计数器、无锁数据结构、阻塞队列等等。但不幸的是,分布式系统中我们没有对应的手段。因为在多机间不能**共享内存**,只能依靠**消息同步**,而且是要经过不可靠网络的消息!
@@ -384,14 +378,12 @@ while (true) {
举个例子,如果你正在驾车,传感器检测到车祸,你肯定不希望你的车载系统此时正在进行 GC 而不能及时处理该信号吧。
> 实时系统真的实时吗?
>
> 在**嵌入式**系统里,实时意味着需要通过设计、测试等多层面来让系统在延迟上提供某种保证。其在 **Web** 中也有实时系统real-time的叫法但更多的侧重于服务器会流式的处理客户端请求并将数据发回客户端但对响应时间并没有严苛的要求。
>
我们需要在全软件栈进行优化才能提供实时保证:
1. **在操作系统上**,需要能提供指定所需 CPU 时间片的实时操作系统(*real-time operating system* RTOS
1. **在操作系统上**,需要能提供指定所需 CPU 时间片的实时操作系统(_real-time operating system_RTOS
2. **在依赖库中**,所有的函数都需要注释其运行时间的上界。
3. **在内存分配上**,要限制甚至禁止动态内存分配(会有实时 GC 器,但不会占用太多时间)。
4. **在观测和测试上**,需要进行详尽的衡量和测试,以保证满足实时要求。
@@ -435,11 +427,11 @@ while (true) {
打个比方,这种情况就像一个噩梦:处于半连接的节点就像躺在棺材里被运向墓地,尽管他持续大喊:“我没有死”,但没有人能听到他的喊声,葬礼继续。
第二个场景,稍微不那么噩梦一些,这个处于半连接的节点意识到了他发出去的消息别人收不到,进而推测出应该是网络出了问题。但纵然如此,该节点仍然被标记为死亡,而它也没有办法做任何事情来改变,*但起码他自己能意识到这一点*
第二个场景,稍微不那么噩梦一些,这个处于半连接的节点意识到了他发出去的消息别人收不到,进而推测出应该是网络出了问题。但纵然如此,该节点仍然被标记为死亡,而它也没有办法做任何事情来改变,_但起码他自己能意识到这一点_
第三个场景,假设一个节点经历了长时间的 GC该节点上的所有线程都被中断长达一分钟此时任何发到该节点的请求都无法被处理自然也就无法收到答复。其他节点经过等待、重试、失掉耐心进而最终标记该节点死亡然后将其送进棺材板。经过漫长的一分钟后终于GC 完成,所有线程被唤醒从中断处继续执行。**从该线程的角度来看,好像没有发生过任何事情**。但是其他节点惊讶地发现棺材板压不住了,该节点做起来了,恢复了健康,并且又开始跟旁边的人很开心的聊天了。
上述几个故事都表明,任何节点都没法**独自断言**其自身当前状态。一个分布式系统不能有单点依赖,因为单个节点可能在任意时刻故障,进而导致整个系统卡住,甚而不能恢复。因此,大部分分布式算法会基于一个**法定人数***quorum*),即让所有节点进行投票:**任何决策都需要达到法定人数才能生效,以避免对单节点的依赖**。
上述几个故事都表明,任何节点都没法**独自断言**其自身当前状态。一个分布式系统不能有单点依赖,因为单个节点可能在任意时刻故障,进而导致整个系统卡住,甚而不能恢复。因此,大部分分布式算法会基于一个**法定人数**_quorum_),即让所有节点进行投票:**任何决策都需要达到法定人数才能生效,以避免对单节点的依赖**。
其中,前面故事中的宣布某个节点死亡就是这样一种决策。如果有达到法定个数的节点宣布某节点死亡,那他就会被标记为死亡。即使他还活着,也不得不服从系统决策而出局。
@@ -483,7 +475,7 @@ while (true) {
## 拜占庭错误
防护令牌只能检测并阻止**无意***inadvertently如不知道自己租约过期了*)中犯错的客户端。但如果某个客户端节点存心想打破系统约定,可以通过**伪造**防护令牌来轻易做到。
防护令牌只能检测并阻止**无意**_inadvertently如不知道自己租约过期了_)中犯错的客户端。但如果某个客户端节点存心想打破系统约定,可以通过**伪造**防护令牌来轻易做到。
在本书中我们假设所有参与系统的节点有可能**不可靠**unreliable、但一定是**诚实的**honest这些节点有可能反应较慢甚至没有响应由于故障他们的状态可能会过期由于 GC 停顿或者网络延迟),但一旦节点响应,“说的都是真话”:**在其认知范围内,尽可能的遵守协议进行响应**。
@@ -496,9 +488,8 @@ while (true) {
> 在该问题的拜占庭版本,有 n 个将军,但由于中间出了一些叛徒,他们想达成共识更具难度。但大部分将军仍然是忠诚的,并且会送出真实的消息;与此同时,叛徒会试图通过送出假的或者失实的消息来欺骗和混淆其他人(同时保持隐蔽)。大家事先都不知道谁是叛徒。
>
> 拜占庭是一个古希腊城市后来罗马皇帝君士坦丁在此建立新都称为“新罗马”但后人普遍被以建立者之名称作君士坦丁堡现在是土耳其的伊斯坦布尔。当然没有任何历史证据表明拜占庭的将军比其他地方更多地使用阴谋诡计。相反这个名字是来自于拜占庭本身在计算机出现很久之前拜占庭就有极度复杂、官僚主义、狡猾多变等含义。Lamport 想选一个不会冒犯任何读者的城市,比如,有人提醒他*阿尔巴尼亚将军问题*就不是一个好名字。
>
如果有一些节点发生故障且不遵守协议,或者恶意攻击者正在扰乱网络,一个系统仍能正确运行,则该系统是**拜占庭容错的***Byzantine fault-tolerant*)。举几个相关的场景例子:
如果有一些节点发生故障且不遵守协议,或者恶意攻击者正在扰乱网络,一个系统仍能正确运行,则该系统是**拜占庭容错的**_Byzantine fault-tolerant_)。举几个相关的场景例子:
- 在航天环境中,由于**高辐射环境**的存在,计算机内存或者寄存器中的数据可能会损坏,进而以任意不可预料的方式响应其他节点的请求。在这种场景下,系统故障代价会非常高昂(如:太空飞船坠毁并致使所有承载人员死亡,或者火箭装上国际空间站),因此**飞控系统**必须容忍拜占庭故障。
- 在一个有**多方组织**参与的系统中,有些参与方可能会尝试作弊或者欺骗别人。在这种环境中,由于恶意消息发送方的存在,无脑的相信其他节点的消息是不安全的。如,类似比特币或者其他区块链的 p2p 网络,就是一种让没有互信基础的多方,在不依赖**中央权威**的情况下,就某个交易达成共识的一种方法。
@@ -527,21 +518,21 @@ Web 应用确实可能遇到由任意终端用户控制的客户端(如浏览
对于时间的假设,有三种系统模型很常用:
1. **同步模型(*synchronous model***。这种模型假设**网络延迟**、**进程停顿**和**时钟错误**都是**有界**的。但这不是说,时钟时完全同步的、网络完全没有延迟,只是说我们知道上述问题永远不会超过一个上界。但当然,这不是一个现实中的模型,因为在实践中,无界延迟和停顿都会实实在在的发生。
1. **同步模型(_synchronous model_**。这种模型假设**网络延迟**、**进程停顿**和**时钟错误**都是**有界**的。但这不是说,时钟时完全同步的、网络完全没有延迟,只是说我们知道上述问题永远不会超过一个上界。但当然,这不是一个现实中的模型,因为在实践中,无界延迟和停顿都会实实在在的发生。
2. **半同步模型partial synchronous**。意思是在大多数情况下,网络延迟、进程停顿和时钟漂移都是有界的,只有偶尔,他们会超过界限。这是一种比较真实的模型,即在**大部分时间里**,系统中的网络和进程都表现良好,否则我们不可能完成任何事情。但与此同时,我们必须要记着,任何关于时限的假设都有可能被打破。且一旦出现出现异常现象,我们需要做好最坏的打算:网络延迟、进程停顿和时钟错误都有可能错的非常离谱。
3. **异步模型(*Asynchronous model***。在这种模型里,算法不能对时间有任何假设,甚至时钟本身都有可能不存在(在这种情况下,超时间隔根本没有意义)。有些算法可能会针对这种场景进行设计,但很少很少。
3. **异步模型(_Asynchronous model_**。在这种模型里,算法不能对时间有任何假设,甚至时钟本身都有可能不存在(在这种情况下,超时间隔根本没有意义)。有些算法可能会针对这种场景进行设计,但很少很少。
除时间问题,我们还需要对节点故障进行抽象。针对节点,有三种最常用的系统模型:
1. **宕机停止故障(*Crash-stop faults***。节点只会通过崩溃的方式宕机,即某个时刻可能会突然宕机无响应,并且之后永远不会再上线。
2. **宕机恢复故障(*Crash-recovery faults***。节点可能会在任意时刻宕机,但在宕机之后某个时刻会重新上线,但恢复所需时间我们是不知道的。在此模型中,我们假设节点的稳定存储中的数据在宕机前后不会丢失,但内存中的数据会丢失。
3. **拜占庭(任意)故障(*Byzantine (arbitrary) faults***。我们不能对节点有任何假设,包括宕机和恢复时间,包括善意和恶意,前面小节已经详细讨论过了这种情形。
1. **宕机停止故障(_Crash-stop faults_**。节点只会通过崩溃的方式宕机,即某个时刻可能会突然宕机无响应,并且之后永远不会再上线。
2. **宕机恢复故障(_Crash-recovery faults_**。节点可能会在任意时刻宕机,但在宕机之后某个时刻会重新上线,但恢复所需时间我们是不知道的。在此模型中,我们假设节点的稳定存储中的数据在宕机前后不会丢失,但内存中的数据会丢失。
3. **拜占庭(任意)故障(_Byzantine (arbitrary) faults_**。我们不能对节点有任何假设,包括宕机和恢复时间,包括善意和恶意,前面小节已经详细讨论过了这种情形。
对于真实世界,**半同步模型**和宕机恢复故障是较为普遍的建模,那我们又要如何设计算法来应对这两种模型呢?
### 算法的正确性
我们可以通过描述算法**需要满足的性质**,来定义其正确性。举个例子,排序算法的输出满足特性:*任取结果列表中的两个元素,左边的都比右边的小*。这是一种简单的对列表有序的形式化定义。
我们可以通过描述算法**需要满足的性质**,来定义其正确性。举个例子,排序算法的输出满足特性:_任取结果列表中的两个元素,左边的都比右边的小_。这是一种简单的对列表有序的形式化定义。
类似的,我们可以给出描述分布式算法的正确性的一些性质。如,我们想通过产生防护令牌的方式来上锁,则我们期望该算法具有以下性质:
@@ -555,7 +546,7 @@ Web 应用确实可能遇到由任意终端用户控制的客户端(如浏览
为了进一步弄清状况,我们需要进一步区分**两类**不同的属性:**安全性**safety和**存活性**liveness。在上面的例子中唯一性和单调有序性属于安全性但可用性属于存活性。
那如何区分这两者呢?一个简单的方法是,在描述存活性的属性的定义里总会包含单词:“**最终***eventually*)”,对,我知道你想说什么,**最终一致性**eventually也是一个存活性属性。
那如何区分这两者呢?一个简单的方法是,在描述存活性的属性的定义里总会包含单词:“**最终**_eventually_)”,对,我知道你想说什么,**最终一致性**eventually也是一个存活性属性。
安全性,通俗的可以理解为**没有坏事发生**nothing bad happens而存活性可以理解为**好的事情最终发生了**something good eventually happens但也不要对这些非正式定义太过咬文嚼字因为所谓的“好”和“坏”都是相对的。安全性和存活性的严格定义都是精确且数学化的

96
ch09.md
View File

@@ -1,7 +1,6 @@
# DDIA 逐章精读(九): 一致性和共识协议Consistency and Consensus
> 本章的线性一致性是在铺垫了多副本、网络问题、时钟问题后的一个综合探讨。首先探讨了线性一致的内涵:让系统表现得好像只有一个数据副本。然后讨论如何实现线性一致性,以及背后所做出的的取舍考量。其间花了一些笔墨探讨 CAP可以看出作者很不喜欢 CAP 的模糊性。
>
如前所述,分布式系统中很多事情都有可能出错。解决出错最**简单粗暴**的方法是让整个**系统宕机**,并给出出错原因。但在实际生产中,这种方式多不可接受,此时我们就需要找到**容错**tolerating faults的方法。即即使系统构件出现了一些问题我们能保证系统仍然正常运行。
@@ -13,10 +12,9 @@
构建一个容错系统最好的方法是:**找到一些基本抽象,可以对上提供某些承诺,应用层可以依赖这些承诺来构建系统,而不必关心底层细节**。在第七章中,通过使用事务,应用层可以假设不会发生宕机(**原子性**,意思是不会因为宕机出现让事务停留在半成功的状态),没有其他应用并发访问数据库数据(**隔离性**),且存储系统非常可靠(**持久性**)。事务模型会隐藏节点宕机、**竞态条件**race conditions、硬盘故障等底层细节即使这些问题出现了应用层也不必关心。
本章将继续讨论一些可以减轻应用层负担的分布式系统中的**基本抽象**。比如,分布式系统中最重要的一个抽象——**共识**consensus*让所有节点在**某件事情**上达成一致*。在本章稍后的讨论可以看出,让系统中的所有节点在有网络故障和节点宕机的情况下达成共识,是一件非常棘手的事情。
本章将继续讨论一些可以减轻应用层负担的分布式系统中的**基本抽象**。比如,分布式系统中最重要的一个抽象——**共识**consensus_让所有节点在**某件事情**上达成一致_。在本章稍后的讨论可以看出,让系统中的所有节点在有网络故障和节点宕机的情况下达成共识,是一件非常棘手的事情。
> 为什么共识协议如此重要呢?他和真实系统的连接点在于哪里?答曰,**操作日志**。而大部分**数据系统**都可以抽象为一系列**数据操作**的依次施加,即状态机模型。而共识协议可以让多机对某个**确定**的**操作序列**达成共识,进而对系统的任意状态达成共识。
>
一旦我们实现了**共识协议**,应用层可以依赖其做很多事情。例如,你有一个使用单主模型的数据库,如果主副本所在节点宕机,我们便可以使用共识协议选出新的主。在第五章处理节点下线(**Handling Node Outages**)一节中我们提到过,只有唯一的主,并且所有副本都认可该主,是一个需要确保的非常重要的特性。如果有超过一个节点都认为自己是主,我们称之为**脑裂**split brain。脑裂很容易导致数据丢失而正确实现是的共识协议能够避免该问题。
@@ -30,7 +28,7 @@
在第五章中日志滞后问题Problems with Replication Lag小节我们分析了一些多副本数据所遇到的时序问题。在相同时刻如果对比多副本数据库中一份数据的两个副本我们可能会看到不一致的数据。这是因为写请求到达不同的数据节点总会存在一个**时间差**。无论我们使用什么数据**副本模型**(单主、多主和无主),这种数据的不一致性都有可能会发生。
大部分**多副本数据库**replicated databases提供**最终一致性**eventually consistency的保证这意味着只要你对数据库停写并等待足够长的时间则所有对相同数据的读取请求最终会返回相同的结果。从另一个角度来说所有的不一致都是暂时的最终都会被解决当然这得是在网络故障能最终修复的假设之下。描述相同意思的一个更好的名字可能**收敛性***convergence*),即最终,所有副本都会收敛到相同的值。
大部分**多副本数据库**replicated databases提供**最终一致性**eventually consistency的保证这意味着只要你对数据库停写并等待足够长的时间则所有对相同数据的读取请求最终会返回相同的结果。从另一个角度来说所有的不一致都是暂时的最终都会被解决当然这得是在网络故障能最终修复的假设之下。描述相同意思的一个更好的名字可能**收敛性**_convergence_),即最终,所有副本都会收敛到相同的值。
但这是一个相当不靠谱的保证——没有提供任何关于**何时**收敛的信息。而在收敛之前,对于相同数据的读取,可能会返回任意值甚至不返回。举个例子,你向多副本数据库中写入了一条数据,并立即读取他。你能读到什么,最终一致性对此不会提供任何保证,因为读取请求可能会被路由到任何其他副本。
@@ -41,7 +39,6 @@
本章我们会一起探究一些更强的一致性模型,但选择这些模型是有代价的。相对弱一致性模型系统来说,这么做要么会牺牲性能,要么会牺牲可用性。但提供强保证会让应用层能更加容易、正确的使用。但当然,我们最终还是得根据具体场景,来选择使用何等强度一致性模型。
> 在实践中,我们常会使用分层策略,让某些底层解决可用性、性能和容量的问题,让上层解决一致性的问题。比如云上各种基于 aws s3 的关系型数据库。另外,也有些系统会同时提供多种一致性模型供用户选择,在一致性和性能间进行取舍。
>
分布式系统中的**一致性模型的强弱**和第七章讲的事物的**隔离级别层次**有一些共通之处,比如在性能和隔离性/一致性间做取舍。但他们是相对独立的抽象:
@@ -80,8 +77,8 @@
在本例中,寄存器支持两种类型的操作:
1. *read*(*x*) ⇒ *v:* 客户端请求读寄存器 x 的值,数据库会返回值 v
2. *write*(*x*, *v*) ⇒ *r* 客户端请求将寄存器 x 的值设置为 v数据返回结果 r可能是成功或者失败
1. _read_(_x_) ⇒ _v:_ 客户端请求读寄存器 x 的值,数据库会返回值 v
2. _write_(_x_, _v_) ⇒ _r_ 客户端请求将寄存器 x 的值设置为 v数据返回结果 r可能是成功或者失败
在上图中x 初始值为 0客户端 C 发出一个写请求将其置为 1。在此期间客户端 A 和 B 不断地向数据库请求 x 的最新值。试问 A 和 B 的每个读请求都会读到什么值?
@@ -92,7 +89,6 @@
不过,这也不足以刻画线性一致性:**如果和写请求并发的多个读请求既可能会返回新值,也可能会返回旧值,则在写请求持续期间,读客户端可能会看到不断交替的新旧值**。这明显不符合我们所期待的,能够提供“单数据副本”抽象的系统。
> 在多副本数据库中,如果要解决线性一致性,就要满足一旦某个客户端读取到新值,则其之后的读请求一定能读到该新值,而不是还可能看到旧值。这很难,由于上一章讲的时钟问题,我们甚至很难对多个客户端定义“**先后**”。此外,这种线性一致性的特性类似于薛定谔的猫,本来可能有多个状态,但一旦有个一个客户端进行了一次观察,就迅速的坍缩到了一个状态,其他后来者,也只能看到这一个状态。从另外一个角度理解,是读取请求**塑造**seal了并发请求的多状态边界。
>
为了让该系统满足线性一致性,我们需要增加一些额外限制,如下图:
@@ -124,22 +120,16 @@
这就是线性一致性背后的一些直觉,[参考文献](https://cs.brown.edu/~mph/HerlihyW90/p463-herlihy.pdf)中有更形式化的定义。可以(但是代价很高)通过记录系统中所有请求的时间线,并检查其能否组成合法的顺序,来测试该系统是否满足线性一致性。
> **线性一致性和可串行化**
>
> **线性一致性**Linearizability很容易和**可串行化**serializability相混淆因为他们看起来都意味着可以进行顺序化组织。但他们是不同维度的约束我们很有必要对其进行区分
>
> **可串行化**Serializability。可串行化是事务的一种隔离级别。每个事务可能会涉及多个数据对象行、文档、记录的读写[之前](https://ddia.qtmuniao.com/#/ch07?id=%e5%8d%95%e5%af%b9%e8%b1%a1%e5%92%8c%e5%a4%9a%e5%af%b9%e8%b1%a1%e6%93%8d%e4%bd%9c)有讨论过单对象和多对象。可串行化可以保证所有事务好像按**某种顺序依次执行**(后一个事务在前一个事务结束后才开始)。需要注意的是,如果某种串行顺序和实际执行顺序不一致也没事,只要是串行执行就行。举个例子,如果 A、B、C 三个事务并发执行,真实顺序是 A、B、C但如果对应用层表现为 CAB 的执行顺序(可能由于多机时间戳不同步),也可以叫可串行化,但 CAB 的执行顺序在某个对象上可能不满足线性一致性。
>
> **线性一致性**Linearizability。线性一致性是一种针对寄存器register单个数据对象的读写新鲜度保证。它不会将多个操作打包成事务因此不能避免像之前提到的[写偏序](https://ddia.qtmuniao.com/#/ch07?id=%e5%86%99%e5%81%8f%e5%ba%8f%e5%92%8c%e5%b9%bb%e8%af%bb)等问题,除非使用某些辅助手段,如[物化冲突](https://ddia.qtmuniao.com/#/ch07?id=%e7%89%a9%e5%8c%96%e5%86%b2%e7%aa%81)。
>
> 一个数据库可以同时提供可串行化和线性一致性保证,我们称之为**严格可串行化***strict serializability*)或者**单副本可串行化**strong one-copy serializability。使用两阶段锁或者真正串行化执行实现的可串行化通常都是线性一致的。
>
> 一个数据库可以同时提供可串行化和线性一致性保证,我们称之为**严格可串行化**_strict serializability_)或者**单副本可串行化**strong one-copy serializability。使用两阶段锁或者真正串行化执行实现的可串行化通常都是线性一致的。
> 然而,基于快照隔离的串行化**通常**不是线性一致的。为了避免读写互相阻塞,所有的读取都会基于某个一致性的快照,则该快照之后的写入不会反映到读请求上,因此,快照读不满足线性一致性。
>
## 依赖线性一致性
@@ -196,26 +186,16 @@
最常用的让系统进行容错的方式就是多副本。让我们回顾下第五章的几种多副本模型,然后逐一考察下其是否能够做成可线性化的:
- **单主模型**Single-leader replicationpotentially linearizable
在一个单主模型的系统中,主副本服务于写请求,其他副本负责备份。如果我们让读取也走主副本,或者使用同步更新从副本的策略,则该系统**有可能**满足线性一致性。但是,并不是所有单主模型的数据都提供线性一致性,有时候是故意的(比如提供快照隔离),有时候是由于并发 bug。
想让主副本也负责读请求,首先我们得确切知道哪一个是主副本。就像我们在“[真相由多数节点定义](https://www.notion.so/Chapter-9-Consistency-and-Consensus-f80d66bdfb7b4d1281722914239a563a)”一节中提到的,很有可能某个节点认为他是主节点,但其事实上不是。如果这个**自以为是的主节点**delusional leader继续提供服务则系统很可能会违反线性一致性。如果使用异步同步策略节点宕机可能甚至会丢数据从而不仅违反线性一致性也违反了可持久性。
- **共识算法**Consensus algorithmslinearizable
我们本章稍后会讨论到有一些共识算法看起来与单主模型类似。但这些共识协议有一些阻止脑裂和过期副本的手段。由于这些额外细节共识算法可以实现安全的线性一致性存储。Zookeeper 和 etcd 就是用的这种手段。
- **多主模型**Multi-leader replicationnot linearizable
由于可以同时在多个节点上处理写入,并且异步同步写入数据,使用多主模型的系统通常不是线性一致的。由于上述原因,这种系统可能会产生需要手动解决的写入冲突。这种冲突便是违反线性一致性要求的一个点:表现的像一个副本。
- **无主模型**Leader replication probably not linearizable
对于使用无主模型的系统Dynamo-style参见前面[无主模型](https://ddia.qtmuniao.com/#/ch05?id=%e6%97%a0%e4%b8%bb%e6%a8%a1%e5%9e%8b)一节来说厂商有时候会声称你可以通过使用法定数目读写Quorum reads and writew+r > n来获得强一致性。这个说法有点模糊并不总是正确这取决于你对法定节点的配置也取决于你如何定义强一致性。
“后者胜”Last write win的冲突解决方法会依赖于多个机器的挂历时钟time-of-day参见[依赖时钟同步](https://ddia.qtmuniao.com/#/ch08?id=%e4%be%9d%e8%b5%96%e5%90%8c%e6%ad%a5%e6%97%b6%e9%92%9f)由于多机时钟存在偏差其物理时间戳不能保证和系统事件顺序一致因此基本上不可能做到线性一致。放松的法定策略Quorum见[放松的 Quorum 和提示转交](https://ddia.qtmuniao.com/#/ch05?id=%e6%94%be%e6%9d%be%e7%9a%84-quorum-%e5%92%8c%e6%8f%90%e7%a4%ba%e8%bd%ac%e4%ba%a4))也是破坏了线性一致性。即使对于严格的法定策略,非线性一致的现象也可能出现,下一节将会详细探讨。
### 线性一致和法定策略Quorum
从直觉出发,在 Dynamo 风格的系统重使用严格的 Quorum 读写应该会满足线性一致性。但在具有不确定延迟的网络中,仍然可能会出现竞态条件。如下图所示:
@@ -271,13 +251,10 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
在分布式系统中有很多其他难以兼顾的有趣结果CAP 现在已经被很多更为精确的描述所取代,因此 CAP 在今天更多的作为一个历史名词。
> CAP 有时候被表述为在做系统设计时一致性consistency、可用性Availability、分区容错性Partition tolerance只能三取其二。然而这种说法极具误导性因为网络分区是一种故障类型而不是一种可以取舍的选项不管你喜欢还是不喜欢它都在那。当然也有人理解为用单机系统可以规避但我们当下讨论的前提是分布式系统。
>
> 在网络正常连通时,系统可以同时提供一致性(线性一致性)和完全的可用性。当网络故障发生时,你必须在线性一致性和完全可用性之间二选一。因此,对于 CAP 更好的一个表述可能是:**当网络出现分区时,一致性和可用性只能二选其一***either Consistent or Available when Partitioned* )。一个可靠的网络,可以减少其上的系统该选择的次数,但无论如何,分布式系统中,该选择是无法避免的。
>
> 在网络正常连通时,系统可以同时提供一致性(线性一致性)和完全的可用性。当网络故障发生时,你必须在线性一致性和完全可用性之间二选一。因此,对于 CAP 更好的一个表述可能是:**当网络出现分区时,一致性和可用性只能二选其一**_either Consistent or Available when Partitioned_)。一个可靠的网络,可以减少其上的系统该选择的次数,但无论如何,分布式系统中,该选择是无法避免的。
> 在有关 CAP 的讨论,有几种关于可用性的大相径庭的定义,且将 CAP 升格为定理并给出证明中的提到的**形式化的可用性**并非通常意义中所说的可用性。很多所谓“高可用”的系统通常并不符合 CAP 定理中关于可用性的独特idiosyncratic定义。总而言之CAP 有很多容易误解和模糊不清的概念,并不能帮助我们更好的理解系统,因此最好不用 CAP 来描述一个系统。
>
### 线性一致性和网络延迟
@@ -327,7 +304,7 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
这和现实生活一样,一件事的发生会引起另一件的事的出现:一个节点读取数据之后,**依据**读取内容依赖于读取写入了一些数据另一个节点读取这些写入进而写入了另外一些数据循环往复。操作operation发生的**依赖链条**定义了系统中事件的因果——也即,**谁居先,谁处后**。
如果一个系统遵循因果约束,则我们称其为**因果一致的***causally consistent*)。比如,快照隔离就可以提供因果一致性:当从数据库读取数据的时候,如果你能读到某个时间点的数据,就一定能读到其之前的数据(当然,要在该数据还没有被删除的情况下)。
如果一个系统遵循因果约束,则我们称其为**因果一致的**_causally consistent_)。比如,快照隔离就可以提供因果一致性:当从数据库读取数据的时候,如果你能读到某个时间点的数据,就一定能读到其之前的数据(当然,要在该数据还没有被删除的情况下)。
### 因果序非全序
@@ -343,19 +320,16 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
并发concurrency意味着时间线的分叉与合并。但在重新合并之时来自两个时间分支的操作就有可能出现不可比的情况。在第五章的图 5-14参见[确定 Happens-Before 关系](https://ddia.qtmuniao.com/#/ch05?id=%e7%a1%ae%e5%ae%9a-happens-before-%e5%85%b3%e7%b3%bb))中我们见过类似的现象,所有的事件不在一条时间线上,而是有相当复杂的图形依赖。图中的每个箭头,本质上定义了一种因果依赖,也即偏序关系。
> 理解全序和偏序、线性一致性和因果一致性的一个关键模型是**有向图**。在该图中,点代表事件,有向边代表因果关系,并且从因事件指向果事件,很自然的,因果性满足**传递性**。**如果该图中有一条单一的路径能串起所有点,且不存在环**,则该系统是线性一致的。可以看出,因果关系是一种局部特性(也即偏序关系),定义在两个点之间(如果两个点之间存在着一条单向途径,则这两点有因果关系);而线性关系是一种全局特性(也即全序关系),定义在整个图上。
>
如果你熟悉一些分布式的版本管理工具,如 Git你就会发现相似的特性。在 Git 中版本间的依赖就类似于一个因果依赖图。大部分时候所有人的提交commit是前后相继的组成一个直线但有时如果团队中的几个人同时并发的为一个项目工作版本历史就会产生分叉并且在提交的时候重又合并。
> 在 Spark 的多个 RDD 之间,也有类似的感觉。如果所有算子都是单输入算子,则执行图会是一条线,即全序没有并发;如果有的算子有多个输入,则不同输入之间可以并发,此时为偏序关系。
>
### 线性一致性强于因果一致性
那么因果顺序和线性一致性的关系是什么?答曰:线性一致性是因果一致性的**充分implies条件**。所有提供线性一致性的系统都能够能够保证因果性。尤其对于有多个通信通道的系统来说,我们不需要做任何额外努力(比如在多个组件间传递时间戳,以建立因果关系),线性一致性就能够保证系统中发生的所有事件满足因果性。
> 用我们之前的图模型来说,就是**不存在环**。即,**因果一致性**(有向无环图) ⇒ **线性一致性**(在有向无环图的基础上,**存在一条能串起所有点的单向路径**)。
>
线性一致性能能够保证因果关系,该特点让系统易于使用,从而对应用层很有吸引力。但任何事情都是有代价的,如我们在之前线性一致性的代价一节中所讨论的:**提供线性一致性非常伤性能和可用性,在网络有显著延迟时**(如全球部署的系统)**,该副作用尤其明显**。因此,很多系统会舍弃线性一致性以换取更好的性能,但当然,代价是更难用了。
@@ -366,7 +340,6 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
这些研究都比较新,还存在很多挑战,也没有进行落地。但无疑,是分布式系统在将来一个很有前景发展方向。
> 真实系统中,在所有的事件集中,只有部分事件是有因果依赖的,这些事件需要在执行时保证因果顺序执行;而其他的大部分事件是没有因果依赖的,因此可以乱序、按需执行以保证性能。但这件事情的难点在于,因果关系是**应用层定义的**。而我们在系统层,就很难识别。可能需要提供某种接口,可以让应用层显示指定因果,但一来不确定这种接口是否能做的足够宽泛;二来,这种因果追踪的额外代价是非常大的。
>
### 捕获因果依赖
@@ -391,7 +364,6 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
进一步,我们可以保证我们产生序列号的方式满足因果关系:**如果操作 A 发生在 B 之前,则 A 获取到的序列号比 B 小**。并发的(无法比较谁先谁后)操作获取到的序列号顺序不确定。序列号本质上是一种全序,通过这种方式可以追踪因果关系,但也施加了一个比因果关系更为严格的全序。
> 联想我们之前用以理解的有向图,相当于在满足原来有向边(因果关系)的基础上,增加了一些有向边,串出了**一条**能串起所有点(操作)的路径。
>
在使用单主模型的多副本系统中,主节点上**操作日志的追加顺序**确定了一个对所有操作的全序,且满足操作发生的因果关系。主节点可以为每条日志按顺序关联一个全局递增的序列号,如果从节点上也按都按此序列号顺序应用操作日志到状态机,则每个副本总能保持一致的状态(但有可能稍落后于主节点)。
@@ -429,7 +401,6 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
如上图,客户端 A 在收到节点 2 的 counter = 5 的回复后,会使用该值向节点 1 发送请求。节点 1 本来的 counter 值是 1在收到该请求后会立即前移为 5。尔后下一个请求操作到来会将其增加为 6。
> 系统中所有的事件event和交互方clientserver都要被纳入 Lamport Clock 体系内,才能追踪系统内的所有因果关系。
>
只要最大的 counter 值通过每个操作被传播,就能保证 Lamport 时间戳满足因果一致。因为每次因果依赖的交互都会推高时间戳。
@@ -456,7 +427,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
小结一下,在分布式系统中,为了实现类似于针对用户名的**唯一性约束**,仅为所有时间进行全局定序是不够的,你还需要知道该定序何时完成。对于某个创建账户的操作,如果我们能够确定在最终的全序里,不会有其他操作插到该操作之前,我们便可以安全的让该操作成功。
确定全局定序何时收敛,将会在接下来的小节 —— **全序广播***total order broadcast*)中讨论。
确定全局定序何时收敛,将会在接下来的小节 —— **全序广播**_total order broadcast_)中讨论。
## 全序广播
@@ -470,7 +441,6 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
在分布式系统的语境下,该问题也被称为**全序广播**total order broadcast或者**原子广播**atomic broadcast
> **顺序保证的范围。** 多分区的数据库,对于每个分区使用单主模型,能够维持每个分区的操作全局有序,但并不能提供跨分区的一致性保证(比如一致性快照,外键约束)。当然,跨分区的全序保证也是可以提供的,只不过需要进行额外的协调。
>
全序广播是一种多个节点间交换消息的协议。它要求系统满足两个安全性质:
@@ -510,15 +480,14 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
3. 检查所有想要使用该用户名的消息,这时你可能会得到多条消息,如果你当初写下的消息在第一条,则你是成功的。此时,你可以“确认”(持久化,比如追加日志,比如写入数据库)占有该用户名的信息,然后给客户端返回成功。如果第一条消息不是你的,则终止请求。
> 这里其实隐藏了一些细节,即我们会将追加消息请求发送给全序广播系统,全序广播系统会真正将消息按之前提到的两条保证的方式(可靠送达和全序送达)同步到每个节点。因此,对于每个节点来说,会首先发起消息追加请求,然后之后某个时刻,可以等到真正同步回来的消息。如果觉得绕,可以带入 Raft 的付之日志来类比。
>
由于所有日志条目都会以同样的顺序送达每个节点,若有并发写入,则所有节点都能依靠日志顺序就谁“先来后到”达成一致。当有同名冲突时,可以选择第一条作为赢家,并舍弃其后的冲突请求。可以使用类似的方式,基于日志来实现涉及到多对象的事务的可串行化。
尽管该方式能够提供线性化的写入,却不能保证线性化的读取。如果你从一个异步同步日志的节点读取日志,就有可能读到陈旧的数据(更精确一点说,上述过程能够提供**顺序一致性**s*equential consistency*,有时也被称为**时间线一致性***timeline consistency*,比线性一致性稍弱 )。在此基础上,如果想让读取也变得可线性化,有几种做法:
尽管该方式能够提供线性化的写入,却不能保证线性化的读取。如果你从一个异步同步日志的节点读取日志,就有可能读到陈旧的数据(更精确一点说,上述过程能够提供**顺序一致性**s*equential consistency*,有时也被称为**时间线一致性**_timeline consistency_,比线性一致性稍弱)。在此基础上,如果想让读取也变得可线性化,有几种做法:
- 让读取也走日志,即通过追加消息的方式将读取顺序化,然后当读取请求所在节点**收到**这条读取日志时才去真正的去读。则消息在日志中的位置定义了整个时间序列中读取真正发生的时间点。etcd 中的法定读取就是用的类似的做法)
- 如果日志服务允许查询最新日志的位置,则可以在请求到来时,获取当时最新位置,然后不断查询日志看是否已经跟到最新位置。如果跟到了,就进行读取。(这是 Zookeeper 中 sync() 操作的背后的原理)
- 可以将读取路由到写入发生的那个节点,或者与写入严格同步的节点,以保证能够读到最新的内容。(这种技术用于**链式复制***chain replication* 中)
- 可以将读取路由到写入发生的那个节点,或者与写入严格同步的节点,以保证能够读到最新的内容。(这种技术用于**链式复制**_chain replication_ 中)
### 使用线性一致存储实现全序广播
@@ -529,10 +498,8 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
算法相当简单:对于每一个发给全序广播系统的消息,使用整数寄存器 increment-and-get 操作关联一个序列号;然后将消息发送给所有节点(重试任何丢失的消息)。每个节点接收到消息后利用序列号顺序对外交付消息。这种机制很像 TCP但并不是描述通信双方而是一个分布式系统。
- 如何判断消息是否丢失?
ACK 或者是否遇到异常。
注意到,和 Lamport 时间戳不同,从线性化的寄存器中获取的数字是**连续的,非跳跃的**。如此一来,当某节点交付了消息 4 后,收到了消息 6但不能立即交付而需要等待消息 5 的到来。但在 Lamport 时间戳系统中则非如此——这(是否连续)**也是全序广播和时间戳顺序的核心不同**。
实现一个支持 increment-and-get 原子操作的线性化整数寄存器有多难?如果所有组件都不会出错,则很简单:**你可以直接用单台机器上的一个变量**。但如果连接到该节点的网络会出问题怎么办这台机器机器宕机后宕机前变量的值又该怎么恢复理论上说只要你对线性序列生成器思考的足够完备就会不可避免的得到一个共识算法consensus algorithm
@@ -550,22 +517,15 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
在很多场景下让多个节点达成共识是非常重要的。比如:
- **Leader 选举**
在使用单主模型的数据库中,所有节点需要对谁是主节点达成一致。当网络问题导致有些节点不能正常通信时,领导权就会出现争议。在这种情形下,共识对于避免错误的故障转移非常重要。引入如果出现两个领导者可以同时接受写入(**脑裂**),所有副本上的数据就会产生分叉,从而变得不一致甚而数据丢失。
- **原子提交**
在一个横跨多节点或具有多分区的数据库中可能会出现某个事务在一些节点执行成功但在另外一些节点却运行失败。如果我们想保持事务的原子性ACID 中的 A参见[原子性](https://ddia.qtmuniao.com/#/ch07?id=%e5%8e%9f%e5%ad%90%e6%80%a7%ef%bc%88atomicity%ef%bc%89)),我们就必须让所有节点就事务的结果达成一致:要么全部回滚(只要有故障),要么提交(没有任何故障)。这个共识的特例也被称为**原子提交**atomic commit
> **共识的不可能性**。你也许听过 FLP —— 以 FischerLynch 和 Paterson 三位作者姓名首字母命名的一种不可能原理——在网络可靠,但允许节点宕机(即便只有一个)的**异步模型**系统中,不存在总是能够达成共识的算法。在分布式系统中,我们又必须得假设节点可能会宕机,因此稳定可靠的共识算法是不存在的。但是,我们现在却在探讨可以达成共识的算法。这又是为啥?这可能吗?
>
> 答案是FLP 不可能是基于异步系统模型(参见[系统模型和现实](https://ddia.qtmuniao.com/#/ch08?id=%e7%b3%bb%e7%bb%9f%e6%a8%a1%e5%9e%8b%e5%92%8c%e7%8e%b0%e5%ae%9e))证明的,这是一种非常苛刻的模型,不能够使用任何时钟系统和超时检测。如果允许使用**超时宕机检测**、或者任何可以**识别节点宕机**的方法,就能够实现可靠的共识算法。甚而,只让算法用随机数来进行故障检测,也能够绕过这个不可能定理。
>
> 因此尽管在理论上FLP 定理非常重要,断言异步网络中共识不可能达到;但在实践中,分布式系统达成共识是可行的。
>
在本小节,我们首先会详细探讨**原子提交**。特别的,我们将会讨论**两阶段提交**2PCtwo-phase commit算法这是一种解决原子提交的最为常见的算法很多数据库和服务端应用都实现了该算法。可以看出2PC 在某种程度上是一种共识协议——虽然不是很完美。
@@ -598,7 +558,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
**事务提交后是不可撤销的**——在事务提交后,你不能再改变主意说,我要重新中止这个事务。这是因为,一旦事务提交了,就会对其他事务可见,从而可能让其他事务依赖于该事务的结果做出一些新的决策;这个原则构成了**读已提交**read commited隔离级别的基础参见[读已提交](https://ddia.qtmuniao.com/#/ch07?id=%e8%af%bb%e5%b7%b2%e6%8f%90%e4%ba%a4))。如果事务允许在提交后中止,其他已经读取了该事务结果的事务也会失效,从而引起事务的级联中止。
当然,事务所造成的**结果**在事实上是可以被撤销的,比如,通过**补偿事务***compensating transaction*)。但,从数据库的视角来看,这就是另外一个事务了;而跨事务的正确性,需要应用层自己来保证。
当然,事务所造成的**结果**在事实上是可以被撤销的,比如,通过**补偿事务**_compensating transaction_)。但,从数据库的视角来看,这就是另外一个事务了;而跨事务的正确性,需要应用层自己来保证。
### 两阶段提交简介
@@ -608,8 +568,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
![Untitled](img/ch09-fig09.png)
> **不要混淆 2PC 和 2PL**。Two-phase *commit* (2PC) 和 two-phase locking (2PL参见[两阶段锁](https://ddia.qtmuniao.com/#/ch07?id=%e4%b8%a4%e9%98%b6%e6%ae%b5%e9%94%81)) 是两个完全不同的概念。2PC 是为了在分布式系统中进行原子提交,而 2PL 是为了进行事务并发控制的一种加锁方式。为了避免歧义,可以忽略他们在名字简写上的相似性,而把它们当成完全不同的概念。
>
> **不要混淆 2PC 和 2PL**。Two-phase _commit_ (2PC) 和 two-phase locking (2PL参见[两阶段锁](https://ddia.qtmuniao.com/#/ch07?id=%e4%b8%a4%e9%98%b6%e6%ae%b5%e9%94%81)) 是两个完全不同的概念。2PC 是为了在分布式系统中进行原子提交,而 2PL 是为了进行事务并发控制的一种加锁方式。为了避免歧义,可以忽略他们在名字简写上的相似性,而把它们当成完全不同的概念。
2PC 引入了一个单机事务中没有的角色:**协调者**coordinator有时也被称为事务管理器transaction manager。协调者通常以库的形式出现并会嵌入到请求事务的应用进程中但当然它也可以以单独进程或者服务的形式出现。比如说Narayana, JOTM, BTM, or MSDTC.
@@ -681,14 +640,10 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
相较于完全弃之不用,我们应当更加细致的考量分布式事务,因为可以从其中学到相当多的经验教训。首先,我们需要精确地定义什么是“分布式事务”。有两种完全不同的分布式事务经常被混淆:
- **数据库内部分布式事务**
在一些分布式数据中(标配支持多分区和多副本的数据库),支持跨节点的**内部分布式事务**。如VoltDB 和 MySQL 集群的 NDB 存储引擎就有这样的内部事务支持。在这种情况下,所有事务参与节点都运行着同样的二进制代码。
- **异构的分布式事务**
在异构的分布式事务中,所有参与者使用了两种以上的技术栈:如,来自不同厂家的两种数据库实例,甚至可能包含非数据库系统,如消息队列。即使每个子系统内部实现完全不同,构建于其上的分布式事务也能够保证原子提交。
数据库内部的事务不需要考虑和其他系统的相容性,因此在实现时可以使用任何协议、可以针对特定技术栈进行任何优化。因此,数据库内部的分布式事务通常能够很好地工作。相反,横跨多个异构系统的事务实现则充满了挑战。
### 恰好一次的消息处理
@@ -737,7 +692,7 @@ XA 不是一个网络协议——它定义了一组和事务协调者交互的 C
唯一的出路是让管理员手动的来提交或者中止事务。管理员首先需要检查所有包含未定事务的参与者,看是否有任何参与者提交或者中止了,从而对其他卡主的参与者手动执行相同操作(**通过外力来让所有参与者达成一致**)。解决该问题需要大量手工操作,并且在线上环境中断服务的巨大压力和时间限制下(不然,为什么协调者会处在此种错误状态下?)。
很多 XA 事务的实现会留有紧急后门,称为**启发式决策***heuristic decisions*):允许一个参与者不用等待协调者的决策,而**单方面**决定中止还是提交一个未定事务。需要说明的是,这里的启发式仅仅是**可能打破原子性**probably breaking atomicity的一种委婉说法。因为这么做可能会违反两阶段提交所提供的保证。因此这种启发式决策仅是为了救急而不能进行日常使用。
很多 XA 事务的实现会留有紧急后门,称为**启发式决策**_heuristic decisions_):允许一个参与者不用等待协调者的决策,而**单方面**决定中止还是提交一个未定事务。需要说明的是,这里的启发式仅仅是**可能打破原子性**probably breaking atomicity的一种委婉说法。因为这么做可能会违反两阶段提交所提供的保证。因此这种启发式决策仅是为了救急而不能进行日常使用。
### 分布式事务的限制
@@ -758,23 +713,15 @@ XA 事务解决了一些很现实而重要的难题:让异构的数据系统
在这种形式化表述中,一个共识协议必须满足以下条件:
- **全局一致性***Uniform agreement*
- **全局一致性**_Uniform agreement_
没有任何两个节点最终做出不同决策。
- **正直性***Integrity*
- **正直性**_Integrity_
没有任何节点会做出两次决策(不会反复横跳)
- **有效性***Validity*
- **有效性**_Validity_
如果一个节点做出了决策,该决策所对应的值一定来自系统内某个节点的提议
- **可终止性***Termination*
- **可终止性**_Termination_
任何没有宕机的节点,最终都会给出对某个值的决策
全局一致和正直性定义了共识协议的核心概念:**所有节点都要决策出同样的结果,并且一旦做出决策,就不能反悔**。加入有效性更多的是为了排除一些无效trivial结果如果无论其他节点提议什么一个算法都会选择 null 作为决策值;该算法虽然满足一致性和正直性约束,但却不满足有效性。
如果**不关心容错性,则仅满足前三个性质就足够**了:比如,可以通过硬编码指定某个节点为“独裁者”,并且让其做所有决策,其他节点只要服从即可。然而,一旦该节点故障,则整个系统不能继续决策和推进。事实上,这正是我们在两阶段提交算法中看到的:一旦协调者故障,所有处于未定状态的参与者都无法独自决策是提交还是中止。
@@ -794,7 +741,6 @@ XA 事务解决了一些很现实而重要的难题:让异构的数据系统
最广为人知的容错性的共识算法有——VSRViewstamped Replication、Paxos、Raft 和 Zab。这些共识算法间有非常多的共同点但他们确实不完全相同虽然 Lamport 说过类似世界上只有一种共识算法——Paxos。在本书中我们不会探究每个共识算法的区别的所有细节只需知道他们在顶层设计中有很多相似之处即可。除非你想自己实现一个共识算法。
> 当然,并不推荐这么做,因为实现一个工业级可用的共识算法很难,需要处理特别多的边角情况,而这些情况不经过大量实践是根本不会想到的。虽然 TLA 可以验证你的算法,但并不能验证你的实现。
>
这些共识算法通常不会直接按上述形式化的定义(如提议并在**单值上**进行决策,同时满足一致性、正直性,有效性和可终止性)来实现。转而,他们通常会在**一系列值**上做出决策,从而事实上变成一种**全序广播算法**,本章前面小节讨论过这个问题。
@@ -868,22 +814,14 @@ Zookeeper 和 etcd 设计目标为**存储小尺度的数据**,比如能装进
Zookeeper 是模仿 Google 的 Chunk 锁服务实现的,不仅实现了全序广播算法(进而实现了共识),也实现了其他一些对分布式系统非常有用的功能集:
- **线性化的原子操作lock**
使用原子的 CAS 操作,可以实现锁:如果多个节点并发执行同一个操作,只有一个会成功。共识协议能够保证,即使随时可能出现节点宕机或者网络故障,操作仍然是原子和线性化的。一个分布式锁通常实现为具有**过期时间的“租约”**lease这样即使客户端宕机锁也能够被最终释放。
- **操作的全序保证zxid**
在[领导者和锁](https://ddia.qtmuniao.com/#/ch08?id=%e9%a2%86%e5%af%bc%e8%80%85%e5%92%8c%e9%94%81)一节中我们讨论过,当某个资源被锁或者租约保护时,你需要**防护令牌机制**来防止由于进程停顿而造成的加锁冲突。防护令牌一个在每次获取锁都会单调自增的数值。Zookeeper 通过给每个操作赋予一个全局自增的事务 idzxid和一个版本号cversion来提供该功能。
- **故障检测ephemeral node**
客户端和 ZooKeeper 的服务器间维持着一个长会话,客户端和服务端通过周期性的心跳来检测对端是否仍然存活。即使该连接短暂断掉,或者 ZooKeeper 节点故障该会话仍然能够存活。但如果心跳停顿间隔过长超过了会话的超时阈值ZooKeeper 会标记该会话死亡。所有该会话关联的锁在超时都将会被释放ZooKeeper 将其称为**暂态节点**ephemeral nodes这类节点可以将生命周期与会话进行绑定
- **变动通知watch**
客户端不仅可以读取其他节点创建的锁或者值,也可以直接对这些对象的变化进行**守望**watch。通过守望机制客户端可以立即发现是否有其他客户端加入集群通过这些客户端写入 ZooKeeper 的值)、其他客户端是否故障(通过这些客户端注册到 ZooKeeper 中的暂态节点的消失)。通过订阅这些通知,客户端可以避免频繁地去 ZooKeeper 拉取信息,比对以确定是否发生了某些变化。
对于这些功能,只有线性化的原子操作真正需要共识算法。但这些操作的组合,使得类似 ZooKeeper 的系统对分布式系统非常有用。
### 为节点分配任务