mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
remove redundant spaces
This commit is contained in:
parent
ae1e797698
commit
fcd2b77c0c
@ -34,7 +34,7 @@
|
||||
|
||||
---------
|
||||
|
||||
> 计算是一种流行文化,流行文化鄙视历史。 流行文化关乎个体身份和参与感,但与合作无关。流行文化活在当下,也与过去和未来无关。 我认为大部分(为了钱)编写代码的人就是这样的, 他们不知道自己的文化来自哪里。
|
||||
> 计算是一种流行文化,流行文化鄙视历史。流行文化关乎个体身份和参与感,但与合作无关。流行文化活在当下,也与过去和未来无关。我认为大部分(为了钱)编写代码的人就是这样的,他们不知道自己的文化来自哪里。
|
||||
>
|
||||
> —— 阿兰・凯接受 Dobb 博士的杂志采访时(2012 年)
|
||||
|
||||
|
24
ch1.md
24
ch1.md
@ -2,7 +2,7 @@
|
||||
|
||||
![](img/ch1.png)
|
||||
|
||||
> 互联网做得太棒了,以至于大多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术, 你还记得是什么时候吗?
|
||||
> 互联网做得太棒了,以至于大多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术,你还记得是什么时候吗?
|
||||
>
|
||||
> —— [艾伦・凯](http://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442) 在接受 Dobb 博士杂志采访时说(2012 年)
|
||||
|
||||
@ -127,8 +127,8 @@
|
||||
* 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API 和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。
|
||||
* 将人们最容易犯错的地方与可能导致失效的地方 **解耦(decouple)**。特别是提供一个功能齐全的非生产环境 **沙箱(sandbox)**,使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验。
|
||||
* 在各个层次进行彻底的测试【3】,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的 **边缘场景(corner case)**。
|
||||
* 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。 例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。
|
||||
* 配置详细和明确的监控,比如性能指标和错误率。 在其他工程学科中这指的是 **遥测(telemetry)**(一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的)。监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。
|
||||
* 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。
|
||||
* 配置详细和明确的监控,比如性能指标和错误率。在其他工程学科中这指的是 **遥测(telemetry)**(一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的)。监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。
|
||||
* 良好的管理实践与充分的培训 —— 一个复杂而重要的方面,但超出了本书的范围。
|
||||
|
||||
### 可靠性有多重要?
|
||||
@ -162,7 +162,7 @@
|
||||
|
||||
处理每秒 12,000 次写入(发推文的速率峰值)还是很简单的。然而推特的伸缩性挑战并不是主要来自推特量,而是来自 **扇出(fan-out)**[^ii]—— 每个用户关注了很多人,也被很多人关注。
|
||||
|
||||
[^ii]: 扇出:从电子工程学中借用的术语,它描述了输入连接到另一个门输出的逻辑门数量。 输出需要提供足够的电流来驱动所有连接的输入。 在事务处理系统中,我们使用它来描述为了服务一个传入请求而需要执行其他服务的请求数量。
|
||||
[^ii]: 扇出:从电子工程学中借用的术语,它描述了输入连接到另一个门输出的逻辑门数量。输出需要提供足够的电流来驱动所有连接的输入。在事务处理系统中,我们使用它来描述为了服务一个传入请求而需要执行其他服务的请求数量。
|
||||
|
||||
大体上讲,这一对操作有两种实现方式。
|
||||
|
||||
@ -180,7 +180,7 @@
|
||||
|
||||
**图 1-2 推特主页时间线的关系型模式简单实现**
|
||||
|
||||
2. 为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱([图 1-3](img/fig1-3.png))。 当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。 因此读取主页时间线的请求开销很小,因为结果已经提前计算好了。
|
||||
2. 为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱([图 1-3](img/fig1-3.png))。当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。因此读取主页时间线的请求开销很小,因为结果已经提前计算好了。
|
||||
|
||||
![](img/fig1-3.png)
|
||||
|
||||
@ -205,7 +205,7 @@
|
||||
|
||||
对于 Hadoop 这样的批处理系统,通常关心的是 **吞吐量(throughput)**,即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间 [^iii]。对于在线系统,通常更重要的是服务的 **响应时间(response time)**,即客户端发送请求到接收响应之间的时间。
|
||||
|
||||
[^iii]: 理想情况下,批量作业的运行时间是数据集的大小除以吞吐量。 在实践中由于数据倾斜(数据不是均匀分布在每个工作进程中),需要等待最慢的任务完成,所以运行时间往往更长。
|
||||
[^iii]: 理想情况下,批量作业的运行时间是数据集的大小除以吞吐量。在实践中由于数据倾斜(数据不是均匀分布在每个工作进程中),需要等待最慢的任务完成,所以运行时间往往更长。
|
||||
|
||||
> #### 延迟和响应时间
|
||||
>
|
||||
@ -219,7 +219,7 @@
|
||||
|
||||
**图 1-4 展示了一个服务 100 次请求响应时间的均值与百分位数**
|
||||
|
||||
通常报表都会展示服务的平均响应时间。 (严格来讲 “平均” 一词并不指代任何特定公式,但实际上它通常被理解为 **算术平均值(arithmetic mean)**:给定 n 个值,加起来除以 n )。然而如果你想知道 “**典型(typical)**” 响应时间,那么平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。
|
||||
通常报表都会展示服务的平均响应时间。(严格来讲 “平均” 一词并不指代任何特定公式,但实际上它通常被理解为 **算术平均值(arithmetic mean)**:给定 n 个值,加起来除以 n )。然而如果你想知道 “**典型(typical)**” 响应时间,那么平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。
|
||||
|
||||
通常使用 **百分位点(percentiles)** 会更好。如果将响应时间列表按最快到最慢排序,那么 **中位数(median)** 就在正中间:举个例子,如果你的响应时间中位数是 200 毫秒,这意味着一半请求的返回时间少于 200 毫秒,另一半比这个要长。
|
||||
|
||||
@ -231,7 +231,7 @@
|
||||
|
||||
另一方面,优化第 99.99 百分位点(一万个请求中最慢的一个)被认为太昂贵了,不能为亚马逊的目标带来足够好处。减小高百分位点处的响应时间相当困难,因为它很容易受到随机事件的影响,这超出了控制范围,而且效益也很小。
|
||||
|
||||
百分位点通常用于 **服务级别目标(SLO, service level objectives)** 和 **服务级别协议(SLA, service level agreements)**,即定义服务预期性能和可用性的合同。 SLA 可能会声明,如果服务响应时间的中位数小于 200 毫秒,且 99.9 百分位点低于 1 秒,则认为服务工作正常(如果响应时间更长,就认为服务不达标)。这些指标为客户设定了期望值,并允许客户在 SLA 未达标的情况下要求退款。
|
||||
百分位点通常用于 **服务级别目标(SLO, service level objectives)** 和 **服务级别协议(SLA, service level agreements)**,即定义服务预期性能和可用性的合同。SLA 可能会声明,如果服务响应时间的中位数小于 200 毫秒,且 99.9 百分位点低于 1 秒,则认为服务工作正常(如果响应时间更长,就认为服务不达标)。这些指标为客户设定了期望值,并允许客户在 SLA 未达标的情况下要求退款。
|
||||
|
||||
**排队延迟(queueing delay)** 通常占了高百分位点处响应时间的很大一部分。由于服务器只能并行处理少量的事务(如受其 CPU 核数的限制),所以只要有少量缓慢的请求就能阻碍后续请求的处理,这种效应有时被称为 **头部阻塞(head-of-line blocking)** 。即使后续请求在服务器上处理的非常迅速,由于需要等待先前请求完成,客户端最终看到的是缓慢的总体响应时间。因为存在这种效应,测量客户端的响应时间非常重要。
|
||||
|
||||
@ -330,11 +330,11 @@
|
||||
|
||||
因为复杂度导致维护困难时,预算和时间安排通常会超支。在复杂的软件中进行变更,引入错误的风险也更大:当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。相反,降低复杂度能极大地提高软件的可维护性,因此简单性应该是构建系统的一个关键目标。
|
||||
|
||||
简化系统并不一定意味着减少功能;它也可以意味着消除 **额外的(accidental)** 的复杂度。 Moseley 和 Marks【32】把 **额外复杂度** 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
|
||||
简化系统并不一定意味着减少功能;它也可以意味着消除 **额外的(accidental)** 的复杂度。Moseley 和 Marks【32】把 **额外复杂度** 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
|
||||
|
||||
用于消除 **额外复杂度** 的最好工具之一是 **抽象(abstraction)**。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。
|
||||
|
||||
例如,高级编程语言是一种抽象,隐藏了机器码、CPU 寄存器和系统调用。 SQL 也是一种抽象,隐藏了复杂的磁盘 / 内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有 **直接(directly)** 使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。
|
||||
例如,高级编程语言是一种抽象,隐藏了机器码、CPU 寄存器和系统调用。SQL 也是一种抽象,隐藏了复杂的磁盘 / 内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有 **直接(directly)** 使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。
|
||||
|
||||
抽象可以帮助我们将系统的复杂度控制在可管理的水平,不过,找到好的抽象是非常困难的。在分布式系统领域虽然有许多好的算法,但我们并不清楚它们应该打包成什么样抽象。
|
||||
|
||||
@ -344,7 +344,7 @@
|
||||
|
||||
系统的需求永远不变,基本是不可能的。更可能的情况是,它们处于常态的变化中,例如:你了解了新的事实、出现意想不到的应用场景、业务优先级发生变化、用户要求新功能、新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。
|
||||
|
||||
在组织流程方面, **敏捷(agile)** 工作模式为适应变化提供了一个框架。敏捷社区还开发了对在频繁变化的环境中开发软件很有帮助的技术工具和模式,如 **测试驱动开发(TDD, test-driven development)** 和 **重构(refactoring)** 。
|
||||
在组织流程方面,**敏捷(agile)** 工作模式为适应变化提供了一个框架。敏捷社区还开发了对在频繁变化的环境中开发软件很有帮助的技术工具和模式,如 **测试驱动开发(TDD, test-driven development)** 和 **重构(refactoring)** 。
|
||||
|
||||
这些敏捷技术的大部分讨论都集中在相当小的规模(同一个应用中的几个代码文件)。本书将探索在更大数据系统层面上提高敏捷性的方法,可能由几个不同的应用或服务组成。例如,为了将装配主页时间线的方法从方法 1 变为方法 2,你会如何 “重构” 推特的架构 ?
|
||||
|
||||
@ -358,7 +358,7 @@
|
||||
一个应用必须满足各种需求才称得上有用。有一些 **功能需求**(functional requirements,即它应该做什么,比如允许以各种方式存储,检索,搜索和处理数据)以及一些 **非功能性需求**(nonfunctional,即通用属性,例如安全性、可靠性、合规性、可伸缩性、兼容性和可维护性)。在本章详细讨论了可靠性,可伸缩性和可维护性。
|
||||
|
||||
|
||||
**可靠性(Reliability)** 意味着即使发生故障,系统也能正常工作。故障可能发生在硬件(通常是随机的和不相关的)、软件(通常是系统性的 Bug,很难处理)和人类(不可避免地时不时出错)。 **容错技术** 可以对终端用户隐藏某些类型的故障。
|
||||
**可靠性(Reliability)** 意味着即使发生故障,系统也能正常工作。故障可能发生在硬件(通常是随机的和不相关的)、软件(通常是系统性的 Bug,很难处理)和人类(不可避免地时不时出错)。**容错技术** 可以对终端用户隐藏某些类型的故障。
|
||||
|
||||
**可伸缩性(Scalability)** 意味着即使在负载增加的情况下也有保持性能的策略。为了讨论可伸缩性,我们首先需要定量描述负载和性能的方法。我们简要了解了推特主页时间线的例子,介绍描述负载的方法,并将响应时间百分位点作为衡量性能的一种方式。在可伸缩的系统中可以添加 **处理容量(processing capacity)** 以在高负载下保持可靠。
|
||||
|
||||
|
64
ch10.md
64
ch10.md
@ -32,7 +32,7 @@ Web 和越来越多的基于 HTTP/REST 的 API 使交互的请求 / 响应风格
|
||||
|
||||
与多年前为数据仓库开发的并行处理系统【3,4】相比,MapReduce 是一个相当低级别的编程模型,但它使得在商用硬件上能进行的处理规模迈上一个新的台阶。虽然 MapReduce 的重要性正在下降【5】,但它仍然值得去理解,因为它描绘了一幅关于批处理为什么有用,以及如何做到有用的清晰图景。
|
||||
|
||||
实际上,批处理是一种非常古老的计算方式。早在可编程数字计算机诞生之前,打孔卡制表机(例如 1890 年美国人口普查【6】中使用的霍尔里斯机)实现了半机械化的批处理形式,从大量输入中汇总计算。 Map-Reduce 与 1940 年代和 1950 年代广泛用于商业数据处理的机电 IBM 卡片分类机器有着惊人的相似之处【7】。正如我们所说,历史总是在不断重复自己。
|
||||
实际上,批处理是一种非常古老的计算方式。早在可编程数字计算机诞生之前,打孔卡制表机(例如 1890 年美国人口普查【6】中使用的霍尔里斯机)实现了半机械化的批处理形式,从大量输入中汇总计算。Map-Reduce 与 1940 年代和 1950 年代广泛用于商业数据处理的机电 IBM 卡片分类机器有着惊人的相似之处【7】。正如我们所说,历史总是在不断重复自己。
|
||||
|
||||
在本章中,我们将了解 MapReduce 和其他一些批处理算法和框架,并探索它们在现代数据系统中的作用。但首先我们将看看使用标准 Unix 工具的数据处理。即使你已经熟悉了它们,Unix 的哲学也值得一读,Unix 的思想和经验教训可以迁移到大规模、异构的分布式数据系统中。
|
||||
|
||||
@ -59,9 +59,9 @@ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36"
|
||||
|
||||
### 简单日志分析
|
||||
|
||||
很多工具可以从这些日志文件生成关于网站流量的漂亮的报告,但为了练手,让我们使用基本的 Unix 功能创建自己的工具。 例如,假设你想在你的网站上找到五个最受欢迎的网页。 则可以在 Unix shell 中这样做:[^i]
|
||||
很多工具可以从这些日志文件生成关于网站流量的漂亮的报告,但为了练手,让我们使用基本的 Unix 功能创建自己的工具。例如,假设你想在你的网站上找到五个最受欢迎的网页。则可以在 Unix shell 中这样做:[^i]
|
||||
|
||||
[^i]: 有些人认为 `cat` 这里并没有必要,因为输入文件可以直接作为 awk 的参数。 但这种写法让线性管道更为显眼。
|
||||
[^i]: 有些人认为 `cat` 这里并没有必要,因为输入文件可以直接作为 awk 的参数。但这种写法让线性管道更为显眼。
|
||||
|
||||
```bash
|
||||
cat /var/log/nginx/access.log | #1
|
||||
@ -75,7 +75,7 @@ cat /var/log/nginx/access.log | #1
|
||||
1. 读取日志文件
|
||||
2. 将每一行按空格分割成不同的字段,每行只输出第七个字段,恰好是请求的 URL。在我们的例子中是 `/css/typography.css`。
|
||||
3. 按字母顺序排列请求的 URL 列表。如果某个 URL 被请求过 n 次,那么排序后,文件将包含连续重复出现 n 次的该 URL。
|
||||
4. `uniq` 命令通过检查两个相邻的行是否相同来过滤掉输入中的重复行。 `-c` 则表示还要输出一个计数器:对于每个不同的 URL,它会报告输入中出现该 URL 的次数。
|
||||
4. `uniq` 命令通过检查两个相邻的行是否相同来过滤掉输入中的重复行。`-c` 则表示还要输出一个计数器:对于每个不同的 URL,它会报告输入中出现该 URL 的次数。
|
||||
5. 第二种排序按每行起始处的数字(`-n`)排序,这是 URL 的请求次数。然后逆序(`-r`)返回结果,大的数字在前。
|
||||
6. 最后,只输出前五行(`-n 5`),并丢弃其余的。该系列命令的输出如下所示:
|
||||
|
||||
@ -118,11 +118,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
#### 排序 VS 内存中的聚合
|
||||
|
||||
Ruby 脚本在内存中保存了一个 URL 的哈希表,将每个 URL 映射到它出现的次数。 Unix 管道没有这样的哈希表,而是依赖于对 URL 列表的排序,在这个 URL 列表中,同一个 URL 的只是简单地重复出现。
|
||||
Ruby 脚本在内存中保存了一个 URL 的哈希表,将每个 URL 映射到它出现的次数。Unix 管道没有这样的哈希表,而是依赖于对 URL 列表的排序,在这个 URL 列表中,同一个 URL 的只是简单地重复出现。
|
||||
|
||||
哪种方法更好?这取决于你有多少个不同的 URL。对于大多数中小型网站,你可能可以为所有不同网址提供一个计数器(假设我们使用 1GB 内存)。在此例中,作业的 **工作集**(working set,即作业需要随机访问的内存大小)仅取决于不同 URL 的数量:如果日志中只有单个 URL,重复出现一百万次,则散列表所需的空间表就只有一个 URL 加上一个计数器的大小。当工作集足够小时,内存散列表表现良好,甚至在性能较差的笔记本电脑上也可以正常工作。
|
||||
|
||||
另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)” 中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。 归并排序具有在磁盘上运行良好的顺序访问模式。 (请记住,针对顺序 I/O 进行优化是 [第三章](ch3.md) 中反复出现的主题,相同的模式在此重现)
|
||||
另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)” 中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。归并排序具有在磁盘上运行良好的顺序访问模式。(请记住,针对顺序 I/O 进行优化是 [第三章](ch3.md) 中反复出现的主题,相同的模式在此重现)
|
||||
|
||||
GNU Coreutils(Linux)中的 `sort` 程序通过溢出至磁盘的方式来自动应对大于内存的数据集,并能同时使用多个 CPU 核进行并行排序【9】。这意味着我们之前看到的简单的 Unix 命令链很容易伸缩至大数据集,且不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。
|
||||
|
||||
@ -142,7 +142,7 @@ Unix 管道的发明者道格・麦克罗伊(Doug McIlroy)在 1964 年首先
|
||||
|
||||
`sort` 工具是一个很好的例子。可以说它比大多数编程语言标准库中的实现(它们不会利用磁盘或使用多线程,即使这样做有很大好处)要更好。然而,单独使用 `sort` 几乎没什么用。它只能与其他 Unix 工具(如 `uniq`)结合使用。
|
||||
|
||||
像 `bash` 这样的 Unix shell 可以让我们轻松地将这些小程序组合成令人讶异的强大数据处理任务。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。 Unix 如何实现这种可组合性?
|
||||
像 `bash` 这样的 Unix shell 可以让我们轻松地将这些小程序组合成令人讶异的强大数据处理任务。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。Unix 如何实现这种可组合性?
|
||||
|
||||
#### 统一的接口
|
||||
|
||||
@ -150,11 +150,11 @@ Unix 管道的发明者道格・麦克罗伊(Doug McIlroy)在 1964 年首先
|
||||
|
||||
在 Unix 中,这种接口是一个 **文件**(file,更准确地说,是一个文件描述符)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件,到另一个进程(Unix 套接字,stdin,stdout)的通信通道,设备驱动程序(比如 `/dev/audio` 或 `/dev/lp0`),表示 TCP 连接的套接字,等等。很容易将这些设计视为理所当然的,但实际上能让这些差异巨大的东西共享一个统一的接口是非常厉害的,这使得它们可以很容易地连接在一起 [^ii]。
|
||||
|
||||
[^ii]: 统一接口的另一个例子是 URL 和 HTTP,这是 Web 的基石。 一个 URL 标识一个网站上的一个特定的东西(资源),你可以链接到任何其他网站的任何网址。 具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织维护。 这个原则现在似乎非常明显,但它却是网络取能取得今天成就的关键。 之前的系统并不是那么统一:例如,在公告板系统(BBS)时代,每个系统都有自己的电话号码和波特率配置。 从一个 BBS 到另一个 BBS 的引用必须以电话号码和调制解调器设置的形式;用户将不得不挂断,拨打其他 BBS,然后手动找到他们正在寻找的信息。 直接链接到另一个 BBS 内的一些内容当时是不可能的。
|
||||
[^ii]: 统一接口的另一个例子是 URL 和 HTTP,这是 Web 的基石。一个 URL 标识一个网站上的一个特定的东西(资源),你可以链接到任何其他网站的任何网址。具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织维护。这个原则现在似乎非常明显,但它却是网络取能取得今天成就的关键。之前的系统并不是那么统一:例如,在公告板系统(BBS)时代,每个系统都有自己的电话号码和波特率配置。从一个 BBS 到另一个 BBS 的引用必须以电话号码和调制解调器设置的形式;用户将不得不挂断,拨打其他 BBS,然后手动找到他们正在寻找的信息。直接链接到另一个 BBS 内的一些内容当时是不可能的。
|
||||
|
||||
按照惯例,许多(但不是全部)Unix 程序将这个字节序列视为 ASCII 文本。我们的日志分析示例使用了这个事实:`awk`、`sort`、`uniq` 和 `head` 都将它们的输入文件视为由 `\n`(换行符,ASCII `0x0A`)字符分隔的记录列表。`\n` 的选择是任意的 —— 可以说,ASCII 记录分隔符 `0x1E` 本来就是一个更好的选择,因为它是为了这个目的而设计的【14】,但是无论如何,所有这些程序都使用相同的记录分隔符允许它们互操作。
|
||||
|
||||
每条记录(即一行输入)的解析则更加模糊。 Unix 工具通常通过空白或制表符将行分割成字段,但也使用 CSV(逗号分隔),管道分隔和其他编码。即使像 `xargs` 这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。
|
||||
每条记录(即一行输入)的解析则更加模糊。Unix 工具通常通过空白或制表符将行分割成字段,但也使用 CSV(逗号分隔),管道分隔和其他编码。即使像 `xargs` 这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。
|
||||
|
||||
ASCII 文本的统一接口大多数时候都能工作,但它不是很优雅:我们的日志分析示例使用 `{print $7}` 来提取网址,这样可读性不是很好。在理想的世界中可能是 `{print $request_url}` 或类似的东西。我们稍后会回顾这个想法。
|
||||
|
||||
@ -169,13 +169,13 @@ ASCII 文本的统一接口大多数时候都能工作,但它不是很优雅
|
||||
|
||||
Unix 工具的另一个特点是使用标准输入(`stdin`)和标准输出(`stdout`)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,你也可以从文件输入和 / 或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入(有个小内存缓冲区,而不需要将整个中间数据流写入磁盘)。
|
||||
|
||||
如果需要,程序仍然可以直接读取和写入文件,但 Unix 方法在程序不关心特定的文件路径、只使用标准输入和标准输出时效果最好。这允许 shell 用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。 (人们可以说这是一种 **松耦合(loose coupling)**,**晚期绑定(late binding)**【15】或 **控制反转(inversion of control)**【16】)。将输入 / 输出布线与程序逻辑分开,可以将小工具组合成更大的系统。
|
||||
如果需要,程序仍然可以直接读取和写入文件,但 Unix 方法在程序不关心特定的文件路径、只使用标准输入和标准输出时效果最好。这允许 shell 用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。(人们可以说这是一种 **松耦合(loose coupling)**,**晚期绑定(late binding)**【15】或 **控制反转(inversion of control)**【16】)。将输入 / 输出布线与程序逻辑分开,可以将小工具组合成更大的系统。
|
||||
|
||||
你甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,它就可以加入数据处理的管道中。在日志分析示例中,你可以编写一个将 Usage-Agent 字符串转换为更灵敏的浏览器标识符,或者将 IP 地址转换为国家代码的工具,并将其插入管道。`sort` 程序并不关心它是否与操作系统的另一部分或者你写的程序通信。
|
||||
|
||||
但是,使用 `stdin` 和 `stdout` 能做的事情是有限的。需要多个输入或输出的程序虽然可能,却非常棘手。你没法将程序的输出管道连接至网络连接中【17,18】[^iii] 。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,那么 I/O 的布线就取决于程序本身了。它仍然可以被配置(例如通过命令行选项),但在 Shell 中对输入和输出进行布线的灵活性就少了。
|
||||
|
||||
[^iii]: 除了使用一个单独的工具,如 `netcat` 或 `curl`。 Unix 起初试图将所有东西都表示为文件,但是 BSD 套接字 API 偏离了这个惯例【17】。研究用操作系统 Plan 9 和 Inferno 在使用文件方面更加一致:它们将 TCP 连接表示为 `/net/tcp` 中的文件【18】。
|
||||
[^iii]: 除了使用一个单独的工具,如 `netcat` 或 `curl`。Unix 起初试图将所有东西都表示为文件,但是 BSD 套接字 API 偏离了这个惯例【17】。研究用操作系统 Plan 9 和 Inferno 在使用文件方面更加一致:它们将 TCP 连接表示为 `/net/tcp` 中的文件【18】。
|
||||
|
||||
|
||||
#### 透明度和实验
|
||||
@ -230,7 +230,7 @@ MapReduce 是一个编程框架,你可以使用它编写代码来处理 HDFS
|
||||
|
||||
* Reducer
|
||||
|
||||
MapReduce 框架拉取由 Mapper 生成的键值对,收集属于同一个键的所有值,并在这组值上迭代调用 Reducer。 Reducer 可以产生输出记录(例如相同 URL 的出现次数)。
|
||||
MapReduce 框架拉取由 Mapper 生成的键值对,收集属于同一个键的所有值,并在这组值上迭代调用 Reducer。Reducer 可以产生输出记录(例如相同 URL 的出现次数)。
|
||||
|
||||
在 Web 服务器日志的例子中,我们在第 5 步中有第二个 `sort` 命令,它按请求数对 URL 进行排序。在 MapReduce 中,如果你需要第二个排序阶段,则可以通过编写第二个 MapReduce 作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,Mapper 的作用是将数据放入一个适合排序的表单中,并且 Reducer 的作用是处理已排序的数据。
|
||||
|
||||
@ -242,7 +242,7 @@ MapReduce 与 Unix 命令管道的主要区别在于,MapReduce 可以在多台
|
||||
|
||||
[图 10-1](img/fig10-1.png) 显示了 Hadoop MapReduce 作业中的数据流。其并行化基于分区(请参阅 [第六章](ch6.md)):作业的输入通常是 HDFS 中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理 map 任务([图 10-1](img/fig10-1.png) 中的 m1,m2 和 m3 标记)。
|
||||
|
||||
每个输入文件的大小通常是数百兆字节。 MapReduce 调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个 Mapper,只要该机器有足够的备用 RAM 和 CPU 资源来运行 Mapper 任务【26】。这个原则被称为 **将计算放在数据附近**【27】:它节省了通过网络复制输入文件的开销,减少网络负载并增加局部性。
|
||||
每个输入文件的大小通常是数百兆字节。MapReduce 调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个 Mapper,只要该机器有足够的备用 RAM 和 CPU 资源来运行 Mapper 任务【26】。这个原则被称为 **将计算放在数据附近**【27】:它节省了通过网络复制输入文件的开销,减少网络负载并增加局部性。
|
||||
|
||||
![](img/fig10-1.png)
|
||||
|
||||
@ -286,11 +286,11 @@ Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【3
|
||||
|
||||
当 MapReduce 作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;数据库会将这种操作称为 **全表扫描**。如果你只想读取少量的记录,则全表扫描与索引查询相比,代价非常高昂。但是在分析查询中(请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,特别是如果能在多台机器上并行处理时,扫描整个输入可能是相当合理的事情。
|
||||
|
||||
当我们在批处理的语境中讨论连接时,我们指的是在数据集中解析某种关联的全量存在。 例如我们假设一个作业是同时处理所有用户的数据,而非仅仅是为某个特定用户查找数据(而这能通过索引更高效地完成)。
|
||||
当我们在批处理的语境中讨论连接时,我们指的是在数据集中解析某种关联的全量存在。例如我们假设一个作业是同时处理所有用户的数据,而非仅仅是为某个特定用户查找数据(而这能通过索引更高效地完成)。
|
||||
|
||||
#### 示例:用户活动事件分析
|
||||
|
||||
[图 10-2](img/fig10-2.png) 给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为 **活动事件**,即 activity events,或 **点击流数据**,即 clickstream data),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(请参阅 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
|
||||
[图 10-2](img/fig10-2.png) 给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为 **活动事件**,即 activity events,或 **点击流数据**,即 clickstream data),右侧是用户数据库。你可以将此示例看作是星型模式的一部分(请参阅 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
|
||||
|
||||
![](img/fig10-2.png)
|
||||
|
||||
@ -312,9 +312,9 @@ Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【3
|
||||
|
||||
**图 10-3 在用户 ID 上进行的 Reduce 端连接。如果输入数据集分区为多个文件,则每个分区都会被多个 Mapper 并行处理**
|
||||
|
||||
当 MapReduce 框架通过键对 Mapper 输出进行分区,然后对键值对进行排序时,效果是具有相同 ID 的所有活动事件和用户记录在 Reducer 输入中彼此相邻。 Map-Reduce 作业甚至可以也让这些记录排序,使 Reducer 总能先看到来自用户数据库的记录,紧接着是按时间戳顺序排序的活动事件 —— 这种技术被称为 **二次排序(secondary sort)**【26】。
|
||||
当 MapReduce 框架通过键对 Mapper 输出进行分区,然后对键值对进行排序时,效果是具有相同 ID 的所有活动事件和用户记录在 Reducer 输入中彼此相邻。Map-Reduce 作业甚至可以也让这些记录排序,使 Reducer 总能先看到来自用户数据库的记录,紧接着是按时间戳顺序排序的活动事件 —— 这种技术被称为 **二次排序(secondary sort)**【26】。
|
||||
|
||||
然后 Reducer 可以容易地执行实际的连接逻辑:每个用户 ID 都会被调用一次 Reducer 函数,且因为二次排序,第一个值应该是来自用户数据库的出生日期记录。 Reducer 将出生日期存储在局部变量中,然后使用相同的用户 ID 遍历活动事件,输出 **已观看网址** 和 **观看者年龄** 的结果对。随后的 Map-Reduce 作业可以计算每个 URL 的查看者年龄分布,并按年龄段进行聚集。
|
||||
然后 Reducer 可以容易地执行实际的连接逻辑:每个用户 ID 都会被调用一次 Reducer 函数,且因为二次排序,第一个值应该是来自用户数据库的出生日期记录。Reducer 将出生日期存储在局部变量中,然后使用相同的用户 ID 遍历活动事件,输出 **已观看网址** 和 **观看者年龄** 的结果对。随后的 Map-Reduce 作业可以计算每个 URL 的查看者年龄分布,并按年龄段进行聚集。
|
||||
|
||||
由于 Reducer 一次处理一个特定用户 ID 的所有记录,因此一次只需要将一条用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为 **排序合并连接(sort-merge join)**,因为 Mapper 的输出是按键排序的,然后 Reducer 将来自连接两侧的有序记录列表合并在一起。
|
||||
|
||||
@ -348,7 +348,7 @@ Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【3
|
||||
|
||||
如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig 中的 **偏斜连接(skewed join)** 方法首先运行一个抽样作业(Sampling Job)来确定哪些键是热键【39】。连接实际执行时,Mapper 会将热键的关联记录 **随机**(相对于传统 MapReduce 基于键散列的确定性方法)发送到几个 Reducer 之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到 **所有** 处理该键的 Reducer 上【40】。
|
||||
|
||||
这种技术将处理热键的工作分散到多个 Reducer 上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个 Reducer 上。 Crunch 中的 **分片连接(sharded join)** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在 “[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)” 中讨论的技术,使用随机化来缓解分区数据库中的热点。
|
||||
这种技术将处理热键的工作分散到多个 Reducer 上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个 Reducer 上。Crunch 中的 **分片连接(sharded join)** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在 “[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)” 中讨论的技术,使用随机化来缓解分区数据库中的热点。
|
||||
|
||||
Hive 的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键,并将与这些键相关的记录单独存放,与其它文件分开。当在该表上执行连接时,对于热键,它会使用 Map 端连接(请参阅下一节)。
|
||||
|
||||
@ -373,7 +373,7 @@ Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其
|
||||
|
||||
参与连接的较大输入的每个文件块各有一个 Mapper(在 [图 10-2](img/fig10-2.png) 的例子中活动事件是较大的输入)。每个 Mapper 都会将较小输入整个加载到内存中。
|
||||
|
||||
这种简单有效的算法被称为 **广播散列连接(broadcast hash join)**:**广播** 一词反映了这样一个事实,每个连接较大输入端分区的 Mapper 都会将较小输入端数据集整个读入内存中(所以较小输入实际上 “广播” 到较大数据的所有分区上),**散列** 一词反映了它使用一个散列表。 Pig(名为 “**复制链接(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支持这种连接。它也被诸如 Impala 的数据仓库查询引擎使用【41】。
|
||||
这种简单有效的算法被称为 **广播散列连接(broadcast hash join)**:**广播** 一词反映了这样一个事实,每个连接较大输入端分区的 Mapper 都会将较小输入端数据集整个读入内存中(所以较小输入实际上 “广播” 到较大数据的所有分区上),**散列** 一词反映了它使用一个散列表。Pig(名为 “**复制链接(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支持这种连接。它也被诸如 Impala 的数据仓库查询引擎使用【41】。
|
||||
|
||||
除了将较小的连接输入加载到内存散列表中,另一种方法是将较小输入存储在本地磁盘上的只读索引中【42】。索引中经常使用的部分将保留在操作系统的页面缓存中,因而这种方法可以提供与内存散列表几乎一样快的随机查找性能,但实际上并不需要数据集能放入内存中。
|
||||
|
||||
@ -412,7 +412,7 @@ Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其
|
||||
|
||||
#### 建立搜索索引
|
||||
|
||||
Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为由 5 到 10 个 MapReduce 作业组成的工作流【1】。虽然 Google 后来也不仅仅是为这个目的而使用 MapReduce 【43】,但如果从构建搜索索引的角度来看,更能帮助理解 MapReduce。 (直至今日,Hadoop MapReduce 仍然是为 Lucene/Solr 构建索引的好方法【44】)
|
||||
Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为由 5 到 10 个 MapReduce 作业组成的工作流【1】。虽然 Google 后来也不仅仅是为这个目的而使用 MapReduce 【43】,但如果从构建搜索索引的角度来看,更能帮助理解 MapReduce。(直至今日,Hadoop MapReduce 仍然是为 Lucene/Solr 构建索引的好方法【44】)
|
||||
|
||||
我们在 “[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)” 中简要地了解了 Lucene 这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档 ID 列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名、纠正拼写错误、解析同义词等等 —— 但这个原则是成立的。
|
||||
|
||||
@ -450,7 +450,7 @@ Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为
|
||||
|
||||
MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不可变且避免副作用(如写入外部数据库),批处理作业不仅实现了良好的性能,而且更容易维护:
|
||||
|
||||
- 如果在代码中引入了一个错误,而输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将重新被纠正。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的概念被称为 **人类容错(human fault tolerance)**【50】)
|
||||
- 如果在代码中引入了一个错误,而输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将重新被纠正。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。(能够从错误代码中恢复的概念被称为 **人类容错(human fault tolerance)**【50】)
|
||||
- 由于回滚很容易,比起在错误意味着不可挽回的伤害的环境,功能开发进展能快很多。这种 **最小化不可逆性(minimizing irreversibility)** 的原则有利于敏捷软件开发【51】。
|
||||
- 如果 Map 或 Reduce 任务失败,MapReduce 框架将自动重新调度,并在同样的输入上再次运行它。如果失败是由代码中的错误造成的,那么它会不断崩溃,并最终导致作业在几次尝试之后失败。但是如果故障是由于临时问题导致的,那么故障就会被容忍。因为输入不可变,这种自动重试是安全的,而失败任务的输出会被 MapReduce 框架丢弃。
|
||||
- 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质(例如,将其与前一次运行的输出进行比较并测量差异) 。
|
||||
@ -462,7 +462,7 @@ MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不
|
||||
|
||||
正如我们所看到的,Hadoop 有点像 Unix 的分布式版本,其中 HDFS 是文件系统,而 MapReduce 是 Unix 进程的怪异实现(总是在 Map 阶段和 Reduce 阶段运行 `sort` 工具)。我们了解了如何在这些原语的基础上实现各种连接和分组操作。
|
||||
|
||||
当 MapReduce 论文发表时【1】,它从某种意义上来说 —— 并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前所谓的 **大规模并行处理(MPP, massively parallel processing)** 数据库中实现了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是这方面的先驱【52】。
|
||||
当 MapReduce 论文发表时【1】,它从某种意义上来说 —— 并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前所谓的 **大规模并行处理(MPP,massively parallel processing)** 数据库中实现了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是这方面的先驱【52】。
|
||||
|
||||
最大的区别是,MPP 数据库专注于在一组机器上并行执行分析 SQL 查询,而 MapReduce 和分布式文件系统【19】的组合则更像是一个可以运行任意程序的通用操作系统。
|
||||
|
||||
@ -474,7 +474,7 @@ MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不
|
||||
|
||||
在纯粹主义者看来,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更高质量的数据来处理。然而实践经验表明,简单地使数据快速可用 —— 即使它很古怪,难以使用,使用原始格式 —— 也通常要比事先决定理想数据模型要更有价值【54】。
|
||||
|
||||
这个想法与数据仓库类似(请参阅 “[数据仓库](ch3.md#数据仓库)”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨越以前相互分离的数据集进行连接。 MPP 数据库所要求的谨慎模式设计拖慢了集中式数据收集速度;以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为 “**数据湖(data lake)**” 或 “**企业数据中心(enterprise data hub)**”【55】)。
|
||||
这个想法与数据仓库类似(请参阅 “[数据仓库](ch3.md#数据仓库)”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨越以前相互分离的数据集进行连接。MPP 数据库所要求的谨慎模式设计拖慢了集中式数据收集速度;以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为 “**数据湖(data lake)**” 或 “**企业数据中心(enterprise data hub)**”【55】)。
|
||||
|
||||
不加区分的数据转储转移了解释数据的负担:数据集的生产者不再需要强制将其转化为标准格式,数据的解释成为消费者的问题(**读时模式** 方法【56】;请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一种优势。甚至可能不存在一个理想的数据模型,对于不同目的有不同的合适视角。以原始形式简单地转储数据,可以允许多种这样的转换。这种方法被称为 **寿司原则(sushi principle)**:“原始数据更好”【57】。
|
||||
|
||||
@ -492,13 +492,13 @@ MapReduce 使工程师能够轻松地在大型数据集上运行自己的代码
|
||||
|
||||
至关重要的是,这些不同的处理模型都可以在共享的单个机器集群上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在 Hadoop 方式中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个集群内不同的工作负载。不需要移动数据,使得从数据中挖掘价值变得容易得多,也使采用新的处理模型容易的多。
|
||||
|
||||
Hadoop 生态系统包括随机访问的 OLTP 数据库,如 HBase(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”)和 MPP 风格的分析型数据库,如 Impala 【41】。 HBase 与 Impala 都不使用 MapReduce,但都使用 HDFS 进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。
|
||||
Hadoop 生态系统包括随机访问的 OLTP 数据库,如 HBase(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”)和 MPP 风格的分析型数据库,如 Impala 【41】。HBase 与 Impala 都不使用 MapReduce,但都使用 HDFS 进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。
|
||||
|
||||
#### 针对频繁故障设计
|
||||
|
||||
当比较 MapReduce 和 MPP 数据库时,两种不同的设计思路出现了:处理故障和使用内存与磁盘的方式。与在线系统相比,批处理对故障不太敏感,因为就算失败也不会立即影响到用户,而且它们总是能再次运行。
|
||||
|
||||
如果一个节点在执行查询时崩溃,大多数 MPP 数据库会中止整个查询,并让用户重新提交查询或自动重新运行它【3】。由于查询通常最多运行几秒钟或几分钟,所以这种错误处理的方法是可以接受的,因为重试的代价不是太大。 MPP 数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的开销。
|
||||
如果一个节点在执行查询时崩溃,大多数 MPP 数据库会中止整个查询,并让用户重新提交查询或自动重新运行它【3】。由于查询通常最多运行几秒钟或几分钟,所以这种错误处理的方法是可以接受的,因为重试的代价不是太大。MPP 数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的开销。
|
||||
|
||||
另一方面,MapReduce 可以容忍单个 Map 或 Reduce 任务的失败,而不会影响作业的整体,通过以单个任务的粒度重试工作。它也会非常急切地将数据写入磁盘,一方面是为了容错,另一部分是因为假设数据集太大而不能适应内存。
|
||||
|
||||
@ -514,7 +514,7 @@ MapReduce 方式更适用于较大的作业:要处理如此之多的数据并
|
||||
|
||||
这就是 MapReduce 被设计为容忍频繁意外任务终止的原因:不是因为硬件很不可靠,而是因为任意终止进程的自由有利于提高计算集群中的资源利用率。
|
||||
|
||||
在开源的集群调度器中,抢占的使用较少。 YARN 的 CapacityScheduler 支持抢占,以平衡不同队列的资源分配【58】,但在编写本文时,YARN,Mesos 或 Kubernetes 不支持通用的优先级抢占【60】。在任务不经常被终止的环境中,MapReduce 的这一设计决策就没有多少意义了。在下一节中,我们将研究一些与 MapReduce 设计决策相异的替代方案。
|
||||
在开源的集群调度器中,抢占的使用较少。YARN 的 CapacityScheduler 支持抢占,以平衡不同队列的资源分配【58】,但在编写本文时,YARN,Mesos 或 Kubernetes 不支持通用的优先级抢占【60】。在任务不经常被终止的环境中,MapReduce 的这一设计决策就没有多少意义了。在下一节中,我们将研究一些与 MapReduce 设计决策相异的替代方案。
|
||||
|
||||
|
||||
## MapReduce之后
|
||||
@ -538,7 +538,7 @@ MapReduce 方式更适用于较大的作业:要处理如此之多的数据并
|
||||
|
||||
但在很多情况下,你知道一个作业的输出只能用作另一个作业的输入,这些作业由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的 **中间状态(intermediate state)**:一种将数据从一个作业传递到下一个作业的方式。在一个用于构建推荐系统的,由 50 或 100 个 MapReduce 作业组成的复杂工作流中,存在着很多这样的中间状态【29】。
|
||||
|
||||
将这个中间状态写入文件的过程称为 **物化(materialization)**。 (在 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)” 中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算)
|
||||
将这个中间状态写入文件的过程称为 **物化(materialization)**。(在 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)” 中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算)
|
||||
|
||||
作为对照,本章开头的日志分析示例使用 Unix 管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态,而是只使用一个小的内存缓冲区,将输出增量地 **流(stream)** 向输入。
|
||||
|
||||
@ -565,7 +565,7 @@ MapReduce 方式更适用于较大的作业:要处理如此之多的数据并
|
||||
- 排序等昂贵的工作只需要在实际需要的地方执行,而不是默认地在每个 Map 和 Reduce 阶段之间出现。
|
||||
- 没有不必要的 Map 任务,因为 Mapper 所做的工作通常可以合并到前面的 Reduce 算子中(因为 Mapper 不会更改数据集的分区)。
|
||||
- 由于工作流中的所有连接和数据依赖都是显式声明的,因此调度程序能够总览全局,知道哪里需要哪些数据,因而能够利用局部性进行优化。例如,它可以尝试将消费某些数据的任务放在与生成这些数据的任务相同的机器上,从而数据可以通过共享内存缓冲区传输,而不必通过网络复制。
|
||||
- 通常,算子间的中间状态足以保存在内存中或写入本地磁盘,这比写入 HDFS 需要更少的 I/O(必须将其复制到多台机器,并将每个副本写入磁盘)。 MapReduce 已经对 Mapper 的输出做了这种优化,但数据流引擎将这种思想推广至所有的中间状态。
|
||||
- 通常,算子间的中间状态足以保存在内存中或写入本地磁盘,这比写入 HDFS 需要更少的 I/O(必须将其复制到多台机器,并将每个副本写入磁盘)。MapReduce 已经对 Mapper 的输出做了这种优化,但数据流引擎将这种思想推广至所有的中间状态。
|
||||
- 算子可以在输入就绪后立即开始执行;后续阶段无需等待前驱阶段整个完成后再开始。
|
||||
- 与 MapReduce(为每个任务启动一个新的 JVM)相比,现有 Java 虚拟机(JVM)进程可以重用来运行新算子,从而减少启动开销。
|
||||
|
||||
@ -617,7 +617,7 @@ Spark、Flink 和 Tez 避免将中间状态写入 HDFS,因此它们采取了
|
||||
|
||||
针对图批处理的优化 —— **批量同步并行(BSP,Bulk Synchronous Parallel)** 计算模型【70】已经开始流行起来。其中,Apache Giraph 【37】,Spark 的 GraphX API 和 Flink 的 Gelly API 【71】实现了它。它也被称为 **Pregel** 模型,因为 Google 的 Pregel 论文推广了这种处理图的方法【72】。
|
||||
|
||||
回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定调用 “发送消息”,因为框架将所有具有相同键的 Mapper 输出集中在一起。 Pregel 背后有一个类似的想法:一个顶点可以向另一个顶点 “发送消息”,通常这些消息是沿着图的边发送的。
|
||||
回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定调用 “发送消息”,因为框架将所有具有相同键的 Mapper 输出集中在一起。Pregel 背后有一个类似的想法:一个顶点可以向另一个顶点 “发送消息”,通常这些消息是沿着图的边发送的。
|
||||
|
||||
在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用 Reducer 一样。与 MapReduce 的不同之处在于,在 Pregel 模型中,顶点在一次迭代到下一次迭代的过程中会记住它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。
|
||||
|
||||
@ -656,15 +656,15 @@ Spark、Flink 和 Tez 避免将中间状态写入 HDFS,因此它们采取了
|
||||
|
||||
#### 向声明式查询语言的转变
|
||||
|
||||
与硬写执行连接的代码相比,指定连接关系算子的优点是,框架可以分析连接输入的属性,并自动决定哪种上述连接算法最适合当前任务。 Hive、Spark 和 Flink 都有基于代价的查询优化器可以做到这一点,甚至可以改变连接顺序,最小化中间状态的数量【66,77,78,79】。
|
||||
与硬写执行连接的代码相比,指定连接关系算子的优点是,框架可以分析连接输入的属性,并自动决定哪种上述连接算法最适合当前任务。Hive、Spark 和 Flink 都有基于代价的查询优化器可以做到这一点,甚至可以改变连接顺序,最小化中间状态的数量【66,77,78,79】。
|
||||
|
||||
连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以 **声明式(declarative)** 的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在 “[数据查询语言](ch2.md#数据查询语言)” 中见过这个想法。
|
||||
|
||||
但 MapReduce 及其数据流后继者在其他方面,与 SQL 的完全声明式查询模型有很大区别。 MapReduce 是围绕着回调函数的概念建立的:对于每条记录或者一组记录,调用一个用户定义的函数(Mapper 或 Reducer),并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以基于大量已有库的生态系统创作:解析、自然语言分析、图像分析以及运行数值或统计算法等。
|
||||
但 MapReduce 及其数据流后继者在其他方面,与 SQL 的完全声明式查询模型有很大区别。MapReduce 是围绕着回调函数的概念建立的:对于每条记录或者一组记录,调用一个用户定义的函数(Mapper 或 Reducer),并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以基于大量已有库的生态系统创作:解析、自然语言分析、图像分析以及运行数值或统计算法等。
|
||||
|
||||
自由运行任意代码,长期以来都是传统 MapReduce 批处理系统与 MPP 数据库的区别所在(请参阅 “[Hadoop 与分布式数据库的对比](#Hadoop与分布式数据库的对比)” 一节)。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统兼容不佳(例如 Java 的 Maven、Javascript 的 npm 以及 Ruby 的 gems)。
|
||||
|
||||
然而数据流引擎已经发现,支持除连接之外的更多 **声明式特性** 还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外 CPU 开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用列式存储布局(请参阅 “[列式存储](ch3.md#列式存储)”),只从磁盘读取所需的列。 Hive、Spark DataFrames 和 Impala 还使用了向量化执行(请参阅 “[内存带宽和矢量化处理](ch3.md#内存带宽和矢量化处理)”):在对 CPU 缓存友好的内部循环中迭代数据,避免函数调用。Spark 生成 JVM 字节码【79】,Impala 使用 LLVM 为这些内部循环生成本机代码【41】。
|
||||
然而数据流引擎已经发现,支持除连接之外的更多 **声明式特性** 还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外 CPU 开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用列式存储布局(请参阅 “[列式存储](ch3.md#列式存储)”),只从磁盘读取所需的列。Hive、Spark DataFrames 和 Impala 还使用了向量化执行(请参阅 “[内存带宽和矢量化处理](ch3.md#内存带宽和矢量化处理)”):在对 CPU 缓存友好的内部循环中迭代数据,避免函数调用。Spark 生成 JVM 字节码【79】,Impala 使用 LLVM 为这些内部循环生成本机代码【41】。
|
||||
|
||||
通过在高级 API 中引入声明式的部分,并使查询优化器可以在执行期间利用这些来做优化,批处理框架看起来越来越像 MPP 数据库了(并且能实现可与之媲美的性能)。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。
|
||||
|
||||
|
34
ch11.md
34
ch11.md
@ -2,7 +2,7 @@
|
||||
|
||||
![](img/ch11.png)
|
||||
|
||||
> 有效的复杂系统总是从简单的系统演化而来。 反之亦然:从零设计的复杂系统没一个能有效工作的。
|
||||
> 有效的复杂系统总是从简单的系统演化而来。反之亦然:从零设计的复杂系统没一个能有效工作的。
|
||||
>
|
||||
> —— 约翰・加尔,Systemantics(1975)
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
* UDP 组播广泛应用于金融行业,例如股票市场,其中低时延非常重要【8】。虽然 UDP 本身是不可靠的,但应用层的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便能按需重新发送数据包)。
|
||||
* 无代理的消息库,如 ZeroMQ 【9】和 nanomsg 采取类似的方法,通过 TCP 或 IP 多播实现发布 / 订阅消息传递。
|
||||
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 消息传递来收集网络中所有机器的指标并对其进行监控。 (在 StatsD 协议中,只有接收到所有消息,才认为计数器指标是正确的;使用 UDP 将使得指标处在一种最佳近似状态【11】。另请参阅 “[TCP 与 UDP](ch8.md#TCP与UDP)”
|
||||
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 消息传递来收集网络中所有机器的指标并对其进行监控。(在 StatsD 协议中,只有接收到所有消息,才认为计数器指标是正确的;使用 UDP 将使得指标处在一种最佳近似状态【11】。另请参阅 “[TCP 与 UDP](ch8.md#TCP与UDP)”
|
||||
* 如果消费者在网络上公开了服务,生产者可以直接发送 HTTP 或 RPC 请求(请参阅 “[服务中的数据流:REST 与 RPC](ch4.md#服务中的数据流:REST与RPC)”)将消息推送给使用者。这就是 webhooks 背后的想法【12】,一种服务的回调 URL 被注册到另一个服务中,并且每当事件发生时都会向该 URL 发出请求。
|
||||
|
||||
尽管这些直接消息传递系统在设计它们的环境中运行良好,但是它们通常要求应用代码意识到消息丢失的可能性。它们的容错程度极为有限:即使协议检测到并重传在网络中丢失的数据包,它们通常也只是假设生产者和消费者始终在线。
|
||||
@ -114,7 +114,7 @@
|
||||
|
||||
消费者随时可能会崩溃,所以有一种可能的情况是:代理向消费者递送消息,但消费者没有处理,或者在消费者崩溃之前只进行了部分处理。为了确保消息不会丢失,消息代理使用 **确认(acknowledgments)**:客户端必须显式告知代理消息处理完毕的时间,以便代理能将消息从队列中移除。
|
||||
|
||||
如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。 (请注意可能发生这样的情况,消息 **实际上是** 处理完毕的,但 **确认** 在网络中丢失了。需要一种原子提交协议才能处理这种情况,正如在 “[实践中的分布式事务](ch9.md#实践中的分布式事务)” 中所讨论的那样)
|
||||
如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。(请注意可能发生这样的情况,消息 **实际上是** 处理完毕的,但 **确认** 在网络中丢失了。需要一种原子提交协议才能处理这种情况,正如在 “[实践中的分布式事务](ch9.md#实践中的分布式事务)” 中所讨论的那样)
|
||||
|
||||
当与负载均衡相结合时,这种重传行为对消息的顺序有种有趣的影响。在 [图 11-2](img/fig11-2.png) 中,消费者通常按照生产者发送的顺序处理消息。然而消费者 2 在处理消息 m3 时崩溃,与此同时消费者 1 正在处理消息 m4。未确认的消息 m3 随后被重新发送给消费者 1,结果消费者 1 按照 m4,m3,m5 的顺序处理消息。因此 m3 和 m4 的交付顺序与生产者 1 的发送顺序不同。
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
|
||||
日志只是磁盘上简单的仅追加记录序列。我们先前在 [第三章](ch3.md) 中日志结构存储引擎和预写式日志的上下文中讨论了日志,在 [第五章](ch5.md) 复制的上下文里也讨论了它。
|
||||
|
||||
同样的结构可以用于实现消息代理:生产者通过将消息追加到日志末尾来发送消息,而消费者通过依次读取日志来接收消息。如果消费者读到日志末尾,则会等待新消息追加的通知。 Unix 工具 `tail -f` 能监视文件被追加写入的数据,基本上就是这样工作的。
|
||||
同样的结构可以用于实现消息代理:生产者通过将消息追加到日志末尾来发送消息,而消费者通过依次读取日志来接收消息。如果消费者读到日志末尾,则会等待新消息追加的通知。Unix 工具 `tail -f` 能监视文件被追加写入的数据,基本上就是这样工作的。
|
||||
|
||||
为了伸缩超出单个磁盘所能提供的更高吞吐量,可以对日志进行 **分区**(按 [第六章](ch6.md) 的定义)。不同的分区可以托管在不同的机器上,使得每个分区都有一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。这种方法如 [图 11-3](img/fig11-3.png) 所示。
|
||||
|
||||
@ -150,7 +150,7 @@
|
||||
|
||||
**图 11-3 生产者通过将消息追加写入主题分区文件来发送消息,消费者依次读取这些文件**
|
||||
|
||||
Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 DistributedLog 【20,21】都是基于日志的消息代理。 Google Cloud Pub/Sub 在架构上类似,但对外暴露的是 JMS 风格的 API,而不是日志抽象【16】。尽管这些消息代理将所有消息写入磁盘,但通过跨多台机器分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性【22,23】。
|
||||
Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 DistributedLog 【20,21】都是基于日志的消息代理。Google Cloud Pub/Sub 在架构上类似,但对外暴露的是 JMS 风格的 API,而不是日志抽象【16】。尽管这些消息代理将所有消息写入磁盘,但通过跨多台机器分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性【22,23】。
|
||||
|
||||
#### 日志与传统的消息传递相比
|
||||
|
||||
@ -175,7 +175,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
|
||||
|
||||
#### 磁盘空间使用
|
||||
|
||||
如果只追加写入日志,则磁盘空间终究会耗尽。为了回收磁盘空间,日志实际上被分割成段,并不时地将旧段删除或移动到归档存储。 (我们将在后面讨论一种更为复杂的磁盘空间释放方式)
|
||||
如果只追加写入日志,则磁盘空间终究会耗尽。为了回收磁盘空间,日志实际上被分割成段,并不时地将旧段删除或移动到归档存储。(我们将在后面讨论一种更为复杂的磁盘空间释放方式)
|
||||
|
||||
这就意味着如果一个慢消费者跟不上消息产生的速率而落后得太多,它的消费偏移量指向了删除的段,那么它就会错过一些消息。实际上,日志实现了一个有限大小的缓冲区,当缓冲区填满时会丢弃旧消息,它也被称为 **循环缓冲区(circular buffer)** 或 **环形缓冲区(ring buffer)**。不过由于缓冲区在磁盘上,因此缓冲区可能相当的大。
|
||||
|
||||
@ -242,7 +242,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
|
||||
|
||||
数十年来,许多数据库根本没有记录在档的获取变更日志的方式。由于这个原因,捕获数据库中所有的变更,然后将其复制到其他存储技术(搜索索引、缓存或数据仓库)中是相当困难的。
|
||||
|
||||
最近,人们对 **变更数据捕获(change data capture, CDC)** 越来越感兴趣,这是一种观察写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式的过程。 CDC 是非常有意思的,尤其是当变更能在被写入后立刻用于流时。
|
||||
最近,人们对 **变更数据捕获(change data capture, CDC)** 越来越感兴趣,这是一种观察写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式的过程。CDC 是非常有意思的,尤其是当变更能在被写入后立刻用于流时。
|
||||
|
||||
例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他衍生数据系统只是变更流的消费者,如 [图 11-5](img/fig11-5.png) 所示。
|
||||
|
||||
@ -258,7 +258,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
|
||||
|
||||
数据库触发器可用来实现变更数据捕获(请参阅 “[基于触发器的复制](ch5.md#基于触发器的复制)”),通过注册观察所有变更的触发器,并将相应的变更项写入变更日志表中。但是它们往往是脆弱的,而且有显著的性能开销。解析复制日志可能是一种更稳健的方法,但它也很有挑战,例如如何应对模式变更。
|
||||
|
||||
LinkedIn 的 Databus【25】,Facebook 的 Wormhole【26】和 Yahoo! 的 Sherpa【27】大规模地应用这个思路。 Bottled Water 使用解码 WAL 的 API 实现了 PostgreSQL 的 CDC【28】,Maxwell 和 Debezium 通过解析 binlog 对 MySQL 做了类似的事情【29,30,31】,Mongoriver 读取 MongoDB oplog【32,33】,而 GoldenGate 为 Oracle 提供类似的功能【34,35】。
|
||||
LinkedIn 的 Databus【25】,Facebook 的 Wormhole【26】和 Yahoo! 的 Sherpa【27】大规模地应用这个思路。Bottled Water 使用解码 WAL 的 API 实现了 PostgreSQL 的 CDC【28】,Maxwell 和 Debezium 通过解析 binlog 对 MySQL 做了类似的事情【29,30,31】,Mongoriver 读取 MongoDB oplog【32,33】,而 GoldenGate 为 Oracle 提供类似的功能【34,35】。
|
||||
|
||||
类似于消息代理,变更数据捕获通常是异步的:记录数据库系统在提交变更之前不会等待消费者应用变更。这种设计具有的运维优势是,添加缓慢的消费者不会过度影响记录系统。不过,所有复制延迟可能有的问题在这里都可能出现(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。
|
||||
|
||||
@ -394,7 +394,7 @@ $$
|
||||
|
||||
#### 不变性的局限性
|
||||
|
||||
许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参阅 “[索引和快照隔离](ch7.md#索引和快照隔离)” )。 Git、Mercurial 和 Fossil 等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。
|
||||
许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参阅 “[索引和快照隔离](ch7.md#索引和快照隔离)” )。Git、Mercurial 和 Fossil 等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。
|
||||
|
||||
永远保持所有变更的不变历史,在多大程度上是可行的?答案取决于数据集的流失率。一些工作负载主要是添加数据,很少更新或删除;它们很容易保持不变。其他工作负载在相对较小的数据集上有较高的更新 / 删除率;在这些情况下,不可变的历史可能增至难以接受的巨大,碎片化可能成为一个问题,压缩与垃圾收集的表现对于运维的稳健性变得至关重要【60,61】。
|
||||
|
||||
@ -438,13 +438,13 @@ $$
|
||||
|
||||
CEP 系统通常使用高层次的声明式查询语言,比如 SQL,或者图形用户界面,来描述应该检测到的事件模式。这些查询被提交给处理引擎,该引擎消费输入流,并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个 **复合事件**(即 complex event,CEP 因此得名),并附有检测到的事件模式详情【67】。
|
||||
|
||||
在这些系统中,查询和数据之间的关系与普通数据库相比是颠倒的。通常情况下,数据库会持久存储数据,并将查询视为临时的:当查询进入时,数据库搜索与查询匹配的数据,然后在查询完成时丢掉查询。 CEP 引擎反转了角色:查询是长期存储的,来自输入流的事件不断流过它们,搜索匹配事件模式的查询【68】。
|
||||
在这些系统中,查询和数据之间的关系与普通数据库相比是颠倒的。通常情况下,数据库会持久存储数据,并将查询视为临时的:当查询进入时,数据库搜索与查询匹配的数据,然后在查询完成时丢掉查询。CEP 引擎反转了角色:查询是长期存储的,来自输入流的事件不断流过它们,搜索匹配事件模式的查询【68】。
|
||||
|
||||
CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIBCO StreamBase 和 SQLstream。像 Samza 这样的分布式流处理组件,支持使用 SQL 在流上进行声明式查询【71】。
|
||||
|
||||
#### 流分析
|
||||
|
||||
使用流处理的另一个领域是对流进行分析。 CEP 与流分析之间的边界是模糊的,但一般来说,分析往往对找出特定事件序列并不关心,而更关注大量事件上的聚合与统计指标 —— 例如:
|
||||
使用流处理的另一个领域是对流进行分析。CEP 与流分析之间的边界是模糊的,但一般来说,分析往往对找出特定事件序列并不关心,而更关注大量事件上的聚合与统计指标 —— 例如:
|
||||
|
||||
* 测量某种类型事件的速率(每个时间间隔内发生的频率)
|
||||
* 滚动计算一段时间窗口内某个值的平均值
|
||||
@ -462,7 +462,7 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
|
||||
|
||||
同样,在事件溯源中,应用程序的状态是通过应用事件日志来维护的;这里的应用程序状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的 **所有** 事件,除了那些可能由日志压缩丢弃的过时事件(请参阅 “[日志压缩](#日志压缩)”)。实际上,你需要一个可以一直延伸到时间开端的窗口。
|
||||
|
||||
原则上讲,任何流处理组件都可以用于维护物化视图,尽管 “永远运行” 与一些面向分析的框架假设的 “主要在有限时间段窗口上运行” 背道而驰, Samza 和 Kafka Streams 支持这种用法,建立在 Kafka 对日志压缩的支持上【75】。
|
||||
原则上讲,任何流处理组件都可以用于维护物化视图,尽管 “永远运行” 与一些面向分析的框架假设的 “主要在有限时间段窗口上运行” 背道而驰,Samza 和 Kafka Streams 支持这种用法,建立在 Kafka 对日志压缩的支持上【75】。
|
||||
|
||||
#### 在流上搜索
|
||||
|
||||
@ -498,9 +498,9 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
|
||||
|
||||
很多原因都可能导致处理延迟:排队,网络故障(请参阅 “[不可靠的网络](ch8.md#不可靠的网络)”),性能问题导致消息代理 / 消息处理器出现争用,流消费者重启,从故障中恢复时重新处理过去的事件(请参阅 “[重播旧消息](#重播旧消息)”),或者在修复代码 BUG 之后。
|
||||
|
||||
而且,消息延迟还可能导致无法预测消息顺序。例如,假设用户首先发出一个 Web 请求(由 Web 服务器 A 处理),然后发出第二个请求(由服务器 B 处理)。 A 和 B 发出描述它们所处理请求的事件,但是 B 的事件在 A 的事件发生之前到达消息代理。现在,流处理器将首先看到 B 事件,然后看到 A 事件,即使它们实际上是以相反的顺序发生的。
|
||||
而且,消息延迟还可能导致无法预测消息顺序。例如,假设用户首先发出一个 Web 请求(由 Web 服务器 A 处理),然后发出第二个请求(由服务器 B 处理)。A 和 B 发出描述它们所处理请求的事件,但是 B 的事件在 A 的事件发生之前到达消息代理。现在,流处理器将首先看到 B 事件,然后看到 A 事件,即使它们实际上是以相反的顺序发生的。
|
||||
|
||||
有一个类比也许能帮助理解,“星球大战” 电影:第四集于 1977 年发行,第五集于 1980 年,第六集于 1983 年,紧随其后的是 1999 年的第一集,2002 年的第二集,和 2005 年的第三集,以及 2015 年的第七集【80】[^ii]。如果你按照按照它们上映的顺序观看电影,你处理电影的顺序与它们叙事的顺序就是不一致的。 (集数编号就像事件时间戳,而你观看电影的日期就是处理时间)作为人类,我们能够应对这种不连续性,但是流处理算法需要专门编写,以适应这种时序与顺序的问题。
|
||||
有一个类比也许能帮助理解,“星球大战” 电影:第四集于 1977 年发行,第五集于 1980 年,第六集于 1983 年,紧随其后的是 1999 年的第一集,2002 年的第二集,和 2005 年的第三集,以及 2015 年的第七集【80】[^ii]。如果你按照按照它们上映的顺序观看电影,你处理电影的顺序与它们叙事的顺序就是不一致的。(集数编号就像事件时间戳,而你观看电影的日期就是处理时间)作为人类,我们能够应对这种不连续性,但是流处理算法需要专门编写,以适应这种时序与顺序的问题。
|
||||
|
||||
[^ii]: 感谢 Flink 社区的 Kostas Kloudas 提出这个比喻。
|
||||
|
||||
@ -612,7 +612,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
流连接直接对应于这个查询中的表连接。时间线实际上是这个查询结果的缓存,每当底层的表发生变化时都会更新 [^iii]。
|
||||
|
||||
[^iii]: 如果你将流视作表的衍生物,如 [图 11-6](img/fig11-6.png) 所示,而把一个连接看作是两个表的乘法u·v,那么会发生一些有趣的事情:物化连接的变化流遵循乘积法则:(u·v)'= u'v + uv'。 换句话说,任何推文的变化量都与当前的关注联系在一起,任何关注的变化量都与当前的推文相连接【49,50】。
|
||||
[^iii]: 如果你将流视作表的衍生物,如 [图 11-6](img/fig11-6.png) 所示,而把一个连接看作是两个表的乘法u·v,那么会发生一些有趣的事情:物化连接的变化流遵循乘积法则:(u·v)'= u'v + uv'。换句话说,任何推文的变化量都与当前的关注联系在一起,任何关注的变化量都与当前的推文相连接【49,50】。
|
||||
|
||||
#### 连接的时间依赖性
|
||||
|
||||
@ -652,7 +652,7 @@ Apache Flink 则使用不同的方法,它会定期生成状态的滚动存档
|
||||
|
||||
这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的上下文中讨论过它(请参阅 “[恰好一次的消息处理](ch9.md#恰好一次的消息处理)”)。
|
||||
|
||||
在 [第九章](ch9.md) 中,我们讨论了分布式事务传统实现中的问题(如 XA)。然而在限制更为严苛的环境中,也是有可能高效实现这种原子提交机制的。 Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了这种方法,Apache Kafka 有计划加入类似的功能【95,96】。与 XA 不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中同时管理状态变更与消息传递来内化事务。事务协议的开销可以通过在单个事务中处理多个输入消息来分摊。
|
||||
在 [第九章](ch9.md) 中,我们讨论了分布式事务传统实现中的问题(如 XA)。然而在限制更为严苛的环境中,也是有可能高效实现这种原子提交机制的。Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了这种方法,Apache Kafka 有计划加入类似的功能【95,96】。与 XA 不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中同时管理状态变更与消息传递来内化事务。事务协议的开销可以通过在单个事务中处理多个输入消息来分摊。
|
||||
|
||||
#### 幂等性
|
||||
|
||||
@ -672,7 +672,7 @@ Storm 的 Trident 基于类似的想法来处理状态【78】。依赖幂等性
|
||||
|
||||
一种选择是将状态保存在远程数据存储中,并进行复制,然而正如在 “[流表连接(流扩充)](#流表连接(流扩充))” 中所述,每个消息都要查询远程数据库可能会很慢。另一种方法是在流处理器本地保存状态,并定期复制。然后当流处理器从故障中恢复时,新任务可以读取状态副本,恢复处理而不丢失数据。
|
||||
|
||||
例如,Flink 定期捕获算子状态的快照,并将它们写入 HDFS 等持久存储中【92,93】。 Samza 和 Kafka Streams 通过将状态变更发送到具有日志压缩功能的专用 Kafka 主题来复制状态变更,这与变更数据捕获类似【84,100】。 VoltDB 通过在多个节点上对每个输入消息进行冗余处理来复制状态(请参阅 “[真的串行执行](ch7.md#真的串行执行)”)。
|
||||
例如,Flink 定期捕获算子状态的快照,并将它们写入 HDFS 等持久存储中【92,93】。Samza 和 Kafka Streams 通过将状态变更发送到具有日志压缩功能的专用 Kafka 主题来复制状态变更,这与变更数据捕获类似【84,100】。VoltDB 通过在多个节点上对每个输入消息进行冗余处理来复制状态(请参阅 “[真的串行执行](ch7.md#真的串行执行)”)。
|
||||
|
||||
在某些情况下,甚至可能都不需要复制状态,因为它可以从输入流重建。例如,如果状态是从相当短的窗口中聚合而成,则简单地重播该窗口中的输入事件可能是足够快的。如果状态是通过变更数据捕获来维护的数据库的本地副本,那么也可以从日志压缩的变更流中重建数据库(请参阅 “[日志压缩](#日志压缩)”)。
|
||||
|
||||
|
16
ch12.md
16
ch12.md
@ -44,7 +44,7 @@
|
||||
|
||||
允许应用程序直接写入搜索索引和数据库引入了如 [图 11-4](img/fig11-4.png) 所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。
|
||||
|
||||
如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。 这是状态机复制方法的一个应用,我们在 “[全序广播](ch9.md#全序广播)” 中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。
|
||||
如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。这是状态机复制方法的一个应用,我们在 “[全序广播](ch9.md#全序广播)” 中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。
|
||||
|
||||
基于事件日志来更新衍生数据的系统,通常可以做到 **确定性** 与 **幂等性**(请参阅 “[幂等性](ch11.md#幂等性)”),使得从故障中恢复相当容易。
|
||||
|
||||
@ -160,7 +160,7 @@ Lambda 架构是一种有影响力的想法,它将数据系统的设计变得
|
||||
|
||||
Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Unix 认为它的目的是为程序员提供一种相当低层次的硬件的逻辑抽象,而关系数据库则希望为应用程序员提供一种高层次的抽象,以隐藏磁盘上数据结构的复杂性、并发性、崩溃恢复等等。Unix 发展出的管道和文件只是字节序列,而数据库则发展出了 SQL 和事务。
|
||||
|
||||
哪种方法更好?当然这取决于你想要的是什么。 Unix 是 “简单的”,因为它是对硬件资源相当薄的包装;关系数据库是 “更简单” 的,因为一个简短的声明性查询可以利用很多强大的基础设施(查询优化、索引、连接方法、并发控制、复制等),而不需要查询的作者理解其实现细节。
|
||||
哪种方法更好?当然这取决于你想要的是什么。Unix 是 “简单的”,因为它是对硬件资源相当薄的包装;关系数据库是 “更简单” 的,因为一个简短的声明性查询可以利用很多强大的基础设施(查询优化、索引、连接方法、并发控制、复制等),而不需要查询的作者理解其实现细节。
|
||||
|
||||
这些哲学之间的矛盾已经持续了几十年(Unix 和关系模型都出现在 70 年代初),仍然没有解决。例如,我将 NoSQL 运动解释为,希望将类 Unix 的低级别抽象方法应用于分布式 OLTP 数据存储的领域。
|
||||
|
||||
@ -273,13 +273,13 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
|
||||
|
||||
现在大多数 Web 应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如函数式编程社区喜欢开玩笑说的那样,“我们相信 **教会(Church)** 与 **国家(state)** 的分离”【37】 [^i]
|
||||
|
||||
[^i]: 解释笑话很少会让人感觉更好,但我不想让任何人感到被遗漏。 在这里,Church 指代的是数学家的阿隆佐・邱奇,他创立了 lambda 演算,这是计算的早期形式,是大多数函数式编程语言的基础。 lambda 演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与 Church 的工作是分离的。
|
||||
[^i]: 解释笑话很少会让人感觉更好,但我不想让任何人感到被遗漏。在这里,Church 指代的是数学家的阿隆佐・邱奇,他创立了 lambda 演算,这是计算的早期形式,是大多数函数式编程语言的基础。lambda 演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与 Church 的工作是分离的。
|
||||
|
||||
在这个典型的 Web 应用模型中,数据库充当一种可以通过网络同步访问的可变共享变量。应用程序可以读取和更新变量,而数据库负责维持它的持久性,提供一些诸如并发控制和容错的功能。
|
||||
|
||||
但是,在大多数编程语言中,你无法订阅可变变量中的变更 —— 你只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知(你可以在自己的代码中实现这样的通知 —— 这被称为 **观察者模式** —— 但大多数语言没有将这种模式作为内置功能)。
|
||||
|
||||
数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅变更只是刚刚开始出现的功能(请参阅 “[变更流的 API 支持](ch11.md#变更流的API支持)”)。
|
||||
数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。订阅变更只是刚刚开始出现的功能(请参阅 “[变更流的 API 支持](ch11.md#变更流的API支持)”)。
|
||||
|
||||
#### 数据流:应用代码与状态变化的交互
|
||||
|
||||
@ -311,7 +311,7 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
|
||||
|
||||
第二种方法能将对另一服务的同步网络请求替换为对本地数据库的查询(可能在同一台机器甚至同一个进程中)[^ii]。数据流方法不仅更快,而且当其他服务失效时也更稳健。最快且最可靠的网络请求就是压根没有网络请求!我们现在不再使用 RPC,而是在购买事件和汇率更新事件之间建立流联接(请参阅 “[流表连接(流扩充)](ch11.md#流表连接(流扩充))”)。
|
||||
|
||||
[^ii]: 在微服务方法中,你也可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。 但是为了保证缓存的新鲜度,你需要定期轮询汇率以获取其更新,或订阅变更流 —— 这恰好是数据流方法中发生的事情。
|
||||
[^ii]: 在微服务方法中,你也可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。但是为了保证缓存的新鲜度,你需要定期轮询汇率以获取其更新,或订阅变更流 —— 这恰好是数据流方法中发生的事情。
|
||||
|
||||
连接是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率可能已经改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论是查询服务还是订阅汇率更新流,你都需要处理这种时间相关性(请参阅 “[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。
|
||||
|
||||
@ -461,7 +461,7 @@ COMMIT;
|
||||
|
||||
即使我们可以抑制数据库客户端与服务器之间的重复事务,我们仍然需要担心终端用户设备与应用服务器之间的网络。例如,如果终端用户的客户端是 Web 浏览器,则它可能会使用 HTTP POST 请求向服务器提交指令。也许用户正处于一个信号微弱的蜂窝数据网络连接中,它们成功地发送了 POST,但却在能够从服务器接收响应之前没了信号。
|
||||
|
||||
在这种情况下,可能会向用户显示错误消息,而他们可能会手动重试。 Web 浏览器警告说,“你确定要再次提交这个表单吗?” —— 用户选 “是”,因为他们希望操作发生(Post/Redirect/Get 模式【54】可以避免在正常操作中出现此警告消息,但 POST 请求超时就没办法了)。从 Web 服务器的角度来看,重试是一个独立的请求;从数据库的角度来看,这是一个独立的事务。通常的除重机制无济于事。
|
||||
在这种情况下,可能会向用户显示错误消息,而他们可能会手动重试。Web 浏览器警告说,“你确定要再次提交这个表单吗?” —— 用户选 “是”,因为他们希望操作发生(Post/Redirect/Get 模式【54】可以避免在正常操作中出现此警告消息,但 POST 请求超时就没办法了)。从 Web 服务器的角度来看,重试是一个独立的请求;从数据库的角度来看,这是一个独立的事务。通常的除重机制无济于事。
|
||||
|
||||
#### 操作标识符
|
||||
|
||||
@ -713,7 +713,7 @@ ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基
|
||||
|
||||
密码学审计与完整性检查通常依赖 **默克尔树(Merkle tree)**【74】,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。除了炒作的沸沸扬扬的加密货币之外,**证书透明性(certificate transparency)** 也是一种依赖 Merkle 树的安全技术,用来检查 TLS/SSL 证书的有效性【75,76】。
|
||||
|
||||
我可以想象,那些在证书透明度与分布式账本中使用的完整性检查和审计算法,将会在通用数据系统中得到越来越广泛的应用。要使得这些算法对于没有密码学审计的系统同样可伸缩,并尽可能降低性能损失还需要一些工作。 但我认为这是一个值得关注的有趣领域。
|
||||
我可以想象,那些在证书透明度与分布式账本中使用的完整性检查和审计算法,将会在通用数据系统中得到越来越广泛的应用。要使得这些算法对于没有密码学审计的系统同样可伸缩,并尽可能降低性能损失还需要一些工作。但我认为这是一个值得关注的有趣领域。
|
||||
|
||||
|
||||
## 做正确的事情
|
||||
@ -774,7 +774,7 @@ ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基
|
||||
|
||||
当系统只存储用户明确输入的数据时,是因为用户希望系统以特定方式存储和处理这些数据,**系统是在为用户提供服务**:用户就是客户。但是,当用户的活动被跟踪并记录,作为他们正在做的其他事情的副作用时,这种关系就没有那么清晰了。该服务不再仅仅完成用户想要它要做的事情,而是服务于它自己的利益,而这可能与用户的利益相冲突。
|
||||
|
||||
追踪用户行为数据对于许多面向用户的在线服务而言,变得越来越重要:追踪用户点击了哪些搜索结果有助于改善搜索结果的排名;推荐 “喜欢 X 的人也喜欢 Y”,可以帮助用户发现实用有趣的东西; A/B 测试和用户流量分析有助于改善用户界面。这些功能需要一定量的用户行为跟踪,而用户也可以从中受益。
|
||||
追踪用户行为数据对于许多面向用户的在线服务而言,变得越来越重要:追踪用户点击了哪些搜索结果有助于改善搜索结果的排名;推荐 “喜欢 X 的人也喜欢 Y”,可以帮助用户发现实用有趣的东西;A/B 测试和用户流量分析有助于改善用户界面。这些功能需要一定量的用户行为跟踪,而用户也可以从中受益。
|
||||
|
||||
但不同公司有着不同的商业模式,追踪并未止步于此。如果服务是通过广告盈利的,那么广告主才是真正的客户,而用户的利益则屈居其次。跟踪的数据会变得更详细,分析变得更深入,数据会保留很长时间,以便为每个人建立详细画像,用于营销。
|
||||
|
||||
|
22
ch3.md
22
ch3.md
@ -88,7 +88,7 @@ $ cat database
|
||||
|
||||
**图 3-1 以类 CSV 格式存储键值对的日志,并使用内存散列映射进行索引。**
|
||||
|
||||
听上去简单,但这是一个可行的方法。现实中,Bitcask 实际上就是这么做的(Riak 中默认的存储引擎)【3】。 Bitcask 提供高性能的读取和写入操作,但要求所有的键必须能放入可用内存中,因为散列映射完全保留在内存中。而数据值可以使用比可用内存更多的空间,因为可以在硬盘上通过一次硬盘查找操作来加载所需部分,如果数据文件的那部分已经在文件系统缓存中,则读取根本不需要任何硬盘 I/O。
|
||||
听上去简单,但这是一个可行的方法。现实中,Bitcask 实际上就是这么做的(Riak 中默认的存储引擎)【3】。Bitcask 提供高性能的读取和写入操作,但要求所有的键必须能放入可用内存中,因为散列映射完全保留在内存中。而数据值可以使用比可用内存更多的空间,因为可以在硬盘上通过一次硬盘查找操作来加载所需部分,如果数据文件的那部分已经在文件系统缓存中,则读取根本不需要任何硬盘 I/O。
|
||||
|
||||
像 Bitcask 这样的存储引擎非常适合每个键的值经常更新的情况。例如,键可能是某个猫咪视频的网址(URL),而值可能是该视频被播放的次数(每次有人点击播放按钮时递增)。在这种类型的工作负载中,有很多写操作,但是没有太多不同的键 —— 每个键有很多的写操作,但是将所有键保存在内存中是可行的。
|
||||
|
||||
@ -118,11 +118,11 @@ $ cat database
|
||||
|
||||
* 崩溃恢复
|
||||
|
||||
如果数据库重新启动,则内存散列映射将丢失。原则上,你可以通过从头到尾读取整个段文件并记录下来每个键的最近值来恢复每个段的散列映射。但是,如果段文件很大,可能需要很长时间,这会使服务的重启比较痛苦。 Bitcask 通过将每个段的散列映射的快照存储在硬盘上来加速恢复,可以使散列映射更快地加载到内存中。
|
||||
如果数据库重新启动,则内存散列映射将丢失。原则上,你可以通过从头到尾读取整个段文件并记录下来每个键的最近值来恢复每个段的散列映射。但是,如果段文件很大,可能需要很长时间,这会使服务的重启比较痛苦。Bitcask 通过将每个段的散列映射的快照存储在硬盘上来加速恢复,可以使散列映射更快地加载到内存中。
|
||||
|
||||
* 部分写入记录
|
||||
|
||||
数据库随时可能崩溃,包括在将记录追加到日志的过程中。 Bitcask 文件包含校验和,允许检测和忽略日志中的这些损坏部分。
|
||||
数据库随时可能崩溃,包括在将记录追加到日志的过程中。Bitcask 文件包含校验和,允许检测和忽略日志中的这些损坏部分。
|
||||
|
||||
* 并发控制
|
||||
|
||||
@ -197,7 +197,7 @@ Lucene,是一种全文搜索的索引引擎,在 Elasticsearch 和 Solr 被
|
||||
|
||||
#### 性能优化
|
||||
|
||||
与往常一样,要让存储引擎在实践中表现良好涉及到大量设计细节。例如,当查找数据库中不存在的键时,LSM 树算法可能会很慢:你必须先检查内存表,然后查看从最近的到最旧的所有的段(可能还必须从硬盘读取每一个段文件),然后才能确定这个键不存在。为了优化这种访问,存储引擎通常使用额外的布隆过滤器(Bloom filters)【15】。 (布隆过滤器是一种节省内存的数据结构,用于近似表达集合的内容,它可以告诉你数据库中是否存在某个键,从而为不存在的键节省掉许多不必要的硬盘读取操作。)
|
||||
与往常一样,要让存储引擎在实践中表现良好涉及到大量设计细节。例如,当查找数据库中不存在的键时,LSM 树算法可能会很慢:你必须先检查内存表,然后查看从最近的到最旧的所有的段(可能还必须从硬盘读取每一个段文件),然后才能确定这个键不存在。为了优化这种访问,存储引擎通常使用额外的布隆过滤器(Bloom filters)【15】。(布隆过滤器是一种节省内存的数据结构,用于近似表达集合的内容,它可以告诉你数据库中是否存在某个键,从而为不存在的键节省掉许多不必要的硬盘读取操作。)
|
||||
|
||||
还有一些不同的策略来确定 SSTables 被压缩和合并的顺序和时间。最常见的选择是 size-tiered 和 leveled compaction。LevelDB 和 RocksDB 使用 leveled compaction(LevelDB 因此得名),HBase 使用 size-tiered,Cassandra 同时支持这两种【16】。对于 sized-tiered,较新和较小的 SSTables 相继被合并到较旧的和较大的 SSTable 中。对于 leveled compaction,key (按照分布范围)被拆分到较小的 SSTables,而较旧的数据被移动到单独的层级(level),这使得压缩(compaction)能够更加增量地进行,并且使用较少的硬盘空间。
|
||||
|
||||
@ -264,7 +264,7 @@ B 树的基本底层写操作是用新数据覆写硬盘上的页面,并假定
|
||||
|
||||
### 比较B树和LSM树
|
||||
|
||||
尽管 B 树实现通常比 LSM 树实现更成熟,但 LSM 树由于性能特征也非常有趣。根据经验,通常 LSM 树的写入速度更快,而 B 树的读取速度更快【23】。 LSM 树上的读取通常比较慢,因为它们必须检查几种不同的数据结构和不同压缩(Compaction)层级的 SSTables。
|
||||
尽管 B 树实现通常比 LSM 树实现更成熟,但 LSM 树由于性能特征也非常有趣。根据经验,通常 LSM 树的写入速度更快,而 B 树的读取速度更快【23】。LSM 树上的读取通常比较慢,因为它们必须检查几种不同的数据结构和不同压缩(Compaction)层级的 SSTables。
|
||||
|
||||
然而,基准测试的结果通常和工作负载的细节相关。你需要用你特有的工作负载来测试系统,以便进行有效的比较。在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
|
||||
|
||||
@ -353,7 +353,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
内存数据库重新启动时,需要从硬盘或通过网络从副本重新加载其状态(除非使用特殊的硬件)。尽管写入硬盘,它仍然是一个内存数据库,因为硬盘仅出于持久性目的进行日志追加,读取请求完全由内存来处理。写入硬盘同时还有运维上的好处:硬盘上的文件可以很容易地由外部程序进行备份、检查和分析。
|
||||
|
||||
诸如 VoltDB、MemSQL 和 Oracle TimesTen 等产品是具有关系模型的内存数据库,供应商声称,通过消除与管理硬盘上的数据结构相关的所有开销,他们可以提供巨大的性能改进【41,42】。 RAM Cloud 是一个开源的内存键值存储器,具有持久性(对内存和硬盘上的数据都使用日志结构化方法)【43】。 Redis 和 Couchbase 通过异步写入硬盘提供了较弱的持久性。
|
||||
诸如 VoltDB、MemSQL 和 Oracle TimesTen 等产品是具有关系模型的内存数据库,供应商声称,通过消除与管理硬盘上的数据结构相关的所有开销,他们可以提供巨大的性能改进【41,42】。RAM Cloud 是一个开源的内存键值存储器,具有持久性(对内存和硬盘上的数据都使用日志结构化方法)【43】。Redis 和 Couchbase 通过异步写入硬盘提供了较弱的持久性。
|
||||
|
||||
反直觉的是,内存数据库的性能优势并不是因为它们不需要从硬盘读取的事实。只要有足够的内存即使是基于硬盘的存储引擎也可能永远不需要从硬盘读取,因为操作系统在内存中缓存了最近使用的硬盘块。相反,它们更快的原因在于省去了将内存数据结构编码为硬盘数据结构的开销【44】。
|
||||
|
||||
@ -392,7 +392,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
[^iv]: OLAP 中的首字母 O(online)的含义并不明确,它可能是指查询并不是用来生成预定义好的报告的事实,也可能是指分析师通常是交互式地使用 OLAP 系统来进行探索式的查询。
|
||||
|
||||
起初,事务处理和分析查询使用了相同的数据库。 SQL 在这方面已证明是非常灵活的:对于 OLTP 类型的查询以及 OLAP 类型的查询来说效果都很好。尽管如此,在二十世纪八十年代末和九十年代初期,企业有停止使用 OLTP 系统进行分析的趋势,转而在单独的数据库上运行分析。这个单独的数据库被称为 **数据仓库(data warehouse)**。
|
||||
起初,事务处理和分析查询使用了相同的数据库。SQL 在这方面已证明是非常灵活的:对于 OLTP 类型的查询以及 OLAP 类型的查询来说效果都很好。尽管如此,在二十世纪八十年代末和九十年代初期,企业有停止使用 OLTP 系统进行分析的趋势,转而在单独的数据库上运行分析。这个单独的数据库被称为 **数据仓库(data warehouse)**。
|
||||
|
||||
### 数据仓库
|
||||
|
||||
@ -418,7 +418,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
一些数据库(例如 Microsoft SQL Server 和 SAP HANA)支持在同一产品中进行事务处理和数据仓库。但是,它们也正日益发展为两套独立的存储和查询引擎,只是这些引擎正好可以通过一个通用的 SQL 接口访问【49,50,51】。
|
||||
|
||||
Teradata、Vertica、SAP HANA 和 ParAccel 等数据仓库供应商通常使用昂贵的商业许可证销售他们的系统。 Amazon RedShift 是 ParAccel 的托管版本。最近,大量的开源 SQL-on-Hadoop 项目已经出现,它们还很年轻,但是正在与商业数据仓库系统竞争,包括 Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo 和 Apache Drill【52,53】。其中一些基于了谷歌 Dremel 的想法【54】。
|
||||
Teradata、Vertica、SAP HANA 和 ParAccel 等数据仓库供应商通常使用昂贵的商业许可证销售他们的系统。Amazon RedShift 是 ParAccel 的托管版本。最近,大量的开源 SQL-on-Hadoop 项目已经出现,它们还很年轻,但是正在与商业数据仓库系统竞争,包括 Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo 和 Apache Drill【52,53】。其中一些基于了谷歌 Dremel 的想法【54】。
|
||||
|
||||
### 星型和雪花型:分析的模式
|
||||
|
||||
@ -434,7 +434,7 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等数据仓库供应商通常使用
|
||||
|
||||
事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(可以用来计算利润率)。事实表中的其他列是对其他表(称为维度表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件发生的对象、内容、地点、时间、方式和原因。
|
||||
|
||||
例如,在 [图 3-9](img/fig3-9.png) 中,其中一个维度是已售出的产品。 `dim_product` 表中的每一行代表一种待售产品,包括库存单位(SKU)、产品描述、品牌名称、类别、脂肪含量、包装尺寸等。`fact_sales` 表中的每一行都使用外键表明在特定交易中销售了什么产品。 (简单起见,如果客户一次购买了几种不同的产品,则它们在事实表中被表示为单独的行)。
|
||||
例如,在 [图 3-9](img/fig3-9.png) 中,其中一个维度是已售出的产品。`dim_product` 表中的每一行代表一种待售产品,包括库存单位(SKU)、产品描述、品牌名称、类别、脂肪含量、包装尺寸等。`fact_sales` 表中的每一行都使用外键表明在特定交易中销售了什么产品。(简单起见,如果客户一次购买了几种不同的产品,则它们在事实表中被表示为单独的行)。
|
||||
|
||||
甚至日期和时间也通常使用维度表来表示,因为这允许对日期的附加信息(诸如公共假期)进行编码,从而允许区分假期和非假期的销售查询。
|
||||
|
||||
@ -482,7 +482,7 @@ GROUP BY
|
||||
|
||||
> 列式存储在关系数据模型中是最容易理解的,但它同样适用于非关系数据。例如,Parquet【57】是一种列式存储格式,支持基于 Google 的 Dremel 的文档数据模型【54】。
|
||||
|
||||
列式存储布局依赖于每个列文件包含相同顺序的行。 因此,如果你需要重新组装完整的行,你可以从每个单独的列文件中获取第 23 项,并将它们放在一起形成表的第 23 行。
|
||||
列式存储布局依赖于每个列文件包含相同顺序的行。因此,如果你需要重新组装完整的行,你可以从每个单独的列文件中获取第 23 项,并将它们放在一起形成表的第 23 行。
|
||||
|
||||
|
||||
### 列压缩
|
||||
@ -594,7 +594,7 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
在 OLTP 这一边,我们能看到两派主流的存储引擎:
|
||||
|
||||
* 日志结构学派:只允许追加到文件和删除过时的文件,但不会更新已经写入的文件。Bitcask、SSTables、LSM 树、LevelDB、Cassandra、HBase、Lucene 等都属于这个类别。
|
||||
* 就地更新学派:将硬盘视为一组可以覆写的固定大小的页面。 B 树是这种理念的典范,用在所有主要的关系数据库和许多非关系型数据库中。
|
||||
* 就地更新学派:将硬盘视为一组可以覆写的固定大小的页面。B 树是这种理念的典范,用在所有主要的关系数据库和许多非关系型数据库中。
|
||||
|
||||
日志结构的存储引擎是相对较新的技术。他们的主要想法是,通过系统性地将随机访问写入转换为硬盘上的顺序写入,由于硬盘驱动器和固态硬盘的性能特点,可以实现更高的写入吞吐量。
|
||||
|
||||
|
78
ch4.md
78
ch4.md
@ -42,20 +42,20 @@
|
||||
|
||||
程序通常(至少)使用两种形式的数据:
|
||||
|
||||
1. 在内存中,数据保存在对象、结构体、列表、数组、散列表、树等中。 这些数据结构针对 CPU 的高效访问和操作进行了优化(通常使用指针)。
|
||||
2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码(encode)** 为某种自包含的字节序列(例如,JSON 文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同 [^i]。
|
||||
1. 在内存中,数据保存在对象、结构体、列表、数组、散列表、树等中。这些数据结构针对 CPU 的高效访问和操作进行了优化(通常使用指针)。
|
||||
2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码(encode)** 为某种自包含的字节序列(例如,JSON 文档)。由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同 [^i]。
|
||||
|
||||
[^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如 “[列压缩](ch3.md#列压缩)” 中所述)。
|
||||
|
||||
所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为 **编码(Encoding)** (也称为 **序列化(serialization)** 或 **编组(marshalling)**),反过来称为 **解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组 (unmarshalling)**)[^译i]。
|
||||
所以,需要在两种表示之间进行某种类型的翻译。从内存中表示到字节序列的转换称为 **编码(Encoding)** (也称为 **序列化(serialization)** 或 **编组(marshalling)**),反过来称为 **解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组(unmarshalling)**)[^译i]。
|
||||
|
||||
[^ii]: 请注意,**编码(encode)** 与 **加密(encryption)** 无关。 本书不讨论加密。
|
||||
[^ii]: 请注意,**编码(encode)** 与 **加密(encryption)** 无关。本书不讨论加密。
|
||||
[^译i]: Marshal 与 Serialization 的区别:Marshal 不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。
|
||||
|
||||
> #### 术语冲突
|
||||
> 不幸的是,在 [第七章](ch7.md): **事务(Transaction)** 的上下文里,**序列化(Serialization)** 这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用 **编码(Encoding)** 表达此含义。
|
||||
|
||||
这是一个常见的问题,因而有许多库和编码格式可供选择。 首先让我们概览一下。
|
||||
这是一个常见的问题,因而有许多库和编码格式可供选择。首先让我们概览一下。
|
||||
|
||||
### 语言特定的格式
|
||||
|
||||
@ -66,21 +66,21 @@
|
||||
* 这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。如果以这类编码存储或传输数据,那你就和这门语言绑死在一起了。并且很难将系统与其他组织的系统(可能用的是不同的语言)进行集成。
|
||||
* 为了恢复相同对象类型的数据,解码过程需要 **实例化任意类** 的能力,这通常是安全问题的一个来源【5】:如果攻击者可以让应用程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执行任意代码【6,7】。
|
||||
* 在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进行编码,所以往往忽略了向前和向后兼容性带来的麻烦问题。
|
||||
* 效率(编码或解码所花费的 CPU 时间,以及编码结构的大小)往往也是事后才考虑的。 例如,Java 的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭著【8】。
|
||||
* 效率(编码或解码所花费的 CPU 时间,以及编码结构的大小)往往也是事后才考虑的。例如,Java 的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭著【8】。
|
||||
|
||||
因此,除非临时使用,采用语言内置编码通常是一个坏主意。
|
||||
|
||||
### JSON、XML和二进制变体
|
||||
|
||||
当我们谈到可以被多种编程语言读写的标准编码时,JSON 和 XML 是最显眼的角逐者。它们广为人知,广受支持,也 “广受憎恶”。 XML 经常收到批评:过于冗长与且过份复杂【9】。 JSON 的流行则主要源于(通过成为 JavaScript 的一个子集)Web 浏览器的内置支持,以及相对于 XML 的简单性。 CSV 是另一种流行的与语言无关的格式,尽管其功能相对较弱。
|
||||
当我们谈到可以被多种编程语言读写的标准编码时,JSON 和 XML 是最显眼的角逐者。它们广为人知,广受支持,也 “广受憎恶”。XML 经常收到批评:过于冗长与且过份复杂【9】。JSON 的流行则主要源于(通过成为 JavaScript 的一个子集)Web 浏览器的内置支持,以及相对于 XML 的简单性。CSV 是另一种流行的与语言无关的格式,尽管其功能相对较弱。
|
||||
|
||||
JSON,XML 和 CSV 属于文本格式,因此具有人类可读性(尽管它们的语法是一个热门争议话题)。除了表面的语法问题之外,它们也存在一些微妙的问题:
|
||||
|
||||
* **数字(numbers)** 编码有很多模糊之处。在 XML 和 CSV 中,无法区分数字和碰巧由数字组成的字符串(除了引用外部模式)。 JSON 虽然区分字符串与数字,但并不区分整数和浮点数,并且不能指定精度。
|
||||
这在处理大数字时是个问题。例如大于 $2^{53}$ 的整数无法使用 IEEE 754 双精度浮点数精确表示,因此在使用浮点数(例如 JavaScript)的语言进行分析时,这些数字会变得不准确。 Twitter 有一个关于大于 $2^{53}$ 的数字的例子,它使用 64 位整数来标识每条推文。 Twitter API 返回的 JSON 包含了两个推特 ID,一个是 JSON 数字,另一个是十进制字符串,以解决 JavaScript 程序中无法正确解析数字的问题【10】。
|
||||
* **数字(numbers)** 编码有很多模糊之处。在 XML 和 CSV 中,无法区分数字和碰巧由数字组成的字符串(除了引用外部模式)。JSON 虽然区分字符串与数字,但并不区分整数和浮点数,并且不能指定精度。
|
||||
这在处理大数字时是个问题。例如大于 $2^{53}$ 的整数无法使用 IEEE 754 双精度浮点数精确表示,因此在使用浮点数(例如 JavaScript)的语言进行分析时,这些数字会变得不准确。Twitter 有一个关于大于 $2^{53}$ 的数字的例子,它使用 64 位整数来标识每条推文。Twitter API 返回的 JSON 包含了两个推特 ID,一个是 JSON 数字,另一个是十进制字符串,以解决 JavaScript 程序中无法正确解析数字的问题【10】。
|
||||
* JSON 和 XML 对 Unicode 字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(即不带 **字符编码(character encoding)** 的字节序列)。二进制串是很有用的功能,人们通过使用 Base64 将二进制数据编码为文本来绕过此限制。其特有的模式标识着这个值应当被解释为 Base64 编码的二进制数据。这种方案虽然管用,但比较 Hacky,并且会增加三分之一的数据大小。
|
||||
* XML 【11】和 JSON 【12】都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来都相当复杂。 XML 模式的使用相当普遍,但许多基于 JSON 的工具才不会去折腾模式。对数据的正确解读(例如区分数值与二进制串)取决于模式中的信息,因此不使用 XML/JSON 模式的应用程序可能需要对相应的编码 / 解码逻辑进行硬编码。
|
||||
* CSV 没有任何模式,因此每行和每列的含义完全由应用程序自行定义。如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。 CSV 也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。
|
||||
* XML 【11】和 JSON 【12】都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来都相当复杂。XML 模式的使用相当普遍,但许多基于 JSON 的工具才不会去折腾模式。对数据的正确解读(例如区分数值与二进制串)取决于模式中的信息,因此不使用 XML/JSON 模式的应用程序可能需要对相应的编码 / 解码逻辑进行硬编码。
|
||||
* CSV 没有任何模式,因此每行和每列的含义完全由应用程序自行定义。如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。CSV 也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。
|
||||
|
||||
尽管存在这些缺陷,但 JSON、XML 和 CSV 对很多需求来说已经足够好了。它们很可能会继续流行下去,特别是作为数据交换格式来说(即将数据从一个组织发送到另一个组织)。在这种情况下,只要人们对格式是什么意见一致,格式有多美观或者效率有多高效就无所谓了。让不同的组织就这些东西达成一致的难度超过了绝大多数问题。
|
||||
|
||||
@ -104,7 +104,7 @@ JSON 比 XML 简洁,但与二进制格式相比还是太占空间。这一事
|
||||
|
||||
我们来看一个 MessagePack 的例子,它是一个 JSON 的二进制编码。图 4-1 显示了如果使用 MessagePack 【14】对 [例 4-1]() 中的 JSON 文档进行编码,则得到的字节序列。前几个字节如下:
|
||||
|
||||
1. 第一个字节 `0x83` 表示接下来是 **3** 个字段(低四位 = `0x03`)的 **对象 object**(高四位 = `0x80`)。 (如果想知道如果一个对象有 15 个以上的字段会发生什么情况,字段的数量塞不进 4 个 bit 里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。
|
||||
1. 第一个字节 `0x83` 表示接下来是 **3** 个字段(低四位 = `0x03`)的 **对象 object**(高四位 = `0x80`)。(如果想知道如果一个对象有 15 个以上的字段会发生什么情况,字段的数量塞不进 4 个 bit 里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。
|
||||
2. 第二个字节 `0xa8` 表示接下来是 **8** 字节长(低四位 = `0x08`)的字符串(高四位 = `0x0a`)。
|
||||
3. 接下来八个字节是 ASCII 字符串形式的字段名称 `userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
|
||||
4. 接下来的七个字节对前缀为 `0xa6` 的六个字母的字符串值 `Martin` 进行编码,依此类推。
|
||||
@ -119,7 +119,7 @@ JSON 比 XML 简洁,但与二进制格式相比还是太占空间。这一事
|
||||
|
||||
### Thrift与Protocol Buffers
|
||||
|
||||
Apache Thrift 【15】和 Protocol Buffers(protobuf)【16】是基于相同原理的二进制编码库。 Protocol Buffers 最初是在 Google 开发的,Thrift 最初是在 Facebook 开发的,并且都是在 2007~2008 开源的【17】。
|
||||
Apache Thrift 【15】和 Protocol Buffers(protobuf)【16】是基于相同原理的二进制编码库。Protocol Buffers 最初是在 Google 开发的,Thrift 最初是在 Facebook 开发的,并且都是在 2007~2008 开源的【17】。
|
||||
Thrift 和 Protocol Buffers 都需要一个模式来编码任何数据。要在 Thrift 的 [例 4-1]() 中对数据进行编码,可以使用 Thrift **接口定义语言(IDL)** 来描述模式,如下所示:
|
||||
|
||||
```c
|
||||
@ -147,7 +147,7 @@ Thrift 和 Protocol Buffers 每一个都带有一个代码生成工具,它采
|
||||
|
||||
**图 4-2 使用 Thrift 二进制协议编码的记录**
|
||||
|
||||
[^iii]: 实际上,Thrift 有三种二进制协议:BinaryProtocol、CompactProtocol 和 DenseProtocol,尽管 DenseProtocol 只支持 C ++ 实现,所以不算作跨语言【18】。 除此之外,它还有两种不同的基于 JSON 的编码格式【19】。 真逗!
|
||||
[^iii]: 实际上,Thrift 有三种二进制协议:BinaryProtocol、CompactProtocol 和 DenseProtocol,尽管 DenseProtocol 只支持 C ++ 实现,所以不算作跨语言【18】。除此之外,它还有两种不同的基于 JSON 的编码格式【19】。真逗!
|
||||
|
||||
与 [图 4-1](Img/fig4-1.png) 类似,每个字段都有一个类型注释(用于指示它是一个字符串、整数、列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串 `(“Martin”, “daydreaming”, “hacking”)` 也被编码为 ASCII(或者说,UTF-8),与之前类似。
|
||||
|
||||
@ -159,7 +159,7 @@ Thrift CompactProtocol 编码在语义上等同于 BinaryProtocol,但是如 [
|
||||
|
||||
**图 4-3 使用 Thrift 压缩协议编码的记录**
|
||||
|
||||
最后,Protocol Buffers(只有一种二进制编码格式)对相同的数据进行编码,如 [图 4-4](img/fig4-4.png) 所示。 它的打包方式稍有不同,但与 Thrift 的 CompactProtocol 非常相似。 Protobuf 将同样的记录塞进了 33 个字节中。
|
||||
最后,Protocol Buffers(只有一种二进制编码格式)对相同的数据进行编码,如 [图 4-4](img/fig4-4.png) 所示。它的打包方式稍有不同,但与 Thrift 的 CompactProtocol 非常相似。Protobuf 将同样的记录塞进了 33 个字节中。
|
||||
|
||||
![](img/fig4-4.png)
|
||||
|
||||
@ -169,7 +169,7 @@ Thrift CompactProtocol 编码在语义上等同于 BinaryProtocol,但是如 [
|
||||
|
||||
#### 字段标签和模式演变
|
||||
|
||||
我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变。 Thrift 和 Protocol Buffers 如何处理模式更改,同时保持向后兼容性?
|
||||
我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变。Thrift 和 Protocol Buffers 如何处理模式更改,同时保持向后兼容性?
|
||||
|
||||
从示例中可以看出,编码的记录就是其编码字段的拼接。每个字段由其标签号码(样本模式中的数字 1,2,3)标识,并用数据类型(例如字符串或整数)注释。如果没有设置字段值,则简单地从编码记录中省略。从中可以看到,字段标记对编码数据的含义至关重要。你可以更改架构中字段的名称,因为编码的数据永远不会引用字段名称,但不能更改字段的标记,因为这会使所有现有的编码数据无效。
|
||||
|
||||
@ -189,9 +189,9 @@ Thrift 有一个专用的列表数据类型,它使用列表元素的数据类
|
||||
|
||||
### Avro
|
||||
|
||||
Apache Avro 【20】是另一种二进制编码格式,与 Protocol Buffers 和 Thrift 有着有趣的不同。 它是作为 Hadoop 的一个子项目在 2009 年开始的,因为 Thrift 不适合 Hadoop 的用例【21】。
|
||||
Apache Avro 【20】是另一种二进制编码格式,与 Protocol Buffers 和 Thrift 有着有趣的不同。它是作为 Hadoop 的一个子项目在 2009 年开始的,因为 Thrift 不适合 Hadoop 的用例【21】。
|
||||
|
||||
Avro 也使用模式来指定正在编码的数据的结构。 它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于 JSON)更易于机器读取。
|
||||
Avro 也使用模式来指定正在编码的数据的结构。它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于 JSON)更易于机器读取。
|
||||
|
||||
我们用 Avro IDL 编写的示例模式可能如下所示:
|
||||
|
||||
@ -217,9 +217,9 @@ record Person {
|
||||
}
|
||||
```
|
||||
|
||||
首先,请注意模式中没有标签号码。 如果我们使用这个模式编码我们的例子记录([例 4-1]()),Avro 二进制编码只有 32 个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如 [图 4-5](img/fig4-5.png) 所示。
|
||||
首先,请注意模式中没有标签号码。如果我们使用这个模式编码我们的例子记录([例 4-1]()),Avro 二进制编码只有 32 个字节长,这是我们所见过的所有编码中最紧凑的。编码字节序列的分解如 [图 4-5](img/fig4-5.png) 所示。
|
||||
|
||||
如果你检查字节序列,你可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀,后跟 UTF-8 字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码(与 Thrift 的 CompactProtocol 相同)进行编码。
|
||||
如果你检查字节序列,你可以看到没有什么可以识别字段或其数据类型。编码只是由连在一起的值组成。一个字符串只是一个长度前缀,后跟 UTF-8 字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。它可以是一个整数,也可以是其他的整数。整数使用可变长度编码(与 Thrift 的 CompactProtocol 相同)进行编码。
|
||||
|
||||
![](img/fig4-5.png)
|
||||
|
||||
@ -235,7 +235,7 @@ record Person {
|
||||
|
||||
当一个应用程序想要解码一些数据(从一个文件或数据库读取数据、从网络接收数据等)时,它希望数据在某个模式中,这就是 Reader 模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能已经从该模式生成。
|
||||
|
||||
Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他们只需要兼容。当数据解码(读取)时,Avro 库通过并排查看 Writer 模式和 Reader 模式并将数据从 Writer 模式转换到 Reader 模式来解决差异。 Avro 规范【20】确切地定义了这种解析的工作原理,如 [图 4-6](img/fig4-6.png) 所示。
|
||||
Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他们只需要兼容。当数据解码(读取)时,Avro 库通过并排查看 Writer 模式和 Reader 模式并将数据从 Writer 模式转换到 Reader 模式来解决差异。Avro 规范【20】确切地定义了这种解析的工作原理,如 [图 4-6](img/fig4-6.png) 所示。
|
||||
|
||||
例如,如果 Writer 模式和 Reader 模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在 Writer 模式中但不在 Reader 模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是 Writer 模式不包含该名称的字段,则使用在 Reader 模式中声明的默认值填充。
|
||||
|
||||
@ -265,7 +265,7 @@ Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他
|
||||
|
||||
* 有很多记录的大文件
|
||||
|
||||
Avro 的一个常见用途 - 尤其是在 Hadoop 环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码(我们将在 [第十章](ch10.md) 讨论这种情况)。在这种情况下,该文件的作者可以在文件的开头只包含一次 Writer 模式。 Avro 指定了一个文件格式(对象容器文件)来做到这一点。
|
||||
Avro 的一个常见用途 - 尤其是在 Hadoop 环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码(我们将在 [第十章](ch10.md) 讨论这种情况)。在这种情况下,该文件的作者可以在文件的开头只包含一次 Writer 模式。Avro 指定了一个文件格式(对象容器文件)来做到这一点。
|
||||
|
||||
* 支持独立写入的记录的数据库
|
||||
|
||||
@ -273,7 +273,7 @@ Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他
|
||||
|
||||
* 通过网络连接发送记录
|
||||
|
||||
当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC 协议(请参阅 “[服务中的数据流:REST 与 RPC](#服务中的数据流:REST与RPC)”)就是这样工作的。
|
||||
当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。Avro RPC 协议(请参阅 “[服务中的数据流:REST 与 RPC](#服务中的数据流:REST与RPC)”)就是这样工作的。
|
||||
|
||||
具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为你提供了检查模式兼容性的机会【24】。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。
|
||||
|
||||
@ -301,7 +301,7 @@ Avro 为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
正如我们所看到的,Protocol Buffers、Thrift 和 Avro 都使用模式来描述二进制编码格式。他们的模式语言比 XML 模式或者 JSON 模式简单得多,而后者支持更详细的验证规则(例如,“该字段的字符串值必须与该正则表达式匹配” 或 “该字段的整数值必须在 0 和 100 之间” )。由于 Protocol Buffers,Thrift 和 Avro 实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。
|
||||
|
||||
这些编码所基于的想法绝不是新的。例如,它们与 ASN.1 有很多相似之处,它是 1984 年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议,例如其二进制编码(DER)仍然被用于编码 SSL 证书(X.509)【28】。 ASN.1 支持使用标签号码的模式演进,类似于 Protocol Buffers 和 Thrift 【29】。然而,它也非常复杂,而且没有好的配套文档,所以 ASN.1 可能不是新应用程序的好选择。
|
||||
这些编码所基于的想法绝不是新的。例如,它们与 ASN.1 有很多相似之处,它是 1984 年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议,例如其二进制编码(DER)仍然被用于编码 SSL 证书(X.509)【28】。ASN.1 支持使用标签号码的模式演进,类似于 Protocol Buffers 和 Thrift 【29】。然而,它也非常复杂,而且没有好的配套文档,所以 ASN.1 可能不是新应用程序的好选择。
|
||||
|
||||
许多数据系统也为其数据实现了某种专有的二进制编码。例如,大多数关系数据库都有一个网络协议,你可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库,并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序(例如使用 ODBC 或 JDBC API)。
|
||||
|
||||
@ -352,7 +352,7 @@ Avro 为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
在部署应用程序的新版本时,也许用不了几分钟就可以将所有的旧版本替换为新版本(至少服务器端应用程序是这样的)。但数据库内容并非如此:对于五年前的数据来说,除非对其进行显式重写,否则它仍然会以原始编码形式存在。这种现象有时被概括为:数据的生命周期超出代码的生命周期。
|
||||
|
||||
将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据 [^v]。读取旧行时,对于磁盘上的编码数据缺少的任何列,数据库将填充空值。 LinkedIn 的文档数据库 Espresso 使用 Avro 存储,允许它使用 Avro 的模式演变规则【23】。
|
||||
将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据 [^v]。读取旧行时,对于磁盘上的编码数据缺少的任何列,数据库将填充空值。LinkedIn 的文档数据库 Espresso 使用 Avro 存储,允许它使用 Avro 的模式演变规则【23】。
|
||||
|
||||
因此,模式演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用各种历史版本的模式编码的记录。
|
||||
|
||||
@ -372,7 +372,7 @@ Avro 为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
当你需要通过网络进行进程间的通讯时,安排该通信的方式有几种。最常见的安排是有两个角色:客户端和服务器。服务器通过网络公开 API,并且客户端可以连接到服务器以向该 API 发出请求。服务器公开的 API 被称为服务。
|
||||
|
||||
Web 以这种方式工作:客户(Web 浏览器)向 Web 服务器发出请求,通过 GET 请求下载 HTML、CSS、JavaScript、图像等,并通过 POST 请求提交数据到服务器。 API 包含一组标准的协议和数据格式(HTTP、URL、SSL/TLS、HTML 等)。由于网络浏览器、网络服务器和网站作者大多同意这些标准,你可以使用任何网络浏览器访问任何网站(至少在理论上!)。
|
||||
Web 以这种方式工作:客户(Web 浏览器)向 Web 服务器发出请求,通过 GET 请求下载 HTML、CSS、JavaScript、图像等,并通过 POST 请求提交数据到服务器。API 包含一组标准的协议和数据格式(HTTP、URL、SSL/TLS、HTML 等)。由于网络浏览器、网络服务器和网站作者大多同意这些标准,你可以使用任何网络浏览器访问任何网站(至少在理论上!)。
|
||||
|
||||
Web 浏览器不是唯一的客户端类型。例如,在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求,并且在 Web 浏览器内运行的客户端 JavaScript 应用程序可以使用 XMLHttpRequest 成为 HTTP 客户端(该技术被称为 Ajax 【30】)。在这种情况下,服务器的响应通常不是用于显示给人的 HTML,而是便于客户端应用程序代码进一步处理的编码数据(如 JSON)。尽管 HTTP 可能被用作传输协议,但顶层实现的 API 是特定于应用程序的,客户端和服务器需要就该 API 的细节达成一致。
|
||||
|
||||
@ -387,20 +387,20 @@ Web 浏览器不是唯一的客户端类型。例如,在移动设备或桌面
|
||||
**当服务使用 HTTP 作为底层通信协议时,可称之为 Web 服务**。这可能是一个小错误,因为 Web 服务不仅在 Web 上使用,而且在几个不同的环境中使用。例如:
|
||||
|
||||
1. 运行在用户设备上的客户端应用程序(例如,移动设备上的本地应用程序,或使用 Ajax 的 JavaScript web 应用程序)通过 HTTP 向服务发出请求。这些请求通常通过公共互联网进行。
|
||||
2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务 / 微服务架构的一部分。 (支持这种用例的软件有时被称为 **中间件(middleware)** )
|
||||
2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务 / 微服务架构的一部分。(支持这种用例的软件有时被称为 **中间件(middleware)** )
|
||||
3. 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务(如信用卡处理系统)提供的公共 API,或用于共享访问用户数据的 OAuth。
|
||||
|
||||
有两种流行的 Web 服务方法:REST 和 SOAP。他们在哲学方面几乎是截然相反的,往往也是各自支持者之间的激烈辩论的主题 [^vi]。
|
||||
|
||||
[^vi]: 即使在每个阵营内也有很多争论。 例如,**HATEOAS(超媒体作为应用程序状态的引擎)** 就经常引发讨论【35】。
|
||||
[^vi]: 即使在每个阵营内也有很多争论。例如,**HATEOAS(超媒体作为应用程序状态的引擎)** 就经常引发讨论【35】。
|
||||
|
||||
REST 不是一个协议,而是一个基于 HTTP 原则的设计哲学【34,35】。它强调简单的数据格式,使用 URL 来标识资源,并使用 HTTP 功能进行缓存控制,身份验证和内容类型协商。与 SOAP 相比,REST 已经越来越受欢迎,至少在跨组织服务集成的背景下【36】,并经常与微服务相关【31】。根据 REST 原则设计的 API 称为 RESTful。
|
||||
|
||||
相比之下,SOAP 是用于制作网络 API 请求的基于 XML 的协议 [^vii]。虽然它最常用于 HTTP,但其目的是独立于 HTTP,并避免使用大多数 HTTP 功能。相反,它带有庞大而复杂的多种相关标准(Web 服务框架,称为 `WS-*`),它们增加了各种功能【37】。
|
||||
|
||||
[^vii]: 尽管首字母缩写词相似,SOAP 并不是 SOA 的要求。 SOAP 是一种特殊的技术,而 SOA 是构建系统的一般方法。
|
||||
[^vii]: 尽管首字母缩写词相似,SOAP 并不是 SOA 的要求。SOAP 是一种特殊的技术,而 SOA 是构建系统的一般方法。
|
||||
|
||||
SOAP Web 服务的 API 使用称为 Web 服务描述语言(WSDL)的基于 XML 的语言来描述。 WSDL 支持代码生成,客户端可以使用本地类和方法调用(编码为 XML 消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少(请参阅 “[代码生成和动态类型的语言](#代码生成和动态类型的语言)”)。
|
||||
SOAP Web 服务的 API 使用称为 Web 服务描述语言(WSDL)的基于 XML 的语言来描述。WSDL 支持代码生成,客户端可以使用本地类和方法调用(编码为 XML 消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少(请参阅 “[代码生成和动态类型的语言](#代码生成和动态类型的语言)”)。
|
||||
|
||||
由于 WSDL 的设计不是人类可读的,而且由于 SOAP 消息通常因为过于复杂而无法手动构建,所以 SOAP 的用户在很大程度上依赖于工具支持,代码生成和 IDE【38】。对于 SOAP 供应商不支持的编程语言的用户来说,与 SOAP 服务的集成是困难的。
|
||||
|
||||
@ -410,30 +410,30 @@ REST 风格的 API 倾向于更简单的方法,通常涉及较少的代码生
|
||||
|
||||
#### 远程过程调用(RPC)的问题
|
||||
|
||||
Web 服务仅仅是通过网络进行 API 请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。 Enterprise JavaBeans(EJB)和 Java 的 **远程方法调用(RMI)** 仅限于 Java。**分布式组件对象模型(DCOM)** 仅限于 Microsoft 平台。**公共对象请求代理体系结构(CORBA)** 过于复杂,不提供向后或向前兼容性【41】。
|
||||
Web 服务仅仅是通过网络进行 API 请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。Enterprise JavaBeans(EJB)和 Java 的 **远程方法调用(RMI)** 仅限于 Java。**分布式组件对象模型(DCOM)** 仅限于 Microsoft 平台。**公共对象请求代理体系结构(CORBA)** 过于复杂,不提供向后或向前兼容性【41】。
|
||||
|
||||
所有这些都是基于 **远程过程调用(RPC)** 的思想,该过程调用自 20 世纪 70 年代以来一直存在【42】。 RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管 RPC 起初看起来很方便,但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同:
|
||||
所有这些都是基于 **远程过程调用(RPC)** 的思想,该过程调用自 20 世纪 70 年代以来一直存在【42】。RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管 RPC 起初看起来很方便,但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同:
|
||||
|
||||
* 本地函数调用是可预测的,并且成功或失败仅取决于受你控制的参数。网络请求是不可预测的:请求或响应可能由于网络问题会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在你的控制范围之内。网络问题很常见,因此必须有所准备,例如重试失败的请求。
|
||||
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它返回时可能没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过(我们将在 [第八章](ch8.md) 更详细地讨论这个问题)。
|
||||
* 如果你重试失败的网络请求,可能会发生请求实际上已经完成,只是响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非你在协议中建立数据去重机制(**幂等性**,即 idempotence)。本地函数调用时没有这样的问题。 (在 [第十一章](ch11.md) 更详细地讨论幂等性)
|
||||
* 如果你重试失败的网络请求,可能会发生请求实际上已经完成,只是响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非你在协议中建立数据去重机制(**幂等性**,即 idempotence)。本地函数调用时没有这样的问题。(在 [第十一章](ch11.md) 更详细地讨论幂等性)
|
||||
* 每次调用本地函数时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:好的时候它可能会在不到一毫秒的时间内完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间才能完成相同的操作。
|
||||
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会出现问题。
|
||||
* 客户端和服务可以用不同的编程语言实现,所以 RPC 框架必须将数据类型从一种语言翻译成另一种语言。这可能会变得很丑陋,因为不是所有的语言都具有相同的类型 —— 例如回想一下 JavaScript 的数字大于 $2^{53}$ 的问题(请参阅 “[JSON、XML 和二进制变体](#JSON、XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
|
||||
|
||||
所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST 的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在 REST 之上构建 RPC 库)。
|
||||
所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。REST 的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在 REST 之上构建 RPC 库)。
|
||||
|
||||
#### RPC的当前方向
|
||||
|
||||
尽管有这样那样的问题,RPC 不会消失。在本章提到的所有编码的基础上构建了各种 RPC 框架:例如,Thrift 和 Avro 带有 RPC 支持,gRPC 是使用 Protocol Buffers 的 RPC 实现,Finagle 也使用 Thrift,Rest.li 使用 JSON over HTTP。
|
||||
|
||||
这种新一代的 RPC 框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle 和 Rest.li 使用 futures(promises)来封装可能失败的异步操作。`Futures` 还可以简化需要并行发出多项服务并将其结果合并的情况【45】。 gRPC 支持流,其中一个调用不仅包括一个请求和一个响应,还可以是随时间的一系列请求和响应【46】。
|
||||
这种新一代的 RPC 框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle 和 Rest.li 使用 futures(promises)来封装可能失败的异步操作。`Futures` 还可以简化需要并行发出多项服务并将其结果合并的情况【45】。gRPC 支持流,其中一个调用不仅包括一个请求和一个响应,还可以是随时间的一系列请求和响应【46】。
|
||||
|
||||
其中一些框架还提供服务发现,即允许客户端找出在哪个 IP 地址和端口号上可以找到特定的服务。我们将在 “[请求路由](ch6.md#请求路由)” 中回到这个主题。
|
||||
|
||||
使用二进制编码格式的自定义 RPC 协议可以实现比通用的 JSON over REST 更好的性能。但是,RESTful API 还有其他一些显著的优点:方便实验和调试(只需使用 Web 浏览器或命令行工具 curl,无需任何代码生成或软件安装即可向其请求),能被所有主流的编程语言和平台所支持,还有大量可用的工具(服务器、缓存、负载平衡器、代理、防火墙、监控、调试工具、测试工具等)的生态系统。
|
||||
|
||||
由于这些原因,REST 似乎是公共 API 的主要风格。 RPC 框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。
|
||||
由于这些原因,REST 似乎是公共 API 的主要风格。RPC 框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。
|
||||
|
||||
#### 数据编码与RPC的演化
|
||||
|
||||
@ -489,9 +489,9 @@ Actor 模型是单个进程中并发的编程模型。逻辑被封装在 actor
|
||||
|
||||
三个流行的分布式 actor 框架处理消息编码如下:
|
||||
|
||||
* 默认情况下,Akka 使用 Java 的内置序列化,不提供向前或向后兼容性。 但是,你可以用类似 Prototol Buffers 的东西替代它,从而获得滚动升级的能力【50】。
|
||||
* Orleans 默认使用不支持滚动升级部署的自定义数据编码格式;要部署新版本的应用程序,你需要设置一个新的集群,将流量从旧集群迁移到新集群,然后关闭旧集群【51,52】。 像 Akka 一样,可以使用自定义序列化插件。
|
||||
* 在 Erlang OTP 中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的,但需要仔细计划【53】。 一个新的实验性的 `maps` 数据类型(2014 年在 Erlang R17 中引入的类似于 JSON 的结构)可能使得这个数据类型在未来更容易【54】。
|
||||
* 默认情况下,Akka 使用 Java 的内置序列化,不提供向前或向后兼容性。但是,你可以用类似 Prototol Buffers 的东西替代它,从而获得滚动升级的能力【50】。
|
||||
* Orleans 默认使用不支持滚动升级部署的自定义数据编码格式;要部署新版本的应用程序,你需要设置一个新的集群,将流量从旧集群迁移到新集群,然后关闭旧集群【51,52】。像 Akka 一样,可以使用自定义序列化插件。
|
||||
* 在 Erlang OTP 中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。滚动升级是可能的,但需要仔细计划【53】。一个新的实验性的 `maps` 数据类型(2014 年在 Erlang R17 中引入的类似于 JSON 的结构)可能使得这个数据类型在未来更容易【54】。
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
22
ch5.md
22
ch5.md
@ -40,7 +40,7 @@
|
||||
|
||||
**图 5-1 基于领导者的(主/从)复制**
|
||||
|
||||
这种复制模式是许多关系数据库的内置功能,如 PostgreSQL(从 9.0 版本开始)、MySQL、Oracle Data Guard【2】和 SQL Server 的 AlwaysOn 可用性组【3】。 它也被用于一些非关系数据库,包括 MongoDB、RethinkDB 和 Espresso【4】。最后,基于领导者的复制并不仅限于数据库:像 Kafka【5】和 RabbitMQ 高可用队列【6】这样的分布式消息代理也使用它。某些网络文件系统,例如 DRBD 这样的块复制设备也与之类似。
|
||||
这种复制模式是许多关系数据库的内置功能,如 PostgreSQL(从 9.0 版本开始)、MySQL、Oracle Data Guard【2】和 SQL Server 的 AlwaysOn 可用性组【3】。它也被用于一些非关系数据库,包括 MongoDB、RethinkDB 和 Espresso【4】。最后,基于领导者的复制并不仅限于数据库:像 Kafka【5】和 RabbitMQ 高可用队列【6】这样的分布式消息代理也使用它。某些网络文件系统,例如 DRBD 这样的块复制设备也与之类似。
|
||||
|
||||
### 同步复制与异步复制
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
|
||||
同步复制的优点是,从库能保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
|
||||
|
||||
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中 **一个** 从库是同步的,而其他的从库则是异步的。如果该同步从库变得不可用或缓慢,则将一个异步从库改为同步运行。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为 **半同步(semi-synchronous)**【7】。
|
||||
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中 **一个** 从库是同步的,而其他的从库则是异步的。如果该同步从库变得不可用或缓慢,则将一个异步从库改为同步运行。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。这种配置有时也被称为 **半同步(semi-synchronous)**【7】。
|
||||
|
||||
通常情况下,基于领导者的复制都配置为完全异步。在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。这意味着即使已经向客户端确认成功,写入也不能保证是 **持久(Durable)** 的。然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
|
||||
的确有办法绕开这些问题 —— 例如,当语句被记录时,主库可以用固定的返回值替换掉任何不确定的函数调用,以便所有从库都能获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
|
||||
|
||||
基于语句的复制在 5.1 版本前的 MySQL 中被使用到。因为它相当紧凑,现在有时候也还在用。但现在在默认情况下,如果语句中存在任何不确定性,MySQL 会切换到基于行的复制(稍后讨论)。 VoltDB 使用了基于语句的复制,但要求事务必须是确定性的,以此来保证安全【15】。
|
||||
基于语句的复制在 5.1 版本前的 MySQL 中被使用到。因为它相当紧凑,现在有时候也还在用。但现在在默认情况下,如果语句中存在任何不确定性,MySQL 会切换到基于行的复制(稍后讨论)。VoltDB 使用了基于语句的复制,但要求事务必须是确定性的,以此来保证安全【15】。
|
||||
|
||||
#### 传输预写式日志(WAL)
|
||||
|
||||
@ -167,7 +167,7 @@
|
||||
* 对于删除的行,日志包含足够的信息来唯一标识被删除的行,这通常是主键,但如果表上没有主键,则需要记录所有列的旧值。
|
||||
* 对于更新的行,日志包含足够的信息来唯一标识被更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
|
||||
|
||||
修改多行的事务会生成多条这样的日志记录,后面跟着一条指明事务已经提交的记录。 MySQL 的二进制日志(当配置为使用基于行的复制时)使用了这种方法【17】。
|
||||
修改多行的事务会生成多条这样的日志记录,后面跟着一条指明事务已经提交的记录。MySQL 的二进制日志(当配置为使用基于行的复制时)使用了这种方法【17】。
|
||||
|
||||
由于逻辑日志与存储引擎的内部实现是解耦的,系统可以更容易地做到向后兼容,从而使主库和从库能够运行不同版本的数据库软件,或者甚至不同的存储引擎。
|
||||
|
||||
@ -247,7 +247,7 @@
|
||||
|
||||
### 一致前缀读
|
||||
|
||||
第三个复制延迟异常的例子违反了因果律。 想象一下 Poons 先生和 Cake 夫人之间的以下简短对话:
|
||||
第三个复制延迟异常的例子违反了因果律。想象一下 Poons 先生和 Cake 夫人之间的以下简短对话:
|
||||
|
||||
*Mr. Poons*
|
||||
> Mrs. Cake,你能看到多远的未来?
|
||||
@ -257,7 +257,7 @@
|
||||
|
||||
这两句话之间有因果关系:Cake 夫人听到了 Poons 先生的问题并回答了这个问题。
|
||||
|
||||
现在,想象第三个人正在通过从库来听这个对话。 Cake 夫人说的内容是从一个延迟很低的从库读取的,但 Poons 先生所说的内容,从库的延迟要大的多(见 [图 5-5](img/fig5-5.png))。于是,这个观察者会听到以下内容:
|
||||
现在,想象第三个人正在通过从库来听这个对话。Cake 夫人说的内容是从一个延迟很低的从库读取的,但 Poons 先生所说的内容,从库的延迟要大的多(见 [图 5-5](img/fig5-5.png))。于是,这个观察者会听到以下内容:
|
||||
|
||||
*Mrs. Cake*
|
||||
> 通常约十秒钟,Mr. Poons.
|
||||
@ -292,7 +292,7 @@
|
||||
|
||||
本章到目前为止,我们只考虑了使用单个主库的复制架构。虽然这是一种常见的方法,但还有其它一些有趣的选择。
|
||||
|
||||
基于领导者的复制有一个主要的缺点:只有一个主库,而且所有的写入都必须通过它 [^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
|
||||
基于领导者的复制有一个主要的缺点:只有一个主库,而且所有的写入都必须通过它 [^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库,就无法向数据库写入。
|
||||
|
||||
[^iv]: 如果数据库被分区(见 [第六章](ch6.md)),每个分区都有一个主库。不同的分区的主库可能在不同的节点上,但是每个分区都必须有一个主库。
|
||||
|
||||
@ -506,7 +506,7 @@
|
||||
|
||||
如果我们知道,每个成功的写操作意味着在三个副本中至少有两个出现,这意味着至多有一个副本可能是陈旧的。因此,如果我们从至少两个副本读取,我们可以确定至少有一个是最新的。如果第三个副本停机或响应速度缓慢,则读取仍可以继续返回最新值。
|
||||
|
||||
更一般地说,如果有 n 个副本,每个写入必须由 w 个节点确认才能被认为是成功的,并且我们必须至少为每个读取查询 r 个节点。 (在我们的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r > n$,我们可以预期在读取时能获得最新的值,因为 r 个读取中至少有一个节点是最新的。遵循这些 r 值和 w 值的读写称为 **法定人数(quorum)**[^vii] 的读和写【44】。你可以认为,r 和 w 是有效读写所需的最低票数。
|
||||
更一般地说,如果有 n 个副本,每个写入必须由 w 个节点确认才能被认为是成功的,并且我们必须至少为每个读取查询 r 个节点。(在我们的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r > n$,我们可以预期在读取时能获得最新的值,因为 r 个读取中至少有一个节点是最新的。遵循这些 r 值和 w 值的读写称为 **法定人数(quorum)**[^vii] 的读和写【44】。你可以认为,r 和 w 是有效读写所需的最低票数。
|
||||
|
||||
[^vii]: 有时候这种法定人数被称为严格的法定人数,其相对 “宽松的法定人数” 而言(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”)
|
||||
|
||||
@ -519,7 +519,7 @@
|
||||
* 如果 $w < n$,当节点不可用时,我们仍然可以处理写入。
|
||||
* 如果 $r < n$,当节点不可用时,我们仍然可以处理读取。
|
||||
* 对于 $n = 3,w = 2,r = 2$,我们可以容忍一个不可用的节点。
|
||||
* 对于 $n = 5,w = 3,r = 3$,我们可以容忍两个不可用的节点。 这个案例如 [图 5-11](img/fig5-11.png) 所示。
|
||||
* 对于 $n = 5,w = 3,r = 3$,我们可以容忍两个不可用的节点。这个案例如 [图 5-11](img/fig5-11.png) 所示。
|
||||
* 通常,读取和写入操作始终并行发送到所有 n 个副本。参数 w 和 r 决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。
|
||||
|
||||
![](img/fig5-11.png)
|
||||
@ -548,7 +548,7 @@
|
||||
* 如果携带新值的节点发生故障,需要从其他带有旧值的副本进行恢复,则存储新值的副本数可能会低于 w,从而打破法定人数条件。
|
||||
* 即使一切工作正常,有时也会不幸地出现关于 **时序(timing)** 的边缘情况,我们将在 “[线性一致性和法定人数](ch9.md#线性一致性和法定人数)” 中看到这点。
|
||||
|
||||
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo 风格的数据库通常针对可以忍受最终一致性的用例进行优化。你可以通过参数 w 和 r 来调整读取到陈旧值的概率,但把它们当成绝对的保证是不明智的。
|
||||
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。Dynamo 风格的数据库通常针对可以忍受最终一致性的用例进行优化。你可以通过参数 w 和 r 来调整读取到陈旧值的概率,但把它们当成绝对的保证是不明智的。
|
||||
|
||||
尤其是,因为通常得不到 “[复制延迟问题](#复制延迟问题)” 中讨论的那些保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要 **事务** 或 **共识**。我们将在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到这些话题。
|
||||
|
||||
@ -679,7 +679,7 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
|
||||
|
||||
#### 合并并发写入的值
|
||||
|
||||
这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:客户端随后必须合并并发写入的值。 Riak 称这些并发值为 **兄弟(siblings)**。
|
||||
这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:客户端随后必须合并并发写入的值。Riak 称这些并发值为 **兄弟(siblings)**。
|
||||
|
||||
合并并发值,本质上是与多主复制中的冲突解决问题相同,我们先前讨论过(请参阅 “[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)来选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中额外做些更聪明的事情。
|
||||
|
||||
|
34
ch6.md
34
ch6.md
@ -2,7 +2,7 @@
|
||||
|
||||
![](img/ch6.png)
|
||||
|
||||
> 我们必须跳出电脑指令序列的窠臼。 叙述定义、描述元数据、梳理关系,而不是编写过程。
|
||||
> 我们必须跳出电脑指令序列的窠臼。叙述定义、描述元数据、梳理关系,而不是编写过程。
|
||||
>
|
||||
> —— Grace Murray Hopper,未来的计算机及其管理(1962)
|
||||
>
|
||||
@ -34,7 +34,7 @@
|
||||
|
||||
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
|
||||
|
||||
一个节点可能存储多个分区。如果使用主从复制模型,则分区和复制的组合如 [图 6-1](img/fig6-1.png) 所示。每个分区领导者(主库)被分配给一个节点,追随者(从库)被分配给其他节点。 每个节点可能是某些分区的主库,同时是其他分区的从库。
|
||||
一个节点可能存储多个分区。如果使用主从复制模型,则分区和复制的组合如 [图 6-1](img/fig6-1.png) 所示。每个分区领导者(主库)被分配给一个节点,追随者(从库)被分配给其他节点。每个节点可能是某些分区的主库,同时是其他分区的从库。
|
||||
|
||||
我们在 [第五章](ch5.md) 讨论的关于数据库复制的所有内容同样适用于分区的复制。大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
|
||||
|
||||
@ -64,13 +64,13 @@
|
||||
|
||||
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在 [图 6-2](img/fig6-2.png) 中,第 1 卷包含以 A 和 B 开头的单词,但第 12 卷则包含以 T、U、V、X、Y 和 Z 开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据,分区边界需要依据数据调整。
|
||||
|
||||
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在 “[分区再平衡](#分区再平衡)” 中更详细地讨论分区边界的选择)。 Bigtable 使用了这种分区策略,以及其开源等价物 HBase 【2, 3】,RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
|
||||
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在 “[分区再平衡](#分区再平衡)” 中更详细地讨论分区边界的选择)。Bigtable 使用了这种分区策略,以及其开源等价物 HBase 【2, 3】、RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
|
||||
|
||||
在每个分区中,我们可以按照一定的顺序保存键(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”)。好处是进行范围扫描非常简单,你可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(请参阅 “[多列索引](ch3.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
|
||||
|
||||
然而,Key Range 分区的缺点是某些特定的访问模式会导致热点。 如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态【5】。
|
||||
然而,Key Range 分区的缺点是某些特定的访问模式会导致热点。如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态【5】。
|
||||
|
||||
为了避免传感器数据库中的这个问题,需要使用除了时间戳以外的其他东西作为主键的第一个部分。 例如,可以在每个时间戳前添加传感器名称,这样会首先按传感器名称,然后按时间进行分区。 假设有多个传感器同时运行,写入负载将最终均匀分布在不同分区上。 现在,当想要在一个时间范围内获取多个传感器的值时,你需要为每个传感器名称执行一个单独的范围查询。
|
||||
为了避免传感器数据库中的这个问题,需要使用除了时间戳以外的其他东西作为主键的第一个部分。例如,可以在每个时间戳前添加传感器名称,这样会首先按传感器名称,然后按时间进行分区。假设有多个传感器同时运行,写入负载将最终均匀分布在不同分区上。现在,当想要在一个时间范围内获取多个传感器的值时,你需要为每个传感器名称执行一个单独的范围查询。
|
||||
|
||||
### 根据键的散列分区
|
||||
|
||||
@ -90,13 +90,13 @@
|
||||
|
||||
> #### 一致性哈希
|
||||
>
|
||||
> 一致性哈希由 Karger 等人定义。【7】 用于跨互联网级别的缓存系统,例如 CDN 中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界(partition boundaries)** 来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅 [第五章](ch5.md))或 ACID 一致性(请参阅 [第七章](ch7.md))无关,而只是描述了一种再平衡(rebalancing)的特定方法。
|
||||
> 一致性哈希由 Karger 等人定义。【7】 用于跨互联网级别的缓存系统,例如 CDN 中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界(partition boundaries)** 来避免中央控制或分布式共识的需要。请注意,这里的一致性与复制一致性(请参阅 [第五章](ch5.md))或 ACID 一致性(请参阅 [第七章](ch7.md))无关,而只是描述了一种再平衡(rebalancing)的特定方法。
|
||||
>
|
||||
> 正如我们将在 “[分区再平衡](#分区再平衡)” 中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然会使用一致性哈希的说法,但是它往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为 **散列分区(hash partitioning)**。
|
||||
> 正如我们将在 “[分区再平衡](#分区再平衡)” 中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然会使用一致性哈希的说法,但是它往往是不准确的)。因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为 **散列分区(hash partitioning)**。
|
||||
|
||||
不幸的是,通过使用键散列进行分区,我们失去了键范围分区的一个很好的属性:高效执行范围查询的能力。曾经相邻的键现在分散在所有分区中,所以它们之间的顺序就丢失了。在 MongoDB 中,如果你使用了基于散列的分区模式,则任何范围查询都必须发送到所有分区【4】。Riak【9】、Couchbase 【10】或 Voldemort 不支持主键上的范围查询。
|
||||
|
||||
Cassandra 采取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据,而其他列则被用作 Casssandra 的 SSTables 中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表,但如果第一列已经指定了固定值,则可以对该键的其他列执行有效的范围扫描。
|
||||
Cassandra 采取了折衷的策略【11, 12, 13】。Cassandra 中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据,而其他列则被用作 Casssandra 的 SSTables 中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表,但如果第一列已经指定了固定值,则可以对该键的其他列执行有效的范围扫描。
|
||||
|
||||
组合索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为 `(user_id, update_timestamp)`,那么你可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,对于每个用户,更新按时间戳顺序存储在单个分区上。
|
||||
|
||||
@ -126,11 +126,11 @@ Cassandra 采取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以
|
||||
|
||||
### 基于文档的次级索引进行分区
|
||||
|
||||
假设你正在经营一个销售二手车的网站(如 [图 6-4](img/fig6-4.png) 所示)。 每个列表都有一个唯一的 ID—— 称之为文档 ID—— 并且用文档 ID 对数据库进行分区(例如,分区 0 中的 ID 0 到 499,分区 1 中的 ID 500 到 999 等)。
|
||||
假设你正在经营一个销售二手车的网站(如 [图 6-4](img/fig6-4.png) 所示)。每个列表都有一个唯一的 ID—— 称之为文档 ID—— 并且用文档 ID 对数据库进行分区(例如,分区 0 中的 ID 0 到 499,分区 1 中的 ID 500 到 999 等)。
|
||||
|
||||
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是 **字段(field)**,关系数据库中这些是 **列(column)** )。 如果你声明了索引,则数据库可以自动执行索引 [^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目 `color:red` 的文档 ID 列表中。
|
||||
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是 **字段(field)**,关系数据库中这些是 **列(column)** )。如果你声明了索引,则数据库可以自动执行索引 [^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目 `color:red` 的文档 ID 列表中。
|
||||
|
||||
[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档 ID 的映射来实现次级索引。 如果沿着这条路线走下去,请万分小心,确保你的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参阅 “[多对象事务的需求](ch7.md#多对象事务的需求)”。
|
||||
[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档 ID 的映射来实现次级索引。如果沿着这条路线走下去,请万分小心,确保你的索引与底层数据保持一致。竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参阅 “[多对象事务的需求](ch7.md#多对象事务的需求)”。
|
||||
|
||||
![](img/fig6-4.png)
|
||||
|
||||
@ -174,7 +174,7 @@ Cassandra 采取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以
|
||||
* 数据集大小增加,所以你想添加更多的磁盘和 RAM 来存储它。
|
||||
* 机器出现故障,其他机器需要接管故障机器的责任。
|
||||
|
||||
所有这些更改都需要数据和请求从一个节点移动到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为 **再平衡(rebalancing)**。
|
||||
所有这些更改都需要数据和请求从一个节点移动到另一个节点。将负载从集群中的一个节点向另一个节点移动的过程称为 **再平衡(rebalancing)**。
|
||||
|
||||
无论使用哪种分区方案,再平衡通常都要满足一些最低要求:
|
||||
|
||||
@ -235,7 +235,7 @@ Cassandra 采取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以
|
||||
|
||||
Cassandra 和 Ketama 使用的第三种方法是使分区数与节点数成正比 —— 换句话说,每个节点具有固定数量的分区【23,27,28】。在这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小较为稳定。
|
||||
|
||||
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上时(在 Cassandra 中,默认情况下,每个节点有 256 个分区),新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0 引入了另一种再平衡的算法来避免不公平的分割【29】。
|
||||
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上时(在 Cassandra 中,默认情况下,每个节点有 256 个分区),新节点最终从现有节点获得公平的负载份额。Cassandra 3.0 引入了另一种再平衡的算法来避免不公平的分割【29】。
|
||||
|
||||
随机选择分区边界要求使用基于散列的分区(可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性哈希的原始定义【7】(请参阅 “[一致性哈希](#一致性哈希)”)。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
|
||||
|
||||
@ -269,15 +269,15 @@ Cassandra 和 Ketama 使用的第三种方法是使分区数与节点数成正
|
||||
|
||||
**图 6-7 将请求路由到正确节点的三种不同方式。**
|
||||
|
||||
这是一个具有挑战性的问题,因为重要的是所有参与者都达成共识 - 否则请求将被发送到错误的节点,得不到正确的处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见 [第九章](ch9.md))。
|
||||
这是一个具有挑战性的问题,因为重要的是所有参与者都达成共识 - 否则请求将被发送到错误的节点,得不到正确的处理。在分布式系统中有达成共识的协议,但很难正确地实现(见 [第九章](ch9.md))。
|
||||
|
||||
许多分布式数据系统都依赖于一个独立的协调服务,比如 ZooKeeper 来跟踪集群元数据,如 [图 6-8](img/fig6-8.png) 所示。 每个节点在 ZooKeeper 中注册自己,ZooKeeper 维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在 ZooKeeper 中订阅此信息。 只要分区分配发生了改变,或者集群中添加或删除了一个节点,ZooKeeper 就会通知路由层使路由信息保持最新状态。
|
||||
许多分布式数据系统都依赖于一个独立的协调服务,比如 ZooKeeper 来跟踪集群元数据,如 [图 6-8](img/fig6-8.png) 所示。每个节点在 ZooKeeper 中注册自己,ZooKeeper 维护分区到节点的可靠映射。其他参与者(如路由层或分区感知客户端)可以在 ZooKeeper 中订阅此信息。只要分区分配发生了改变,或者集群中添加或删除了一个节点,ZooKeeper 就会通知路由层使路由信息保持最新状态。
|
||||
|
||||
![](img/fig6-8.png)
|
||||
|
||||
**图 6-8 使用 ZooKeeper 跟踪分区分配给节点。**
|
||||
|
||||
例如,LinkedIn的Espresso使用Helix 【31】进行集群管理(依靠ZooKeeper),实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBase、SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。MongoDB具有类似的体系结构,但它依赖于自己的**配置服务器(config server)** 实现和mongos守护进程作为路由层。
|
||||
例如,LinkedIn的Espresso使用Helix 【31】进行集群管理(依靠ZooKeeper),实现了如[图6-8](img/fig6-8.png)所示的路由层。HBase、SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。MongoDB具有类似的体系结构,但它依赖于自己的**配置服务器(config server)** 实现和mongos守护进程作为路由层。
|
||||
|
||||
Cassandra 和 Riak 采取不同的方法:他们在节点之间使用 **流言协议(gossip protocol)** 来传播集群状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图 6-7](img/fig6-7.png) 中的方法 1)。这个模型在数据库节点中增加了更多的复杂性,但是避免了对像 ZooKeeper 这样的外部协调服务的依赖。
|
||||
|
||||
@ -289,7 +289,7 @@ Couchbase 不会自动进行再平衡,这简化了设计。通常情况下,
|
||||
|
||||
到目前为止,我们只关注读取或写入单个键的非常简单的查询(加上基于文档分区的次级索引场景下的分散 / 聚集查询)。这也是大多数 NoSQL 分布式数据存储所支持的访问层级。
|
||||
|
||||
然而,通常用于分析的 **大规模并行处理(MPP, Massively parallel processing)** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP 查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
|
||||
然而,通常用于分析的 **大规模并行处理(MPP, Massively parallel processing)** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。MPP 查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
|
||||
|
||||
数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在 [第十章](ch10.md) 讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述,请参阅参考文献【1,33】。
|
||||
|
||||
|
20
ch7.md
20
ch7.md
@ -176,7 +176,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
一些数据库也提供更复杂的原子操作 [^iv],例如自增操作,这样就不再需要像 [图 7-1](img/fig7-1.png) 那样的读取 - 修改 - 写入序列了。同样流行的是 **[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))** 操作,仅当值没有被其他并发修改过时,才允许执行写操作。
|
||||
|
||||
[^iv]: 严格地说,**原子自增(atomic increment)** 这个术语在多线程编程的意义上使用了原子这个词。 在 ACID 的情况下,它实际上应该被称为 **隔离的(isolated)** 的或 **可串行的(serializable)** 的增量。 但这就太吹毛求疵了。
|
||||
[^iv]: 严格地说,**原子自增(atomic increment)** 这个术语在多线程编程的意义上使用了原子这个词。在 ACID 的情况下,它实际上应该被称为 **隔离的(isolated)** 的或 **可串行的(serializable)** 的增量。但这就太吹毛求疵了。
|
||||
|
||||
这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(请参阅 “[防止丢失更新](#防止丢失更新)”)。但它们不是通常意义上的事务。CAS 以及其他单一对象操作被称为 “轻量级事务”,甚至出于营销目的被称为 “ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。
|
||||
|
||||
@ -196,7 +196,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
#### 处理错误和中止
|
||||
|
||||
事务的一个关键特性是,如果发生错误,它可以中止并安全地重试。 ACID 数据库基于这样的哲学:如果数据库有违反其原子性,隔离性或持久性的危险,则宁愿完全放弃事务,而不是留下半成品。
|
||||
事务的一个关键特性是,如果发生错误,它可以中止并安全地重试。ACID 数据库基于这样的哲学:如果数据库有违反其原子性,隔离性或持久性的危险,则宁愿完全放弃事务,而不是留下半成品。
|
||||
|
||||
然而并不是所有的系统都遵循这个哲学。特别是具有 [无主复制](ch5.md#无主复制) 的数据存储,主要是在 “尽力而为” 的基础上进行工作。可以概括为 “数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情” —— 所以,从错误中恢复是应用程序的责任。
|
||||
|
||||
@ -279,7 +279,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
但是要求读锁的办法在实践中效果并不好。因为一个长时间运行的写入事务会迫使许多只读事务等到这个慢写入事务完成。这会影响只读事务的响应时间,并且不利于可操作性:因为等待锁,应用某个部分的迟缓可能由于连锁效应,导致其他部分出现问题。
|
||||
|
||||
出于这个原因,大多数数据库 [^vi] 使用 [图 7-4](img/fig7-4.png) 的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。
|
||||
出于这个原因,大多数数据库 [^vi] 使用 [图 7-4](img/fig7-4.png) 的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。只有当新值提交后,事务才会切换到读取新值。
|
||||
|
||||
[^vi]: 在撰写本文时,唯一在读已提交隔离级别使用读锁的主流数据库是 IBM DB2 和使用 `read_committed_snapshot = off` 配置的 Microsoft SQL Server【23,36】。
|
||||
|
||||
@ -325,7 +325,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
|
||||
|
||||
[图 7-7](img/fig7-7.png) 说明了 PostgreSQL 如何实现基于 MVCC 的快照隔离【31】(其他实现类似)。当一个事务开始时,它被赋予一个唯一的,永远增长 [^vii] 的事务 ID(`txid`)。每当事务向数据库写入任何内容时,它所写入的数据都会被标记上写入者的事务 ID。
|
||||
|
||||
[^vii]: 事实上,事务 ID 是 32 位整数,所以大约会在 40 亿次事务之后溢出。 PostgreSQL 的 Vacuum 过程会清理老旧的事务 ID,确保事务 ID 溢出(回卷)不会影响到数据。
|
||||
[^vii]: 事实上,事务 ID 是 32 位整数,所以大约会在 40 亿次事务之后溢出。PostgreSQL 的 Vacuum 过程会清理老旧的事务 ID,确保事务 ID 溢出(回卷)不会影响到数据。
|
||||
|
||||
![](img/fig7-7.png)
|
||||
|
||||
@ -369,7 +369,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
|
||||
|
||||
快照隔离是一个有用的隔离级别,特别对于只读事务而言。但是,许多数据库实现了它,却用不同的名字来称呼。在 Oracle 中称为 **可串行化(Serializable)** 的,在 PostgreSQL 和 MySQL 中称为 **可重复读(repeatable read)**【23】。
|
||||
|
||||
这种命名混淆的原因是 SQL 标准没有 **快照隔离** 的概念,因为标准是基于 System R 1975 年定义的隔离级别【2】,那时候 **快照隔离** 尚未发明。相反,它定义了 **可重复读**,表面上看起来与快照隔离很相似。 PostgreSQL 和 MySQL 称其 **快照隔离** 级别为 **可重复读(repeatable read)**,因为这样符合标准要求,所以它们可以声称自己 “标准兼容”。
|
||||
这种命名混淆的原因是 SQL 标准没有 **快照隔离** 的概念,因为标准是基于 System R 1975 年定义的隔离级别【2】,那时候 **快照隔离** 尚未发明。相反,它定义了 **可重复读**,表面上看起来与快照隔离很相似。PostgreSQL 和 MySQL 称其 **快照隔离** 级别为 **可重复读(repeatable read)**,因为这样符合标准要求,所以它们可以声称自己 “标准兼容”。
|
||||
|
||||
不幸的是,SQL 标准对隔离级别的定义是有缺陷的 —— 模糊,不精确,并不像标准应有的样子独立于实现【28】。有几个数据库实现了可重复读,但它们实际提供的保证存在很大的差异,尽管表面上是标准化的【23】。在研究文献【29,30】中已经有了可重复读的正式定义,但大多数的实现并不能满足这个正式定义。最后,IBM DB2 使用 “可重复读” 来引用可串行化【8】。
|
||||
|
||||
@ -582,7 +582,7 @@ COMMIT;
|
||||
在本章中,已经看到了几个易于出现竞争条件的事务例子。**读已提交** 和 **快照隔离** 级别会阻止某些竞争条件,但不会阻止另一些。我们遇到了一些特别棘手的例子,**写入偏差** 和 **幻读**。这是一个可悲的情况:
|
||||
|
||||
- 隔离级别难以理解,并且在不同的数据库中实现的不一致(例如,“可重复读” 的含义天差地别)。
|
||||
- 光检查应用代码很难判断在特定的隔离级别运行是否安全。 特别是在大型应用程序中,你可能并不知道并发发生的所有事情。
|
||||
- 光检查应用代码很难判断在特定的隔离级别运行是否安全。特别是在大型应用程序中,你可能并不知道并发发生的所有事情。
|
||||
- 没有检测竞争条件的好工具。原则上来说,静态分析可能会有帮助【26】,但研究中的技术还没法实际应用。并发问题的测试是很难的,因为它们通常是非确定性的 —— 只有在倒霉的时序下才会出现问题。
|
||||
|
||||
这不是一个新问题,从 20 世纪 70 年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用 **可串行化(serializable)** 的隔离级别!
|
||||
@ -648,7 +648,7 @@ VoltDB 还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
但是,对于需要访问多个分区的任何事务,数据库必须在触及的所有分区之间协调事务。存储过程需要跨越所有分区锁定执行,以确保整个系统的可串行性。
|
||||
|
||||
由于跨分区事务具有额外的协调开销,所以它们比单分区事务慢得多。 VoltDB 报告的吞吐量大约是每秒 1000 个跨分区写入,比单分区吞吐量低几个数量级,并且不能通过增加更多的机器来增加吞吐量【49】。
|
||||
由于跨分区事务具有额外的协调开销,所以它们比单分区事务慢得多。VoltDB 报告的吞吐量大约是每秒 1000 个跨分区写入,比单分区吞吐量低几个数量级,并且不能通过增加更多的机器来增加吞吐量【49】。
|
||||
|
||||
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个次级索引的数据可能需要大量的跨分区协调(请参阅 “[分区与次级索引](ch6.md#分区与次级索引)”)。
|
||||
|
||||
@ -751,7 +751,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
本章描绘了数据库中并发控制的黯淡画面。一方面,我们实现了性能不好(2PL)或者伸缩性不好(串行执行)的可串行化隔离级别。另一方面,我们有性能良好的弱隔离级别,但容易出现各种竞争条件(丢失更新、写入偏差、幻读等)。串行化的隔离级别和高性能是从根本上相互矛盾的吗?
|
||||
|
||||
也许不是:一个称为 **可串行化快照隔离(SSI, serializable snapshot isolation)** 的算法是非常有前途的。它提供了完整的可串行化隔离级别,但与快照隔离相比只有很小的性能损失。 SSI 是相当新的:它在 2008 年首次被描述【40】,并且是 Michael Cahill 的博士论文【51】的主题。
|
||||
也许不是:一个称为 **可串行化快照隔离(SSI, serializable snapshot isolation)** 的算法是非常有前途的。它提供了完整的可串行化隔离级别,但与快照隔离相比只有很小的性能损失。SSI 是相当新的:它在 2008 年首次被描述【40】,并且是 Michael Cahill 的博士论文【51】的主题。
|
||||
|
||||
今天,SSI 既用于单节点数据库(PostgreSQL9.1 以后的可串行化隔离级别),也用于分布式数据库(FoundationDB 使用类似的算法)。由于 SSI 与其他并发控制机制相比还很年轻,还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。
|
||||
|
||||
@ -775,7 +775,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
换句话说,事务基于一个 **前提(premise)** 采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变 —— 前提可能不再成立。
|
||||
|
||||
当应用程序进行查询时(例如,“当前有多少医生正在值班?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可串行化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
|
||||
当应用程序进行查询时(例如,“当前有多少医生正在值班?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。换而言之,事务中的查询与写入可能存在因果依赖。为了提供可串行化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
|
||||
|
||||
数据库如何知道查询结果是否可能已经改变?有两种情况需要考虑:
|
||||
|
||||
@ -814,7 +814,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
与往常一样,许多工程细节会影响算法的实际表现。例如一个权衡是跟踪事务的读取和写入的 **粒度(granularity)**。如果数据库详细地跟踪每个事务的活动(细粒度),那么可以准确地确定哪些事务需要中止,但是簿记开销可能变得很显著。简略的跟踪速度更快(粗粒度),但可能会导致更多不必要的事务中止。
|
||||
|
||||
在某些情况下,事务可以读取被另一个事务覆盖的信息:这取决于发生了什么,有时可以证明执行结果无论如何都是可串行化的。 PostgreSQL 使用这个理论来减少不必要的中止次数【11,41】。
|
||||
在某些情况下,事务可以读取被另一个事务覆盖的信息:这取决于发生了什么,有时可以证明执行结果无论如何都是可串行化的。PostgreSQL 使用这个理论来减少不必要的中止次数【11,41】。
|
||||
|
||||
与两阶段锁定相比,可串行化快照隔离的最大优点是一个事务不需要阻塞等待另一个事务所持有的锁。就像在快照隔离下一样,写不会阻塞读,反之亦然。这种设计原则使得查询延迟更可预测,波动更少。特别是,只读查询可以运行在一致快照上,而不需要任何锁定,这对于读取繁重的工作负载非常有吸引力。
|
||||
|
||||
|
30
ch8.md
30
ch8.md
@ -24,9 +24,9 @@
|
||||
|
||||
使用分布式系统与在一台计算机上编写软件有着根本的区别,主要的区别在于,有许多新颖和刺激的方法可以使事情出错【1,2】。在这一章中,我们将了解实践中出现的问题,理解我们能够依赖,和不可以依赖的东西。
|
||||
|
||||
最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。 在 [第九章](ch9.md) 中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。 但首先,在本章中,我们必须了解我们面临的挑战。
|
||||
最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。在 [第九章](ch9.md) 中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。但首先,在本章中,我们必须了解我们面临的挑战。
|
||||
|
||||
本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。 我们将研究网络的问题(“[不可靠的网络](#不可靠的网络)”); 时钟和时序问题(“[不可靠的时钟](#不可靠的时钟)”); 我们将讨论他们可以避免的程度。 所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(“[知识、真相与谎言](#知识、真相与谎言)”)。
|
||||
本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。我们将研究网络的问题(“[不可靠的网络](#不可靠的网络)”); 时钟和时序问题(“[不可靠的时钟](#不可靠的时钟)”); 我们将讨论他们可以避免的程度。所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(“[知识、真相与谎言](#知识、真相与谎言)”)。
|
||||
|
||||
|
||||
## 故障与部分失效
|
||||
@ -35,7 +35,7 @@
|
||||
|
||||
单个计算机上的软件没有根本性的不可靠原因:当硬件正常工作时,相同的操作总是产生相同的结果(这是确定性的)。如果存在硬件问题(例如,内存损坏或连接器松动),其后果通常是整个系统故障(例如,内核恐慌,“蓝屏死机”,启动失败)。装有良好软件的个人计算机通常要么功能完好,要么完全失效,而不是介于两者之间。
|
||||
|
||||
这是计算机设计中的一个有意的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因为计算机隐藏了模糊不清的物理实现,并呈现出一个理想化的系统模型,并以数学一样的完美的方式运作。 CPU 指令总是做同样的事情;如果你将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。从第一台数字计算机开始,*始终正确地计算* 这个设计目标贯穿始终【3】。
|
||||
这是计算机设计中的一个有意的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因为计算机隐藏了模糊不清的物理实现,并呈现出一个理想化的系统模型,并以数学一样的完美的方式运作。CPU 指令总是做同样的事情;如果你将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。从第一台数字计算机开始,*始终正确地计算* 这个设计目标贯穿始终【3】。
|
||||
|
||||
当你编写运行在多台计算机上的软件时,情况有本质上的区别。在分布式系统中,我们不再处于理想化的系统模型中,我们别无选择,只能面对现实世界的混乱现实。而在现实世界中,各种各样的事情都可能会出现问题【4】,如下面的轶事所述:
|
||||
|
||||
@ -82,9 +82,9 @@
|
||||
> 你可能想知道这是否有意义 —— 直观地看来,系统只能像其最不可靠的组件(最薄弱的环节)一样可靠。事实并非如此:事实上,从不太可靠的潜在基础构建更可靠的系统是计算机领域的一个古老思想【11】。例如:
|
||||
>
|
||||
> * 纠错码允许数字数据在通信信道上准确传输,偶尔会出现一些错误,例如由于无线网络上的无线电干扰【12】。
|
||||
> * **互联网协议(Internet Protocol, IP)** 不可靠:可能丢弃、延迟、重复或重排数据包。 传输控制协议(Transmission Control Protocol, TCP)在互联网协议(IP)之上提供了更可靠的传输层:它确保丢失的数据包被重新传输,消除重复,并且数据包被重新组装成它们被发送的顺序。
|
||||
> * **互联网协议(Internet Protocol, IP)** 不可靠:可能丢弃、延迟、重复或重排数据包。传输控制协议(Transmission Control Protocol, TCP)在互联网协议(IP)之上提供了更可靠的传输层:它确保丢失的数据包被重新传输,消除重复,并且数据包被重新组装成它们被发送的顺序。
|
||||
>
|
||||
> 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过信道可以得到多少数据,是有根本性的限制的【13】。 TCP 可以隐藏数据包的丢失,重复和重新排序,但是它不能神奇地消除网络中的延迟。
|
||||
> 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过信道可以得到多少数据,是有根本性的限制的【13】。TCP 可以隐藏数据包的丢失,重复和重新排序,但是它不能神奇地消除网络中的延迟。
|
||||
>
|
||||
> 虽然更可靠的高级系统并不完美,但它仍然有用,因为它处理了一些棘手的低级错误,所以其余的错误通常更容易推理和处理。我们将在 “[数据库的端到端原则](ch12.md#数据库的端到端原则)” 中进一步探讨这个问题。
|
||||
|
||||
@ -190,7 +190,7 @@
|
||||
|
||||
在这种环境下,你只能通过实验方式选择超时:在一段较长的时期内、在多台机器上测量网络往返时间的分布,以确定延迟的预期变化。然后,考虑到应用程序的特性,可以确定 **故障检测延迟** 与 **过早超时风险** 之间的适当折衷。
|
||||
|
||||
更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过 Phi Accrual 故障检测器【30】来完成,该检测器在例如 Akka 和 Cassandra 【31】中使用。 TCP 的超时重传机制也是以类似的方式工作【27】。
|
||||
更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过 Phi Accrual 故障检测器【30】来完成,该检测器在例如 Akka 和 Cassandra 【31】中使用。TCP 的超时重传机制也是以类似的方式工作【27】。
|
||||
|
||||
### 同步网络与异步网络
|
||||
|
||||
@ -204,7 +204,7 @@
|
||||
|
||||
#### 我们不能简单地使网络延迟可预测吗?
|
||||
|
||||
请注意,电话网络中的电路与 TCP 连接有很大不同:电路是固定数量的预留带宽,在电路建立时没有其他人可以使用,而 TCP 连接的数据包 **机会性地** 使用任何可用的网络带宽。你可以给 TCP 一个可变大小的数据块(例如,一个电子邮件或一个网页),它会尽可能在最短的时间内传输它。 TCP 连接空闲时,不使用任何带宽 [^ii]。
|
||||
请注意,电话网络中的电路与 TCP 连接有很大不同:电路是固定数量的预留带宽,在电路建立时没有其他人可以使用,而 TCP 连接的数据包 **机会性地** 使用任何可用的网络带宽。你可以给 TCP 一个可变大小的数据块(例如,一个电子邮件或一个网页),它会尽可能在最短的时间内传输它。TCP 连接空闲时,不使用任何带宽 [^ii]。
|
||||
|
||||
[^ii]: 除了偶尔的 keepalive 数据包,如果 TCP keepalive 被启用。
|
||||
|
||||
@ -286,7 +286,7 @@
|
||||
|
||||
单调钟不需要同步,但是日历时钟需要根据 NTP 服务器或其他外部时间源来设置才能有用。不幸的是,我们获取时钟的方法并不像你所希望的那样可靠或准确 —— 硬件时钟和 NTP 可能会变幻莫测。举几个例子:
|
||||
|
||||
* 计算机中的石英钟不够精确:它会 **漂移**(drifts,即运行速度快于或慢于预期)。时钟漂移取决于机器的温度。 Google 假设其服务器时钟漂移为 200 ppm(百万分之一)【41】,相当于每 30 秒与服务器重新同步一次的时钟漂移为 6 毫秒,或者每天重新同步的时钟漂移为 17 秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。
|
||||
* 计算机中的石英钟不够精确:它会 **漂移**(drifts,即运行速度快于或慢于预期)。时钟漂移取决于机器的温度。Google 假设其服务器时钟漂移为 200 ppm(百万分之一)【41】,相当于每 30 秒与服务器重新同步一次的时钟漂移为 6 毫秒,或者每天重新同步的时钟漂移为 17 秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。
|
||||
* 如果计算机的时钟与 NTP 服务器的时钟差别太大,可能会拒绝同步,或者本地时钟将被强制重置【37】。任何观察重置前后时间的应用程序都可能会看到时间倒退或突然跳跃。
|
||||
* 如果某个节点被 NTP 服务器的防火墙意外阻塞,有可能会持续一段时间都没有人会注意到。有证据表明,这在实践中确实发生过。
|
||||
* NTP 同步只能和网络延迟一样好,所以当你在拥有可变数据包延迟的拥塞网络上时,NTP 同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35 毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致 NTP 客户端完全放弃。
|
||||
@ -311,9 +311,9 @@
|
||||
|
||||
#### 有序事件的时间戳
|
||||
|
||||
让我们考虑一个特别的情况,一件很有诱惑但也很危险的事情:依赖时钟,在多个节点上对事件进行排序。 例如,如果两个客户端写入分布式数据库,谁先到达? 哪一个更近?
|
||||
让我们考虑一个特别的情况,一件很有诱惑但也很危险的事情:依赖时钟,在多个节点上对事件进行排序。例如,如果两个客户端写入分布式数据库,谁先到达? 哪一个更近?
|
||||
|
||||
[图 8-3](img/fig8-3.png) 显示了在具有多主复制的数据库中对时钟的危险使用(该例子类似于 [图 5-9](img/fig5-9.png))。 客户端 A 在节点 1 上写入 `x = 1`;写入被复制到节点 3;客户端 B 在节点 3 上增加 x(我们现在有 `x = 2`);最后这两个写入都被复制到节点 2。
|
||||
[图 8-3](img/fig8-3.png) 显示了在具有多主复制的数据库中对时钟的危险使用(该例子类似于 [图 5-9](img/fig5-9.png))。客户端 A 在节点 1 上写入 `x = 1`;写入被复制到节点 3;客户端 B 在节点 3 上增加 x(我们现在有 `x = 2`);最后这两个写入都被复制到节点 2。
|
||||
|
||||
![](img/fig8-3.png)
|
||||
|
||||
@ -359,7 +359,7 @@ NTP 同步是否能足够准确,以至于这种不正确的排序不会发生
|
||||
|
||||
我们可以使用同步时钟的时间戳作为事务 ID 吗?如果我们能够获得足够好的同步性,那么这种方法将具有很合适的属性:更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。
|
||||
|
||||
Spanner 以这种方式实现跨数据中心的快照隔离【59,60】。它使用 TrueTime API 报告的时钟置信区间,并基于以下观察结果:如果你有两个置信区间,每个置信区间包含最早和最晚可能的时间戳($A = [A_{earliest}, A_{latest}]$, $B=[B_{earliest}, B_{latest}]$),这两个区间不重叠(即:$A_{earliest} <A_{latest} <B_{earliest} <B_{latest}$)的话,那么 B 肯定发生在 A 之后 —— 这是毫无疑问的。只有当区间重叠时,我们才不确定 A 和 B 发生的顺序。
|
||||
Spanner 以这种方式实现跨数据中心的快照隔离【59,60】。它使用 TrueTime API 报告的时钟置信区间,并基于以下观察结果:如果你有两个置信区间,每个置信区间包含最早和最晚可能的时间戳($A = [A_{earliest}, A_{latest}]$,$B=[B_{earliest}, B_{latest}]$),这两个区间不重叠(即:$A_{earliest} <A_{latest} <B_{earliest} <B_{latest}$)的话,那么 B 肯定发生在 A 之后 —— 这是毫无疑问的。只有当区间重叠时,我们才不确定 A 和 B 发生的顺序。
|
||||
|
||||
为了确保事务时间戳反映因果关系,在提交读写事务之前,Spanner 在提交读写事务时,会故意等待置信区间长度的时间。通过这样,它可以确保任何可能读取数据的事务处于足够晚的时间,因此它们的置信区间不会重叠。为了保持尽可能短的等待时间,Spanner 需要保持尽可能小的时钟不确定性,为此,Google 在每个数据中心都部署了一个 GPS 接收器或原子钟,这允许时钟同步到大约 7 毫秒以内【41】。
|
||||
|
||||
@ -401,9 +401,9 @@ while (true) {
|
||||
* 在虚拟化环境中,可以 **挂起(suspend)** 虚拟机(暂停执行所有进程并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行)。这个暂停可以在进程执行的任何时候发生,并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移,而不需要重新启动,在这种情况下,暂停的长度取决于进程写入内存的速率【67】。
|
||||
* 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
|
||||
* 当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时),当前正在运行的线程可能在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的 CPU 时间被称为 **窃取时间(steal time)**。如果机器处于沉重的负载下(即,如果等待运行的线程队列很长),暂停的线程再次运行可能需要一些时间。
|
||||
* 如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘 I/O 操作完成【68】。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生 —— 例如,Java 类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。 I/O 暂停和 GC 暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的 EBS),I/O 延迟进一步受到网络延迟变化的影响【29】。
|
||||
* 如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘 I/O 操作完成【68】。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生 —— 例如,Java 类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。I/O 暂停和 GC 暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的 EBS),I/O 延迟进一步受到网络延迟变化的影响【29】。
|
||||
* 如果操作系统配置为允许交换到磁盘(页面交换),则简单的内存访问可能导致 **页面错误(page fault)**,要求将磁盘中的页面装入内存。当这个缓慢的 I/O 操作发生时,线程暂停。如果内存压力很高,则可能需要将另一个页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为 **抖动**,即 thrashing)。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
|
||||
* 可以通过发送 SIGSTOP 信号来暂停 Unix 进程,例如通过在 shell 中按下 Ctrl-Z。 这个信号立即阻止进程继续执行更多的 CPU 周期,直到 SIGCONT 恢复为止,此时它将继续运行。 即使你的环境通常不使用 SIGSTOP,也可能由运维工程师意外发送。
|
||||
* 可以通过发送 SIGSTOP 信号来暂停 Unix 进程,例如通过在 shell 中按下 Ctrl-Z。这个信号立即阻止进程继续执行更多的 CPU 周期,直到 SIGCONT 恢复为止,此时它将继续运行。即使你的环境通常不使用 SIGSTOP,也可能由运维工程师意外发送。
|
||||
|
||||
所有这些事件都可以随时 **抢占(preempt)** 正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时序做任何假设,因为随时可能发生上下文切换,或者出现并行运行。
|
||||
|
||||
@ -478,7 +478,7 @@ while (true) {
|
||||
|
||||
如果一个节点继续表现为 **天选者**,即使大多数节点已经声明它已经死了,则在考虑不周的系统中可能会导致问题。这样的节点能以自己赋予的权能向其他节点发送消息,如果其他节点相信,整个系统可能会做一些不正确的事情。
|
||||
|
||||
例如,[图 8-4](img/fig8-4.png) 显示了由于不正确的锁实现导致的数据损坏错误。 (这个错误不仅仅是理论上的:HBase 曾经有这个问题【74,75】)假设你要确保一个存储服务中的文件一次只能被一个客户访问,因为如果多个客户试图对此写入,该文件将被损坏。你尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。
|
||||
例如,[图 8-4](img/fig8-4.png) 显示了由于不正确的锁实现导致的数据损坏错误。(这个错误不仅仅是理论上的:HBase 曾经有这个问题【74,75】)假设你要确保一个存储服务中的文件一次只能被一个客户访问,因为如果多个客户试图对此写入,该文件将被损坏。你尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。
|
||||
|
||||
![](img/fig8-4.png)
|
||||
|
||||
@ -648,7 +648,7 @@ Web 应用程序确实需要预期受终端用户控制的客户端(如 Web
|
||||
|
||||
在本章中,我们也转换了几次话题,探讨了网络、时钟和进程的不可靠性是否是不可避免的自然规律。我们看到这并不是:有可能给网络提供硬实时的响应保证和有限的延迟,但是这样做非常昂贵,且导致硬件资源的利用率降低。大多数非安全关键系统会选择 **便宜而不可靠**,而不是 **昂贵和可靠**。
|
||||
|
||||
我们还谈到了超级计算机,它们采用可靠的组件,因此当组件发生故障时必须完全停止并重新启动。相比之下,分布式系统可以永久运行而不会在服务层面中断,因为所有的错误和维护都可以在节点级别进行处理 —— 至少在理论上是如此。 (实际上,如果一个错误的配置变更被应用到所有的节点,仍然会使分布式系统瘫痪)。
|
||||
我们还谈到了超级计算机,它们采用可靠的组件,因此当组件发生故障时必须完全停止并重新启动。相比之下,分布式系统可以永久运行而不会在服务层面中断,因为所有的错误和维护都可以在节点级别进行处理 —— 至少在理论上是如此。(实际上,如果一个错误的配置变更被应用到所有的节点,仍然会使分布式系统瘫痪)。
|
||||
|
||||
本章一直在讲存在的问题,给我们展现了一幅黯淡的前景。在 [下一章](ch9.md) 中,我们将继续讨论解决方案,并讨论一些旨在解决分布式系统中所有问题的算法。
|
||||
|
||||
|
50
ch9.md
50
ch9.md
@ -84,7 +84,7 @@
|
||||
* $read(x)⇒v$表示客户端请求读取寄存器 `x` 的值,数据库返回值 `v`。
|
||||
* $write(x,v)⇒r$ 表示客户端请求将寄存器 `x` 设置为值 `v` ,数据库返回响应 `r` (可能正确,可能错误)。
|
||||
|
||||
在 [图 9-2](img/fig9-2.png) 中,`x` 的值最初为 `0`,客户端 C 执行写请求将其设置为 `1`。发生这种情况时,客户端 A 和 B 反复轮询数据库以读取最新值。 A 和 B 的请求可能会收到怎样的响应?
|
||||
在 [图 9-2](img/fig9-2.png) 中,`x` 的值最初为 `0`,客户端 C 执行写请求将其设置为 `1`。发生这种情况时,客户端 A 和 B 反复轮询数据库以读取最新值。A 和 B 的请求可能会收到怎样的响应?
|
||||
|
||||
* 客户端 A 的第一个读操作,完成于写操作开始之前,因此必须返回旧值 `0`。
|
||||
* 客户端 A 的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值 `1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则读取处理一定发生在写入完成之后,因此它必须看到写入的新值。
|
||||
@ -108,7 +108,7 @@
|
||||
|
||||
在 [图 9-4](img/fig9-4.png) 中,除了读写之外,还增加了第三种类型的操作:
|
||||
|
||||
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的 [**比较与设置**](ch7.md#比较并设置(CAS)) 操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x$ 不等于 $v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。 $r$ 是数据库的响应(正确或错误)。
|
||||
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的 [**比较与设置**](ch7.md#比较并设置(CAS)) 操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x$ 不等于 $v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。$r$ 是数据库的响应(正确或错误)。
|
||||
|
||||
[图 9-4](img/fig9-4.png) 中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
|
||||
|
||||
@ -116,7 +116,7 @@
|
||||
|
||||
![](img/fig9-4.png)
|
||||
|
||||
**图 9-4 可视化读取和写入看起来已经生效的时间点。 B 的最后读取不是线性一致性的**
|
||||
**图 9-4 可视化读取和写入看起来已经生效的时间点。B 的最后读取不是线性一致性的**
|
||||
|
||||
[图 9-4](img/fig9-4.png) 中有一些有趣的细节需要指出:
|
||||
|
||||
@ -126,9 +126,9 @@
|
||||
|
||||
* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C 首先读取 `1` ,然后读取 `2` ,因为两次读取之间的值由 B 更改。可以使用原子 **比较并设置(cas)** 操作来检查该值是否未被另一客户端同时更改:B 和 C 的 **cas** 请求成功,但是 D 的 **cas** 请求失败(在数据库处理它时,`x` 的值不再是 `0` )。
|
||||
|
||||
* 客户 B 的最后一次读取(阴影条柱中)不是线性一致性的。 该操作与 C 的 **cas** 写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B 的读取返回 `2` 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 `4` ,因此不允许 B 读取比 A 更旧的值。再次,与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同。
|
||||
* 客户 B 的最后一次读取(阴影条柱中)不是线性一致性的。该操作与 C 的 **cas** 写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B 的读取返回 `2` 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 `4` ,因此不允许 B 读取比 A 更旧的值。再次,与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同。
|
||||
|
||||
这就是线性一致性背后的直觉。 正式的定义【6】更准确地描述了它。 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,以测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。
|
||||
这就是线性一致性背后的直觉。正式的定义【6】更准确地描述了它。通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,以测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。
|
||||
|
||||
|
||||
> ### 线性一致性与可串行化
|
||||
@ -180,7 +180,7 @@
|
||||
|
||||
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如 [图 9-5](img/fig9-5.png) 所示。
|
||||
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是 Web 服务器通过消息队列发送的(请参阅 [第十一章](ch11.md))。 Web 服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是 Web 服务器通过消息队列发送的(请参阅 [第十一章](ch11.md))。Web 服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。
|
||||
|
||||
![](img/fig9-5.png)
|
||||
|
||||
@ -204,7 +204,7 @@
|
||||
|
||||
在具有单主复制功能的系统中(请参阅 “[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们 **可能(potential)** 是线性一致性的 [^iv]。然而,实际上并不是每个单主数据库都是线性一致性的,无论是因为设计的原因(例如,因为使用了快照隔离)还是因为在并发处理上存在错误【10】。
|
||||
|
||||
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(请参阅 “[分布式事务与共识](#分布式事务与共识)”)。
|
||||
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。交叉分区事务是一个不同的问题(请参阅 “[分布式事务与共识](#分布式事务与共识)”)。
|
||||
|
||||
从主库读取依赖一个假设,你确切地知道领导者是谁。正如在 “[真相由多数所定义](ch8.md#真相由多数所定义)” 中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此 —— 如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障切换时甚至可能会丢失已提交的写入(请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
|
||||
@ -232,9 +232,9 @@
|
||||
|
||||
在 [图 9-6](img/fig9-6.png) 中,$x$ 的初始值为 0,写入客户端通过向所有三个副本( $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端 A 并发地从两个节点组成的法定人群( $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端 B 也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 `0` 。
|
||||
|
||||
法定人数条件满足( $w + r> n$ ),但是这个执行是非线性一致的:B 的请求在 A 的请求完成后开始,但是 B 返回旧值,而 A 返回新值。 (又一次,如同 Alice 和 Bob 的例子 [图 9-1](img/fig9-1.png))
|
||||
法定人数条件满足( $w + r> n$ ),但是这个执行是非线性一致的:B 的请求在 A 的请求完成后开始,但是 B 返回旧值,而 A 返回新值。(又一次,如同 Alice 和 Bob 的例子 [图 9-1](img/fig9-1.png))
|
||||
|
||||
有趣的是,通过牺牲性能,可以使 Dynamo 风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(请参阅 “[读修复和反熵](ch5.md#读修复和反熵)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak 不执行同步读修复【26】。 Cassandra 在进行法定人数读取时,**确实** 在等待读修复完成【27】;但是由于使用了最后写入胜利的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
有趣的是,通过牺牲性能,可以使 Dynamo 风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(请参阅 “[读修复和反熵](ch5.md#读修复和反熵)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak 不执行同步读修复【26】。Cassandra 在进行法定人数读取时,**确实** 在等待读修复完成【27】;但是由于使用了最后写入胜利的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
|
||||
而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置(CAS)操作,因为它需要一个共识算法【28】。
|
||||
|
||||
@ -268,11 +268,11 @@
|
||||
* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都 **不可用**)。
|
||||
* 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题解决前保持可用,但其行为不是线性一致的。
|
||||
|
||||
[^v]: 这两种选择有时分别称为 CP(在网络分区下一致但不可用)和 AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好不要这样用。
|
||||
[^v]: 这两种选择有时分别称为 CP(在网络分区下一致但不可用)和 AP(在网络分区下可用但不一致)。但是,这种分类方案存在一些缺陷【9】,所以最好不要这样用。
|
||||
|
||||
因此,不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为 CAP 定理【29,30,31,32】,由 Eric Brewer 于 2000 年命名,尽管 70 年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。
|
||||
|
||||
CAP 最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP 定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP 值得赞扬 —— 它见证了自 00 年代中期以来新数据库的技术爆炸(即 NoSQL)。
|
||||
CAP 最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP 定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。对于这种文化上的转变,CAP 值得赞扬 —— 它见证了自 00 年代中期以来新数据库的技术爆炸(即 NoSQL)。
|
||||
|
||||
> #### CAP定理没有帮助
|
||||
>
|
||||
@ -282,7 +282,7 @@ CAP 最初是作为一个经验法则提出的,没有准确的定义,目的
|
||||
>
|
||||
> 在 CAP 的讨论中,术语可用性有几个相互矛盾的定义,形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的 “高可用”(容错)系统实际上不符合 CAP 对可用性的特殊定义。总而言之,围绕着 CAP 有很多误解和困惑,并不能帮助我们更好地理解系统,所以最好避免使用 CAP。
|
||||
|
||||
CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一个一致性模型(即线性一致性)和一种故障(网络分区 [^vi],或活跃但彼此断开的节点)。它没有讨论任何关于网络延迟,死亡节点或其他权衡的事。 因此,尽管 CAP 在历史上有一些影响力,但对于设计系统而言并没有实际价值【9,40】。
|
||||
CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一个一致性模型(即线性一致性)和一种故障(网络分区 [^vi],或活跃但彼此断开的节点)。它没有讨论任何关于网络延迟,死亡节点或其他权衡的事。因此,尽管 CAP 在历史上有一些影响力,但对于设计系统而言并没有实际价值【9,40】。
|
||||
|
||||
在分布式系统中有更多有趣的 “不可能” 的结果【41】,且 CAP 定理现在已经被更精确的结果取代【2,42】,所以它现在基本上成了历史古迹了。
|
||||
|
||||
@ -417,7 +417,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
尽管刚才描述的三个序列号生成器与因果不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利・兰伯特(Leslie Lamport)于 1978 年提出【56】,现在是分布式系统领域中被引用最多的论文之一。
|
||||
|
||||
[图 9-8](img/fig9-8.png) 说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。 兰伯特时间戳就是两者的简单组合:(计数器,节点 ID)$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点 ID,每个时间戳都是唯一的。
|
||||
[图 9-8](img/fig9-8.png) 说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。兰伯特时间戳就是两者的简单组合:(计数器,节点 ID)$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点 ID,每个时间戳都是唯一的。
|
||||
|
||||
![](img/fig9-8.png)
|
||||
|
||||
@ -432,7 +432,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
只要每一个操作都携带着最大计数器值,这个方案确保兰伯特时间戳的排序与因果一致,因为每个因果依赖都会导致时间戳增长。
|
||||
|
||||
兰伯特时间戳有时会与我们在 “[检测并发写入](ch5.md#检测并发写入)” 中看到的版本向量相混淆。虽然两者有一些相似之处,但它们有着不同的目的:版本向量可以区分两个操作是并发的,还是一个因果依赖另一个;而兰伯特时间戳总是施行一个全序。从兰伯特时间戳的全序中,你无法分辨两个操作是并发的还是因果依赖的。 兰伯特时间戳优于版本向量的地方是,它更加紧凑。
|
||||
兰伯特时间戳有时会与我们在 “[检测并发写入](ch5.md#检测并发写入)” 中看到的版本向量相混淆。虽然两者有一些相似之处,但它们有着不同的目的:版本向量可以区分两个操作是并发的,还是一个因果依赖另一个;而兰伯特时间戳总是施行一个全序。从兰伯特时间戳的全序中,你无法分辨两个操作是并发的还是因果依赖的。兰伯特时间戳优于版本向量的地方是,它更加紧凑。
|
||||
|
||||
#### 光有时间戳排序还不够
|
||||
|
||||
@ -462,9 +462,9 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
> #### 顺序保证的范围
|
||||
>
|
||||
> 每个分区各有一个主库的分区数据库,通常只在每个分区内维持顺序,这意味着它们不能提供跨分区的一致性保证(例如,一致性快照,外键引用)。 跨所有分区的全序是可能的,但需要额外的协调【59】。
|
||||
> 每个分区各有一个主库的分区数据库,通常只在每个分区内维持顺序,这意味着它们不能提供跨分区的一致性保证(例如,一致性快照,外键引用)。跨所有分区的全序是可能的,但需要额外的协调【59】。
|
||||
|
||||
全序广播通常被描述为在节点间交换消息的协议。 非正式地讲,它要满足两个安全属性:
|
||||
全序广播通常被描述为在节点间交换消息的协议。非正式地讲,它要满足两个安全属性:
|
||||
|
||||
* 可靠交付(reliable delivery)
|
||||
|
||||
@ -494,7 +494,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
如 [图 9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有着密切的联系 [^x]。
|
||||
|
||||
[^x]: 从形式上讲,线性一致读写寄存器是一个 “更容易” 的问题。 全序广播等价于共识【67】,而共识问题在异步的崩溃 - 停止模型【68】中没有确定性的解决方案,而线性一致的读写寄存器 **可以** 在这种模型中实现【23,24,25】。 然而,支持诸如 **比较并设置(CAS, compare-and-set)**,或 **自增并返回(increment-and-get)** 的原子操作使它等价于共识问题【28】。 因此,共识问题与线性一致寄存器问题密切相关。
|
||||
[^x]: 从形式上讲,线性一致读写寄存器是一个 “更容易” 的问题。全序广播等价于共识【67】,而共识问题在异步的崩溃 - 停止模型【68】中没有确定性的解决方案,而线性一致的读写寄存器 **可以** 在这种模型中实现【23,24,25】。然而,支持诸如 **比较并设置(CAS, compare-and-set)**,或 **自增并返回(increment-and-get)** 的原子操作使它等价于共识问题【28】。因此,共识问题与线性一致寄存器问题密切相关。
|
||||
|
||||
全序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息 **何时** 被送达(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是新鲜性的保证:读取一定能看见最新的写入值。
|
||||
|
||||
@ -508,14 +508,14 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
2. 读日志,并等待你刚才追加的消息被读回。[^xi]
|
||||
4. 检查是否有任何消息声称目标用户名的所有权。如果这些消息中的第一条就是你自己的消息,那么你就成功了:你可以提交声称的用户名(也许是通过向日志追加另一条消息)并向客户端确认。如果所需用户名的第一条消息来自其他用户,则中止操作。
|
||||
|
||||
[^xi]: 如果你不等待,而是在消息入队之后立即确认写入,则会得到类似于多核 x86 处理器内存的一致性模型【43】。 该模型既不是线性一致的也不是顺序一致的。
|
||||
[^xi]: 如果你不等待,而是在消息入队之后立即确认写入,则会得到类似于多核 x86 处理器内存的一致性模型【43】。该模型既不是线性一致的也不是顺序一致的。
|
||||
|
||||
由于日志项是以相同顺序送达至所有节点,因此如果有多个并发写入,则所有节点会对最先到达者达成一致。选择冲突写入中的第一个作为胜利者,并中止后来者,以此确定所有节点对某个写入是提交还是中止达成一致。类似的方法可以在一个日志的基础上实现可串行化的多对象事务【62】。
|
||||
|
||||
尽管这一过程保证写入是线性一致的,但它并不保证读取也是线性一致的 —— 如果你从与日志异步更新的存储中读取数据,结果可能是陈旧的。 (精确地说,这里描述的过程提供了 **顺序一致性(sequential consistency)**【47,64】,有时也称为 **时间线一致性(timeline consistency)**【65,66】,比线性一致性稍微弱一些的保证)。为了使读取也线性一致,有几个选项:
|
||||
尽管这一过程保证写入是线性一致的,但它并不保证读取也是线性一致的 —— 如果你从与日志异步更新的存储中读取数据,结果可能是陈旧的。(精确地说,这里描述的过程提供了 **顺序一致性(sequential consistency)**【47,64】,有时也称为 **时间线一致性(timeline consistency)**【65,66】,比线性一致性稍微弱一些的保证)。为了使读取也线性一致,有几个选项:
|
||||
|
||||
* 你可以通过在日志中追加一条消息,然后读取日志,直到该消息被读回才执行实际的读取操作。消息在日志中的位置因此定义了读取发生的时间点(etcd 的法定人数读取有些类似这种情况【16】)。
|
||||
* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待该位置前的所有消息都传达到你,然后执行读取。 (这是 Zookeeper `sync()` 操作背后的思想【15】)。
|
||||
* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待该位置前的所有消息都传达到你,然后执行读取。(这是 Zookeeper `sync()` 操作背后的思想【15】)。
|
||||
* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的(这种技术用于链式复制(chain replication)【63】;请参阅 “[关于复制的研究](ch5.md#关于复制的研究)”)。
|
||||
|
||||
#### 使用线性一致性存储实现全序广播
|
||||
@ -551,7 +551,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就 ACID 而言,请参阅 “[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止 / 回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为 **原子提交(atomic commit)** 问题 [^xii]。
|
||||
|
||||
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在 **所有** 参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就 **任意一个** 被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞** 原子提交则要比共识更为困难 —— 请参阅 “[三阶段提交](#三阶段提交)”。
|
||||
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在 **所有** 参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。共识则允许就 **任意一个** 被参与者提出的候选值达成一致。然而,原子提交和共识可以相互简化为对方【70,71】。**非阻塞** 原子提交则要比共识更为困难 —— 请参阅 “[三阶段提交](#三阶段提交)”。
|
||||
|
||||
> ### 共识的不可能性
|
||||
>
|
||||
@ -594,7 +594,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
#### 两阶段提交简介
|
||||
|
||||
**两阶段提交(two-phase commit)** 是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC 在某些数据库内部使用,也以 **XA 事务** 的形式对应用可用【76,77】(例如 Java Transaction API 支持)或以 SOAP Web 服务的 `WS-AtomicTransaction` 形式提供给应用【78,79】。
|
||||
**两阶段提交(two-phase commit)** 是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。它是分布式数据库中的经典算法【13,35,75】。2PC 在某些数据库内部使用,也以 **XA 事务** 的形式对应用可用【76,77】(例如 Java Transaction API 支持)或以 SOAP Web 服务的 `WS-AtomicTransaction` 形式提供给应用【78,79】。
|
||||
|
||||
[图 9-9](img/fig9-9.png) 说明了 2PC 的基本流程。2PC 中的提交 / 中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。
|
||||
|
||||
@ -604,7 +604,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
> #### 不要把2PC和2PL搞混了
|
||||
>
|
||||
> 两阶段提交(2PC)和两阶段锁定(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”)是两个完全不同的东西。 2PC 在分布式数据库中提供原子提交,而 2PL 提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。
|
||||
> 两阶段提交(2PC)和两阶段锁定(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”)是两个完全不同的东西。2PC 在分布式数据库中提供原子提交,而 2PL 提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。
|
||||
|
||||
2PC 使用一个通常不会出现在单节点事务中的新组件:**协调者**(coordinator,也称为 **事务管理器**,即 transaction manager)。协调者通常在请求事务的相同应用进程中以库的形式实现(例如,嵌入在 Java EE 容器中),但也可以是单独的进程或服务。这种协调者的例子包括 Narayana、JOTM、BTM 或 MSDTC。
|
||||
|
||||
@ -617,7 +617,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了
|
||||
|
||||
#### 系统承诺
|
||||
|
||||
这个简短的描述可能并没有说清楚为什么两阶段提交保证了原子性,而跨多个节点的一阶段提交却没有。在两阶段提交的情况下,准备请求和提交请求当然也可以轻易丢失。 2PC 又有什么不同呢?
|
||||
这个简短的描述可能并没有说清楚为什么两阶段提交保证了原子性,而跨多个节点的一阶段提交却没有。在两阶段提交的情况下,准备请求和提交请求当然也可以轻易丢失。2PC 又有什么不同呢?
|
||||
|
||||
为了理解它的工作原理,我们必须更详细地分解这个过程:
|
||||
|
||||
@ -872,7 +872,7 @@ ZooKeeper/Chubby 模型运行良好的一个例子是,如果你有几个进程
|
||||
|
||||
ZooKeeper、etcd 和 Consul 也经常用于服务发现 —— 也就是找出你需要连接到哪个 IP 地址才能到达特定的服务。在云数据中心环境中,虚拟机来来往往很常见,你通常不会事先知道服务的 IP 地址。相反,你可以配置你的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。
|
||||
|
||||
但是,服务发现是否需要达成共识还不太清楚。 DNS 是查找服务名称的 IP 地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从 DNS 读取是绝对不线性一致性的,如果 DNS 查询的结果有点陈旧,通常不会有问题【109】。 DNS 的可用性和对网络中断的鲁棒性更重要。
|
||||
但是,服务发现是否需要达成共识还不太清楚。DNS 是查找服务名称的 IP 地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从 DNS 读取是绝对不线性一致性的,如果 DNS 查询的结果有点陈旧,通常不会有问题【109】。DNS 的可用性和对网络中断的鲁棒性更重要。
|
||||
|
||||
尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果你的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。
|
||||
|
||||
|
50
glossary.md
50
glossary.md
@ -1,6 +1,6 @@
|
||||
# 术语表
|
||||
|
||||
> 请注意,本术语表中的定义简短而简单,旨在传达核心思想,而不是术语的完整细微之处。 有关更多详细信息,请参阅正文中的参考资料。
|
||||
> 请注意,本术语表中的定义简短而简单,旨在传达核心思想,而不是术语的完整细微之处。有关更多详细信息,请参阅正文中的参考资料。
|
||||
|
||||
|
||||
* **异步(asynchronous)**
|
||||
@ -99,7 +99,7 @@
|
||||
|
||||
* **图(graph)**
|
||||
|
||||
一种数据结构,由顶点(可以指向的东西,也称为节点或实体)和边(从一个顶点到另一个顶点的连接,也称为关系或弧)组成。请参阅“[图数据模型](ch2.md#图数据模型)”。
|
||||
一种数据结构,由顶点(可以指向的东西,也称为节点或实体)和边(从一个顶点到另一个顶点的连接,也称为关系或弧)组成。请参阅“[图数据模型](ch2.md#图数据模型)”。
|
||||
|
||||
* **散列(hash)**
|
||||
|
||||
@ -151,35 +151,35 @@
|
||||
|
||||
* **规范化(normalized)**
|
||||
|
||||
以没有冗余或重复的方式进行结构化。 在规范化数据库中,当某些数据发生变化时,你只需要在一个地方进行更改,而不是在许多不同的地方复制很多次。 请参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”。
|
||||
以没有冗余或重复的方式进行结构化。在规范化数据库中,当某些数据发生变化时,你只需要在一个地方进行更改,而不是在许多不同的地方复制很多次。请参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”。
|
||||
|
||||
* **OLAP(Online Analytic Processing)**
|
||||
|
||||
在线分析处理。 通过对大量记录进行聚合(例如,计数,总和,平均)来表征的访问模式。 请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
|
||||
在线分析处理。通过对大量记录进行聚合(例如,计数,总和,平均)来表征的访问模式。请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
|
||||
|
||||
* **OLTP(Online Transaction Processing)**
|
||||
|
||||
在线事务处理。 访问模式的特点是快速查询,读取或写入少量记录,这些记录通常通过键索引。 请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
|
||||
在线事务处理。访问模式的特点是快速查询,读取或写入少量记录,这些记录通常通过键索引。请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
|
||||
|
||||
* **分区(partitioning)**
|
||||
|
||||
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。 也称为分片。见[第六章](ch6.md)。
|
||||
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。也称为分片。见[第六章](ch6.md)。
|
||||
|
||||
* **百分位点(percentile)**
|
||||
|
||||
通过计算有多少值高于或低于某个阈值来衡量值分布的方法。 例如,某个时间段的第95个百分位响应时间是时间t,则该时间段中,95%的请求完成时间小于t,5%的请求完成时间要比t长。 请参阅“[描述性能](ch1.md#描述性能)”。
|
||||
通过计算有多少值高于或低于某个阈值来衡量值分布的方法。例如,某个时间段的第95个百分位响应时间是时间t,则该时间段中,95%的请求完成时间小于t,5%的请求完成时间要比t长。请参阅“[描述性能](ch1.md#描述性能)”。
|
||||
|
||||
* **主键(primary key)**
|
||||
|
||||
唯一标识记录的值(通常是数字或字符串)。 在许多应用程序中,主键由系统在创建记录时生成(例如,按顺序或随机); 它们通常不由用户设置。 另请参阅次级索引。
|
||||
唯一标识记录的值(通常是数字或字符串)。在许多应用程序中,主键由系统在创建记录时生成(例如,按顺序或随机); 它们通常不由用户设置。另请参阅次级索引。
|
||||
|
||||
* **法定人数(quorum)**
|
||||
|
||||
在操作完成之前,需要对操作进行投票的最少节点数量。 请参阅“[读写的法定人数](ch5.md#读写的法定人数)”。
|
||||
在操作完成之前,需要对操作进行投票的最少节点数量。请参阅“[读写的法定人数](ch5.md#读写的法定人数)”。
|
||||
|
||||
* **再平衡(rebalance)**
|
||||
|
||||
将数据或服务从一个节点移动到另一个节点以实现负载均衡。 请参阅“[分区再平衡](ch6.md#分区再平衡)”。
|
||||
将数据或服务从一个节点移动到另一个节点以实现负载均衡。请参阅“[分区再平衡](ch6.md#分区再平衡)”。
|
||||
|
||||
* **复制(replication)**
|
||||
|
||||
@ -187,37 +187,37 @@
|
||||
|
||||
* **模式(schema)**
|
||||
|
||||
一些数据结构的描述,包括其字段和数据类型。 可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),模式可以随时间变化(请参阅[第四章](ch4.md))。
|
||||
一些数据结构的描述,包括其字段和数据类型。可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),模式可以随时间变化(请参阅[第四章](ch4.md))。
|
||||
|
||||
* **次级索引(secondary index)**
|
||||
|
||||
与主要数据存储器一起维护的附加数据结构,使你可以高效地搜索与某种条件相匹配的记录。 请参阅“[其他索引结构](ch3.md#其他索引结构)”和“[分区与次级索引](ch6.md#分区与次级索引)”。
|
||||
与主要数据存储器一起维护的附加数据结构,使你可以高效地搜索与某种条件相匹配的记录。请参阅“[其他索引结构](ch3.md#其他索引结构)”和“[分区与次级索引](ch6.md#分区与次级索引)”。
|
||||
|
||||
* **可串行化(serializable)**
|
||||
|
||||
保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。 请参阅第七章的“[可串行化](ch7.md#可串行化)”。
|
||||
保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。请参阅第七章的“[可串行化](ch7.md#可串行化)”。
|
||||
|
||||
* **无共享(shared-nothing)**
|
||||
|
||||
与共享内存或共享磁盘架构相比,独立节点(每个节点都有自己的CPU,内存和磁盘)通过传统网络连接。 见[第二部分](part-ii.md)的介绍。
|
||||
与共享内存或共享磁盘架构相比,独立节点(每个节点都有自己的CPU,内存和磁盘)通过传统网络连接。见[第二部分](part-ii.md)的介绍。
|
||||
|
||||
* **偏斜(skew)**
|
||||
|
||||
各分区负载不平衡,例如某些分区有大量请求或数据,而其他分区则少得多。也被称为热点。请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”和“[处理偏斜](ch10.md#处理偏斜)”。
|
||||
|
||||
时间线异常导致事件以不期望的顺序出现。 请参阅“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中的关于读取偏差的讨论,“[写入偏差与幻读](ch7.md#写入偏差与幻读)”中的写入偏差以及“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中的时钟偏斜。
|
||||
时间线异常导致事件以不期望的顺序出现。请参阅“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中的关于读取偏差的讨论,“[写入偏差与幻读](ch7.md#写入偏差与幻读)”中的写入偏差以及“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中的时钟偏斜。
|
||||
|
||||
* **脑裂(split brain)**
|
||||
|
||||
两个节点同时认为自己是领导者的情况,这种情况可能违反系统担保。 请参阅“[处理节点宕机](ch5.md#处理节点宕机)”和“[真相由多数所定义](ch8.md#真相由多数所定义)”。
|
||||
两个节点同时认为自己是领导者的情况,这种情况可能违反系统担保。请参阅“[处理节点宕机](ch5.md#处理节点宕机)”和“[真相由多数所定义](ch8.md#真相由多数所定义)”。
|
||||
|
||||
* **存储过程(stored procedure)**
|
||||
|
||||
一种对事务逻辑进行编码的方式,它可以完全在数据库服务器上执行,事务执行期间无需与客户端通信。 请参阅“[真的串行执行](ch7.md#真的串行执行)”。
|
||||
一种对事务逻辑进行编码的方式,它可以完全在数据库服务器上执行,事务执行期间无需与客户端通信。请参阅“[真的串行执行](ch7.md#真的串行执行)”。
|
||||
|
||||
* **流处理(stream process)**
|
||||
|
||||
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。 见[第十一章](ch11.md)。
|
||||
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。见[第十一章](ch11.md)。
|
||||
|
||||
* **同步(synchronous)**
|
||||
|
||||
@ -225,28 +225,28 @@
|
||||
|
||||
* **记录系统(system of record)**
|
||||
|
||||
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。 请参阅[第三部分](part-iii.md)的介绍。
|
||||
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。请参阅[第三部分](part-iii.md)的介绍。
|
||||
|
||||
* **超时(timeout)**
|
||||
|
||||
检测故障的最简单方法之一,即在一段时间内观察是否缺乏响应。 但是,不可能知道超时是由于远程节点的问题还是网络中的问题造成的。 请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”。
|
||||
检测故障的最简单方法之一,即在一段时间内观察是否缺乏响应。但是,不可能知道超时是由于远程节点的问题还是网络中的问题造成的。请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”。
|
||||
|
||||
* **全序(total order)**
|
||||
|
||||
一种比较事物的方法(例如时间戳),可以让你总是说出两件事中哪一件更大,哪件更小。 总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。 请参阅“[因果顺序不是全序的](ch9.md#因果顺序不是全序的)”。
|
||||
一种比较事物的方法(例如时间戳),可以让你总是说出两件事中哪一件更大,哪件更小。总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。请参阅“[因果顺序不是全序的](ch9.md#因果顺序不是全序的)”。
|
||||
|
||||
* **事务(transaction)**
|
||||
|
||||
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。 见[第七章](ch7.md)。
|
||||
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。见[第七章](ch7.md)。
|
||||
|
||||
* **两阶段提交(2PC, two-phase commit)**
|
||||
|
||||
一种确保多个数据库节点全部提交或全部中止事务的算法。 请参阅“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”。
|
||||
一种确保多个数据库节点全部提交或全部中止事务的算法。请参阅“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”。
|
||||
|
||||
* **两阶段锁定(2PL, two-phase locking)**
|
||||
|
||||
一种用于实现可串行化隔离的算法,该算法通过事务获取对其读取或写入的所有数据的锁,直到事务结束。 请参阅“[两阶段锁定](ch7.md#两阶段锁定)”。
|
||||
一种用于实现可串行化隔离的算法,该算法通过事务获取对其读取或写入的所有数据的锁,直到事务结束。请参阅“[两阶段锁定](ch7.md#两阶段锁定)”。
|
||||
|
||||
* **无边界(unbounded)**
|
||||
|
||||
没有任何已知的上限或大小。 反义词是边界(bounded)。
|
||||
没有任何已知的上限或大小。反义词是边界(bounded)。
|
@ -27,7 +27,7 @@
|
||||
|
||||
如果你需要的只是伸缩至更高的 **载荷(load)**,最简单的方法就是购买更强大的机器(有时称为 **垂直伸缩**,即 vertical scaling,或 **向上伸缩**,即 scale up)。许多处理器,内存和磁盘可以在同一个操作系统下相互连接,快速的相互连接允许任意处理器访问内存或磁盘的任意部分。在这种 **共享内存架构(shared-memory architecture)** 中,所有的组件都可以看作一台单独的机器 [^i]。
|
||||
|
||||
[^i]: 在大型机中,尽管任意处理器都可以访问内存的任意部分,但总有一些内存区域与一些处理器更接近(称为 **非均匀内存访问(nonuniform memory access, NUMA)**【1】)。 为了有效利用这种架构特性,需要对处理进行细分,以便每个处理器主要访问临近的内存,这意味着即使表面上看起来只有一台机器在运行,**分区(partitioning)** 仍然是必要的。
|
||||
[^i]: 在大型机中,尽管任意处理器都可以访问内存的任意部分,但总有一些内存区域与一些处理器更接近(称为 **非均匀内存访问(nonuniform memory access, NUMA)**【1】)。为了有效利用这种架构特性,需要对处理进行细分,以便每个处理器主要访问临近的内存,这意味着即使表面上看起来只有一台机器在运行,**分区(partitioning)** 仍然是必要的。
|
||||
|
||||
共享内存方法的问题在于,成本增长速度快于线性增长:一台有着双倍处理器数量,双倍内存大小,双倍磁盘容量的机器,通常成本会远远超过原来的两倍。而且可能因为存在瓶颈,并不足以处理双倍的载荷。
|
||||
|
||||
@ -53,11 +53,11 @@
|
||||
|
||||
* 复制(Replication)
|
||||
|
||||
在几个不同的节点上保存数据的相同副本,可能放在不同的位置。 复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务。 复制也有助于改善性能。 [第五章](ch5.md) 将讨论复制。
|
||||
在几个不同的节点上保存数据的相同副本,可能放在不同的位置。复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务。复制也有助于改善性能。[第五章](ch5.md) 将讨论复制。
|
||||
|
||||
* 分区 (Partitioning)
|
||||
|
||||
将一个大型数据库拆分成较小的子集(称为 **分区**,即 partitions),从而不同的分区可以指派给不同的 **节点**(nodes,亦称 **分片**,即 sharding)。 [第六章](ch6.md) 将讨论分区。
|
||||
将一个大型数据库拆分成较小的子集(称为 **分区**,即 partitions),从而不同的分区可以指派给不同的 **节点**(nodes,亦称 **分片**,即 sharding)。[第六章](ch6.md) 将讨论分区。
|
||||
|
||||
复制和分区是不同的机制,但它们经常同时使用。如 [图 II-1](img/figii-1.png) 所示。
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
理解了这些概念,就可以开始讨论在分布式系统中需要做出的困难抉择。[第七章](ch7.md) 将讨论 **事务(Transaction)**,这对于了解数据系统中可能出现的各种问题,以及我们可以做些什么很有帮助。[第八章](ch8.md) 和 [第九章](ch9.md) 将讨论分布式系统的根本局限性。
|
||||
|
||||
在本书的 [第三部分](part-iii.md) 中,将讨论如何将多个(可能是分布式的)数据存储集成为一个更大的系统,以满足复杂的应用需求。 但首先,我们来聊聊分布式的数据。
|
||||
在本书的 [第三部分](part-iii.md) 中,将讨论如何将多个(可能是分布式的)数据存储集成为一个更大的系统,以满足复杂的应用需求。但首先,我们来聊聊分布式的数据。
|
||||
|
||||
|
||||
## 索引
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 序言
|
||||
|
||||
如果近几年从业于软件工程,特别是服务器端和后端系统开发,那么你很有可能已经被大量关于数据存储和处理的时髦词汇轰炸过了: NoSQL!大数据!Web-Scale!分片!最终一致性!ACID! CAP 定理!云服务!MapReduce!实时!
|
||||
如果近几年从业于软件工程,特别是服务器端和后端系统开发,那么你很有可能已经被大量关于数据存储和处理的时髦词汇轰炸过了: NoSQL!大数据!Web-Scale!分片!最终一致性!ACID!CAP 定理!云服务!MapReduce!实时!
|
||||
|
||||
在最近十年中,我们看到了很多有趣的进展,关于数据库,分布式系统,以及在此基础上构建应用程序的方式。这些进展有着各种各样的驱动力:
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
|
||||
## 参考文献与延伸阅读
|
||||
|
||||
本书中讨论的大部分内容已经在其它地方以某种形式出现过了 —— 会议演示文稿、研究论文、博客文章、代码、BUG 跟踪器、邮件列表以及工程习惯中。本书总结了不同来源资料中最重要的想法,并在文本中包含了指向原始文献的链接。 如果你想更深入地探索一个领域,那么每章末尾的参考文献都是很好的资源,其中大部分可以免费在线获取。
|
||||
本书中讨论的大部分内容已经在其它地方以某种形式出现过了 —— 会议演示文稿、研究论文、博客文章、代码、BUG 跟踪器、邮件列表以及工程习惯中。本书总结了不同来源资料中最重要的想法,并在文本中包含了指向原始文献的链接。如果你想更深入地探索一个领域,那么每章末尾的参考文献都是很好的资源,其中大部分可以免费在线获取。
|
||||
|
||||
|
||||
## O‘Reilly Safari
|
||||
|
@ -34,7 +34,7 @@
|
||||
|
||||
---------
|
||||
|
||||
> 計算是一種流行文化,流行文化鄙視歷史。 流行文化關乎個體身份和參與感,但與合作無關。流行文化活在當下,也與過去和未來無關。 我認為大部分(為了錢)編寫程式碼的人就是這樣的, 他們不知道自己的文化來自哪裡。
|
||||
> 計算是一種流行文化,流行文化鄙視歷史。流行文化關乎個體身份和參與感,但與合作無關。流行文化活在當下,也與過去和未來無關。我認為大部分(為了錢)編寫程式碼的人就是這樣的,他們不知道自己的文化來自哪裡。
|
||||
>
|
||||
> —— 阿蘭・凱接受 Dobb 博士的雜誌採訪時(2012 年)
|
||||
|
||||
|
24
zh-tw/ch1.md
24
zh-tw/ch1.md
@ -2,7 +2,7 @@
|
||||
|
||||
![](../img/ch1.png)
|
||||
|
||||
> 網際網路做得太棒了,以至於大多數人將它看作像太平洋這樣的自然資源,而不是什麼人工產物。上一次出現這種大規模且無差錯的技術, 你還記得是什麼時候嗎?
|
||||
> 網際網路做得太棒了,以至於大多數人將它看作像太平洋這樣的自然資源,而不是什麼人工產物。上一次出現這種大規模且無差錯的技術,你還記得是什麼時候嗎?
|
||||
>
|
||||
> —— [艾倫・凱](http://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442) 在接受 Dobb 博士雜誌採訪時說(2012 年)
|
||||
|
||||
@ -127,8 +127,8 @@
|
||||
* 以最小化犯錯機會的方式設計系統。例如,精心設計的抽象、API 和管理後臺使做對事情更容易,搞砸事情更困難。但如果介面限制太多,人們就會忽略它們的好處而想辦法繞開。很難正確把握這種微妙的平衡。
|
||||
* 將人們最容易犯錯的地方與可能導致失效的地方 **解耦(decouple)**。特別是提供一個功能齊全的非生產環境 **沙箱(sandbox)**,使人們可以在不影響真實使用者的情況下,使用真實資料安全地探索和實驗。
|
||||
* 在各個層次進行徹底的測試【3】,從單元測試、全系統整合測試到手動測試。自動化測試易於理解,已經被廣泛使用,特別適合用來覆蓋正常情況中少見的 **邊緣場景(corner case)**。
|
||||
* 允許從人為錯誤中簡單快速地恢復,以最大限度地減少失效情況帶來的影響。 例如,快速回滾配置變更,分批發布新程式碼(以便任何意外錯誤隻影響一小部分使用者),並提供資料重算工具(以備舊的計算出錯)。
|
||||
* 配置詳細和明確的監控,比如效能指標和錯誤率。 在其他工程學科中這指的是 **遙測(telemetry)**(一旦火箭離開了地面,遙測技術對於跟蹤發生的事情和理解失敗是至關重要的)。監控可以向我們發出預警訊號,並允許我們檢查是否有任何地方違反了假設和約束。當出現問題時,指標資料對於問題診斷是非常寶貴的。
|
||||
* 允許從人為錯誤中簡單快速地恢復,以最大限度地減少失效情況帶來的影響。例如,快速回滾配置變更,分批發布新程式碼(以便任何意外錯誤隻影響一小部分使用者),並提供資料重算工具(以備舊的計算出錯)。
|
||||
* 配置詳細和明確的監控,比如效能指標和錯誤率。在其他工程學科中這指的是 **遙測(telemetry)**(一旦火箭離開了地面,遙測技術對於跟蹤發生的事情和理解失敗是至關重要的)。監控可以向我們發出預警訊號,並允許我們檢查是否有任何地方違反了假設和約束。當出現問題時,指標資料對於問題診斷是非常寶貴的。
|
||||
* 良好的管理實踐與充分的培訓 —— 一個複雜而重要的方面,但超出了本書的範圍。
|
||||
|
||||
### 可靠性有多重要?
|
||||
@ -162,7 +162,7 @@
|
||||
|
||||
處理每秒 12,000 次寫入(發推文的速率峰值)還是很簡單的。然而推特的伸縮性挑戰並不是主要來自推特量,而是來自 **扇出(fan-out)**[^ii]—— 每個使用者關注了很多人,也被很多人關注。
|
||||
|
||||
[^ii]: 扇出:從電子工程學中借用的術語,它描述了輸入連線到另一個門輸出的邏輯閘數量。 輸出需要提供足夠的電流來驅動所有連線的輸入。 在事務處理系統中,我們使用它來描述為了服務一個傳入請求而需要執行其他服務的請求數量。
|
||||
[^ii]: 扇出:從電子工程學中借用的術語,它描述了輸入連線到另一個門輸出的邏輯閘數量。輸出需要提供足夠的電流來驅動所有連線的輸入。在事務處理系統中,我們使用它來描述為了服務一個傳入請求而需要執行其他服務的請求數量。
|
||||
|
||||
大體上講,這一對操作有兩種實現方式。
|
||||
|
||||
@ -180,7 +180,7 @@
|
||||
|
||||
**圖 1-2 推特主頁時間線的關係型模式簡單實現**
|
||||
|
||||
2. 為每個使用者的主頁時間線維護一個快取,就像每個使用者的推文收件箱([圖 1-3](../img/fig1-3.png))。 當一個使用者釋出推文時,查詢所有關注該使用者的人,並將新的推文插入到每個主頁時間線快取中。 因此讀取主頁時間線的請求開銷很小,因為結果已經提前計算好了。
|
||||
2. 為每個使用者的主頁時間線維護一個快取,就像每個使用者的推文收件箱([圖 1-3](../img/fig1-3.png))。當一個使用者釋出推文時,查詢所有關注該使用者的人,並將新的推文插入到每個主頁時間線快取中。因此讀取主頁時間線的請求開銷很小,因為結果已經提前計算好了。
|
||||
|
||||
![](../img/fig1-3.png)
|
||||
|
||||
@ -205,7 +205,7 @@
|
||||
|
||||
對於 Hadoop 這樣的批處理系統,通常關心的是 **吞吐量(throughput)**,即每秒可以處理的記錄數量,或者在特定規模資料集上執行作業的總時間 [^iii]。對於線上系統,通常更重要的是服務的 **響應時間(response time)**,即客戶端傳送請求到接收響應之間的時間。
|
||||
|
||||
[^iii]: 理想情況下,批次作業的執行時間是資料集的大小除以吞吐量。 在實踐中由於資料傾斜(資料不是均勻分佈在每個工作程序中),需要等待最慢的任務完成,所以執行時間往往更長。
|
||||
[^iii]: 理想情況下,批次作業的執行時間是資料集的大小除以吞吐量。在實踐中由於資料傾斜(資料不是均勻分佈在每個工作程序中),需要等待最慢的任務完成,所以執行時間往往更長。
|
||||
|
||||
> #### 延遲和響應時間
|
||||
>
|
||||
@ -219,7 +219,7 @@
|
||||
|
||||
**圖 1-4 展示了一個服務 100 次請求響應時間的均值與百分位數**
|
||||
|
||||
通常報表都會展示服務的平均響應時間。 (嚴格來講 “平均” 一詞並不指代任何特定公式,但實際上它通常被理解為 **算術平均值(arithmetic mean)**:給定 n 個值,加起來除以 n )。然而如果你想知道 “**典型(typical)**” 響應時間,那麼平均值並不是一個非常好的指標,因為它不能告訴你有多少使用者實際上經歷了這個延遲。
|
||||
通常報表都會展示服務的平均響應時間。(嚴格來講 “平均” 一詞並不指代任何特定公式,但實際上它通常被理解為 **算術平均值(arithmetic mean)**:給定 n 個值,加起來除以 n )。然而如果你想知道 “**典型(typical)**” 響應時間,那麼平均值並不是一個非常好的指標,因為它不能告訴你有多少使用者實際上經歷了這個延遲。
|
||||
|
||||
通常使用 **百分位點(percentiles)** 會更好。如果將響應時間列表按最快到最慢排序,那麼 **中位數(median)** 就在正中間:舉個例子,如果你的響應時間中位數是 200 毫秒,這意味著一半請求的返回時間少於 200 毫秒,另一半比這個要長。
|
||||
|
||||
@ -231,7 +231,7 @@
|
||||
|
||||
另一方面,最佳化第 99.99 百分位點(一萬個請求中最慢的一個)被認為太昂貴了,不能為亞馬遜的目標帶來足夠好處。減小高百分位點處的響應時間相當困難,因為它很容易受到隨機事件的影響,這超出了控制範圍,而且效益也很小。
|
||||
|
||||
百分位點通常用於 **服務級別目標(SLO, service level objectives)** 和 **服務級別協議(SLA, service level agreements)**,即定義服務預期效能和可用性的合同。 SLA 可能會宣告,如果服務響應時間的中位數小於 200 毫秒,且 99.9 百分位點低於 1 秒,則認為服務工作正常(如果響應時間更長,就認為服務不達標)。這些指標為客戶設定了期望值,並允許客戶在 SLA 未達標的情況下要求退款。
|
||||
百分位點通常用於 **服務級別目標(SLO, service level objectives)** 和 **服務級別協議(SLA, service level agreements)**,即定義服務預期效能和可用性的合同。SLA 可能會宣告,如果服務響應時間的中位數小於 200 毫秒,且 99.9 百分位點低於 1 秒,則認為服務工作正常(如果響應時間更長,就認為服務不達標)。這些指標為客戶設定了期望值,並允許客戶在 SLA 未達標的情況下要求退款。
|
||||
|
||||
**排隊延遲(queueing delay)** 通常佔了高百分位點處響應時間的很大一部分。由於伺服器只能並行處理少量的事務(如受其 CPU 核數的限制),所以只要有少量緩慢的請求就能阻礙後續請求的處理,這種效應有時被稱為 **頭部阻塞(head-of-line blocking)** 。即使後續請求在伺服器上處理的非常迅速,由於需要等待先前請求完成,客戶端最終看到的是緩慢的總體響應時間。因為存在這種效應,測量客戶端的響應時間非常重要。
|
||||
|
||||
@ -330,11 +330,11 @@
|
||||
|
||||
因為複雜度導致維護困難時,預算和時間安排通常會超支。在複雜的軟體中進行變更,引入錯誤的風險也更大:當開發人員難以理解系統時,隱藏的假設、無意的後果和意外的互動就更容易被忽略。相反,降低複雜度能極大地提高軟體的可維護性,因此簡單性應該是構建系統的一個關鍵目標。
|
||||
|
||||
簡化系統並不一定意味著減少功能;它也可以意味著消除 **額外的(accidental)** 的複雜度。 Moseley 和 Marks【32】把 **額外複雜度** 定義為:由具體實現中湧現,而非(從使用者視角看,系統所解決的)問題本身固有的複雜度。
|
||||
簡化系統並不一定意味著減少功能;它也可以意味著消除 **額外的(accidental)** 的複雜度。Moseley 和 Marks【32】把 **額外複雜度** 定義為:由具體實現中湧現,而非(從使用者視角看,系統所解決的)問題本身固有的複雜度。
|
||||
|
||||
用於消除 **額外複雜度** 的最好工具之一是 **抽象(abstraction)**。一個好的抽象可以將大量實現細節隱藏在一個乾淨,簡單易懂的外觀下面。一個好的抽象也可以廣泛用於各類不同應用。比起重複造很多輪子,重用抽象不僅更有效率,而且有助於開發高質量的軟體。抽象元件的質量改進將使所有使用它的應用受益。
|
||||
|
||||
例如,高階程式語言是一種抽象,隱藏了機器碼、CPU 暫存器和系統呼叫。 SQL 也是一種抽象,隱藏了複雜的磁碟 / 記憶體資料結構、來自其他客戶端的併發請求、崩潰後的不一致性。當然在用高階語言程式設計時,我們仍然用到了機器碼;只不過沒有 **直接(directly)** 使用罷了,正是因為程式語言的抽象,我們才不必去考慮這些實現細節。
|
||||
例如,高階程式語言是一種抽象,隱藏了機器碼、CPU 暫存器和系統呼叫。SQL 也是一種抽象,隱藏了複雜的磁碟 / 記憶體資料結構、來自其他客戶端的併發請求、崩潰後的不一致性。當然在用高階語言程式設計時,我們仍然用到了機器碼;只不過沒有 **直接(directly)** 使用罷了,正是因為程式語言的抽象,我們才不必去考慮這些實現細節。
|
||||
|
||||
抽象可以幫助我們將系統的複雜度控制在可管理的水平,不過,找到好的抽象是非常困難的。在分散式系統領域雖然有許多好的演算法,但我們並不清楚它們應該打包成什麼樣抽象。
|
||||
|
||||
@ -344,7 +344,7 @@
|
||||
|
||||
系統的需求永遠不變,基本是不可能的。更可能的情況是,它們處於常態的變化中,例如:你瞭解了新的事實、出現意想不到的應用場景、業務優先順序發生變化、使用者要求新功能、新平臺取代舊平臺、法律或監管要求發生變化、系統增長迫使架構變化等。
|
||||
|
||||
在組織流程方面, **敏捷(agile)** 工作模式為適應變化提供了一個框架。敏捷社群還開發了對在頻繁變化的環境中開發軟體很有幫助的技術工具和模式,如 **測試驅動開發(TDD, test-driven development)** 和 **重構(refactoring)** 。
|
||||
在組織流程方面,**敏捷(agile)** 工作模式為適應變化提供了一個框架。敏捷社群還開發了對在頻繁變化的環境中開發軟體很有幫助的技術工具和模式,如 **測試驅動開發(TDD, test-driven development)** 和 **重構(refactoring)** 。
|
||||
|
||||
這些敏捷技術的大部分討論都集中在相當小的規模(同一個應用中的幾個程式碼檔案)。本書將探索在更大資料系統層面上提高敏捷性的方法,可能由幾個不同的應用或服務組成。例如,為了將裝配主頁時間線的方法從方法 1 變為方法 2,你會如何 “重構” 推特的架構 ?
|
||||
|
||||
@ -358,7 +358,7 @@
|
||||
一個應用必須滿足各種需求才稱得上有用。有一些 **功能需求**(functional requirements,即它應該做什麼,比如允許以各種方式儲存,檢索,搜尋和處理資料)以及一些 **非功能性需求**(nonfunctional,即通用屬性,例如安全性、可靠性、合規性、可伸縮性、相容性和可維護性)。在本章詳細討論了可靠性,可伸縮性和可維護性。
|
||||
|
||||
|
||||
**可靠性(Reliability)** 意味著即使發生故障,系統也能正常工作。故障可能發生在硬體(通常是隨機的和不相關的)、軟體(通常是系統性的 Bug,很難處理)和人類(不可避免地時不時出錯)。 **容錯技術** 可以對終端使用者隱藏某些型別的故障。
|
||||
**可靠性(Reliability)** 意味著即使發生故障,系統也能正常工作。故障可能發生在硬體(通常是隨機的和不相關的)、軟體(通常是系統性的 Bug,很難處理)和人類(不可避免地時不時出錯)。**容錯技術** 可以對終端使用者隱藏某些型別的故障。
|
||||
|
||||
**可伸縮性(Scalability)** 意味著即使在負載增加的情況下也有保持效能的策略。為了討論可伸縮性,我們首先需要定量描述負載和效能的方法。我們簡要了解了推特主頁時間線的例子,介紹描述負載的方法,並將響應時間百分位點作為衡量效能的一種方式。在可伸縮的系統中可以新增 **處理容量(processing capacity)** 以在高負載下保持可靠。
|
||||
|
||||
|
@ -32,7 +32,7 @@ Web 和越來越多的基於 HTTP/REST 的 API 使互動的請求 / 響應風格
|
||||
|
||||
與多年前為資料倉庫開發的並行處理系統【3,4】相比,MapReduce 是一個相當低級別的程式設計模型,但它使得在商用硬體上能進行的處理規模邁上一個新的臺階。雖然 MapReduce 的重要性正在下降【5】,但它仍然值得去理解,因為它描繪了一幅關於批處理為什麼有用,以及如何做到有用的清晰圖景。
|
||||
|
||||
實際上,批處理是一種非常古老的計算方式。早在可程式設計數字計算機誕生之前,打孔卡製表機(例如 1890 年美國人口普查【6】中使用的霍爾里斯機)實現了半機械化的批處理形式,從大量輸入中彙總計算。 Map-Reduce 與 1940 年代和 1950 年代廣泛用於商業資料處理的機電 IBM 卡片分類機器有著驚人的相似之處【7】。正如我們所說,歷史總是在不斷重複自己。
|
||||
實際上,批處理是一種非常古老的計算方式。早在可程式設計數字計算機誕生之前,打孔卡製表機(例如 1890 年美國人口普查【6】中使用的霍爾里斯機)實現了半機械化的批處理形式,從大量輸入中彙總計算。Map-Reduce 與 1940 年代和 1950 年代廣泛用於商業資料處理的機電 IBM 卡片分類機器有著驚人的相似之處【7】。正如我們所說,歷史總是在不斷重複自己。
|
||||
|
||||
在本章中,我們將瞭解 MapReduce 和其他一些批處理演算法和框架,並探索它們在現代資料系統中的作用。但首先我們將看看使用標準 Unix 工具的資料處理。即使你已經熟悉了它們,Unix 的哲學也值得一讀,Unix 的思想和經驗教訓可以遷移到大規模、異構的分散式資料系統中。
|
||||
|
||||
@ -59,9 +59,9 @@ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36"
|
||||
|
||||
### 簡單日誌分析
|
||||
|
||||
很多工具可以從這些日誌檔案生成關於網站流量的漂亮的報告,但為了練手,讓我們使用基本的 Unix 功能建立自己的工具。 例如,假設你想在你的網站上找到五個最受歡迎的網頁。 則可以在 Unix shell 中這樣做:[^i]
|
||||
很多工具可以從這些日誌檔案生成關於網站流量的漂亮的報告,但為了練手,讓我們使用基本的 Unix 功能建立自己的工具。例如,假設你想在你的網站上找到五個最受歡迎的網頁。則可以在 Unix shell 中這樣做:[^i]
|
||||
|
||||
[^i]: 有些人認為 `cat` 這裡並沒有必要,因為輸入檔案可以直接作為 awk 的引數。 但這種寫法讓線性管道更為顯眼。
|
||||
[^i]: 有些人認為 `cat` 這裡並沒有必要,因為輸入檔案可以直接作為 awk 的引數。但這種寫法讓線性管道更為顯眼。
|
||||
|
||||
```bash
|
||||
cat /var/log/nginx/access.log | #1
|
||||
@ -75,7 +75,7 @@ cat /var/log/nginx/access.log | #1
|
||||
1. 讀取日誌檔案
|
||||
2. 將每一行按空格分割成不同的欄位,每行只輸出第七個欄位,恰好是請求的 URL。在我們的例子中是 `/css/typography.css`。
|
||||
3. 按字母順序排列請求的 URL 列表。如果某個 URL 被請求過 n 次,那麼排序後,檔案將包含連續重複出現 n 次的該 URL。
|
||||
4. `uniq` 命令透過檢查兩個相鄰的行是否相同來過濾掉輸入中的重複行。 `-c` 則表示還要輸出一個計數器:對於每個不同的 URL,它會報告輸入中出現該 URL 的次數。
|
||||
4. `uniq` 命令透過檢查兩個相鄰的行是否相同來過濾掉輸入中的重複行。`-c` 則表示還要輸出一個計數器:對於每個不同的 URL,它會報告輸入中出現該 URL 的次數。
|
||||
5. 第二種排序按每行起始處的數字(`-n`)排序,這是 URL 的請求次數。然後逆序(`-r`)返回結果,大的數字在前。
|
||||
6. 最後,只輸出前五行(`-n 5`),並丟棄其餘的。該系列命令的輸出如下所示:
|
||||
|
||||
@ -118,11 +118,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
#### 排序 VS 記憶體中的聚合
|
||||
|
||||
Ruby 指令碼在記憶體中儲存了一個 URL 的雜湊表,將每個 URL 對映到它出現的次數。 Unix 管道沒有這樣的雜湊表,而是依賴於對 URL 列表的排序,在這個 URL 列表中,同一個 URL 的只是簡單地重複出現。
|
||||
Ruby 指令碼在記憶體中儲存了一個 URL 的雜湊表,將每個 URL 對映到它出現的次數。Unix 管道沒有這樣的雜湊表,而是依賴於對 URL 列表的排序,在這個 URL 列表中,同一個 URL 的只是簡單地重複出現。
|
||||
|
||||
哪種方法更好?這取決於你有多少個不同的 URL。對於大多數中小型網站,你可能可以為所有不同網址提供一個計數器(假設我們使用 1GB 記憶體)。在此例中,作業的 **工作集**(working set,即作業需要隨機訪問的記憶體大小)僅取決於不同 URL 的數量:如果日誌中只有單個 URL,重複出現一百萬次,則散列表所需的空間表就只有一個 URL 加上一個計數器的大小。當工作集足夠小時,記憶體散列表表現良好,甚至在效能較差的膝上型電腦上也可以正常工作。
|
||||
|
||||
另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)” 中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。 歸併排序具有在磁碟上執行良好的順序訪問模式。 (請記住,針對順序 I/O 進行最佳化是 [第三章](ch3.md) 中反覆出現的主題,相同的模式在此重現)
|
||||
另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)” 中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。歸併排序具有在磁碟上執行良好的順序訪問模式。(請記住,針對順序 I/O 進行最佳化是 [第三章](ch3.md) 中反覆出現的主題,相同的模式在此重現)
|
||||
|
||||
GNU Coreutils(Linux)中的 `sort` 程式透過溢位至磁碟的方式來自動應對大於記憶體的資料集,並能同時使用多個 CPU 核進行並行排序【9】。這意味著我們之前看到的簡單的 Unix 命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。
|
||||
|
||||
@ -142,7 +142,7 @@ Unix 管道的發明者道格・麥克羅伊(Doug McIlroy)在 1964 年首先
|
||||
|
||||
`sort` 工具是一個很好的例子。可以說它比大多數程式語言標準庫中的實現(它們不會利用磁碟或使用多執行緒,即使這樣做有很大好處)要更好。然而,單獨使用 `sort` 幾乎沒什麼用。它只能與其他 Unix 工具(如 `uniq`)結合使用。
|
||||
|
||||
像 `bash` 這樣的 Unix shell 可以讓我們輕鬆地將這些小程式組合成令人訝異的強大資料處理任務。儘管這些程式中有很多是由不同人群編寫的,但它們可以靈活地結合在一起。 Unix 如何實現這種可組合性?
|
||||
像 `bash` 這樣的 Unix shell 可以讓我們輕鬆地將這些小程式組合成令人訝異的強大資料處理任務。儘管這些程式中有很多是由不同人群編寫的,但它們可以靈活地結合在一起。Unix 如何實現這種可組合性?
|
||||
|
||||
#### 統一的介面
|
||||
|
||||
@ -150,11 +150,11 @@ Unix 管道的發明者道格・麥克羅伊(Doug McIlroy)在 1964 年首先
|
||||
|
||||
在 Unix 中,這種介面是一個 **檔案**(file,更準確地說,是一個檔案描述符)。一個檔案只是一串有序的位元組序列。因為這是一個非常簡單的介面,所以可以使用相同的介面來表示許多不同的東西:檔案系統上的真實檔案,到另一個程序(Unix 套接字,stdin,stdout)的通訊通道,裝置驅動程式(比如 `/dev/audio` 或 `/dev/lp0`),表示 TCP 連線的套接字,等等。很容易將這些設計視為理所當然的,但實際上能讓這些差異巨大的東西共享一個統一的介面是非常厲害的,這使得它們可以很容易地連線在一起 [^ii]。
|
||||
|
||||
[^ii]: 統一介面的另一個例子是 URL 和 HTTP,這是 Web 的基石。 一個 URL 標識一個網站上的一個特定的東西(資源),你可以連結到任何其他網站的任何網址。 具有網路瀏覽器的使用者因此可以透過跟隨連結在網站之間無縫跳轉,即使伺服器可能由完全不相關的組織維護。 這個原則現在似乎非常明顯,但它卻是網路取能取得今天成就的關鍵。 之前的系統並不是那麼統一:例如,在公告板系統(BBS)時代,每個系統都有自己的電話號碼和波特率配置。 從一個 BBS 到另一個 BBS 的引用必須以電話號碼和調變解調器設定的形式;使用者將不得不掛斷,撥打其他 BBS,然後手動找到他們正在尋找的資訊。 直接連結到另一個 BBS 內的一些內容當時是不可能的。
|
||||
[^ii]: 統一介面的另一個例子是 URL 和 HTTP,這是 Web 的基石。一個 URL 標識一個網站上的一個特定的東西(資源),你可以連結到任何其他網站的任何網址。具有網路瀏覽器的使用者因此可以透過跟隨連結在網站之間無縫跳轉,即使伺服器可能由完全不相關的組織維護。這個原則現在似乎非常明顯,但它卻是網路取能取得今天成就的關鍵。之前的系統並不是那麼統一:例如,在公告板系統(BBS)時代,每個系統都有自己的電話號碼和波特率配置。從一個 BBS 到另一個 BBS 的引用必須以電話號碼和調變解調器設定的形式;使用者將不得不掛斷,撥打其他 BBS,然後手動找到他們正在尋找的資訊。直接連結到另一個 BBS 內的一些內容當時是不可能的。
|
||||
|
||||
按照慣例,許多(但不是全部)Unix 程式將這個位元組序列視為 ASCII 文字。我們的日誌分析示例使用了這個事實:`awk`、`sort`、`uniq` 和 `head` 都將它們的輸入檔案視為由 `\n`(換行符,ASCII `0x0A`)字元分隔的記錄列表。`\n` 的選擇是任意的 —— 可以說,ASCII 記錄分隔符 `0x1E` 本來就是一個更好的選擇,因為它是為了這個目的而設計的【14】,但是無論如何,所有這些程式都使用相同的記錄分隔符允許它們互操作。
|
||||
|
||||
每條記錄(即一行輸入)的解析則更加模糊。 Unix 工具通常透過空白或製表符將行分割成欄位,但也使用 CSV(逗號分隔),管道分隔和其他編碼。即使像 `xargs` 這樣一個相當簡單的工具也有六個命令列選項,用於指定如何解析輸入。
|
||||
每條記錄(即一行輸入)的解析則更加模糊。Unix 工具通常透過空白或製表符將行分割成欄位,但也使用 CSV(逗號分隔),管道分隔和其他編碼。即使像 `xargs` 這樣一個相當簡單的工具也有六個命令列選項,用於指定如何解析輸入。
|
||||
|
||||
ASCII 文字的統一介面大多數時候都能工作,但它不是很優雅:我們的日誌分析示例使用 `{print $7}` 來提取網址,這樣可讀性不是很好。在理想的世界中可能是 `{print $request_url}` 或類似的東西。我們稍後會回顧這個想法。
|
||||
|
||||
@ -169,13 +169,13 @@ ASCII 文字的統一介面大多數時候都能工作,但它不是很優雅
|
||||
|
||||
Unix 工具的另一個特點是使用標準輸入(`stdin`)和標準輸出(`stdout`)。如果你執行一個程式,而不指定任何其他的東西,標準輸入來自鍵盤,標準輸出指向螢幕。但是,你也可以從檔案輸入和 / 或將輸出重定向到檔案。管道允許你將一個程序的標準輸出附加到另一個程序的標準輸入(有個小記憶體緩衝區,而不需要將整個中間資料流寫入磁碟)。
|
||||
|
||||
如果需要,程式仍然可以直接讀取和寫入檔案,但 Unix 方法在程式不關心特定的檔案路徑、只使用標準輸入和標準輸出時效果最好。這允許 shell 使用者以任何他們想要的方式連線輸入和輸出;該程式不知道或不關心輸入來自哪裡以及輸出到哪裡。 (人們可以說這是一種 **松耦合(loose coupling)**,**晚期繫結(late binding)**【15】或 **控制反轉(inversion of control)**【16】)。將輸入 / 輸出佈線與程式邏輯分開,可以將小工具組合成更大的系統。
|
||||
如果需要,程式仍然可以直接讀取和寫入檔案,但 Unix 方法在程式不關心特定的檔案路徑、只使用標準輸入和標準輸出時效果最好。這允許 shell 使用者以任何他們想要的方式連線輸入和輸出;該程式不知道或不關心輸入來自哪裡以及輸出到哪裡。(人們可以說這是一種 **松耦合(loose coupling)**,**晚期繫結(late binding)**【15】或 **控制反轉(inversion of control)**【16】)。將輸入 / 輸出佈線與程式邏輯分開,可以將小工具組合成更大的系統。
|
||||
|
||||
你甚至可以編寫自己的程式,並將它們與作業系統提供的工具組合在一起。你的程式只需要從標準輸入讀取輸入,並將輸出寫入標準輸出,它就可以加入資料處理的管道中。在日誌分析示例中,你可以編寫一個將 Usage-Agent 字串轉換為更靈敏的瀏覽器識別符號,或者將 IP 地址轉換為國家程式碼的工具,並將其插入管道。`sort` 程式並不關心它是否與作業系統的另一部分或者你寫的程式通訊。
|
||||
|
||||
但是,使用 `stdin` 和 `stdout` 能做的事情是有限的。需要多個輸入或輸出的程式雖然可能,卻非常棘手。你沒法將程式的輸出管道連線至網路連線中【17,18】[^iii] 。如果程式直接開啟檔案進行讀取和寫入,或者將另一個程式作為子程序啟動,或者開啟網路連線,那麼 I/O 的佈線就取決於程式本身了。它仍然可以被配置(例如透過命令列選項),但在 Shell 中對輸入和輸出進行佈線的靈活性就少了。
|
||||
|
||||
[^iii]: 除了使用一個單獨的工具,如 `netcat` 或 `curl`。 Unix 起初試圖將所有東西都表示為檔案,但是 BSD 套接字 API 偏離了這個慣例【17】。研究用作業系統 Plan 9 和 Inferno 在使用檔案方面更加一致:它們將 TCP 連線表示為 `/net/tcp` 中的檔案【18】。
|
||||
[^iii]: 除了使用一個單獨的工具,如 `netcat` 或 `curl`。Unix 起初試圖將所有東西都表示為檔案,但是 BSD 套接字 API 偏離了這個慣例【17】。研究用作業系統 Plan 9 和 Inferno 在使用檔案方面更加一致:它們將 TCP 連線表示為 `/net/tcp` 中的檔案【18】。
|
||||
|
||||
|
||||
#### 透明度和實驗
|
||||
@ -230,7 +230,7 @@ MapReduce 是一個程式設計框架,你可以使用它編寫程式碼來處
|
||||
|
||||
* Reducer
|
||||
|
||||
MapReduce 框架拉取由 Mapper 生成的鍵值對,收集屬於同一個鍵的所有值,並在這組值上迭代呼叫 Reducer。 Reducer 可以產生輸出記錄(例如相同 URL 的出現次數)。
|
||||
MapReduce 框架拉取由 Mapper 生成的鍵值對,收集屬於同一個鍵的所有值,並在這組值上迭代呼叫 Reducer。Reducer 可以產生輸出記錄(例如相同 URL 的出現次數)。
|
||||
|
||||
在 Web 伺服器日誌的例子中,我們在第 5 步中有第二個 `sort` 命令,它按請求數對 URL 進行排序。在 MapReduce 中,如果你需要第二個排序階段,則可以透過編寫第二個 MapReduce 作業並將第一個作業的輸出用作第二個作業的輸入來實現它。這樣看來,Mapper 的作用是將資料放入一個適合排序的表單中,並且 Reducer 的作用是處理已排序的資料。
|
||||
|
||||
@ -242,7 +242,7 @@ MapReduce 與 Unix 命令管道的主要區別在於,MapReduce 可以在多臺
|
||||
|
||||
[圖 10-1](../img/fig10-1.png) 顯示了 Hadoop MapReduce 作業中的資料流。其並行化基於分割槽(請參閱 [第六章](ch6.md)):作業的輸入通常是 HDFS 中的一個目錄,輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽,可以單獨處理 map 任務([圖 10-1](../img/fig10-1.png) 中的 m1,m2 和 m3 標記)。
|
||||
|
||||
每個輸入檔案的大小通常是數百兆位元組。 MapReduce 排程器(圖中未顯示)試圖在其中一臺儲存輸入檔案副本的機器上執行每個 Mapper,只要該機器有足夠的備用 RAM 和 CPU 資源來執行 Mapper 任務【26】。這個原則被稱為 **將計算放在資料附近**【27】:它節省了透過網路複製輸入檔案的開銷,減少網路負載並增加區域性。
|
||||
每個輸入檔案的大小通常是數百兆位元組。MapReduce 排程器(圖中未顯示)試圖在其中一臺儲存輸入檔案副本的機器上執行每個 Mapper,只要該機器有足夠的備用 RAM 和 CPU 資源來執行 Mapper 任務【26】。這個原則被稱為 **將計算放在資料附近**【27】:它節省了透過網路複製輸入檔案的開銷,減少網路負載並增加區域性。
|
||||
|
||||
![](../img/fig10-1.png)
|
||||
|
||||
@ -286,11 +286,11 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
|
||||
|
||||
當 MapReduce 作業被賦予一組檔案作為輸入時,它讀取所有這些檔案的全部內容;資料庫會將這種操作稱為 **全表掃描**。如果你只想讀取少量的記錄,則全表掃描與索引查詢相比,代價非常高昂。但是在分析查詢中(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”),通常需要計算大量記錄的聚合。在這種情況下,特別是如果能在多臺機器上並行處理時,掃描整個輸入可能是相當合理的事情。
|
||||
|
||||
當我們在批處理的語境中討論連線時,我們指的是在資料集中解析某種關聯的全量存在。 例如我們假設一個作業是同時處理所有使用者的資料,而非僅僅是為某個特定使用者查詢資料(而這能透過索引更高效地完成)。
|
||||
當我們在批處理的語境中討論連線時,我們指的是在資料集中解析某種關聯的全量存在。例如我們假設一個作業是同時處理所有使用者的資料,而非僅僅是為某個特定使用者查詢資料(而這能透過索引更高效地完成)。
|
||||
|
||||
#### 示例:使用者活動事件分析
|
||||
|
||||
[圖 10-2](../img/fig10-2.png) 給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為 **活動事件**,即 activity events,或 **點選流資料**,即 clickstream data),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
|
||||
[圖 10-2](../img/fig10-2.png) 給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為 **活動事件**,即 activity events,或 **點選流資料**,即 clickstream data),右側是使用者資料庫。你可以將此示例看作是星型模式的一部分(請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
|
||||
|
||||
![](../img/fig10-2.png)
|
||||
|
||||
@ -312,9 +312,9 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
|
||||
|
||||
**圖 10-3 在使用者 ID 上進行的 Reduce 端連線。如果輸入資料集分割槽為多個檔案,則每個分割槽都會被多個 Mapper 並行處理**
|
||||
|
||||
當 MapReduce 框架透過鍵對 Mapper 輸出進行分割槽,然後對鍵值對進行排序時,效果是具有相同 ID 的所有活動事件和使用者記錄在 Reducer 輸入中彼此相鄰。 Map-Reduce 作業甚至可以也讓這些記錄排序,使 Reducer 總能先看到來自使用者資料庫的記錄,緊接著是按時間戳順序排序的活動事件 —— 這種技術被稱為 **二次排序(secondary sort)**【26】。
|
||||
當 MapReduce 框架透過鍵對 Mapper 輸出進行分割槽,然後對鍵值對進行排序時,效果是具有相同 ID 的所有活動事件和使用者記錄在 Reducer 輸入中彼此相鄰。Map-Reduce 作業甚至可以也讓這些記錄排序,使 Reducer 總能先看到來自使用者資料庫的記錄,緊接著是按時間戳順序排序的活動事件 —— 這種技術被稱為 **二次排序(secondary sort)**【26】。
|
||||
|
||||
然後 Reducer 可以容易地執行實際的連線邏輯:每個使用者 ID 都會被呼叫一次 Reducer 函式,且因為二次排序,第一個值應該是來自使用者資料庫的出生日期記錄。 Reducer 將出生日期儲存在區域性變數中,然後使用相同的使用者 ID 遍歷活動事件,輸出 **已觀看網址** 和 **觀看者年齡** 的結果對。隨後的 Map-Reduce 作業可以計算每個 URL 的檢視者年齡分佈,並按年齡段進行聚集。
|
||||
然後 Reducer 可以容易地執行實際的連線邏輯:每個使用者 ID 都會被呼叫一次 Reducer 函式,且因為二次排序,第一個值應該是來自使用者資料庫的出生日期記錄。Reducer 將出生日期儲存在區域性變數中,然後使用相同的使用者 ID 遍歷活動事件,輸出 **已觀看網址** 和 **觀看者年齡** 的結果對。隨後的 Map-Reduce 作業可以計算每個 URL 的檢視者年齡分佈,並按年齡段進行聚集。
|
||||
|
||||
由於 Reducer 一次處理一個特定使用者 ID 的所有記錄,因此一次只需要將一條使用者記錄儲存在記憶體中,而不需要透過網路發出任何請求。這個演算法被稱為 **排序合併連線(sort-merge join)**,因為 Mapper 的輸出是按鍵排序的,然後 Reducer 將來自連線兩側的有序記錄列表合併在一起。
|
||||
|
||||
@ -348,7 +348,7 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
|
||||
|
||||
如果連線的輸入存在熱鍵,可以使用一些演算法進行補償。例如,Pig 中的 **偏斜連線(skewed join)** 方法首先執行一個抽樣作業(Sampling Job)來確定哪些鍵是熱鍵【39】。連線實際執行時,Mapper 會將熱鍵的關聯記錄 **隨機**(相對於傳統 MapReduce 基於鍵雜湊的確定性方法)傳送到幾個 Reducer 之一。對於另外一側的連線輸入,與熱鍵相關的記錄需要被複制到 **所有** 處理該鍵的 Reducer 上【40】。
|
||||
|
||||
這種技術將處理熱鍵的工作分散到多個 Reducer 上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個 Reducer 上。 Crunch 中的 **分片連線(sharded join)** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)” 中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
|
||||
這種技術將處理熱鍵的工作分散到多個 Reducer 上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個 Reducer 上。Crunch 中的 **分片連線(sharded join)** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)” 中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
|
||||
|
||||
Hive 的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用 Map 端連線(請參閱下一節)。
|
||||
|
||||
@ -373,7 +373,7 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
|
||||
|
||||
參與連線的較大輸入的每個檔案塊各有一個 Mapper(在 [圖 10-2](../img/fig10-2.png) 的例子中活動事件是較大的輸入)。每個 Mapper 都會將較小輸入整個載入到記憶體中。
|
||||
|
||||
這種簡單有效的演算法被稱為 **廣播雜湊連線(broadcast hash join)**:**廣播** 一詞反映了這樣一個事實,每個連線較大輸入端分割槽的 Mapper 都會將較小輸入端資料集整個讀入記憶體中(所以較小輸入實際上 “廣播” 到較大資料的所有分割槽上),**雜湊** 一詞反映了它使用一個散列表。 Pig(名為 “**複製連結(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支援這種連線。它也被諸如 Impala 的資料倉庫查詢引擎使用【41】。
|
||||
這種簡單有效的演算法被稱為 **廣播雜湊連線(broadcast hash join)**:**廣播** 一詞反映了這樣一個事實,每個連線較大輸入端分割槽的 Mapper 都會將較小輸入端資料集整個讀入記憶體中(所以較小輸入實際上 “廣播” 到較大資料的所有分割槽上),**雜湊** 一詞反映了它使用一個散列表。Pig(名為 “**複製連結(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支援這種連線。它也被諸如 Impala 的資料倉庫查詢引擎使用【41】。
|
||||
|
||||
除了將較小的連線輸入載入到記憶體散列表中,另一種方法是將較小輸入儲存在本地磁碟上的只讀索引中【42】。索引中經常使用的部分將保留在作業系統的頁面快取中,因而這種方法可以提供與記憶體散列表幾乎一樣快的隨機查詢效能,但實際上並不需要資料集能放入記憶體中。
|
||||
|
||||
@ -412,7 +412,7 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
|
||||
|
||||
#### 建立搜尋索引
|
||||
|
||||
Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】,但如果從構建搜尋索引的角度來看,更能幫助理解 MapReduce。 (直至今日,Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】)
|
||||
Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】,但如果從構建搜尋索引的角度來看,更能幫助理解 MapReduce。(直至今日,Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】)
|
||||
|
||||
我們在 “[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)” 中簡要地瞭解了 Lucene 這樣的全文搜尋索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件 ID 列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名、糾正拼寫錯誤、解析同義詞等等 —— 但這個原則是成立的。
|
||||
|
||||
@ -450,7 +450,7 @@ Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為
|
||||
|
||||
MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不可變且避免副作用(如寫入外部資料庫),批處理作業不僅實現了良好的效能,而且更容易維護:
|
||||
|
||||
- 如果在程式碼中引入了一個錯誤,而輸出錯誤或損壞了,則可以簡單地回滾到程式碼的先前版本,然後重新執行該作業,輸出將重新被糾正。或者,甚至更簡單,你可以將舊的輸出儲存在不同的目錄中,然後切換回原來的目錄。具有讀寫事務的資料庫沒有這個屬性:如果你部署了錯誤的程式碼,將錯誤的資料寫入資料庫,那麼回滾程式碼將無法修復資料庫中的資料。 (能夠從錯誤程式碼中恢復的概念被稱為 **人類容錯(human fault tolerance)**【50】)
|
||||
- 如果在程式碼中引入了一個錯誤,而輸出錯誤或損壞了,則可以簡單地回滾到程式碼的先前版本,然後重新執行該作業,輸出將重新被糾正。或者,甚至更簡單,你可以將舊的輸出儲存在不同的目錄中,然後切換回原來的目錄。具有讀寫事務的資料庫沒有這個屬性:如果你部署了錯誤的程式碼,將錯誤的資料寫入資料庫,那麼回滾程式碼將無法修復資料庫中的資料。(能夠從錯誤程式碼中恢復的概念被稱為 **人類容錯(human fault tolerance)**【50】)
|
||||
- 由於回滾很容易,比起在錯誤意味著不可挽回的傷害的環境,功能開發進展能快很多。這種 **最小化不可逆性(minimizing irreversibility)** 的原則有利於敏捷軟體開發【51】。
|
||||
- 如果 Map 或 Reduce 任務失敗,MapReduce 框架將自動重新排程,並在同樣的輸入上再次執行它。如果失敗是由程式碼中的錯誤造成的,那麼它會不斷崩潰,並最終導致作業在幾次嘗試之後失敗。但是如果故障是由於臨時問題導致的,那麼故障就會被容忍。因為輸入不可變,這種自動重試是安全的,而失敗任務的輸出會被 MapReduce 框架丟棄。
|
||||
- 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。
|
||||
@ -462,7 +462,7 @@ MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不
|
||||
|
||||
正如我們所看到的,Hadoop 有點像 Unix 的分散式版本,其中 HDFS 是檔案系統,而 MapReduce 是 Unix 程序的怪異實現(總是在 Map 階段和 Reduce 階段執行 `sort` 工具)。我們瞭解瞭如何在這些原語的基礎上實現各種連線和分組操作。
|
||||
|
||||
當 MapReduce 論文發表時【1】,它從某種意義上來說 —— 並不新鮮。我們在前幾節中討論的所有處理和並行連線演算法已經在十多年前所謂的 **大規模並行處理(MPP, massively parallel processing)** 資料庫中實現了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是這方面的先驅【52】。
|
||||
當 MapReduce 論文發表時【1】,它從某種意義上來說 —— 並不新鮮。我們在前幾節中討論的所有處理和並行連線演算法已經在十多年前所謂的 **大規模並行處理(MPP,massively parallel processing)** 資料庫中實現了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是這方面的先驅【52】。
|
||||
|
||||
最大的區別是,MPP 資料庫專注於在一組機器上並行執行分析 SQL 查詢,而 MapReduce 和分散式檔案系統【19】的組合則更像是一個可以執行任意程式的通用作業系統。
|
||||
|
||||
@ -474,7 +474,7 @@ MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不
|
||||
|
||||
在純粹主義者看來,這種仔細的建模和匯入似乎是可取的,因為這意味著資料庫的使用者有更高質量的資料來處理。然而實踐經驗表明,簡單地使資料快速可用 —— 即使它很古怪,難以使用,使用原始格式 —— 也通常要比事先決定理想資料模型要更有價值【54】。
|
||||
|
||||
這個想法與資料倉庫類似(請參閱 “[資料倉庫](ch3.md#資料倉庫)”):將大型組織的各個部分的資料集中在一起是很有價值的,因為它可以跨越以前相互分離的資料集進行連線。 MPP 資料庫所要求的謹慎模式設計拖慢了集中式資料收集速度;以原始形式收集資料,稍後再操心模式的設計,能使資料收集速度加快(有時被稱為 “**資料湖(data lake)**” 或 “**企業資料中心(enterprise data hub)**”【55】)。
|
||||
這個想法與資料倉庫類似(請參閱 “[資料倉庫](ch3.md#資料倉庫)”):將大型組織的各個部分的資料集中在一起是很有價值的,因為它可以跨越以前相互分離的資料集進行連線。MPP 資料庫所要求的謹慎模式設計拖慢了集中式資料收集速度;以原始形式收集資料,稍後再操心模式的設計,能使資料收集速度加快(有時被稱為 “**資料湖(data lake)**” 或 “**企業資料中心(enterprise data hub)**”【55】)。
|
||||
|
||||
不加區分的資料轉儲轉移瞭解釋資料的負擔:資料集的生產者不再需要強制將其轉化為標準格式,資料的解釋成為消費者的問題(**讀時模式** 方法【56】;請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。如果生產者和消費者是不同優先順序的不同團隊,這可能是一種優勢。甚至可能不存在一個理想的資料模型,對於不同目的有不同的合適視角。以原始形式簡單地轉儲資料,可以允許多種這樣的轉換。這種方法被稱為 **壽司原則(sushi principle)**:“原始資料更好”【57】。
|
||||
|
||||
@ -492,13 +492,13 @@ MapReduce 使工程師能夠輕鬆地在大型資料集上執行自己的程式
|
||||
|
||||
至關重要的是,這些不同的處理模型都可以在共享的單個機器叢集上執行,所有這些機器都可以訪問分散式檔案系統上的相同檔案。在 Hadoop 方式中,不需要將資料匯入到幾個不同的專用系統中進行不同型別的處理:系統足夠靈活,可以支援同一個叢集內不同的工作負載。不需要移動資料,使得從資料中挖掘價值變得容易得多,也使採用新的處理模型容易的多。
|
||||
|
||||
Hadoop 生態系統包括隨機訪問的 OLTP 資料庫,如 HBase(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”)和 MPP 風格的分析型資料庫,如 Impala 【41】。 HBase 與 Impala 都不使用 MapReduce,但都使用 HDFS 進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。
|
||||
Hadoop 生態系統包括隨機訪問的 OLTP 資料庫,如 HBase(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”)和 MPP 風格的分析型資料庫,如 Impala 【41】。HBase 與 Impala 都不使用 MapReduce,但都使用 HDFS 進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。
|
||||
|
||||
#### 針對頻繁故障設計
|
||||
|
||||
當比較 MapReduce 和 MPP 資料庫時,兩種不同的設計思路出現了:處理故障和使用記憶體與磁碟的方式。與線上系統相比,批處理對故障不太敏感,因為就算失敗也不會立即影響到使用者,而且它們總是能再次執行。
|
||||
|
||||
如果一個節點在執行查詢時崩潰,大多數 MPP 資料庫會中止整個查詢,並讓使用者重新提交查詢或自動重新執行它【3】。由於查詢通常最多執行幾秒鐘或幾分鐘,所以這種錯誤處理的方法是可以接受的,因為重試的代價不是太大。 MPP 資料庫還傾向於在記憶體中保留儘可能多的資料(例如,使用雜湊連線)以避免從磁碟讀取的開銷。
|
||||
如果一個節點在執行查詢時崩潰,大多數 MPP 資料庫會中止整個查詢,並讓使用者重新提交查詢或自動重新執行它【3】。由於查詢通常最多執行幾秒鐘或幾分鐘,所以這種錯誤處理的方法是可以接受的,因為重試的代價不是太大。MPP 資料庫還傾向於在記憶體中保留儘可能多的資料(例如,使用雜湊連線)以避免從磁碟讀取的開銷。
|
||||
|
||||
另一方面,MapReduce 可以容忍單個 Map 或 Reduce 任務的失敗,而不會影響作業的整體,透過以單個任務的粒度重試工作。它也會非常急切地將資料寫入磁碟,一方面是為了容錯,另一部分是因為假設資料集太大而不能適應記憶體。
|
||||
|
||||
@ -514,7 +514,7 @@ MapReduce 方式更適用於較大的作業:要處理如此之多的資料並
|
||||
|
||||
這就是 MapReduce 被設計為容忍頻繁意外任務終止的原因:不是因為硬體很不可靠,而是因為任意終止程序的自由有利於提高計算叢集中的資源利用率。
|
||||
|
||||
在開源的叢集排程器中,搶佔的使用較少。 YARN 的 CapacityScheduler 支援搶佔,以平衡不同佇列的資源分配【58】,但在編寫本文時,YARN,Mesos 或 Kubernetes 不支援通用的優先順序搶佔【60】。在任務不經常被終止的環境中,MapReduce 的這一設計決策就沒有多少意義了。在下一節中,我們將研究一些與 MapReduce 設計決策相異的替代方案。
|
||||
在開源的叢集排程器中,搶佔的使用較少。YARN 的 CapacityScheduler 支援搶佔,以平衡不同佇列的資源分配【58】,但在編寫本文時,YARN,Mesos 或 Kubernetes 不支援通用的優先順序搶佔【60】。在任務不經常被終止的環境中,MapReduce 的這一設計決策就沒有多少意義了。在下一節中,我們將研究一些與 MapReduce 設計決策相異的替代方案。
|
||||
|
||||
|
||||
## MapReduce之後
|
||||
@ -538,7 +538,7 @@ MapReduce 方式更適用於較大的作業:要處理如此之多的資料並
|
||||
|
||||
但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的 **中間狀態(intermediate state)**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由 50 或 100 個 MapReduce 作業組成的複雜工作流中,存在著很多這樣的中間狀態【29】。
|
||||
|
||||
將這個中間狀態寫入檔案的過程稱為 **物化(materialization)**。 (在 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算)
|
||||
將這個中間狀態寫入檔案的過程稱為 **物化(materialization)**。(在 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算)
|
||||
|
||||
作為對照,本章開頭的日誌分析示例使用 Unix 管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地 **流(stream)** 向輸入。
|
||||
|
||||
@ -565,7 +565,7 @@ MapReduce 方式更適用於較大的作業:要處理如此之多的資料並
|
||||
- 排序等昂貴的工作只需要在實際需要的地方執行,而不是預設地在每個 Map 和 Reduce 階段之間出現。
|
||||
- 沒有不必要的 Map 任務,因為 Mapper 所做的工作通常可以合併到前面的 Reduce 運算元中(因為 Mapper 不會更改資料集的分割槽)。
|
||||
- 由於工作流中的所有連線和資料依賴都是顯式宣告的,因此排程程式能夠總覽全域性,知道哪裡需要哪些資料,因而能夠利用區域性進行最佳化。例如,它可以嘗試將消費某些資料的任務放在與生成這些資料的任務相同的機器上,從而資料可以透過共享記憶體緩衝區傳輸,而不必透過網路複製。
|
||||
- 通常,運算元間的中間狀態足以儲存在記憶體中或寫入本地磁碟,這比寫入 HDFS 需要更少的 I/O(必須將其複製到多臺機器,並將每個副本寫入磁碟)。 MapReduce 已經對 Mapper 的輸出做了這種最佳化,但資料流引擎將這種思想推廣至所有的中間狀態。
|
||||
- 通常,運算元間的中間狀態足以儲存在記憶體中或寫入本地磁碟,這比寫入 HDFS 需要更少的 I/O(必須將其複製到多臺機器,並將每個副本寫入磁碟)。MapReduce 已經對 Mapper 的輸出做了這種最佳化,但資料流引擎將這種思想推廣至所有的中間狀態。
|
||||
- 運算元可以在輸入就緒後立即開始執行;後續階段無需等待前驅階段整個完成後再開始。
|
||||
- 與 MapReduce(為每個任務啟動一個新的 JVM)相比,現有 Java 虛擬機器(JVM)程序可以重用來執行新運算元,從而減少啟動開銷。
|
||||
|
||||
@ -617,7 +617,7 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS,因此它們採取了
|
||||
|
||||
針對圖批處理的最佳化 —— **批次同步並行(BSP,Bulk Synchronous Parallel)** 計算模型【70】已經開始流行起來。其中,Apache Giraph 【37】,Spark 的 GraphX API 和 Flink 的 Gelly API 【71】實現了它。它也被稱為 **Pregel** 模型,因為 Google 的 Pregel 論文推廣了這種處理圖的方法【72】。
|
||||
|
||||
回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定呼叫 “傳送訊息”,因為框架將所有具有相同鍵的 Mapper 輸出集中在一起。 Pregel 背後有一個類似的想法:一個頂點可以向另一個頂點 “傳送訊息”,通常這些訊息是沿著圖的邊傳送的。
|
||||
回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定呼叫 “傳送訊息”,因為框架將所有具有相同鍵的 Mapper 輸出集中在一起。Pregel 背後有一個類似的想法:一個頂點可以向另一個頂點 “傳送訊息”,通常這些訊息是沿著圖的邊傳送的。
|
||||
|
||||
在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫 Reducer 一樣。與 MapReduce 的不同之處在於,在 Pregel 模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被傳送訊息,那裡就不需要做任何工作。
|
||||
|
||||
@ -656,15 +656,15 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS,因此它們採取了
|
||||
|
||||
#### 向宣告式查詢語言的轉變
|
||||
|
||||
與硬寫執行連線的程式碼相比,指定連線關係運算元的優點是,框架可以分析連線輸入的屬性,並自動決定哪種上述連線演算法最適合當前任務。 Hive、Spark 和 Flink 都有基於代價的查詢最佳化器可以做到這一點,甚至可以改變連線順序,最小化中間狀態的數量【66,77,78,79】。
|
||||
與硬寫執行連線的程式碼相比,指定連線關係運算元的優點是,框架可以分析連線輸入的屬性,並自動決定哪種上述連線演算法最適合當前任務。Hive、Spark 和 Flink 都有基於代價的查詢最佳化器可以做到這一點,甚至可以改變連線順序,最小化中間狀態的數量【66,77,78,79】。
|
||||
|
||||
連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以 **宣告式(declarative)** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在 “[資料查詢語言](ch2.md#資料查詢語言)” 中見過這個想法。
|
||||
|
||||
但 MapReduce 及其資料流後繼者在其他方面,與 SQL 的完全宣告式查詢模型有很大區別。 MapReduce 是圍繞著回撥函式的概念建立的:對於每條記錄或者一組記錄,呼叫一個使用者定義的函式(Mapper 或 Reducer),並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作:解析、自然語言分析、影象分析以及執行數值或統計算法等。
|
||||
但 MapReduce 及其資料流後繼者在其他方面,與 SQL 的完全宣告式查詢模型有很大區別。MapReduce 是圍繞著回撥函式的概念建立的:對於每條記錄或者一組記錄,呼叫一個使用者定義的函式(Mapper 或 Reducer),並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作:解析、自然語言分析、影象分析以及執行數值或統計算法等。
|
||||
|
||||
自由執行任意程式碼,長期以來都是傳統 MapReduce 批處理系統與 MPP 資料庫的區別所在(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)” 一節)。雖然資料庫具有編寫使用者定義函式的功能,但是它們通常使用起來很麻煩,而且與大多數程式語言中廣泛使用的程式包管理器和依賴管理系統相容不佳(例如 Java 的 Maven、Javascript 的 npm 以及 Ruby 的 gems)。
|
||||
|
||||
然而資料流引擎已經發現,支援除連線之外的更多 **宣告式特性** 還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外 CPU 開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用列式儲存佈局(請參閱 “[列式儲存](ch3.md#列式儲存)”),只從磁碟讀取所需的列。 Hive、Spark DataFrames 和 Impala 還使用了向量化執行(請參閱 “[記憶體頻寬和向量化處理](ch3.md#記憶體頻寬和向量化處理)”):在對 CPU 快取友好的內部迴圈中迭代資料,避免函式呼叫。Spark 生成 JVM 位元組碼【79】,Impala 使用 LLVM 為這些內部迴圈生成本機程式碼【41】。
|
||||
然而資料流引擎已經發現,支援除連線之外的更多 **宣告式特性** 還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外 CPU 開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用列式儲存佈局(請參閱 “[列式儲存](ch3.md#列式儲存)”),只從磁碟讀取所需的列。Hive、Spark DataFrames 和 Impala 還使用了向量化執行(請參閱 “[記憶體頻寬和向量化處理](ch3.md#記憶體頻寬和向量化處理)”):在對 CPU 快取友好的內部迴圈中迭代資料,避免函式呼叫。Spark 生成 JVM 位元組碼【79】,Impala 使用 LLVM 為這些內部迴圈生成本機程式碼【41】。
|
||||
|
||||
透過在高階 API 中引入宣告式的部分,並使查詢最佳化器可以在執行期間利用這些來做最佳化,批處理框架看起來越來越像 MPP 資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
![](../img/ch11.png)
|
||||
|
||||
> 有效的複雜系統總是從簡單的系統演化而來。 反之亦然:從零設計的複雜系統沒一個能有效工作的。
|
||||
> 有效的複雜系統總是從簡單的系統演化而來。反之亦然:從零設計的複雜系統沒一個能有效工作的。
|
||||
>
|
||||
> —— 約翰・加爾,Systemantics(1975)
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
* UDP 組播廣泛應用於金融行業,例如股票市場,其中低時延非常重要【8】。雖然 UDP 本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。
|
||||
* 無代理的訊息庫,如 ZeroMQ 【9】和 nanomsg 採取類似的方法,透過 TCP 或 IP 多播實現釋出 / 訂閱訊息傳遞。
|
||||
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並對其進行監控。 (在 StatsD 協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用 UDP 將使得指標處在一種最佳近似狀態【11】。另請參閱 “[TCP 與 UDP](ch8.md#TCP與UDP)”
|
||||
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並對其進行監控。(在 StatsD 協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用 UDP 將使得指標處在一種最佳近似狀態【11】。另請參閱 “[TCP 與 UDP](ch8.md#TCP與UDP)”
|
||||
* 如果消費者在網路上公開了服務,生產者可以直接傳送 HTTP 或 RPC 請求(請參閱 “[服務中的資料流:REST 與 RPC](ch4.md#服務中的資料流:REST與RPC)”)將訊息推送給使用者。這就是 webhooks 背後的想法【12】,一種服務的回撥 URL 被註冊到另一個服務中,並且每當事件發生時都會向該 URL 發出請求。
|
||||
|
||||
儘管這些直接訊息傳遞系統在設計它們的環境中執行良好,但是它們通常要求應用程式碼意識到訊息丟失的可能性。它們的容錯程度極為有限:即使協議檢測到並重傳在網路中丟失的資料包,它們通常也只是假設生產者和消費者始終線上。
|
||||
@ -114,7 +114,7 @@
|
||||
|
||||
消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用 **確認(acknowledgments)**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。
|
||||
|
||||
如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。 (請注意可能發生這樣的情況,訊息 **實際上是** 處理完畢的,但 **確認** 在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)” 中所討論的那樣)
|
||||
如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。(請注意可能發生這樣的情況,訊息 **實際上是** 處理完畢的,但 **確認** 在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)” 中所討論的那樣)
|
||||
|
||||
當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在 [圖 11-2](../img/fig11-2.png) 中,消費者通常按照生產者傳送的順序處理訊息。然而消費者 2 在處理訊息 m3 時崩潰,與此同時消費者 1 正在處理訊息 m4。未確認的訊息 m3 隨後被重新發送給消費者 1,結果消費者 1 按照 m4,m3,m5 的順序處理訊息。因此 m3 和 m4 的交付順序與生產者 1 的傳送順序不同。
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
|
||||
日誌只是磁碟上簡單的僅追加記錄序列。我們先前在 [第三章](ch3.md) 中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在 [第五章](ch5.md) 複製的上下文裡也討論了它。
|
||||
|
||||
同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。 Unix 工具 `tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。
|
||||
同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。Unix 工具 `tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。
|
||||
|
||||
為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行 **分割槽**(按 [第六章](ch6.md) 的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如 [圖 11-3](../img/fig11-3.png) 所示。
|
||||
|
||||
@ -150,7 +150,7 @@
|
||||
|
||||
**圖 11-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案**
|
||||
|
||||
Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 DistributedLog 【20,21】都是基於日誌的訊息代理。 Google Cloud Pub/Sub 在架構上類似,但對外暴露的是 JMS 風格的 API,而不是日誌抽象【16】。儘管這些訊息代理將所有訊息寫入磁碟,但透過跨多臺機器分割槽,每秒能夠實現數百萬條訊息的吞吐量,並透過複製訊息來實現容錯性【22,23】。
|
||||
Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 DistributedLog 【20,21】都是基於日誌的訊息代理。Google Cloud Pub/Sub 在架構上類似,但對外暴露的是 JMS 風格的 API,而不是日誌抽象【16】。儘管這些訊息代理將所有訊息寫入磁碟,但透過跨多臺機器分割槽,每秒能夠實現數百萬條訊息的吞吐量,並透過複製訊息來實現容錯性【22,23】。
|
||||
|
||||
#### 日誌與傳統的訊息傳遞相比
|
||||
|
||||
@ -175,7 +175,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
|
||||
|
||||
#### 磁碟空間使用
|
||||
|
||||
如果只追加寫入日誌,則磁碟空間終究會耗盡。為了回收磁碟空間,日誌實際上被分割成段,並不時地將舊段刪除或移動到歸檔儲存。 (我們將在後面討論一種更為複雜的磁碟空間釋放方式)
|
||||
如果只追加寫入日誌,則磁碟空間終究會耗盡。為了回收磁碟空間,日誌實際上被分割成段,並不時地將舊段刪除或移動到歸檔儲存。(我們將在後面討論一種更為複雜的磁碟空間釋放方式)
|
||||
|
||||
這就意味著如果一個慢消費者跟不上訊息產生的速率而落後得太多,它的消費偏移量指向了刪除的段,那麼它就會錯過一些訊息。實際上,日誌實現了一個有限大小的緩衝區,當緩衝區填滿時會丟棄舊訊息,它也被稱為 **迴圈緩衝區(circular buffer)** 或 **環形緩衝區(ring buffer)**。不過由於緩衝區在磁碟上,因此緩衝區可能相當的大。
|
||||
|
||||
@ -242,7 +242,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
|
||||
|
||||
數十年來,許多資料庫根本沒有記錄在檔的獲取變更日誌的方式。由於這個原因,捕獲資料庫中所有的變更,然後將其複製到其他儲存技術(搜尋索引、快取或資料倉庫)中是相當困難的。
|
||||
|
||||
最近,人們對 **變更資料捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。 CDC 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。
|
||||
最近,人們對 **變更資料捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。CDC 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。
|
||||
|
||||
例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如 [圖 11-5](../img/fig11-5.png) 所示。
|
||||
|
||||
@ -258,7 +258,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
|
||||
|
||||
資料庫觸發器可用來實現變更資料捕獲(請參閱 “[基於觸發器的複製](ch5.md#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
|
||||
|
||||
LinkedIn 的 Databus【25】,Facebook 的 Wormhole【26】和 Yahoo! 的 Sherpa【27】大規模地應用這個思路。 Bottled Water 使用解碼 WAL 的 API 實現了 PostgreSQL 的 CDC【28】,Maxwell 和 Debezium 透過解析 binlog 對 MySQL 做了類似的事情【29,30,31】,Mongoriver 讀取 MongoDB oplog【32,33】,而 GoldenGate 為 Oracle 提供類似的功能【34,35】。
|
||||
LinkedIn 的 Databus【25】,Facebook 的 Wormhole【26】和 Yahoo! 的 Sherpa【27】大規模地應用這個思路。Bottled Water 使用解碼 WAL 的 API 實現了 PostgreSQL 的 CDC【28】,Maxwell 和 Debezium 透過解析 binlog 對 MySQL 做了類似的事情【29,30,31】,Mongoriver 讀取 MongoDB oplog【32,33】,而 GoldenGate 為 Oracle 提供類似的功能【34,35】。
|
||||
|
||||
類似於訊息代理,變更資料捕獲通常是非同步的:記錄資料庫系統在提交變更之前不會等待消費者應用變更。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。
|
||||
|
||||
@ -394,7 +394,7 @@ $$
|
||||
|
||||
#### 不變性的侷限性
|
||||
|
||||
許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱 “[索引和快照隔離](ch7.md#索引和快照隔離)” )。 Git、Mercurial 和 Fossil 等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
|
||||
許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱 “[索引和快照隔離](ch7.md#索引和快照隔離)” )。Git、Mercurial 和 Fossil 等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
|
||||
|
||||
永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新 / 刪除率;在這些情況下,不可變的歷史可能增至難以接受的巨大,碎片化可能成為一個問題,壓縮與垃圾收集的表現對於運維的穩健性變得至關重要【60,61】。
|
||||
|
||||
@ -438,13 +438,13 @@ $$
|
||||
|
||||
CEP 系統通常使用高層次的宣告式查詢語言,比如 SQL,或者圖形使用者介面,來描述應該檢測到的事件模式。這些查詢被提交給處理引擎,該引擎消費輸入流,並在內部維護一個執行所需匹配的狀態機。當發現匹配時,引擎發出一個 **複合事件**(即 complex event,CEP 因此得名),並附有檢測到的事件模式詳情【67】。
|
||||
|
||||
在這些系統中,查詢和資料之間的關係與普通資料庫相比是顛倒的。通常情況下,資料庫會持久儲存資料,並將查詢視為臨時的:當查詢進入時,資料庫搜尋與查詢匹配的資料,然後在查詢完成時丟掉查詢。 CEP 引擎反轉了角色:查詢是長期儲存的,來自輸入流的事件不斷流過它們,搜尋匹配事件模式的查詢【68】。
|
||||
在這些系統中,查詢和資料之間的關係與普通資料庫相比是顛倒的。通常情況下,資料庫會持久儲存資料,並將查詢視為臨時的:當查詢進入時,資料庫搜尋與查詢匹配的資料,然後在查詢完成時丟掉查詢。CEP 引擎反轉了角色:查詢是長期儲存的,來自輸入流的事件不斷流過它們,搜尋匹配事件模式的查詢【68】。
|
||||
|
||||
CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIBCO StreamBase 和 SQLstream。像 Samza 這樣的分散式流處理元件,支援使用 SQL 在流上進行宣告式查詢【71】。
|
||||
|
||||
#### 流分析
|
||||
|
||||
使用流處理的另一個領域是對流進行分析。 CEP 與流分析之間的邊界是模糊的,但一般來說,分析往往對找出特定事件序列並不關心,而更關注大量事件上的聚合與統計指標 —— 例如:
|
||||
使用流處理的另一個領域是對流進行分析。CEP 與流分析之間的邊界是模糊的,但一般來說,分析往往對找出特定事件序列並不關心,而更關注大量事件上的聚合與統計指標 —— 例如:
|
||||
|
||||
* 測量某種型別事件的速率(每個時間間隔內發生的頻率)
|
||||
* 滾動計算一段時間視窗內某個值的平均值
|
||||
@ -462,7 +462,7 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
|
||||
|
||||
同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的 **所有** 事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱 “[日誌壓縮](#日誌壓縮)”)。實際上,你需要一個可以一直延伸到時間開端的視窗。
|
||||
|
||||
原則上講,任何流處理元件都可以用於維護物化檢視,儘管 “永遠執行” 與一些面向分析的框架假設的 “主要在有限時間段視窗上執行” 背道而馳, Samza 和 Kafka Streams 支援這種用法,建立在 Kafka 對日誌壓縮的支援上【75】。
|
||||
原則上講,任何流處理元件都可以用於維護物化檢視,儘管 “永遠執行” 與一些面向分析的框架假設的 “主要在有限時間段視窗上執行” 背道而馳,Samza 和 Kafka Streams 支援這種用法,建立在 Kafka 對日誌壓縮的支援上【75】。
|
||||
|
||||
#### 在流上搜索
|
||||
|
||||
@ -498,9 +498,9 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
|
||||
|
||||
很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#重播舊訊息)”),或者在修復程式碼 BUG 之後。
|
||||
|
||||
而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個 Web 請求(由 Web 伺服器 A 處理),然後發出第二個請求(由伺服器 B 處理)。 A 和 B 發出描述它們所處理請求的事件,但是 B 的事件在 A 的事件發生之前到達訊息代理。現在,流處理器將首先看到 B 事件,然後看到 A 事件,即使它們實際上是以相反的順序發生的。
|
||||
而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個 Web 請求(由 Web 伺服器 A 處理),然後發出第二個請求(由伺服器 B 處理)。A 和 B 發出描述它們所處理請求的事件,但是 B 的事件在 A 的事件發生之前到達訊息代理。現在,流處理器將首先看到 B 事件,然後看到 A 事件,即使它們實際上是以相反的順序發生的。
|
||||
|
||||
有一個類比也許能幫助理解,“星球大戰” 電影:第四集於 1977 年發行,第五集於 1980 年,第六集於 1983 年,緊隨其後的是 1999 年的第一集,2002 年的第二集,和 2005 年的第三集,以及 2015 年的第七集【80】[^ii]。如果你按照按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。 (集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時序與順序的問題。
|
||||
有一個類比也許能幫助理解,“星球大戰” 電影:第四集於 1977 年發行,第五集於 1980 年,第六集於 1983 年,緊隨其後的是 1999 年的第一集,2002 年的第二集,和 2005 年的第三集,以及 2015 年的第七集【80】[^ii]。如果你按照按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。(集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時序與順序的問題。
|
||||
|
||||
[^ii]: 感謝 Flink 社群的 Kostas Kloudas 提出這個比喻。
|
||||
|
||||
@ -612,7 +612,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新 [^iii]。
|
||||
|
||||
[^iii]: 如果你將流視作表的衍生物,如 [圖 11-6](../img/fig11-6.png) 所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。 換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。
|
||||
[^iii]: 如果你將流視作表的衍生物,如 [圖 11-6](../img/fig11-6.png) 所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。
|
||||
|
||||
#### 連線的時間依賴性
|
||||
|
||||
@ -652,7 +652,7 @@ Apache Flink 則使用不同的方法,它會定期生成狀態的滾動存檔
|
||||
|
||||
這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱 “[恰好一次的訊息處理](ch9.md#恰好一次的訊息處理)”)。
|
||||
|
||||
在 [第九章](ch9.md) 中,我們討論了分散式事務傳統實現中的問題(如 XA)。然而在限制更為嚴苛的環境中,也是有可能高效實現這種原子提交機制的。 Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了這種方法,Apache Kafka 有計劃加入類似的功能【95,96】。與 XA 不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
|
||||
在 [第九章](ch9.md) 中,我們討論了分散式事務傳統實現中的問題(如 XA)。然而在限制更為嚴苛的環境中,也是有可能高效實現這種原子提交機制的。Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了這種方法,Apache Kafka 有計劃加入類似的功能【95,96】。與 XA 不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
|
||||
|
||||
#### 冪等性
|
||||
|
||||
@ -672,7 +672,7 @@ Storm 的 Trident 基於類似的想法來處理狀態【78】。依賴冪等性
|
||||
|
||||
一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在 “[流表連線(流擴充)](#流表連線(流擴充))” 中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。
|
||||
|
||||
例如,Flink 定期捕獲運算元狀態的快照,並將它們寫入 HDFS 等持久儲存中【92,93】。 Samza 和 Kafka Streams 透過將狀態變更傳送到具有日誌壓縮功能的專用 Kafka 主題來複制狀態變更,這與變更資料捕獲類似【84,100】。 VoltDB 透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(請參閱 “[真的序列執行](ch7.md#真的序列執行)”)。
|
||||
例如,Flink 定期捕獲運算元狀態的快照,並將它們寫入 HDFS 等持久儲存中【92,93】。Samza 和 Kafka Streams 透過將狀態變更傳送到具有日誌壓縮功能的專用 Kafka 主題來複制狀態變更,這與變更資料捕獲類似【84,100】。VoltDB 透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(請參閱 “[真的序列執行](ch7.md#真的序列執行)”)。
|
||||
|
||||
在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(請參閱 “[日誌壓縮](#日誌壓縮)”)。
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
||||
|
||||
允許應用程式直接寫入搜尋索引和資料庫引入瞭如 [圖 11-4](../img/fig11-4.png) 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
|
||||
|
||||
如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。 這是狀態機複製方法的一個應用,我們在 “[全序廣播](ch9.md#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。
|
||||
如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。這是狀態機複製方法的一個應用,我們在 “[全序廣播](ch9.md#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。
|
||||
|
||||
基於事件日誌來更新衍生資料的系統,通常可以做到 **確定性** 與 **冪等性**(請參閱 “[冪等性](ch11.md#冪等性)”),使得從故障中恢復相當容易。
|
||||
|
||||
@ -160,7 +160,7 @@ Lambda 架構是一種有影響力的想法,它將資料系統的設計變得
|
||||
|
||||
Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Unix 認為它的目的是為程式設計師提供一種相當低層次的硬體的邏輯抽象,而關係資料庫則希望為應用程式設計師提供一種高層次的抽象,以隱藏磁碟上資料結構的複雜性、併發性、崩潰恢復等等。Unix 發展出的管道和檔案只是位元組序列,而資料庫則發展出了 SQL 和事務。
|
||||
|
||||
哪種方法更好?當然這取決於你想要的是什麼。 Unix 是 “簡單的”,因為它是對硬體資源相當薄的包裝;關係資料庫是 “更簡單” 的,因為一個簡短的宣告性查詢可以利用很多強大的基礎設施(查詢最佳化、索引、連線方法、併發控制、複製等),而不需要查詢的作者理解其實現細節。
|
||||
哪種方法更好?當然這取決於你想要的是什麼。Unix 是 “簡單的”,因為它是對硬體資源相當薄的包裝;關係資料庫是 “更簡單” 的,因為一個簡短的宣告性查詢可以利用很多強大的基礎設施(查詢最佳化、索引、連線方法、併發控制、複製等),而不需要查詢的作者理解其實現細節。
|
||||
|
||||
這些哲學之間的矛盾已經持續了幾十年(Unix 和關係模型都出現在 70 年代初),仍然沒有解決。例如,我將 NoSQL 運動解釋為,希望將類 Unix 的低級別抽象方法應用於分散式 OLTP 資料儲存的領域。
|
||||
|
||||
@ -273,13 +273,13 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
|
||||
|
||||
現在大多數 Web 應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中【36】。正如函數語言程式設計社群喜歡開玩笑說的那樣,“我們相信 **教會(Church)** 與 **國家(state)** 的分離”【37】 [^i]
|
||||
|
||||
[^i]: 解釋笑話很少會讓人感覺更好,但我不想讓任何人感到被遺漏。 在這裡,Church 指代的是數學家的阿隆佐・邱奇,他創立了 lambda 演算,這是計算的早期形式,是大多數函數語言程式設計語言的基礎。 lambda 演算不具有可變狀態(即沒有變數可以被覆蓋),所以可以說可變狀態與 Church 的工作是分離的。
|
||||
[^i]: 解釋笑話很少會讓人感覺更好,但我不想讓任何人感到被遺漏。在這裡,Church 指代的是數學家的阿隆佐・邱奇,他創立了 lambda 演算,這是計算的早期形式,是大多數函數語言程式設計語言的基礎。lambda 演算不具有可變狀態(即沒有變數可以被覆蓋),所以可以說可變狀態與 Church 的工作是分離的。
|
||||
|
||||
在這個典型的 Web 應用模型中,資料庫充當一種可以透過網路同步訪問的可變共享變數。應用程式可以讀取和更新變數,而資料庫負責維持它的永續性,提供一些諸如併發控制和容錯的功能。
|
||||
|
||||
但是,在大多數程式語言中,你無法訂閱可變變數中的變更 —— 你只能定期讀取它。與電子表格不同,如果變數的值發生變化,變數的讀者不會收到通知(你可以在自己的程式碼中實現這樣的通知 —— 這被稱為 **觀察者模式** —— 但大多數語言沒有將這種模式作為內建功能)。
|
||||
|
||||
資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。 訂閱變更只是剛剛開始出現的功能(請參閱 “[變更流的 API 支援](ch11.md#變更流的API支援)”)。
|
||||
資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。訂閱變更只是剛剛開始出現的功能(請參閱 “[變更流的 API 支援](ch11.md#變更流的API支援)”)。
|
||||
|
||||
#### 資料流:應用程式碼與狀態變化的互動
|
||||
|
||||
@ -311,7 +311,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
|
||||
|
||||
第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC,而是在購買事件和匯率更新事件之間建立流聯接(請參閱 “[流表連線(流擴充)](ch11.md#流表連線(流擴充))”)。
|
||||
|
||||
[^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。 但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。
|
||||
[^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。
|
||||
|
||||
連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱 “[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。
|
||||
|
||||
@ -461,7 +461,7 @@ COMMIT;
|
||||
|
||||
即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是 Web 瀏覽器,則它可能會使用 HTTP POST 請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了 POST,但卻在能夠從伺服器接收響應之前沒了訊號。
|
||||
|
||||
在這種情況下,可能會向用戶顯示錯誤訊息,而他們可能會手動重試。 Web 瀏覽器警告說,“你確定要再次提交這個表單嗎?” —— 使用者選 “是”,因為他們希望操作發生(Post/Redirect/Get 模式【54】可以避免在正常操作中出現此警告訊息,但 POST 請求超時就沒辦法了)。從 Web 伺服器的角度來看,重試是一個獨立的請求;從資料庫的角度來看,這是一個獨立的事務。通常的除重機制無濟於事。
|
||||
在這種情況下,可能會向用戶顯示錯誤訊息,而他們可能會手動重試。Web 瀏覽器警告說,“你確定要再次提交這個表單嗎?” —— 使用者選 “是”,因為他們希望操作發生(Post/Redirect/Get 模式【54】可以避免在正常操作中出現此警告訊息,但 POST 請求超時就沒辦法了)。從 Web 伺服器的角度來看,重試是一個獨立的請求;從資料庫的角度來看,這是一個獨立的事務。通常的除重機制無濟於事。
|
||||
|
||||
#### 操作識別符號
|
||||
|
||||
@ -713,7 +713,7 @@ ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基
|
||||
|
||||
密碼學審計與完整性檢查通常依賴 **默克爾樹(Merkle tree)**【74】,這是一顆雜湊值的樹,能夠用於高效地證明一條記錄出現在一個數據集中(以及其他一些特性)。除了炒作的沸沸揚揚的加密貨幣之外,**證書透明性(certificate transparency)** 也是一種依賴 Merkle 樹的安全技術,用來檢查 TLS/SSL 證書的有效性【75,76】。
|
||||
|
||||
我可以想象,那些在證書透明度與分散式賬本中使用的完整性檢查和審計算法,將會在通用資料系統中得到越來越廣泛的應用。要使得這些演算法對於沒有密碼學審計的系統同樣可伸縮,並儘可能降低效能損失還需要一些工作。 但我認為這是一個值得關注的有趣領域。
|
||||
我可以想象,那些在證書透明度與分散式賬本中使用的完整性檢查和審計算法,將會在通用資料系統中得到越來越廣泛的應用。要使得這些演算法對於沒有密碼學審計的系統同樣可伸縮,並儘可能降低效能損失還需要一些工作。但我認為這是一個值得關注的有趣領域。
|
||||
|
||||
|
||||
## 做正確的事情
|
||||
@ -774,7 +774,7 @@ ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基
|
||||
|
||||
當系統只儲存使用者明確輸入的資料時,是因為使用者希望系統以特定方式儲存和處理這些資料,**系統是在為使用者提供服務**:使用者就是客戶。但是,當用戶的活動被跟蹤並記錄,作為他們正在做的其他事情的副作用時,這種關係就沒有那麼清晰了。該服務不再僅僅完成使用者想要它要做的事情,而是服務於它自己的利益,而這可能與使用者的利益相沖突。
|
||||
|
||||
追蹤使用者行為資料對於許多面向用戶的線上服務而言,變得越來越重要:追蹤使用者點選了哪些搜尋結果有助於改善搜尋結果的排名;推薦 “喜歡 X 的人也喜歡 Y”,可以幫助使用者發現實用有趣的東西; A/B 測試和使用者流量分析有助於改善使用者介面。這些功能需要一定量的使用者行為跟蹤,而使用者也可以從中受益。
|
||||
追蹤使用者行為資料對於許多面向用戶的線上服務而言,變得越來越重要:追蹤使用者點選了哪些搜尋結果有助於改善搜尋結果的排名;推薦 “喜歡 X 的人也喜歡 Y”,可以幫助使用者發現實用有趣的東西;A/B 測試和使用者流量分析有助於改善使用者介面。這些功能需要一定量的使用者行為跟蹤,而使用者也可以從中受益。
|
||||
|
||||
但不同公司有著不同的商業模式,追蹤並未止步於此。如果服務是透過廣告盈利的,那麼廣告主才是真正的客戶,而使用者的利益則屈居其次。跟蹤的資料會變得更詳細,分析變得更深入,資料會保留很長時間,以便為每個人建立詳細畫像,用於營銷。
|
||||
|
||||
|
22
zh-tw/ch3.md
22
zh-tw/ch3.md
@ -88,7 +88,7 @@ $ cat database
|
||||
|
||||
**圖 3-1 以類 CSV 格式儲存鍵值對的日誌,並使用記憶體雜湊對映進行索引。**
|
||||
|
||||
聽上去簡單,但這是一個可行的方法。現實中,Bitcask 實際上就是這麼做的(Riak 中預設的儲存引擎)【3】。 Bitcask 提供高效能的讀取和寫入操作,但要求所有的鍵必須能放入可用記憶體中,因為雜湊對映完全保留在記憶體中。而資料值可以使用比可用記憶體更多的空間,因為可以在硬碟上透過一次硬碟查詢操作來載入所需部分,如果資料檔案的那部分已經在檔案系統快取中,則讀取根本不需要任何硬碟 I/O。
|
||||
聽上去簡單,但這是一個可行的方法。現實中,Bitcask 實際上就是這麼做的(Riak 中預設的儲存引擎)【3】。Bitcask 提供高效能的讀取和寫入操作,但要求所有的鍵必須能放入可用記憶體中,因為雜湊對映完全保留在記憶體中。而資料值可以使用比可用記憶體更多的空間,因為可以在硬碟上透過一次硬碟查詢操作來載入所需部分,如果資料檔案的那部分已經在檔案系統快取中,則讀取根本不需要任何硬碟 I/O。
|
||||
|
||||
像 Bitcask 這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如,鍵可能是某個貓咪影片的網址(URL),而值可能是該影片被播放的次數(每次有人點選播放按鈕時遞增)。在這種型別的工作負載中,有很多寫操作,但是沒有太多不同的鍵 —— 每個鍵有很多的寫操作,但是將所有鍵儲存在記憶體中是可行的。
|
||||
|
||||
@ -118,11 +118,11 @@ $ cat database
|
||||
|
||||
* 崩潰恢復
|
||||
|
||||
如果資料庫重新啟動,則記憶體雜湊對映將丟失。原則上,你可以透過從頭到尾讀取整個段檔案並記錄下來每個鍵的最近值來恢復每個段的雜湊對映。但是,如果段檔案很大,可能需要很長時間,這會使服務的重啟比較痛苦。 Bitcask 透過將每個段的雜湊對映的快照儲存在硬碟上來加速恢復,可以使雜湊對映更快地載入到記憶體中。
|
||||
如果資料庫重新啟動,則記憶體雜湊對映將丟失。原則上,你可以透過從頭到尾讀取整個段檔案並記錄下來每個鍵的最近值來恢復每個段的雜湊對映。但是,如果段檔案很大,可能需要很長時間,這會使服務的重啟比較痛苦。Bitcask 透過將每個段的雜湊對映的快照儲存在硬碟上來加速恢復,可以使雜湊對映更快地載入到記憶體中。
|
||||
|
||||
* 部分寫入記錄
|
||||
|
||||
資料庫隨時可能崩潰,包括在將記錄追加到日誌的過程中。 Bitcask 檔案包含校驗和,允許檢測和忽略日誌中的這些損壞部分。
|
||||
資料庫隨時可能崩潰,包括在將記錄追加到日誌的過程中。Bitcask 檔案包含校驗和,允許檢測和忽略日誌中的這些損壞部分。
|
||||
|
||||
* 併發控制
|
||||
|
||||
@ -197,7 +197,7 @@ Lucene,是一種全文搜尋的索引引擎,在 Elasticsearch 和 Solr 被
|
||||
|
||||
#### 效能最佳化
|
||||
|
||||
與往常一樣,要讓儲存引擎在實踐中表現良好涉及到大量設計細節。例如,當查詢資料庫中不存在的鍵時,LSM 樹演算法可能會很慢:你必須先檢查記憶體表,然後檢視從最近的到最舊的所有的段(可能還必須從硬碟讀取每一個段檔案),然後才能確定這個鍵不存在。為了最佳化這種訪問,儲存引擎通常使用額外的布隆過濾器(Bloom filters)【15】。 (布隆過濾器是一種節省記憶體的資料結構,用於近似表達集合的內容,它可以告訴你資料庫中是否存在某個鍵,從而為不存在的鍵節省掉許多不必要的硬碟讀取操作。)
|
||||
與往常一樣,要讓儲存引擎在實踐中表現良好涉及到大量設計細節。例如,當查詢資料庫中不存在的鍵時,LSM 樹演算法可能會很慢:你必須先檢查記憶體表,然後檢視從最近的到最舊的所有的段(可能還必須從硬碟讀取每一個段檔案),然後才能確定這個鍵不存在。為了最佳化這種訪問,儲存引擎通常使用額外的布隆過濾器(Bloom filters)【15】。(布隆過濾器是一種節省記憶體的資料結構,用於近似表達集合的內容,它可以告訴你資料庫中是否存在某個鍵,從而為不存在的鍵節省掉許多不必要的硬碟讀取操作。)
|
||||
|
||||
還有一些不同的策略來確定 SSTables 被壓縮和合並的順序和時間。最常見的選擇是 size-tiered 和 leveled compaction。LevelDB 和 RocksDB 使用 leveled compaction(LevelDB 因此得名),HBase 使用 size-tiered,Cassandra 同時支援這兩種【16】。對於 sized-tiered,較新和較小的 SSTables 相繼被合併到較舊的和較大的 SSTable 中。對於 leveled compaction,key (按照分佈範圍)被拆分到較小的 SSTables,而較舊的資料被移動到單獨的層級(level),這使得壓縮(compaction)能夠更加增量地進行,並且使用較少的硬碟空間。
|
||||
|
||||
@ -264,7 +264,7 @@ B 樹的基本底層寫操作是用新資料覆寫硬碟上的頁面,並假定
|
||||
|
||||
### 比較B樹和LSM樹
|
||||
|
||||
儘管 B 樹實現通常比 LSM 樹實現更成熟,但 LSM 樹由於效能特徵也非常有趣。根據經驗,通常 LSM 樹的寫入速度更快,而 B 樹的讀取速度更快【23】。 LSM 樹上的讀取通常比較慢,因為它們必須檢查幾種不同的資料結構和不同壓縮(Compaction)層級的 SSTables。
|
||||
儘管 B 樹實現通常比 LSM 樹實現更成熟,但 LSM 樹由於效能特徵也非常有趣。根據經驗,通常 LSM 樹的寫入速度更快,而 B 樹的讀取速度更快【23】。LSM 樹上的讀取通常比較慢,因為它們必須檢查幾種不同的資料結構和不同壓縮(Compaction)層級的 SSTables。
|
||||
|
||||
然而,基準測試的結果通常和工作負載的細節相關。你需要用你特有的工作負載來測試系統,以便進行有效的比較。在本節中,我們將簡要討論一些在衡量儲存引擎效能時值得考慮的事情。
|
||||
|
||||
@ -353,7 +353,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
記憶體資料庫重新啟動時,需要從硬碟或透過網路從副本重新載入其狀態(除非使用特殊的硬體)。儘管寫入硬碟,它仍然是一個記憶體資料庫,因為硬碟僅出於永續性目的進行日誌追加,讀取請求完全由記憶體來處理。寫入硬碟同時還有運維上的好處:硬碟上的檔案可以很容易地由外部程式進行備份、檢查和分析。
|
||||
|
||||
諸如 VoltDB、MemSQL 和 Oracle TimesTen 等產品是具有關係模型的記憶體資料庫,供應商聲稱,透過消除與管理硬碟上的資料結構相關的所有開銷,他們可以提供巨大的效能改進【41,42】。 RAM Cloud 是一個開源的記憶體鍵值儲存器,具有永續性(對記憶體和硬碟上的資料都使用日誌結構化方法)【43】。 Redis 和 Couchbase 透過非同步寫入硬碟提供了較弱的永續性。
|
||||
諸如 VoltDB、MemSQL 和 Oracle TimesTen 等產品是具有關係模型的記憶體資料庫,供應商聲稱,透過消除與管理硬碟上的資料結構相關的所有開銷,他們可以提供巨大的效能改進【41,42】。RAM Cloud 是一個開源的記憶體鍵值儲存器,具有永續性(對記憶體和硬碟上的資料都使用日誌結構化方法)【43】。Redis 和 Couchbase 透過非同步寫入硬碟提供了較弱的永續性。
|
||||
|
||||
反直覺的是,記憶體資料庫的效能優勢並不是因為它們不需要從硬碟讀取的事實。只要有足夠的記憶體即使是基於硬碟的儲存引擎也可能永遠不需要從硬碟讀取,因為作業系統在記憶體中快取了最近使用的硬碟塊。相反,它們更快的原因在於省去了將記憶體資料結構編碼為硬碟資料結構的開銷【44】。
|
||||
|
||||
@ -392,7 +392,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
[^iv]: OLAP 中的首字母 O(online)的含義並不明確,它可能是指查詢並不是用來生成預定義好的報告的事實,也可能是指分析師通常是互動式地使用 OLAP 系統來進行探索式的查詢。
|
||||
|
||||
起初,事務處理和分析查詢使用了相同的資料庫。 SQL 在這方面已證明是非常靈活的:對於 OLTP 型別的查詢以及 OLAP 型別的查詢來說效果都很好。儘管如此,在二十世紀八十年代末和九十年代初期,企業有停止使用 OLTP 系統進行分析的趨勢,轉而在單獨的資料庫上執行分析。這個單獨的資料庫被稱為 **資料倉庫(data warehouse)**。
|
||||
起初,事務處理和分析查詢使用了相同的資料庫。SQL 在這方面已證明是非常靈活的:對於 OLTP 型別的查詢以及 OLAP 型別的查詢來說效果都很好。儘管如此,在二十世紀八十年代末和九十年代初期,企業有停止使用 OLTP 系統進行分析的趨勢,轉而在單獨的資料庫上執行分析。這個單獨的資料庫被稱為 **資料倉庫(data warehouse)**。
|
||||
|
||||
### 資料倉庫
|
||||
|
||||
@ -418,7 +418,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
一些資料庫(例如 Microsoft SQL Server 和 SAP HANA)支援在同一產品中進行事務處理和資料倉庫。但是,它們也正日益發展為兩套獨立的儲存和查詢引擎,只是這些引擎正好可以透過一個通用的 SQL 介面訪問【49,50,51】。
|
||||
|
||||
Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用昂貴的商業許可證銷售他們的系統。 Amazon RedShift 是 ParAccel 的託管版本。最近,大量的開源 SQL-on-Hadoop 專案已經出現,它們還很年輕,但是正在與商業資料倉庫系統競爭,包括 Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo 和 Apache Drill【52,53】。其中一些基於了谷歌 Dremel 的想法【54】。
|
||||
Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用昂貴的商業許可證銷售他們的系統。Amazon RedShift 是 ParAccel 的託管版本。最近,大量的開源 SQL-on-Hadoop 專案已經出現,它們還很年輕,但是正在與商業資料倉庫系統競爭,包括 Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo 和 Apache Drill【52,53】。其中一些基於了谷歌 Dremel 的想法【54】。
|
||||
|
||||
### 星型和雪花型:分析的模式
|
||||
|
||||
@ -434,7 +434,7 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用
|
||||
|
||||
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(可以用來計算利潤率)。事實表中的其他列是對其他表(稱為維度表)的外來鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件發生的物件、內容、地點、時間、方式和原因。
|
||||
|
||||
例如,在 [圖 3-9](../img/fig3-9.png) 中,其中一個維度是已售出的產品。 `dim_product` 表中的每一行代表一種待售產品,包括庫存單位(SKU)、產品描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行都使用外來鍵表明在特定交易中銷售了什麼產品。 (簡單起見,如果客戶一次購買了幾種不同的產品,則它們在事實表中被表示為單獨的行)。
|
||||
例如,在 [圖 3-9](../img/fig3-9.png) 中,其中一個維度是已售出的產品。`dim_product` 表中的每一行代表一種待售產品,包括庫存單位(SKU)、產品描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行都使用外來鍵表明在特定交易中銷售了什麼產品。(簡單起見,如果客戶一次購買了幾種不同的產品,則它們在事實表中被表示為單獨的行)。
|
||||
|
||||
甚至日期和時間也通常使用維度表來表示,因為這允許對日期的附加資訊(諸如公共假期)進行編碼,從而允許區分假期和非假期的銷售查詢。
|
||||
|
||||
@ -482,7 +482,7 @@ GROUP BY
|
||||
|
||||
> 列式儲存在關係資料模型中是最容易理解的,但它同樣適用於非關係資料。例如,Parquet【57】是一種列式儲存格式,支援基於 Google 的 Dremel 的文件資料模型【54】。
|
||||
|
||||
列式儲存佈局依賴於每個列檔案包含相同順序的行。 因此,如果你需要重新組裝完整的行,你可以從每個單獨的列檔案中獲取第 23 項,並將它們放在一起形成表的第 23 行。
|
||||
列式儲存佈局依賴於每個列檔案包含相同順序的行。因此,如果你需要重新組裝完整的行,你可以從每個單獨的列檔案中獲取第 23 項,並將它們放在一起形成表的第 23 行。
|
||||
|
||||
|
||||
### 列壓縮
|
||||
@ -594,7 +594,7 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
在 OLTP 這一邊,我們能看到兩派主流的儲存引擎:
|
||||
|
||||
* 日誌結構學派:只允許追加到檔案和刪除過時的檔案,但不會更新已經寫入的檔案。Bitcask、SSTables、LSM 樹、LevelDB、Cassandra、HBase、Lucene 等都屬於這個類別。
|
||||
* 就地更新學派:將硬碟視為一組可以覆寫的固定大小的頁面。 B 樹是這種理念的典範,用在所有主要的關係資料庫和許多非關係型資料庫中。
|
||||
* 就地更新學派:將硬碟視為一組可以覆寫的固定大小的頁面。B 樹是這種理念的典範,用在所有主要的關係資料庫和許多非關係型資料庫中。
|
||||
|
||||
日誌結構的儲存引擎是相對較新的技術。他們的主要想法是,透過系統性地將隨機訪問寫入轉換為硬碟上的順序寫入,由於硬碟驅動器和固態硬碟的效能特點,可以實現更高的寫入吞吐量。
|
||||
|
||||
|
78
zh-tw/ch4.md
78
zh-tw/ch4.md
@ -42,20 +42,20 @@
|
||||
|
||||
程式通常(至少)使用兩種形式的資料:
|
||||
|
||||
1. 在記憶體中,資料儲存在物件、結構體、列表、陣列、散列表、樹等中。 這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。
|
||||
2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼(encode)** 為某種自包含的位元組序列(例如,JSON 文件)。 由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。
|
||||
1. 在記憶體中,資料儲存在物件、結構體、列表、陣列、散列表、樹等中。這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。
|
||||
2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼(encode)** 為某種自包含的位元組序列(例如,JSON 文件)。由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。
|
||||
|
||||
[^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](ch3.md#列壓縮)” 中所述)。
|
||||
|
||||
所以,需要在兩種表示之間進行某種型別的翻譯。 從記憶體中表示到位元組序列的轉換稱為 **編碼(Encoding)** (也稱為 **序列化(serialization)** 或 **編組(marshalling)**),反過來稱為 **解碼(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反編組 (unmarshalling)**)[^譯i]。
|
||||
所以,需要在兩種表示之間進行某種型別的翻譯。從記憶體中表示到位元組序列的轉換稱為 **編碼(Encoding)** (也稱為 **序列化(serialization)** 或 **編組(marshalling)**),反過來稱為 **解碼(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反編組(unmarshalling)**)[^譯i]。
|
||||
|
||||
[^ii]: 請注意,**編碼(encode)** 與 **加密(encryption)** 無關。 本書不討論加密。
|
||||
[^ii]: 請注意,**編碼(encode)** 與 **加密(encryption)** 無關。本書不討論加密。
|
||||
[^譯i]: Marshal 與 Serialization 的區別:Marshal 不僅傳輸物件的狀態,而且會一起傳輸物件的方法(相關程式碼)。
|
||||
|
||||
> #### 術語衝突
|
||||
> 不幸的是,在 [第七章](ch7.md): **事務(Transaction)** 的上下文裡,**序列化(Serialization)** 這個術語也出現了,而且具有完全不同的含義。儘管序列化可能是更常見的術語,為了避免術語過載,本書中堅持使用 **編碼(Encoding)** 表達此含義。
|
||||
|
||||
這是一個常見的問題,因而有許多庫和編碼格式可供選擇。 首先讓我們概覽一下。
|
||||
這是一個常見的問題,因而有許多庫和編碼格式可供選擇。首先讓我們概覽一下。
|
||||
|
||||
### 語言特定的格式
|
||||
|
||||
@ -66,21 +66,21 @@
|
||||
* 這類編碼通常與特定的程式語言深度繫結,其他語言很難讀取這種資料。如果以這類編碼儲存或傳輸資料,那你就和這門語言綁死在一起了。並且很難將系統與其他組織的系統(可能用的是不同的語言)進行整合。
|
||||
* 為了恢復相同物件型別的資料,解碼過程需要 **例項化任意類** 的能力,這通常是安全問題的一個來源【5】:如果攻擊者可以讓應用程式解碼任意的位元組序列,他們就能例項化任意的類,這會允許他們做可怕的事情,如遠端執行任意程式碼【6,7】。
|
||||
* 在這些庫中,資料版本控制通常是事後才考慮的。因為它們旨在快速簡便地對資料進行編碼,所以往往忽略了向前和向後相容性帶來的麻煩問題。
|
||||
* 效率(編碼或解碼所花費的 CPU 時間,以及編碼結構的大小)往往也是事後才考慮的。 例如,Java 的內建序列化由於其糟糕的效能和臃腫的編碼而臭名昭著【8】。
|
||||
* 效率(編碼或解碼所花費的 CPU 時間,以及編碼結構的大小)往往也是事後才考慮的。例如,Java 的內建序列化由於其糟糕的效能和臃腫的編碼而臭名昭著【8】。
|
||||
|
||||
因此,除非臨時使用,採用語言內建編碼通常是一個壞主意。
|
||||
|
||||
### JSON、XML和二進位制變體
|
||||
|
||||
當我們談到可以被多種程式語言讀寫的標準編碼時,JSON 和 XML 是最顯眼的角逐者。它們廣為人知,廣受支援,也 “廣受憎惡”。 XML 經常收到批評:過於冗長與且過份複雜【9】。 JSON 的流行則主要源於(透過成為 JavaScript 的一個子集)Web 瀏覽器的內建支援,以及相對於 XML 的簡單性。 CSV 是另一種流行的與語言無關的格式,儘管其功能相對較弱。
|
||||
當我們談到可以被多種程式語言讀寫的標準編碼時,JSON 和 XML 是最顯眼的角逐者。它們廣為人知,廣受支援,也 “廣受憎惡”。XML 經常收到批評:過於冗長與且過份複雜【9】。JSON 的流行則主要源於(透過成為 JavaScript 的一個子集)Web 瀏覽器的內建支援,以及相對於 XML 的簡單性。CSV 是另一種流行的與語言無關的格式,儘管其功能相對較弱。
|
||||
|
||||
JSON,XML 和 CSV 屬於文字格式,因此具有人類可讀性(儘管它們的語法是一個熱門爭議話題)。除了表面的語法問題之外,它們也存在一些微妙的問題:
|
||||
|
||||
* **數字(numbers)** 編碼有很多模糊之處。在 XML 和 CSV 中,無法區分數字和碰巧由數字組成的字串(除了引用外部模式)。 JSON 雖然區分字串與數字,但並不區分整數和浮點數,並且不能指定精度。
|
||||
這在處理大數字時是個問題。例如大於 $2^{53}$ 的整數無法使用 IEEE 754 雙精度浮點數精確表示,因此在使用浮點數(例如 JavaScript)的語言進行分析時,這些數字會變得不準確。 Twitter 有一個關於大於 $2^{53}$ 的數字的例子,它使用 64 位整數來標識每條推文。 Twitter API 返回的 JSON 包含了兩個推特 ID,一個是 JSON 數字,另一個是十進位制字串,以解決 JavaScript 程式中無法正確解析數字的問題【10】。
|
||||
* **數字(numbers)** 編碼有很多模糊之處。在 XML 和 CSV 中,無法區分數字和碰巧由數字組成的字串(除了引用外部模式)。JSON 雖然區分字串與數字,但並不區分整數和浮點數,並且不能指定精度。
|
||||
這在處理大數字時是個問題。例如大於 $2^{53}$ 的整數無法使用 IEEE 754 雙精度浮點數精確表示,因此在使用浮點數(例如 JavaScript)的語言進行分析時,這些數字會變得不準確。Twitter 有一個關於大於 $2^{53}$ 的數字的例子,它使用 64 位整數來標識每條推文。Twitter API 返回的 JSON 包含了兩個推特 ID,一個是 JSON 數字,另一個是十進位制字串,以解決 JavaScript 程式中無法正確解析數字的問題【10】。
|
||||
* JSON 和 XML 對 Unicode 字串(即人類可讀的文字)有很好的支援,但是它們不支援二進位制資料(即不帶 **字元編碼(character encoding)** 的位元組序列)。二進位制串是很有用的功能,人們透過使用 Base64 將二進位制資料編碼為文字來繞過此限制。其特有的模式標識著這個值應當被解釋為 Base64 編碼的二進位制資料。這種方案雖然管用,但比較 Hacky,並且會增加三分之一的資料大小。
|
||||
* XML 【11】和 JSON 【12】都有可選的模式支援。這些模式語言相當強大,所以學習和實現起來都相當複雜。 XML 模式的使用相當普遍,但許多基於 JSON 的工具才不會去折騰模式。對資料的正確解讀(例如區分數值與二進位制串)取決於模式中的資訊,因此不使用 XML/JSON 模式的應用程式可能需要對相應的編碼 / 解碼邏輯進行硬編碼。
|
||||
* CSV 沒有任何模式,因此每行和每列的含義完全由應用程式自行定義。如果應用程式變更添加了新的行或列,那麼這種變更必須透過手工處理。 CSV 也是一個相當模糊的格式(如果一個值包含逗號或換行符,會發生什麼?)。儘管其轉義規則已經被正式指定【13】,但並不是所有的解析器都正確的實現了標準。
|
||||
* XML 【11】和 JSON 【12】都有可選的模式支援。這些模式語言相當強大,所以學習和實現起來都相當複雜。XML 模式的使用相當普遍,但許多基於 JSON 的工具才不會去折騰模式。對資料的正確解讀(例如區分數值與二進位制串)取決於模式中的資訊,因此不使用 XML/JSON 模式的應用程式可能需要對相應的編碼 / 解碼邏輯進行硬編碼。
|
||||
* CSV 沒有任何模式,因此每行和每列的含義完全由應用程式自行定義。如果應用程式變更添加了新的行或列,那麼這種變更必須透過手工處理。CSV 也是一個相當模糊的格式(如果一個值包含逗號或換行符,會發生什麼?)。儘管其轉義規則已經被正式指定【13】,但並不是所有的解析器都正確的實現了標準。
|
||||
|
||||
儘管存在這些缺陷,但 JSON、XML 和 CSV 對很多需求來說已經足夠好了。它們很可能會繼續流行下去,特別是作為資料交換格式來說(即將資料從一個組織傳送到另一個組織)。在這種情況下,只要人們對格式是什麼意見一致,格式有多美觀或者效率有多高效就無所謂了。讓不同的組織就這些東西達成一致的難度超過了絕大多數問題。
|
||||
|
||||
@ -104,7 +104,7 @@ JSON 比 XML 簡潔,但與二進位制格式相比還是太佔空間。這一
|
||||
|
||||
我們來看一個 MessagePack 的例子,它是一個 JSON 的二進位制編碼。圖 4-1 顯示瞭如果使用 MessagePack 【14】對 [例 4-1]() 中的 JSON 文件進行編碼,則得到的位元組序列。前幾個位元組如下:
|
||||
|
||||
1. 第一個位元組 `0x83` 表示接下來是 **3** 個欄位(低四位 = `0x03`)的 **物件 object**(高四位 = `0x80`)。 (如果想知道如果一個物件有 15 個以上的欄位會發生什麼情況,欄位的數量塞不進 4 個 bit 裡,那麼它會用另一個不同的型別識別符號,欄位的數量被編碼兩個或四個位元組)。
|
||||
1. 第一個位元組 `0x83` 表示接下來是 **3** 個欄位(低四位 = `0x03`)的 **物件 object**(高四位 = `0x80`)。(如果想知道如果一個物件有 15 個以上的欄位會發生什麼情況,欄位的數量塞不進 4 個 bit 裡,那麼它會用另一個不同的型別識別符號,欄位的數量被編碼兩個或四個位元組)。
|
||||
2. 第二個位元組 `0xa8` 表示接下來是 **8** 位元組長(低四位 = `0x08`)的字串(高四位 = `0x0a`)。
|
||||
3. 接下來八個位元組是 ASCII 字串形式的欄位名稱 `userName`。由於之前已經指明長度,不需要任何標記來標識字串的結束位置(或者任何轉義)。
|
||||
4. 接下來的七個位元組對字首為 `0xa6` 的六個字母的字串值 `Martin` 進行編碼,依此類推。
|
||||
@ -119,7 +119,7 @@ JSON 比 XML 簡潔,但與二進位制格式相比還是太佔空間。這一
|
||||
|
||||
### Thrift與Protocol Buffers
|
||||
|
||||
Apache Thrift 【15】和 Protocol Buffers(protobuf)【16】是基於相同原理的二進位制編碼庫。 Protocol Buffers 最初是在 Google 開發的,Thrift 最初是在 Facebook 開發的,並且都是在 2007~2008 開源的【17】。
|
||||
Apache Thrift 【15】和 Protocol Buffers(protobuf)【16】是基於相同原理的二進位制編碼庫。Protocol Buffers 最初是在 Google 開發的,Thrift 最初是在 Facebook 開發的,並且都是在 2007~2008 開源的【17】。
|
||||
Thrift 和 Protocol Buffers 都需要一個模式來編碼任何資料。要在 Thrift 的 [例 4-1]() 中對資料進行編碼,可以使用 Thrift **介面定義語言(IDL)** 來描述模式,如下所示:
|
||||
|
||||
```c
|
||||
@ -147,7 +147,7 @@ Thrift 和 Protocol Buffers 每一個都帶有一個程式碼生成工具,它
|
||||
|
||||
**圖 4-2 使用 Thrift 二進位制協議編碼的記錄**
|
||||
|
||||
[^iii]: 實際上,Thrift 有三種二進位制協議:BinaryProtocol、CompactProtocol 和 DenseProtocol,儘管 DenseProtocol 只支援 C ++ 實現,所以不算作跨語言【18】。 除此之外,它還有兩種不同的基於 JSON 的編碼格式【19】。 真逗!
|
||||
[^iii]: 實際上,Thrift 有三種二進位制協議:BinaryProtocol、CompactProtocol 和 DenseProtocol,儘管 DenseProtocol 只支援 C ++ 實現,所以不算作跨語言【18】。除此之外,它還有兩種不同的基於 JSON 的編碼格式【19】。真逗!
|
||||
|
||||
與 [圖 4-1](Img/fig4-1.png) 類似,每個欄位都有一個型別註釋(用於指示它是一個字串、整數、列表等),還可以根據需要指定長度(字串的長度,列表中的專案數) 。出現在資料中的字串 `(“Martin”, “daydreaming”, “hacking”)` 也被編碼為 ASCII(或者說,UTF-8),與之前類似。
|
||||
|
||||
@ -159,7 +159,7 @@ Thrift CompactProtocol 編碼在語義上等同於 BinaryProtocol,但是如 [
|
||||
|
||||
**圖 4-3 使用 Thrift 壓縮協議編碼的記錄**
|
||||
|
||||
最後,Protocol Buffers(只有一種二進位制編碼格式)對相同的資料進行編碼,如 [圖 4-4](../img/fig4-4.png) 所示。 它的打包方式稍有不同,但與 Thrift 的 CompactProtocol 非常相似。 Protobuf 將同樣的記錄塞進了 33 個位元組中。
|
||||
最後,Protocol Buffers(只有一種二進位制編碼格式)對相同的資料進行編碼,如 [圖 4-4](../img/fig4-4.png) 所示。它的打包方式稍有不同,但與 Thrift 的 CompactProtocol 非常相似。Protobuf 將同樣的記錄塞進了 33 個位元組中。
|
||||
|
||||
![](../img/fig4-4.png)
|
||||
|
||||
@ -169,7 +169,7 @@ Thrift CompactProtocol 編碼在語義上等同於 BinaryProtocol,但是如 [
|
||||
|
||||
#### 欄位標籤和模式演變
|
||||
|
||||
我們之前說過,模式不可避免地需要隨著時間而改變。我們稱之為模式演變。 Thrift 和 Protocol Buffers 如何處理模式更改,同時保持向後相容性?
|
||||
我們之前說過,模式不可避免地需要隨著時間而改變。我們稱之為模式演變。Thrift 和 Protocol Buffers 如何處理模式更改,同時保持向後相容性?
|
||||
|
||||
從示例中可以看出,編碼的記錄就是其編碼欄位的拼接。每個欄位由其標籤號碼(樣本模式中的數字 1,2,3)標識,並用資料型別(例如字串或整數)註釋。如果沒有設定欄位值,則簡單地從編碼記錄中省略。從中可以看到,欄位標記對編碼資料的含義至關重要。你可以更改架構中欄位的名稱,因為編碼的資料永遠不會引用欄位名稱,但不能更改欄位的標記,因為這會使所有現有的編碼資料無效。
|
||||
|
||||
@ -189,9 +189,9 @@ Thrift 有一個專用的列表資料型別,它使用列表元素的資料型
|
||||
|
||||
### Avro
|
||||
|
||||
Apache Avro 【20】是另一種二進位制編碼格式,與 Protocol Buffers 和 Thrift 有著有趣的不同。 它是作為 Hadoop 的一個子專案在 2009 年開始的,因為 Thrift 不適合 Hadoop 的用例【21】。
|
||||
Apache Avro 【20】是另一種二進位制編碼格式,與 Protocol Buffers 和 Thrift 有著有趣的不同。它是作為 Hadoop 的一個子專案在 2009 年開始的,因為 Thrift 不適合 Hadoop 的用例【21】。
|
||||
|
||||
Avro 也使用模式來指定正在編碼的資料的結構。 它有兩種模式語言:一種(Avro IDL)用於人工編輯,一種(基於 JSON)更易於機器讀取。
|
||||
Avro 也使用模式來指定正在編碼的資料的結構。它有兩種模式語言:一種(Avro IDL)用於人工編輯,一種(基於 JSON)更易於機器讀取。
|
||||
|
||||
我們用 Avro IDL 編寫的示例模式可能如下所示:
|
||||
|
||||
@ -217,9 +217,9 @@ record Person {
|
||||
}
|
||||
```
|
||||
|
||||
首先,請注意模式中沒有標籤號碼。 如果我們使用這個模式編碼我們的例子記錄([例 4-1]()),Avro 二進位制編碼只有 32 個位元組長,這是我們所見過的所有編碼中最緊湊的。 編碼位元組序列的分解如 [圖 4-5](../img/fig4-5.png) 所示。
|
||||
首先,請注意模式中沒有標籤號碼。如果我們使用這個模式編碼我們的例子記錄([例 4-1]()),Avro 二進位制編碼只有 32 個位元組長,這是我們所見過的所有編碼中最緊湊的。編碼位元組序列的分解如 [圖 4-5](../img/fig4-5.png) 所示。
|
||||
|
||||
如果你檢查位元組序列,你可以看到沒有什麼可以識別字段或其資料型別。 編碼只是由連在一起的值組成。 一個字串只是一個長度字首,後跟 UTF-8 位元組,但是在被包含的資料中沒有任何內容告訴你它是一個字串。 它可以是一個整數,也可以是其他的整數。 整數使用可變長度編碼(與 Thrift 的 CompactProtocol 相同)進行編碼。
|
||||
如果你檢查位元組序列,你可以看到沒有什麼可以識別字段或其資料型別。編碼只是由連在一起的值組成。一個字串只是一個長度字首,後跟 UTF-8 位元組,但是在被包含的資料中沒有任何內容告訴你它是一個字串。它可以是一個整數,也可以是其他的整數。整數使用可變長度編碼(與 Thrift 的 CompactProtocol 相同)進行編碼。
|
||||
|
||||
![](../img/fig4-5.png)
|
||||
|
||||
@ -235,7 +235,7 @@ record Person {
|
||||
|
||||
當一個應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料、從網路接收資料等)時,它希望資料在某個模式中,這就是 Reader 模式。這是應用程式程式碼所依賴的模式,在應用程式的構建過程中,程式碼可能已經從該模式生成。
|
||||
|
||||
Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他們只需要相容。當資料解碼(讀取)時,Avro 庫透過並排檢視 Writer 模式和 Reader 模式並將資料從 Writer 模式轉換到 Reader 模式來解決差異。 Avro 規範【20】確切地定義了這種解析的工作原理,如 [圖 4-6](../img/fig4-6.png) 所示。
|
||||
Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他們只需要相容。當資料解碼(讀取)時,Avro 庫透過並排檢視 Writer 模式和 Reader 模式並將資料從 Writer 模式轉換到 Reader 模式來解決差異。Avro 規範【20】確切地定義了這種解析的工作原理,如 [圖 4-6](../img/fig4-6.png) 所示。
|
||||
|
||||
例如,如果 Writer 模式和 Reader 模式的欄位順序不同,這是沒有問題的,因為模式解析透過欄位名匹配欄位。如果讀取資料的程式碼遇到出現在 Writer 模式中但不在 Reader 模式中的欄位,則忽略它。如果讀取資料的程式碼需要某個欄位,但是 Writer 模式不包含該名稱的欄位,則使用在 Reader 模式中宣告的預設值填充。
|
||||
|
||||
@ -265,7 +265,7 @@ Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他
|
||||
|
||||
* 有很多記錄的大檔案
|
||||
|
||||
Avro 的一個常見用途 - 尤其是在 Hadoop 環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼(我們將在 [第十章](ch10.md) 討論這種情況)。在這種情況下,該檔案的作者可以在檔案的開頭只包含一次 Writer 模式。 Avro 指定了一個檔案格式(物件容器檔案)來做到這一點。
|
||||
Avro 的一個常見用途 - 尤其是在 Hadoop 環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼(我們將在 [第十章](ch10.md) 討論這種情況)。在這種情況下,該檔案的作者可以在檔案的開頭只包含一次 Writer 模式。Avro 指定了一個檔案格式(物件容器檔案)來做到這一點。
|
||||
|
||||
* 支援獨立寫入的記錄的資料庫
|
||||
|
||||
@ -273,7 +273,7 @@ Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他
|
||||
|
||||
* 透過網路連線傳送記錄
|
||||
|
||||
當兩個程序透過雙向網路連線進行通訊時,他們可以在連線設定上協商模式版本,然後在連線的生命週期中使用該模式。 Avro RPC 協議(請參閱 “[服務中的資料流:REST 與 RPC](#服務中的資料流:REST與RPC)”)就是這樣工作的。
|
||||
當兩個程序透過雙向網路連線進行通訊時,他們可以在連線設定上協商模式版本,然後在連線的生命週期中使用該模式。Avro RPC 協議(請參閱 “[服務中的資料流:REST 與 RPC](#服務中的資料流:REST與RPC)”)就是這樣工作的。
|
||||
|
||||
具有模式版本的資料庫在任何情況下都是非常有用的,因為它充當文件併為你提供了檢查模式相容性的機會【24】。作為版本號,你可以使用一個簡單的遞增整數,或者你可以使用模式的雜湊。
|
||||
|
||||
@ -301,7 +301,7 @@ Avro 為靜態型別程式語言提供了可選的程式碼生成功能,但是
|
||||
|
||||
正如我們所看到的,Protocol Buffers、Thrift 和 Avro 都使用模式來描述二進位制編碼格式。他們的模式語言比 XML 模式或者 JSON 模式簡單得多,而後者支援更詳細的驗證規則(例如,“該欄位的字串值必須與該正則表示式匹配” 或 “該欄位的整數值必須在 0 和 100 之間” )。由於 Protocol Buffers,Thrift 和 Avro 實現起來更簡單,使用起來也更簡單,所以它們已經發展到支援相當廣泛的程式語言。
|
||||
|
||||
這些編碼所基於的想法絕不是新的。例如,它們與 ASN.1 有很多相似之處,它是 1984 年首次被標準化的模式定義語言【27】。它被用來定義各種網路協議,例如其二進位制編碼(DER)仍然被用於編碼 SSL 證書(X.509)【28】。 ASN.1 支援使用標籤號碼的模式演進,類似於 Protocol Buffers 和 Thrift 【29】。然而,它也非常複雜,而且沒有好的配套文件,所以 ASN.1 可能不是新應用程式的好選擇。
|
||||
這些編碼所基於的想法絕不是新的。例如,它們與 ASN.1 有很多相似之處,它是 1984 年首次被標準化的模式定義語言【27】。它被用來定義各種網路協議,例如其二進位制編碼(DER)仍然被用於編碼 SSL 證書(X.509)【28】。ASN.1 支援使用標籤號碼的模式演進,類似於 Protocol Buffers 和 Thrift 【29】。然而,它也非常複雜,而且沒有好的配套文件,所以 ASN.1 可能不是新應用程式的好選擇。
|
||||
|
||||
許多資料系統也為其資料實現了某種專有的二進位制編碼。例如,大多數關係資料庫都有一個網路協議,你可以透過該協議向資料庫傳送查詢並獲取響應。這些協議通常特定於特定的資料庫,並且資料庫供應商提供將來自資料庫的網路協議的響應解碼為記憶體資料結構的驅動程式(例如使用 ODBC 或 JDBC API)。
|
||||
|
||||
@ -352,7 +352,7 @@ Avro 為靜態型別程式語言提供了可選的程式碼生成功能,但是
|
||||
|
||||
在部署應用程式的新版本時,也許用不了幾分鐘就可以將所有的舊版本替換為新版本(至少伺服器端應用程式是這樣的)。但資料庫內容並非如此:對於五年前的資料來說,除非對其進行顯式重寫,否則它仍然會以原始編碼形式存在。這種現象有時被概括為:資料的生命週期超出程式碼的生命週期。
|
||||
|
||||
將資料重寫(遷移)到一個新的模式當然是可能的,但是在一個大資料集上執行是一個昂貴的事情,所以大多數資料庫如果可能的話就避免它。大多數關係資料庫都允許簡單的模式更改,例如新增一個預設值為空的新列,而不重寫現有資料 [^v]。讀取舊行時,對於磁碟上的編碼資料缺少的任何列,資料庫將填充空值。 LinkedIn 的文件資料庫 Espresso 使用 Avro 儲存,允許它使用 Avro 的模式演變規則【23】。
|
||||
將資料重寫(遷移)到一個新的模式當然是可能的,但是在一個大資料集上執行是一個昂貴的事情,所以大多數資料庫如果可能的話就避免它。大多數關係資料庫都允許簡單的模式更改,例如新增一個預設值為空的新列,而不重寫現有資料 [^v]。讀取舊行時,對於磁碟上的編碼資料缺少的任何列,資料庫將填充空值。LinkedIn 的文件資料庫 Espresso 使用 Avro 儲存,允許它使用 Avro 的模式演變規則【23】。
|
||||
|
||||
因此,模式演變允許整個資料庫看起來好像是用單個模式編碼的,即使底層儲存可能包含用各種歷史版本的模式編碼的記錄。
|
||||
|
||||
@ -372,7 +372,7 @@ Avro 為靜態型別程式語言提供了可選的程式碼生成功能,但是
|
||||
|
||||
當你需要透過網路進行程序間的通訊時,安排該通訊的方式有幾種。最常見的安排是有兩個角色:客戶端和伺服器。伺服器透過網路公開 API,並且客戶端可以連線到伺服器以向該 API 發出請求。伺服器公開的 API 被稱為服務。
|
||||
|
||||
Web 以這種方式工作:客戶(Web 瀏覽器)向 Web 伺服器發出請求,透過 GET 請求下載 HTML、CSS、JavaScript、影象等,並透過 POST 請求提交資料到伺服器。 API 包含一組標準的協議和資料格式(HTTP、URL、SSL/TLS、HTML 等)。由於網路瀏覽器、網路伺服器和網站作者大多同意這些標準,你可以使用任何網路瀏覽器訪問任何網站(至少在理論上!)。
|
||||
Web 以這種方式工作:客戶(Web 瀏覽器)向 Web 伺服器發出請求,透過 GET 請求下載 HTML、CSS、JavaScript、影象等,並透過 POST 請求提交資料到伺服器。API 包含一組標準的協議和資料格式(HTTP、URL、SSL/TLS、HTML 等)。由於網路瀏覽器、網路伺服器和網站作者大多同意這些標準,你可以使用任何網路瀏覽器訪問任何網站(至少在理論上!)。
|
||||
|
||||
Web 瀏覽器不是唯一的客戶端型別。例如,在移動裝置或桌面計算機上執行的本地應用程式也可以向伺服器發出網路請求,並且在 Web 瀏覽器內執行的客戶端 JavaScript 應用程式可以使用 XMLHttpRequest 成為 HTTP 客戶端(該技術被稱為 Ajax 【30】)。在這種情況下,伺服器的響應通常不是用於顯示給人的 HTML,而是便於客戶端應用程式程式碼進一步處理的編碼資料(如 JSON)。儘管 HTTP 可能被用作傳輸協議,但頂層實現的 API 是特定於應用程式的,客戶端和伺服器需要就該 API 的細節達成一致。
|
||||
|
||||
@ -387,20 +387,20 @@ Web 瀏覽器不是唯一的客戶端型別。例如,在移動裝置或桌面
|
||||
**當服務使用 HTTP 作為底層通訊協議時,可稱之為 Web 服務**。這可能是一個小錯誤,因為 Web 服務不僅在 Web 上使用,而且在幾個不同的環境中使用。例如:
|
||||
|
||||
1. 執行在使用者裝置上的客戶端應用程式(例如,移動裝置上的本地應用程式,或使用 Ajax 的 JavaScript web 應用程式)透過 HTTP 向服務發出請求。這些請求通常透過公共網際網路進行。
|
||||
2. 一種服務向同一組織擁有的另一項服務提出請求,這些服務通常位於同一資料中心內,作為面向服務 / 微服務架構的一部分。 (支援這種用例的軟體有時被稱為 **中介軟體(middleware)** )
|
||||
2. 一種服務向同一組織擁有的另一項服務提出請求,這些服務通常位於同一資料中心內,作為面向服務 / 微服務架構的一部分。(支援這種用例的軟體有時被稱為 **中介軟體(middleware)** )
|
||||
3. 一種服務透過網際網路向不同組織所擁有的服務提出請求。這用於不同組織後端系統之間的資料交換。此類別包括由線上服務(如信用卡處理系統)提供的公共 API,或用於共享訪問使用者資料的 OAuth。
|
||||
|
||||
有兩種流行的 Web 服務方法:REST 和 SOAP。他們在哲學方面幾乎是截然相反的,往往也是各自支持者之間的激烈辯論的主題 [^vi]。
|
||||
|
||||
[^vi]: 即使在每個陣營內也有很多爭論。 例如,**HATEOAS(超媒體作為應用程式狀態的引擎)** 就經常引發討論【35】。
|
||||
[^vi]: 即使在每個陣營內也有很多爭論。例如,**HATEOAS(超媒體作為應用程式狀態的引擎)** 就經常引發討論【35】。
|
||||
|
||||
REST 不是一個協議,而是一個基於 HTTP 原則的設計哲學【34,35】。它強調簡單的資料格式,使用 URL 來標識資源,並使用 HTTP 功能進行快取控制,身份驗證和內容型別協商。與 SOAP 相比,REST 已經越來越受歡迎,至少在跨組織服務整合的背景下【36】,並經常與微服務相關【31】。根據 REST 原則設計的 API 稱為 RESTful。
|
||||
|
||||
相比之下,SOAP 是用於製作網路 API 請求的基於 XML 的協議 [^vii]。雖然它最常用於 HTTP,但其目的是獨立於 HTTP,並避免使用大多數 HTTP 功能。相反,它帶有龐大而複雜的多種相關標準(Web 服務框架,稱為 `WS-*`),它們增加了各種功能【37】。
|
||||
|
||||
[^vii]: 儘管首字母縮寫詞相似,SOAP 並不是 SOA 的要求。 SOAP 是一種特殊的技術,而 SOA 是構建系統的一般方法。
|
||||
[^vii]: 儘管首字母縮寫詞相似,SOAP 並不是 SOA 的要求。SOAP 是一種特殊的技術,而 SOA 是構建系統的一般方法。
|
||||
|
||||
SOAP Web 服務的 API 使用稱為 Web 服務描述語言(WSDL)的基於 XML 的語言來描述。 WSDL 支援程式碼生成,客戶端可以使用本地類和方法呼叫(編碼為 XML 訊息並由框架再次解碼)訪問遠端服務。這在靜態型別程式語言中非常有用,但在動態型別程式語言中很少(請參閱 “[程式碼生成和動態型別的語言](#程式碼生成和動態型別的語言)”)。
|
||||
SOAP Web 服務的 API 使用稱為 Web 服務描述語言(WSDL)的基於 XML 的語言來描述。WSDL 支援程式碼生成,客戶端可以使用本地類和方法呼叫(編碼為 XML 訊息並由框架再次解碼)訪問遠端服務。這在靜態型別程式語言中非常有用,但在動態型別程式語言中很少(請參閱 “[程式碼生成和動態型別的語言](#程式碼生成和動態型別的語言)”)。
|
||||
|
||||
由於 WSDL 的設計不是人類可讀的,而且由於 SOAP 訊息通常因為過於複雜而無法手動構建,所以 SOAP 的使用者在很大程度上依賴於工具支援,程式碼生成和 IDE【38】。對於 SOAP 供應商不支援的程式語言的使用者來說,與 SOAP 服務的整合是困難的。
|
||||
|
||||
@ -410,30 +410,30 @@ REST 風格的 API 傾向於更簡單的方法,通常涉及較少的程式碼
|
||||
|
||||
#### 遠端過程呼叫(RPC)的問題
|
||||
|
||||
Web 服務僅僅是透過網路進行 API 請求的一系列技術的最新版本,其中許多技術受到了大量的炒作,但是存在嚴重的問題。 Enterprise JavaBeans(EJB)和 Java 的 **遠端方法呼叫(RMI)** 僅限於 Java。**分散式元件物件模型(DCOM)** 僅限於 Microsoft 平臺。**公共物件請求代理體系結構(CORBA)** 過於複雜,不提供向後或向前相容性【41】。
|
||||
Web 服務僅僅是透過網路進行 API 請求的一系列技術的最新版本,其中許多技術受到了大量的炒作,但是存在嚴重的問題。Enterprise JavaBeans(EJB)和 Java 的 **遠端方法呼叫(RMI)** 僅限於 Java。**分散式元件物件模型(DCOM)** 僅限於 Microsoft 平臺。**公共物件請求代理體系結構(CORBA)** 過於複雜,不提供向後或向前相容性【41】。
|
||||
|
||||
所有這些都是基於 **遠端過程呼叫(RPC)** 的思想,該過程呼叫自 20 世紀 70 年代以來一直存在【42】。 RPC 模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管 RPC 起初看起來很方便,但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同:
|
||||
所有這些都是基於 **遠端過程呼叫(RPC)** 的思想,該過程呼叫自 20 世紀 70 年代以來一直存在【42】。RPC 模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管 RPC 起初看起來很方便,但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同:
|
||||
|
||||
* 本地函式呼叫是可預測的,並且成功或失敗僅取決於受你控制的引數。網路請求是不可預測的:請求或響應可能由於網路問題會丟失,或者遠端計算機可能很慢或不可用,這些問題完全不在你的控制範圍之內。網路問題很常見,因此必須有所準備,例如重試失敗的請求。
|
||||
* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它返回時可能沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過(我們將在 [第八章](ch8.md) 更詳細地討論這個問題)。
|
||||
* 如果你重試失敗的網路請求,可能會發生請求實際上已經完成,只是響應丟失的情況。在這種情況下,重試將導致該操作被執行多次,除非你在協議中建立資料去重機制(**冪等性**,即 idempotence)。本地函式呼叫時沒有這樣的問題。 (在 [第十一章](ch11.md) 更詳細地討論冪等性)
|
||||
* 如果你重試失敗的網路請求,可能會發生請求實際上已經完成,只是響應丟失的情況。在這種情況下,重試將導致該操作被執行多次,除非你在協議中建立資料去重機制(**冪等性**,即 idempotence)。本地函式呼叫時沒有這樣的問題。(在 [第十一章](ch11.md) 更詳細地討論冪等性)
|
||||
* 每次呼叫本地函式時,通常需要大致相同的時間來執行。網路請求比函式呼叫要慢得多,而且其延遲也是非常可變的:好的時候它可能會在不到一毫秒的時間內完成,但是當網路擁塞或者遠端服務超載時,可能需要幾秒鐘的時間才能完成相同的操作。
|
||||
* 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會出現問題。
|
||||
* 客戶端和服務可以用不同的程式語言實現,所以 RPC 框架必須將資料型別從一種語言翻譯成另一種語言。這可能會變得很醜陋,因為不是所有的語言都具有相同的型別 —— 例如回想一下 JavaScript 的數字大於 $2^{53}$ 的問題(請參閱 “[JSON、XML 和二進位制變體](#JSON、XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。
|
||||
|
||||
所有這些因素意味著嘗試使遠端服務看起來像程式語言中的本地物件一樣毫無意義,因為這是一個根本不同的事情。 REST 的部分吸引力在於,它並不試圖隱藏它是一個網路協議的事實(儘管這似乎並沒有阻止人們在 REST 之上構建 RPC 庫)。
|
||||
所有這些因素意味著嘗試使遠端服務看起來像程式語言中的本地物件一樣毫無意義,因為這是一個根本不同的事情。REST 的部分吸引力在於,它並不試圖隱藏它是一個網路協議的事實(儘管這似乎並沒有阻止人們在 REST 之上構建 RPC 庫)。
|
||||
|
||||
#### RPC的當前方向
|
||||
|
||||
儘管有這樣那樣的問題,RPC 不會消失。在本章提到的所有編碼的基礎上構建了各種 RPC 框架:例如,Thrift 和 Avro 帶有 RPC 支援,gRPC 是使用 Protocol Buffers 的 RPC 實現,Finagle 也使用 Thrift,Rest.li 使用 JSON over HTTP。
|
||||
|
||||
這種新一代的 RPC 框架更加明確的是,遠端請求與本地函式呼叫不同。例如,Finagle 和 Rest.li 使用 futures(promises)來封裝可能失敗的非同步操作。`Futures` 還可以簡化需要並行發出多項服務並將其結果合併的情況【45】。 gRPC 支援流,其中一個呼叫不僅包括一個請求和一個響應,還可以是隨時間的一系列請求和響應【46】。
|
||||
這種新一代的 RPC 框架更加明確的是,遠端請求與本地函式呼叫不同。例如,Finagle 和 Rest.li 使用 futures(promises)來封裝可能失敗的非同步操作。`Futures` 還可以簡化需要並行發出多項服務並將其結果合併的情況【45】。gRPC 支援流,其中一個呼叫不僅包括一個請求和一個響應,還可以是隨時間的一系列請求和響應【46】。
|
||||
|
||||
其中一些框架還提供服務發現,即允許客戶端找出在哪個 IP 地址和埠號上可以找到特定的服務。我們將在 “[請求路由](ch6.md#請求路由)” 中回到這個主題。
|
||||
|
||||
使用二進位制編碼格式的自定義 RPC 協議可以實現比通用的 JSON over REST 更好的效能。但是,RESTful API 還有其他一些顯著的優點:方便實驗和除錯(只需使用 Web 瀏覽器或命令列工具 curl,無需任何程式碼生成或軟體安裝即可向其請求),能被所有主流的程式語言和平臺所支援,還有大量可用的工具(伺服器、快取、負載平衡器、代理、防火牆、監控、除錯工具、測試工具等)的生態系統。
|
||||
|
||||
由於這些原因,REST 似乎是公共 API 的主要風格。 RPC 框架的主要重點在於同一組織擁有的服務之間的請求,通常在同一資料中心內。
|
||||
由於這些原因,REST 似乎是公共 API 的主要風格。RPC 框架的主要重點在於同一組織擁有的服務之間的請求,通常在同一資料中心內。
|
||||
|
||||
#### 資料編碼與RPC的演化
|
||||
|
||||
@ -489,9 +489,9 @@ Actor 模型是單個程序中併發的程式設計模型。邏輯被封裝在 a
|
||||
|
||||
三個流行的分散式 actor 框架處理訊息編碼如下:
|
||||
|
||||
* 預設情況下,Akka 使用 Java 的內建序列化,不提供向前或向後相容性。 但是,你可以用類似 Prototol Buffers 的東西替代它,從而獲得滾動升級的能力【50】。
|
||||
* Orleans 預設使用不支援滾動升級部署的自定義資料編碼格式;要部署新版本的應用程式,你需要設定一個新的叢集,將流量從舊叢集遷移到新叢集,然後關閉舊叢集【51,52】。 像 Akka 一樣,可以使用自定義序列化外掛。
|
||||
* 在 Erlang OTP 中,對記錄模式進行更改是非常困難的(儘管系統具有許多為高可用性設計的功能)。 滾動升級是可能的,但需要仔細計劃【53】。 一個新的實驗性的 `maps` 資料型別(2014 年在 Erlang R17 中引入的類似於 JSON 的結構)可能使得這個資料型別在未來更容易【54】。
|
||||
* 預設情況下,Akka 使用 Java 的內建序列化,不提供向前或向後相容性。但是,你可以用類似 Prototol Buffers 的東西替代它,從而獲得滾動升級的能力【50】。
|
||||
* Orleans 預設使用不支援滾動升級部署的自定義資料編碼格式;要部署新版本的應用程式,你需要設定一個新的叢集,將流量從舊叢集遷移到新叢集,然後關閉舊叢集【51,52】。像 Akka 一樣,可以使用自定義序列化外掛。
|
||||
* 在 Erlang OTP 中,對記錄模式進行更改是非常困難的(儘管系統具有許多為高可用性設計的功能)。滾動升級是可能的,但需要仔細計劃【53】。一個新的實驗性的 `maps` 資料型別(2014 年在 Erlang R17 中引入的類似於 JSON 的結構)可能使得這個資料型別在未來更容易【54】。
|
||||
|
||||
|
||||
## 本章小結
|
||||
|
22
zh-tw/ch5.md
22
zh-tw/ch5.md
@ -40,7 +40,7 @@
|
||||
|
||||
**圖 5-1 基於領導者的(主/從)複製**
|
||||
|
||||
這種複製模式是許多關係資料庫的內建功能,如 PostgreSQL(從 9.0 版本開始)、MySQL、Oracle Data Guard【2】和 SQL Server 的 AlwaysOn 可用性組【3】。 它也被用於一些非關係資料庫,包括 MongoDB、RethinkDB 和 Espresso【4】。最後,基於領導者的複製並不僅限於資料庫:像 Kafka【5】和 RabbitMQ 高可用佇列【6】這樣的分散式訊息代理也使用它。某些網路檔案系統,例如 DRBD 這樣的塊複製裝置也與之類似。
|
||||
這種複製模式是許多關係資料庫的內建功能,如 PostgreSQL(從 9.0 版本開始)、MySQL、Oracle Data Guard【2】和 SQL Server 的 AlwaysOn 可用性組【3】。它也被用於一些非關係資料庫,包括 MongoDB、RethinkDB 和 Espresso【4】。最後,基於領導者的複製並不僅限於資料庫:像 Kafka【5】和 RabbitMQ 高可用佇列【6】這樣的分散式訊息代理也使用它。某些網路檔案系統,例如 DRBD 這樣的塊複製裝置也與之類似。
|
||||
|
||||
### 同步複製與非同步複製
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
|
||||
同步複製的優點是,從庫能保證有與主庫一致的最新資料副本。如果主庫突然失效,我們可以確信這些資料仍然能在從庫上找到。缺點是,如果同步從庫沒有響應(比如它已經崩潰,或者出現網路故障,或其它任何原因),主庫就無法處理寫入操作。主庫必須阻止所有寫入,並等待同步副本再次可用。
|
||||
|
||||
因此,將所有從庫都設定為同步的是不切實際的:任何一個節點的中斷都會導致整個系統停滯不前。實際上,如果在資料庫上啟用同步複製,通常意味著其中 **一個** 從庫是同步的,而其他的從庫則是非同步的。如果該同步從庫變得不可用或緩慢,則將一個非同步從庫改為同步執行。這保證你至少在兩個節點上擁有最新的資料副本:主庫和同步從庫。 這種配置有時也被稱為 **半同步(semi-synchronous)**【7】。
|
||||
因此,將所有從庫都設定為同步的是不切實際的:任何一個節點的中斷都會導致整個系統停滯不前。實際上,如果在資料庫上啟用同步複製,通常意味著其中 **一個** 從庫是同步的,而其他的從庫則是非同步的。如果該同步從庫變得不可用或緩慢,則將一個非同步從庫改為同步執行。這保證你至少在兩個節點上擁有最新的資料副本:主庫和同步從庫。這種配置有時也被稱為 **半同步(semi-synchronous)**【7】。
|
||||
|
||||
通常情況下,基於領導者的複製都配置為完全非同步。在這種情況下,如果主庫失效且不可恢復,則任何尚未複製給從庫的寫入都會丟失。這意味著即使已經向客戶端確認成功,寫入也不能保證是 **持久(Durable)** 的。然而,一個完全非同步的配置也有優點:即使所有的從庫都落後了,主庫也可以繼續處理寫入。
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
|
||||
的確有辦法繞開這些問題 —— 例如,當語句被記錄時,主庫可以用固定的返回值替換掉任何不確定的函式呼叫,以便所有從庫都能獲得相同的值。但是由於邊緣情況實在太多了,現在通常會選擇其他的複製方法。
|
||||
|
||||
基於語句的複製在 5.1 版本前的 MySQL 中被使用到。因為它相當緊湊,現在有時候也還在用。但現在在預設情況下,如果語句中存在任何不確定性,MySQL 會切換到基於行的複製(稍後討論)。 VoltDB 使用了基於語句的複製,但要求事務必須是確定性的,以此來保證安全【15】。
|
||||
基於語句的複製在 5.1 版本前的 MySQL 中被使用到。因為它相當緊湊,現在有時候也還在用。但現在在預設情況下,如果語句中存在任何不確定性,MySQL 會切換到基於行的複製(稍後討論)。VoltDB 使用了基於語句的複製,但要求事務必須是確定性的,以此來保證安全【15】。
|
||||
|
||||
#### 傳輸預寫式日誌(WAL)
|
||||
|
||||
@ -167,7 +167,7 @@
|
||||
* 對於刪除的行,日誌包含足夠的資訊來唯一標識被刪除的行,這通常是主鍵,但如果表上沒有主鍵,則需要記錄所有列的舊值。
|
||||
* 對於更新的行,日誌包含足夠的資訊來唯一標識被更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
|
||||
|
||||
修改多行的事務會生成多條這樣的日誌記錄,後面跟著一條指明事務已經提交的記錄。 MySQL 的二進位制日誌(當配置為使用基於行的複製時)使用了這種方法【17】。
|
||||
修改多行的事務會生成多條這樣的日誌記錄,後面跟著一條指明事務已經提交的記錄。MySQL 的二進位制日誌(當配置為使用基於行的複製時)使用了這種方法【17】。
|
||||
|
||||
由於邏輯日誌與儲存引擎的內部實現是解耦的,系統可以更容易地做到向後相容,從而使主庫和從庫能夠執行不同版本的資料庫軟體,或者甚至不同的儲存引擎。
|
||||
|
||||
@ -247,7 +247,7 @@
|
||||
|
||||
### 一致字首讀
|
||||
|
||||
第三個複製延遲異常的例子違反了因果律。 想象一下 Poons 先生和 Cake 夫人之間的以下簡短對話:
|
||||
第三個複製延遲異常的例子違反了因果律。想象一下 Poons 先生和 Cake 夫人之間的以下簡短對話:
|
||||
|
||||
*Mr. Poons*
|
||||
> Mrs. Cake,你能看到多遠的未來?
|
||||
@ -257,7 +257,7 @@
|
||||
|
||||
這兩句話之間有因果關係:Cake 夫人聽到了 Poons 先生的問題並回答了這個問題。
|
||||
|
||||
現在,想象第三個人正在透過從庫來聽這個對話。 Cake 夫人說的內容是從一個延遲很低的從庫讀取的,但 Poons 先生所說的內容,從庫的延遲要大的多(見 [圖 5-5](../img/fig5-5.png))。於是,這個觀察者會聽到以下內容:
|
||||
現在,想象第三個人正在透過從庫來聽這個對話。Cake 夫人說的內容是從一個延遲很低的從庫讀取的,但 Poons 先生所說的內容,從庫的延遲要大的多(見 [圖 5-5](../img/fig5-5.png))。於是,這個觀察者會聽到以下內容:
|
||||
|
||||
*Mrs. Cake*
|
||||
> 通常約十秒鐘,Mr. Poons.
|
||||
@ -292,7 +292,7 @@
|
||||
|
||||
本章到目前為止,我們只考慮了使用單個主庫的複製架構。雖然這是一種常見的方法,但還有其它一些有趣的選擇。
|
||||
|
||||
基於領導者的複製有一個主要的缺點:只有一個主庫,而且所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫, 就無法向資料庫寫入。
|
||||
基於領導者的複製有一個主要的缺點:只有一個主庫,而且所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫,就無法向資料庫寫入。
|
||||
|
||||
[^iv]: 如果資料庫被分割槽(見 [第六章](ch6.md)),每個分割槽都有一個主庫。不同的分割槽的主庫可能在不同的節點上,但是每個分割槽都必須有一個主庫。
|
||||
|
||||
@ -506,7 +506,7 @@
|
||||
|
||||
如果我們知道,每個成功的寫操作意味著在三個副本中至少有兩個出現,這意味著至多有一個副本可能是陳舊的。因此,如果我們從至少兩個副本讀取,我們可以確定至少有一個是最新的。如果第三個副本停機或響應速度緩慢,則讀取仍可以繼續返回最新值。
|
||||
|
||||
更一般地說,如果有 n 個副本,每個寫入必須由 w 個節點確認才能被認為是成功的,並且我們必須至少為每個讀取查詢 r 個節點。 (在我們的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r > n$,我們可以預期在讀取時能獲得最新的值,因為 r 個讀取中至少有一個節點是最新的。遵循這些 r 值和 w 值的讀寫稱為 **法定人數(quorum)**[^vii] 的讀和寫【44】。你可以認為,r 和 w 是有效讀寫所需的最低票數。
|
||||
更一般地說,如果有 n 個副本,每個寫入必須由 w 個節點確認才能被認為是成功的,並且我們必須至少為每個讀取查詢 r 個節點。(在我們的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r > n$,我們可以預期在讀取時能獲得最新的值,因為 r 個讀取中至少有一個節點是最新的。遵循這些 r 值和 w 值的讀寫稱為 **法定人數(quorum)**[^vii] 的讀和寫【44】。你可以認為,r 和 w 是有效讀寫所需的最低票數。
|
||||
|
||||
[^vii]: 有時候這種法定人數被稱為嚴格的法定人數,其相對 “寬鬆的法定人數” 而言(見 “[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”)
|
||||
|
||||
@ -519,7 +519,7 @@
|
||||
* 如果 $w < n$,當節點不可用時,我們仍然可以處理寫入。
|
||||
* 如果 $r < n$,當節點不可用時,我們仍然可以處理讀取。
|
||||
* 對於 $n = 3,w = 2,r = 2$,我們可以容忍一個不可用的節點。
|
||||
* 對於 $n = 5,w = 3,r = 3$,我們可以容忍兩個不可用的節點。 這個案例如 [圖 5-11](../img/fig5-11.png) 所示。
|
||||
* 對於 $n = 5,w = 3,r = 3$,我們可以容忍兩個不可用的節點。這個案例如 [圖 5-11](../img/fig5-11.png) 所示。
|
||||
* 通常,讀取和寫入操作始終並行傳送到所有 n 個副本。引數 w 和 r 決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。
|
||||
|
||||
![](../img/fig5-11.png)
|
||||
@ -548,7 +548,7 @@
|
||||
* 如果攜帶新值的節點發生故障,需要從其他帶有舊值的副本進行恢復,則儲存新值的副本數可能會低於 w,從而打破法定人數條件。
|
||||
* 即使一切工作正常,有時也會不幸地出現關於 **時序(timing)** 的邊緣情況,我們將在 “[線性一致性和法定人數](ch9.md#線性一致性和法定人數)” 中看到這點。
|
||||
|
||||
因此,儘管法定人數似乎保證讀取返回最新的寫入值,但在實踐中並不那麼簡單。 Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。你可以透過引數 w 和 r 來調整讀取到陳舊值的機率,但把它們當成絕對的保證是不明智的。
|
||||
因此,儘管法定人數似乎保證讀取返回最新的寫入值,但在實踐中並不那麼簡單。Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。你可以透過引數 w 和 r 來調整讀取到陳舊值的機率,但把它們當成絕對的保證是不明智的。
|
||||
|
||||
尤其是,因為通常得不到 “[複製延遲問題](#複製延遲問題)” 中討論的那些保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務** 或 **共識**。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到這些話題。
|
||||
|
||||
@ -679,7 +679,7 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
|
||||
|
||||
#### 合併併發寫入的值
|
||||
|
||||
這種演算法可以確保沒有資料被無聲地丟棄,但不幸的是,客戶端需要做一些額外的工作:客戶端隨後必須合併併發寫入的值。 Riak 稱這些併發值為 **兄弟(siblings)**。
|
||||
這種演算法可以確保沒有資料被無聲地丟棄,但不幸的是,客戶端需要做一些額外的工作:客戶端隨後必須合併併發寫入的值。Riak 稱這些併發值為 **兄弟(siblings)**。
|
||||
|
||||
合併併發值,本質上是與多主複製中的衝突解決問題相同,我們先前討論過(請參閱 “[處理寫入衝突](#處理寫入衝突)”)。一個簡單的方法是根據版本號或時間戳(最後寫入勝利)來選擇一個值,但這意味著丟失資料。所以,你可能需要在應用程式程式碼中額外做些更聰明的事情。
|
||||
|
||||
|
34
zh-tw/ch6.md
34
zh-tw/ch6.md
@ -2,7 +2,7 @@
|
||||
|
||||
![](../img/ch6.png)
|
||||
|
||||
> 我們必須跳出電腦指令序列的窠臼。 敘述定義、描述元資料、梳理關係,而不是編寫過程。
|
||||
> 我們必須跳出電腦指令序列的窠臼。敘述定義、描述元資料、梳理關係,而不是編寫過程。
|
||||
>
|
||||
> —— Grace Murray Hopper,未來的計算機及其管理(1962)
|
||||
>
|
||||
@ -34,7 +34,7 @@
|
||||
|
||||
分割槽通常與複製結合使用,使得每個分割槽的副本儲存在多個節點上。這意味著,即使每條記錄屬於一個分割槽,它仍然可以儲存在多個不同的節點上以獲得容錯能力。
|
||||
|
||||
一個節點可能儲存多個分割槽。如果使用主從複製模型,則分割槽和複製的組合如 [圖 6-1](../img/fig6-1.png) 所示。每個分割槽領導者(主庫)被分配給一個節點,追隨者(從庫)被分配給其他節點。 每個節點可能是某些分割槽的主庫,同時是其他分割槽的從庫。
|
||||
一個節點可能儲存多個分割槽。如果使用主從複製模型,則分割槽和複製的組合如 [圖 6-1](../img/fig6-1.png) 所示。每個分割槽領導者(主庫)被分配給一個節點,追隨者(從庫)被分配給其他節點。每個節點可能是某些分割槽的主庫,同時是其他分割槽的從庫。
|
||||
|
||||
我們在 [第五章](ch5.md) 討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
|
||||
|
||||
@ -64,13 +64,13 @@
|
||||
|
||||
鍵的範圍不一定均勻分佈,因為資料也很可能不均勻分佈。例如在 [圖 6-2](../img/fig6-2.png) 中,第 1 捲包含以 A 和 B 開頭的單詞,但第 12 卷則包含以 T、U、V、X、Y 和 Z 開頭的單詞。只是簡單的規定每個捲包含兩個字母會導致一些卷比其他卷大。為了均勻分配資料,分割槽邊界需要依據資料調整。
|
||||
|
||||
分割槽邊界可以由管理員手動選擇,也可以由資料庫自動選擇(我們會在 “[分割槽再平衡](#分割槽再平衡)” 中更詳細地討論分割槽邊界的選擇)。 Bigtable 使用了這種分割槽策略,以及其開源等價物 HBase 【2, 3】,RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
|
||||
分割槽邊界可以由管理員手動選擇,也可以由資料庫自動選擇(我們會在 “[分割槽再平衡](#分割槽再平衡)” 中更詳細地討論分割槽邊界的選擇)。Bigtable 使用了這種分割槽策略,以及其開源等價物 HBase 【2, 3】、RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
|
||||
|
||||
在每個分割槽中,我們可以按照一定的順序儲存鍵(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”)。好處是進行範圍掃描非常簡單,你可以將鍵作為聯合索引來處理,以便在一次查詢中獲取多個相關記錄(請參閱 “[多列索引](ch3.md#多列索引)”)。例如,假設我們有一個程式來儲存感測器網路的資料,其中主鍵是測量的時間戳(年月日時分秒)。範圍掃描在這種情況下非常有用,因為我們可以輕鬆獲取某個月份的所有資料。
|
||||
|
||||
然而,Key Range 分割槽的缺點是某些特定的訪問模式會導致熱點。 如果主鍵是時間戳,則分割槽對應於時間範圍,例如,給每天分配一個分割槽。 不幸的是,由於我們在測量發生時將資料從感測器寫入資料庫,因此所有寫入操作都會轉到同一個分割槽(即今天的分割槽),這樣分割槽可能會因寫入而過載,而其他分割槽則處於空閒狀態【5】。
|
||||
然而,Key Range 分割槽的缺點是某些特定的訪問模式會導致熱點。如果主鍵是時間戳,則分割槽對應於時間範圍,例如,給每天分配一個分割槽。不幸的是,由於我們在測量發生時將資料從感測器寫入資料庫,因此所有寫入操作都會轉到同一個分割槽(即今天的分割槽),這樣分割槽可能會因寫入而過載,而其他分割槽則處於空閒狀態【5】。
|
||||
|
||||
為了避免感測器資料庫中的這個問題,需要使用除了時間戳以外的其他東西作為主鍵的第一個部分。 例如,可以在每個時間戳前新增感測器名稱,這樣會首先按感測器名稱,然後按時間進行分割槽。 假設有多個感測器同時執行,寫入負載將最終均勻分佈在不同分割槽上。 現在,當想要在一個時間範圍內獲取多個感測器的值時,你需要為每個感測器名稱執行一個單獨的範圍查詢。
|
||||
為了避免感測器資料庫中的這個問題,需要使用除了時間戳以外的其他東西作為主鍵的第一個部分。例如,可以在每個時間戳前新增感測器名稱,這樣會首先按感測器名稱,然後按時間進行分割槽。假設有多個感測器同時執行,寫入負載將最終均勻分佈在不同分割槽上。現在,當想要在一個時間範圍內獲取多個感測器的值時,你需要為每個感測器名稱執行一個單獨的範圍查詢。
|
||||
|
||||
### 根據鍵的雜湊分割槽
|
||||
|
||||
@ -90,13 +90,13 @@
|
||||
|
||||
> #### 一致性雜湊
|
||||
>
|
||||
> 一致性雜湊由 Karger 等人定義。【7】 用於跨網際網路級別的快取系統,例如 CDN 中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界(partition boundaries)** 來避免中央控制或分散式共識的需要。 請注意,這裡的一致性與複製一致性(請參閱 [第五章](ch5.md))或 ACID 一致性(請參閱 [第七章](ch7.md))無關,而只是描述了一種再平衡(rebalancing)的特定方法。
|
||||
> 一致性雜湊由 Karger 等人定義。【7】 用於跨網際網路級別的快取系統,例如 CDN 中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界(partition boundaries)** 來避免中央控制或分散式共識的需要。請注意,這裡的一致性與複製一致性(請參閱 [第五章](ch5.md))或 ACID 一致性(請參閱 [第七章](ch7.md))無關,而只是描述了一種再平衡(rebalancing)的特定方法。
|
||||
>
|
||||
> 正如我們將在 “[分割槽再平衡](#分割槽再平衡)” 中所看到的,這種特殊的方法對於資料庫實際上並不是很好,所以在實際中很少使用(某些資料庫的文件仍然會使用一致性雜湊的說法,但是它往往是不準確的)。 因為有可能產生混淆,所以最好避免使用一致性雜湊這個術語,而只是把它稱為 **雜湊分割槽(hash partitioning)**。
|
||||
> 正如我們將在 “[分割槽再平衡](#分割槽再平衡)” 中所看到的,這種特殊的方法對於資料庫實際上並不是很好,所以在實際中很少使用(某些資料庫的文件仍然會使用一致性雜湊的說法,但是它往往是不準確的)。因為有可能產生混淆,所以最好避免使用一致性雜湊這個術語,而只是把它稱為 **雜湊分割槽(hash partitioning)**。
|
||||
|
||||
不幸的是,透過使用鍵雜湊進行分割槽,我們失去了鍵範圍分割槽的一個很好的屬性:高效執行範圍查詢的能力。曾經相鄰的鍵現在分散在所有分割槽中,所以它們之間的順序就丟失了。在 MongoDB 中,如果你使用了基於雜湊的分割槽模式,則任何範圍查詢都必須傳送到所有分割槽【4】。Riak【9】、Couchbase 【10】或 Voldemort 不支援主鍵上的範圍查詢。
|
||||
|
||||
Cassandra 採取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以使用由多個列組成的複合主鍵來宣告。鍵中只有第一列會作為雜湊的依據,而其他列則被用作 Casssandra 的 SSTables 中排序資料的連線索引。儘管查詢無法在複合主鍵的第一列中按範圍掃表,但如果第一列已經指定了固定值,則可以對該鍵的其他列執行有效的範圍掃描。
|
||||
Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以使用由多個列組成的複合主鍵來宣告。鍵中只有第一列會作為雜湊的依據,而其他列則被用作 Casssandra 的 SSTables 中排序資料的連線索引。儘管查詢無法在複合主鍵的第一列中按範圍掃表,但如果第一列已經指定了固定值,則可以對該鍵的其他列執行有效的範圍掃描。
|
||||
|
||||
組合索引方法為一對多關係提供了一個優雅的資料模型。例如,在社交媒體網站上,一個使用者可能會發布很多更新。如果更新的主鍵被選擇為 `(user_id, update_timestamp)`,那麼你可以有效地檢索特定使用者在某個時間間隔內按時間戳排序的所有更新。不同的使用者可以儲存在不同的分割槽上,對於每個使用者,更新按時間戳順序儲存在單個分割槽上。
|
||||
|
||||
@ -126,11 +126,11 @@ Cassandra 採取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以
|
||||
|
||||
### 基於文件的次級索引進行分割槽
|
||||
|
||||
假設你正在經營一個銷售二手車的網站(如 [圖 6-4](../img/fig6-4.png) 所示)。 每個列表都有一個唯一的 ID—— 稱之為文件 ID—— 並且用文件 ID 對資料庫進行分割槽(例如,分割槽 0 中的 ID 0 到 499,分割槽 1 中的 ID 500 到 999 等)。
|
||||
假設你正在經營一個銷售二手車的網站(如 [圖 6-4](../img/fig6-4.png) 所示)。每個列表都有一個唯一的 ID—— 稱之為文件 ID—— 並且用文件 ID 對資料庫進行分割槽(例如,分割槽 0 中的 ID 0 到 499,分割槽 1 中的 ID 500 到 999 等)。
|
||||
|
||||
你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是 **欄位(field)**,關係資料庫中這些是 **列(column)** )。 如果你聲明瞭索引,則資料庫可以自動執行索引 [^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目 `color:red` 的文件 ID 列表中。
|
||||
你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是 **欄位(field)**,關係資料庫中這些是 **列(column)** )。如果你聲明瞭索引,則資料庫可以自動執行索引 [^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目 `color:red` 的文件 ID 列表中。
|
||||
|
||||
[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件 ID 的對映來實現次級索引。 如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。 競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱 “[多物件事務的需求](ch7.md#多物件事務的需求)”。
|
||||
[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件 ID 的對映來實現次級索引。如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱 “[多物件事務的需求](ch7.md#多物件事務的需求)”。
|
||||
|
||||
![](../img/fig6-4.png)
|
||||
|
||||
@ -174,7 +174,7 @@ Cassandra 採取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以
|
||||
* 資料集大小增加,所以你想新增更多的磁碟和 RAM 來儲存它。
|
||||
* 機器出現故障,其他機器需要接管故障機器的責任。
|
||||
|
||||
所有這些更改都需要資料和請求從一個節點移動到另一個節點。 將負載從叢集中的一個節點向另一個節點移動的過程稱為 **再平衡(rebalancing)**。
|
||||
所有這些更改都需要資料和請求從一個節點移動到另一個節點。將負載從叢集中的一個節點向另一個節點移動的過程稱為 **再平衡(rebalancing)**。
|
||||
|
||||
無論使用哪種分割槽方案,再平衡通常都要滿足一些最低要求:
|
||||
|
||||
@ -235,7 +235,7 @@ Cassandra 採取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以
|
||||
|
||||
Cassandra 和 Ketama 使用的第三種方法是使分割槽數與節點數成正比 —— 換句話說,每個節點具有固定數量的分割槽【23,27,28】。在這種情況下,每個分割槽的大小與資料集大小成比例地增長,而節點數量保持不變,但是當增加節點數時,分割槽將再次變小。由於較大的資料量通常需要較大數量的節點進行儲存,因此這種方法也使每個分割槽的大小較為穩定。
|
||||
|
||||
當一個新節點加入叢集時,它隨機選擇固定數量的現有分割槽進行拆分,然後佔有這些拆分分割槽中每個分割槽的一半,同時將每個分割槽的另一半留在原地。隨機化可能會產生不公平的分割,但是平均在更大數量的分割槽上時(在 Cassandra 中,預設情況下,每個節點有 256 個分割槽),新節點最終從現有節點獲得公平的負載份額。 Cassandra 3.0 引入了另一種再平衡的演算法來避免不公平的分割【29】。
|
||||
當一個新節點加入叢集時,它隨機選擇固定數量的現有分割槽進行拆分,然後佔有這些拆分分割槽中每個分割槽的一半,同時將每個分割槽的另一半留在原地。隨機化可能會產生不公平的分割,但是平均在更大數量的分割槽上時(在 Cassandra 中,預設情況下,每個節點有 256 個分割槽),新節點最終從現有節點獲得公平的負載份額。Cassandra 3.0 引入了另一種再平衡的演算法來避免不公平的分割【29】。
|
||||
|
||||
隨機選擇分割槽邊界要求使用基於雜湊的分割槽(可以從雜湊函式產生的數字範圍中挑選邊界)。實際上,這種方法最符合一致性雜湊的原始定義【7】(請參閱 “[一致性雜湊](#一致性雜湊)”)。最新的雜湊函式可以在較低元資料開銷的情況下達到類似的效果【8】。
|
||||
|
||||
@ -269,15 +269,15 @@ Cassandra 和 Ketama 使用的第三種方法是使分割槽數與節點數成
|
||||
|
||||
**圖 6-7 將請求路由到正確節點的三種不同方式。**
|
||||
|
||||
這是一個具有挑戰性的問題,因為重要的是所有參與者都達成共識 - 否則請求將被傳送到錯誤的節點,得不到正確的處理。 在分散式系統中有達成共識的協議,但很難正確地實現(見 [第九章](ch9.md))。
|
||||
這是一個具有挑戰性的問題,因為重要的是所有參與者都達成共識 - 否則請求將被傳送到錯誤的節點,得不到正確的處理。在分散式系統中有達成共識的協議,但很難正確地實現(見 [第九章](ch9.md))。
|
||||
|
||||
許多分散式資料系統都依賴於一個獨立的協調服務,比如 ZooKeeper 來跟蹤叢集元資料,如 [圖 6-8](../img/fig6-8.png) 所示。 每個節點在 ZooKeeper 中註冊自己,ZooKeeper 維護分割槽到節點的可靠對映。 其他參與者(如路由層或分割槽感知客戶端)可以在 ZooKeeper 中訂閱此資訊。 只要分割槽分配發生了改變,或者叢集中新增或刪除了一個節點,ZooKeeper 就會通知路由層使路由資訊保持最新狀態。
|
||||
許多分散式資料系統都依賴於一個獨立的協調服務,比如 ZooKeeper 來跟蹤叢集元資料,如 [圖 6-8](../img/fig6-8.png) 所示。每個節點在 ZooKeeper 中註冊自己,ZooKeeper 維護分割槽到節點的可靠對映。其他參與者(如路由層或分割槽感知客戶端)可以在 ZooKeeper 中訂閱此資訊。只要分割槽分配發生了改變,或者叢集中新增或刪除了一個節點,ZooKeeper 就會通知路由層使路由資訊保持最新狀態。
|
||||
|
||||
![](../img/fig6-8.png)
|
||||
|
||||
**圖 6-8 使用 ZooKeeper 跟蹤分割槽分配給節點。**
|
||||
|
||||
例如,LinkedIn的Espresso使用Helix 【31】進行叢集管理(依靠ZooKeeper),實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。 HBase、SolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。MongoDB具有類似的體系結構,但它依賴於自己的**配置伺服器(config server)** 實現和mongos守護程序作為路由層。
|
||||
例如,LinkedIn的Espresso使用Helix 【31】進行叢集管理(依靠ZooKeeper),實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。HBase、SolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。MongoDB具有類似的體系結構,但它依賴於自己的**配置伺服器(config server)** 實現和mongos守護程序作為路由層。
|
||||
|
||||
Cassandra 和 Riak 採取不同的方法:他們在節點之間使用 **流言協議(gossip protocol)** 來傳播叢集狀態的變化。請求可以傳送到任意節點,該節點會轉發到包含所請求的分割槽的適當節點([圖 6-7](../img/fig6-7.png) 中的方法 1)。這個模型在資料庫節點中增加了更多的複雜性,但是避免了對像 ZooKeeper 這樣的外部協調服務的依賴。
|
||||
|
||||
@ -289,7 +289,7 @@ Couchbase 不會自動進行再平衡,這簡化了設計。通常情況下,
|
||||
|
||||
到目前為止,我們只關注讀取或寫入單個鍵的非常簡單的查詢(加上基於文件分割槽的次級索引場景下的分散 / 聚集查詢)。這也是大多數 NoSQL 分散式資料儲存所支援的訪問層級。
|
||||
|
||||
然而,通常用於分析的 **大規模並行處理(MPP, Massively parallel processing)** 關係型資料庫產品在其支援的查詢型別方面要複雜得多。一個典型的資料倉庫查詢包含多個連線,過濾,分組和聚合操作。 MPP 查詢最佳化器將這個複雜的查詢分解成許多執行階段和分割槽,其中許多可以在資料庫叢集的不同節點上並行執行。涉及掃描大規模資料集的查詢特別受益於這種並行執行。
|
||||
然而,通常用於分析的 **大規模並行處理(MPP, Massively parallel processing)** 關係型資料庫產品在其支援的查詢型別方面要複雜得多。一個典型的資料倉庫查詢包含多個連線,過濾,分組和聚合操作。MPP 查詢最佳化器將這個複雜的查詢分解成許多執行階段和分割槽,其中許多可以在資料庫叢集的不同節點上並行執行。涉及掃描大規模資料集的查詢特別受益於這種並行執行。
|
||||
|
||||
資料倉庫查詢的快速並行執行是一個專門的話題,由於分析有很重要的商業意義,可以帶來很多利益。我們將在 [第十章](ch10.md) 討論並行查詢執行的一些技巧。有關並行資料庫中使用的技術的更詳細的概述,請參閱參考文獻【1,33】。
|
||||
|
||||
|
20
zh-tw/ch7.md
20
zh-tw/ch7.md
@ -176,7 +176,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
一些資料庫也提供更複雜的原子操作 [^iv],例如自增操作,這樣就不再需要像 [圖 7-1](../img/fig7-1.png) 那樣的讀取 - 修改 - 寫入序列了。同樣流行的是 **[比較和設定(CAS, compare-and-set)](#比較並設定(CAS))** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
|
||||
|
||||
[^iv]: 嚴格地說,**原子自增(atomic increment)** 這個術語在多執行緒程式設計的意義上使用了原子這個詞。 在 ACID 的情況下,它實際上應該被稱為 **隔離的(isolated)** 的或 **可序列的(serializable)** 的增量。 但這就太吹毛求疵了。
|
||||
[^iv]: 嚴格地說,**原子自增(atomic increment)** 這個術語在多執行緒程式設計的意義上使用了原子這個詞。在 ACID 的情況下,它實際上應該被稱為 **隔離的(isolated)** 的或 **可序列的(serializable)** 的增量。但這就太吹毛求疵了。
|
||||
|
||||
這些單物件操作很有用,因為它們可以防止在多個客戶端嘗試同時寫入同一個物件時丟失更新(請參閱 “[防止丟失更新](#防止丟失更新)”)。但它們不是通常意義上的事務。CAS 以及其他單一物件操作被稱為 “輕量級事務”,甚至出於營銷目的被稱為 “ACID”【20,21,22】,但是這個術語是誤導性的。事務通常被理解為,**將多個物件上的多個操作合併為一個執行單元的機制**。
|
||||
|
||||
@ -196,7 +196,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
#### 處理錯誤和中止
|
||||
|
||||
事務的一個關鍵特性是,如果發生錯誤,它可以中止並安全地重試。 ACID 資料庫基於這樣的哲學:如果資料庫有違反其原子性,隔離性或永續性的危險,則寧願完全放棄事務,而不是留下半成品。
|
||||
事務的一個關鍵特性是,如果發生錯誤,它可以中止並安全地重試。ACID 資料庫基於這樣的哲學:如果資料庫有違反其原子性,隔離性或永續性的危險,則寧願完全放棄事務,而不是留下半成品。
|
||||
|
||||
然而並不是所有的系統都遵循這個哲學。特別是具有 [無主複製](ch5.md#無主複製) 的資料儲存,主要是在 “盡力而為” 的基礎上進行工作。可以概括為 “資料庫將做盡可能多的事,執行遇到錯誤時,它不會撤消它已經完成的事情” —— 所以,從錯誤中恢復是應用程式的責任。
|
||||
|
||||
@ -279,7 +279,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
但是要求讀鎖的辦法在實踐中效果並不好。因為一個長時間執行的寫入事務會迫使許多隻讀事務等到這個慢寫入事務完成。這會影響只讀事務的響應時間,並且不利於可操作性:因為等待鎖,應用某個部分的遲緩可能由於連鎖效應,導致其他部分出現問題。
|
||||
|
||||
出於這個原因,大多數資料庫 [^vi] 使用 [圖 7-4](../img/fig7-4.png) 的方式防止髒讀:對於寫入的每個物件,資料庫都會記住舊的已提交值,和由當前持有寫入鎖的事務設定的新值。當事務正在進行時,任何其他讀取物件的事務都會拿到舊值。 只有當新值提交後,事務才會切換到讀取新值。
|
||||
出於這個原因,大多數資料庫 [^vi] 使用 [圖 7-4](../img/fig7-4.png) 的方式防止髒讀:對於寫入的每個物件,資料庫都會記住舊的已提交值,和由當前持有寫入鎖的事務設定的新值。當事務正在進行時,任何其他讀取物件的事務都會拿到舊值。只有當新值提交後,事務才會切換到讀取新值。
|
||||
|
||||
[^vi]: 在撰寫本文時,唯一在讀已提交隔離級別使用讀鎖的主流資料庫是 IBM DB2 和使用 `read_committed_snapshot = off` 配置的 Microsoft SQL Server【23,36】。
|
||||
|
||||
@ -325,7 +325,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
|
||||
|
||||
[圖 7-7](../img/fig7-7.png) 說明了 PostgreSQL 如何實現基於 MVCC 的快照隔離【31】(其他實現類似)。當一個事務開始時,它被賦予一個唯一的,永遠增長 [^vii] 的事務 ID(`txid`)。每當事務向資料庫寫入任何內容時,它所寫入的資料都會被標記上寫入者的事務 ID。
|
||||
|
||||
[^vii]: 事實上,事務 ID 是 32 位整數,所以大約會在 40 億次事務之後溢位。 PostgreSQL 的 Vacuum 過程會清理老舊的事務 ID,確保事務 ID 溢位(回捲)不會影響到資料。
|
||||
[^vii]: 事實上,事務 ID 是 32 位整數,所以大約會在 40 億次事務之後溢位。PostgreSQL 的 Vacuum 過程會清理老舊的事務 ID,確保事務 ID 溢位(回捲)不會影響到資料。
|
||||
|
||||
![](../img/fig7-7.png)
|
||||
|
||||
@ -369,7 +369,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
|
||||
|
||||
快照隔離是一個有用的隔離級別,特別對於只讀事務而言。但是,許多資料庫實現了它,卻用不同的名字來稱呼。在 Oracle 中稱為 **可序列化(Serializable)** 的,在 PostgreSQL 和 MySQL 中稱為 **可重複讀(repeatable read)**【23】。
|
||||
|
||||
這種命名混淆的原因是 SQL 標準沒有 **快照隔離** 的概念,因為標準是基於 System R 1975 年定義的隔離級別【2】,那時候 **快照隔離** 尚未發明。相反,它定義了 **可重複讀**,表面上看起來與快照隔離很相似。 PostgreSQL 和 MySQL 稱其 **快照隔離** 級別為 **可重複讀(repeatable read)**,因為這樣符合標準要求,所以它們可以聲稱自己 “標準相容”。
|
||||
這種命名混淆的原因是 SQL 標準沒有 **快照隔離** 的概念,因為標準是基於 System R 1975 年定義的隔離級別【2】,那時候 **快照隔離** 尚未發明。相反,它定義了 **可重複讀**,表面上看起來與快照隔離很相似。PostgreSQL 和 MySQL 稱其 **快照隔離** 級別為 **可重複讀(repeatable read)**,因為這樣符合標準要求,所以它們可以聲稱自己 “標準相容”。
|
||||
|
||||
不幸的是,SQL 標準對隔離級別的定義是有缺陷的 —— 模糊,不精確,並不像標準應有的樣子獨立於實現【28】。有幾個資料庫實現了可重複讀,但它們實際提供的保證存在很大的差異,儘管表面上是標準化的【23】。在研究文獻【29,30】中已經有了可重複讀的正式定義,但大多數的實現並不能滿足這個正式定義。最後,IBM DB2 使用 “可重複讀” 來引用可序列化【8】。
|
||||
|
||||
@ -582,7 +582,7 @@ COMMIT;
|
||||
在本章中,已經看到了幾個易於出現競爭條件的事務例子。**讀已提交** 和 **快照隔離** 級別會阻止某些競爭條件,但不會阻止另一些。我們遇到了一些特別棘手的例子,**寫入偏差** 和 **幻讀**。這是一個可悲的情況:
|
||||
|
||||
- 隔離級別難以理解,並且在不同的資料庫中實現的不一致(例如,“可重複讀” 的含義天差地別)。
|
||||
- 光檢查應用程式碼很難判斷在特定的隔離級別執行是否安全。 特別是在大型應用程式中,你可能並不知道併發發生的所有事情。
|
||||
- 光檢查應用程式碼很難判斷在特定的隔離級別執行是否安全。特別是在大型應用程式中,你可能並不知道併發發生的所有事情。
|
||||
- 沒有檢測競爭條件的好工具。原則上來說,靜態分析可能會有幫助【26】,但研究中的技術還沒法實際應用。併發問題的測試是很難的,因為它們通常是非確定性的 —— 只有在倒楣的時序下才會出現問題。
|
||||
|
||||
這不是一個新問題,從 20 世紀 70 年代以來就一直是這樣了,當時首先引入了較弱的隔離級別【2】。一直以來,研究人員的答案都很簡單:使用 **可序列化(serializable)** 的隔離級別!
|
||||
@ -648,7 +648,7 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
|
||||
|
||||
但是,對於需要訪問多個分割槽的任何事務,資料庫必須在觸及的所有分割槽之間協調事務。儲存過程需要跨越所有分割槽鎖定執行,以確保整個系統的可序列性。
|
||||
|
||||
由於跨分割槽事務具有額外的協調開銷,所以它們比單分割槽事務慢得多。 VoltDB 報告的吞吐量大約是每秒 1000 個跨分割槽寫入,比單分割槽吞吐量低幾個數量級,並且不能透過增加更多的機器來增加吞吐量【49】。
|
||||
由於跨分割槽事務具有額外的協調開銷,所以它們比單分割槽事務慢得多。VoltDB 報告的吞吐量大約是每秒 1000 個跨分割槽寫入,比單分割槽吞吐量低幾個數量級,並且不能透過增加更多的機器來增加吞吐量【49】。
|
||||
|
||||
事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。
|
||||
|
||||
@ -751,7 +751,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
本章描繪了資料庫中併發控制的黯淡畫面。一方面,我們實現了效能不好(2PL)或者伸縮性不好(序列執行)的可序列化隔離級別。另一方面,我們有效能良好的弱隔離級別,但容易出現各種競爭條件(丟失更新、寫入偏差、幻讀等)。序列化的隔離級別和高效能是從根本上相互矛盾的嗎?
|
||||
|
||||
也許不是:一個稱為 **可序列化快照隔離(SSI, serializable snapshot isolation)** 的演算法是非常有前途的。它提供了完整的可序列化隔離級別,但與快照隔離相比只有很小的效能損失。 SSI 是相當新的:它在 2008 年首次被描述【40】,並且是 Michael Cahill 的博士論文【51】的主題。
|
||||
也許不是:一個稱為 **可序列化快照隔離(SSI, serializable snapshot isolation)** 的演算法是非常有前途的。它提供了完整的可序列化隔離級別,但與快照隔離相比只有很小的效能損失。SSI 是相當新的:它在 2008 年首次被描述【40】,並且是 Michael Cahill 的博士論文【51】的主題。
|
||||
|
||||
今天,SSI 既用於單節點資料庫(PostgreSQL9.1 以後的可序列化隔離級別),也用於分散式資料庫(FoundationDB 使用類似的演算法)。由於 SSI 與其他併發控制機制相比還很年輕,還處於在實踐中證明自己表現的階段。但它有可能因為足夠快而在未來成為新的預設選項。
|
||||
|
||||
@ -775,7 +775,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
換句話說,事務基於一個 **前提(premise)** 採取行動(事務開始時候的事實,例如:“目前有兩名醫生正在值班”)。之後當事務要提交時,原始資料可能已經改變 —— 前提可能不再成立。
|
||||
|
||||
當應用程式進行查詢時(例如,“當前有多少醫生正在值班?”),資料庫不知道應用邏輯如何使用該查詢結果。在這種情況下為了安全,資料庫需要假設任何對該結果集的變更都可能會使該事務中的寫入變得無效。 換而言之,事務中的查詢與寫入可能存在因果依賴。為了提供可序列化的隔離級別,如果事務在過時的前提下執行操作,資料庫必須能檢測到這種情況,並中止事務。
|
||||
當應用程式進行查詢時(例如,“當前有多少醫生正在值班?”),資料庫不知道應用邏輯如何使用該查詢結果。在這種情況下為了安全,資料庫需要假設任何對該結果集的變更都可能會使該事務中的寫入變得無效。換而言之,事務中的查詢與寫入可能存在因果依賴。為了提供可序列化的隔離級別,如果事務在過時的前提下執行操作,資料庫必須能檢測到這種情況,並中止事務。
|
||||
|
||||
資料庫如何知道查詢結果是否可能已經改變?有兩種情況需要考慮:
|
||||
|
||||
@ -814,7 +814,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
與往常一樣,許多工程細節會影響演算法的實際表現。例如一個權衡是跟蹤事務的讀取和寫入的 **粒度(granularity)**。如果資料庫詳細地跟蹤每個事務的活動(細粒度),那麼可以準確地確定哪些事務需要中止,但是簿記開銷可能變得很顯著。簡略的跟蹤速度更快(粗粒度),但可能會導致更多不必要的事務中止。
|
||||
|
||||
在某些情況下,事務可以讀取被另一個事務覆蓋的資訊:這取決於發生了什麼,有時可以證明執行結果無論如何都是可序列化的。 PostgreSQL 使用這個理論來減少不必要的中止次數【11,41】。
|
||||
在某些情況下,事務可以讀取被另一個事務覆蓋的資訊:這取決於發生了什麼,有時可以證明執行結果無論如何都是可序列化的。PostgreSQL 使用這個理論來減少不必要的中止次數【11,41】。
|
||||
|
||||
與兩階段鎖定相比,可序列化快照隔離的最大優點是一個事務不需要阻塞等待另一個事務所持有的鎖。就像在快照隔離下一樣,寫不會阻塞讀,反之亦然。這種設計原則使得查詢延遲更可預測,波動更少。特別是,只讀查詢可以執行在一致快照上,而不需要任何鎖定,這對於讀取繁重的工作負載非常有吸引力。
|
||||
|
||||
|
30
zh-tw/ch8.md
30
zh-tw/ch8.md
@ -24,9 +24,9 @@
|
||||
|
||||
使用分散式系統與在一臺計算機上編寫軟體有著根本的區別,主要的區別在於,有許多新穎和刺激的方法可以使事情出錯【1,2】。在這一章中,我們將瞭解實踐中出現的問題,理解我們能夠依賴,和不可以依賴的東西。
|
||||
|
||||
最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。 在 [第九章](ch9.md) 中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。 但首先,在本章中,我們必須瞭解我們面臨的挑戰。
|
||||
最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。在 [第九章](ch9.md) 中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。但首先,在本章中,我們必須瞭解我們面臨的挑戰。
|
||||
|
||||
本章對分散式系統中可能出現的問題進行徹底的悲觀和沮喪的總結。 我們將研究網路的問題(“[不可靠的網路](#不可靠的網路)”); 時鐘和時序問題(“[不可靠的時鐘](#不可靠的時鐘)”); 我們將討論他們可以避免的程度。 所有這些問題的後果都是困惑的,所以我們將探索如何思考一個分散式系統的狀態,以及如何推理發生的事情(“[知識、真相與謊言](#知識、真相與謊言)”)。
|
||||
本章對分散式系統中可能出現的問題進行徹底的悲觀和沮喪的總結。我們將研究網路的問題(“[不可靠的網路](#不可靠的網路)”); 時鐘和時序問題(“[不可靠的時鐘](#不可靠的時鐘)”); 我們將討論他們可以避免的程度。所有這些問題的後果都是困惑的,所以我們將探索如何思考一個分散式系統的狀態,以及如何推理發生的事情(“[知識、真相與謊言](#知識、真相與謊言)”)。
|
||||
|
||||
|
||||
## 故障與部分失效
|
||||
@ -35,7 +35,7 @@
|
||||
|
||||
單個計算機上的軟體沒有根本性的不可靠原因:當硬體正常工作時,相同的操作總是產生相同的結果(這是確定性的)。如果存在硬體問題(例如,記憶體損壞或聯結器鬆動),其後果通常是整個系統故障(例如,核心恐慌,“藍色畫面宕機”,啟動失敗)。裝有良好軟體的個人計算機通常要麼功能完好,要麼完全失效,而不是介於兩者之間。
|
||||
|
||||
這是計算機設計中的一個有意的選擇:如果發生內部錯誤,我們寧願電腦完全崩潰,而不是返回錯誤的結果,因為錯誤的結果很難處理。因為計算機隱藏了模糊不清的物理實現,並呈現出一個理想化的系統模型,並以數學一樣的完美的方式運作。 CPU 指令總是做同樣的事情;如果你將一些資料寫入記憶體或磁碟,那麼這些資料將保持不變,並且不會被隨機破壞。從第一臺數字計算機開始,*始終正確地計算* 這個設計目標貫穿始終【3】。
|
||||
這是計算機設計中的一個有意的選擇:如果發生內部錯誤,我們寧願電腦完全崩潰,而不是返回錯誤的結果,因為錯誤的結果很難處理。因為計算機隱藏了模糊不清的物理實現,並呈現出一個理想化的系統模型,並以數學一樣的完美的方式運作。CPU 指令總是做同樣的事情;如果你將一些資料寫入記憶體或磁碟,那麼這些資料將保持不變,並且不會被隨機破壞。從第一臺數字計算機開始,*始終正確地計算* 這個設計目標貫穿始終【3】。
|
||||
|
||||
當你編寫執行在多臺計算機上的軟體時,情況有本質上的區別。在分散式系統中,我們不再處於理想化的系統模型中,我們別無選擇,只能面對現實世界的混亂現實。而在現實世界中,各種各樣的事情都可能會出現問題【4】,如下面的軼事所述:
|
||||
|
||||
@ -82,9 +82,9 @@
|
||||
> 你可能想知道這是否有意義 —— 直觀地看來,系統只能像其最不可靠的元件(最薄弱的環節)一樣可靠。事實並非如此:事實上,從不太可靠的潛在基礎構建更可靠的系統是計算機領域的一個古老思想【11】。例如:
|
||||
>
|
||||
> * 糾錯碼允許數字資料在通訊通道上準確傳輸,偶爾會出現一些錯誤,例如由於無線網路上的無線電干擾【12】。
|
||||
> * **網際網路協議(Internet Protocol, IP)** 不可靠:可能丟棄、延遲、重複或重排資料包。 傳輸控制協議(Transmission Control Protocol, TCP)在網際網路協議(IP)之上提供了更可靠的傳輸層:它確保丟失的資料包被重新傳輸,消除重複,並且資料包被重新組裝成它們被傳送的順序。
|
||||
> * **網際網路協議(Internet Protocol, IP)** 不可靠:可能丟棄、延遲、重複或重排資料包。傳輸控制協議(Transmission Control Protocol, TCP)在網際網路協議(IP)之上提供了更可靠的傳輸層:它確保丟失的資料包被重新傳輸,消除重複,並且資料包被重新組裝成它們被傳送的順序。
|
||||
>
|
||||
> 雖然這個系統可以比它的底層部分更可靠,但它的可靠性總是有限的。例如,糾錯碼可以處理少量的單位元錯誤,但是如果你的訊號被幹擾所淹沒,那麼透過通道可以得到多少資料,是有根本性的限制的【13】。 TCP 可以隱藏資料包的丟失,重複和重新排序,但是它不能神奇地消除網路中的延遲。
|
||||
> 雖然這個系統可以比它的底層部分更可靠,但它的可靠性總是有限的。例如,糾錯碼可以處理少量的單位元錯誤,但是如果你的訊號被幹擾所淹沒,那麼透過通道可以得到多少資料,是有根本性的限制的【13】。TCP 可以隱藏資料包的丟失,重複和重新排序,但是它不能神奇地消除網路中的延遲。
|
||||
>
|
||||
> 雖然更可靠的高階系統並不完美,但它仍然有用,因為它處理了一些棘手的低階錯誤,所以其餘的錯誤通常更容易推理和處理。我們將在 “[資料庫的端到端原則](ch12.md#資料庫的端到端原則)” 中進一步探討這個問題。
|
||||
|
||||
@ -190,7 +190,7 @@
|
||||
|
||||
在這種環境下,你只能透過實驗方式選擇超時:在一段較長的時期內、在多臺機器上測量網路往返時間的分佈,以確定延遲的預期變化。然後,考慮到應用程式的特性,可以確定 **故障檢測延遲** 與 **過早超時風險** 之間的適當折衷。
|
||||
|
||||
更好的一種做法是,系統不是使用配置的常量超時時間,而是連續測量響應時間及其變化(抖動),並根據觀察到的響應時間分佈自動調整超時時間。這可以透過 Phi Accrual 故障檢測器【30】來完成,該檢測器在例如 Akka 和 Cassandra 【31】中使用。 TCP 的超時重傳機制也是以類似的方式工作【27】。
|
||||
更好的一種做法是,系統不是使用配置的常量超時時間,而是連續測量響應時間及其變化(抖動),並根據觀察到的響應時間分佈自動調整超時時間。這可以透過 Phi Accrual 故障檢測器【30】來完成,該檢測器在例如 Akka 和 Cassandra 【31】中使用。TCP 的超時重傳機制也是以類似的方式工作【27】。
|
||||
|
||||
### 同步網路與非同步網路
|
||||
|
||||
@ -204,7 +204,7 @@
|
||||
|
||||
#### 我們不能簡單地使網路延遲可預測嗎?
|
||||
|
||||
請注意,電話網路中的電路與 TCP 連線有很大不同:電路是固定數量的預留頻寬,在電路建立時沒有其他人可以使用,而 TCP 連線的資料包 **機會性地** 使用任何可用的網路頻寬。你可以給 TCP 一個可變大小的資料塊(例如,一個電子郵件或一個網頁),它會盡可能在最短的時間內傳輸它。 TCP 連線空閒時,不使用任何頻寬 [^ii]。
|
||||
請注意,電話網路中的電路與 TCP 連線有很大不同:電路是固定數量的預留頻寬,在電路建立時沒有其他人可以使用,而 TCP 連線的資料包 **機會性地** 使用任何可用的網路頻寬。你可以給 TCP 一個可變大小的資料塊(例如,一個電子郵件或一個網頁),它會盡可能在最短的時間內傳輸它。TCP 連線空閒時,不使用任何頻寬 [^ii]。
|
||||
|
||||
[^ii]: 除了偶爾的 keepalive 資料包,如果 TCP keepalive 被啟用。
|
||||
|
||||
@ -286,7 +286,7 @@
|
||||
|
||||
單調鐘不需要同步,但是日曆時鐘需要根據 NTP 伺服器或其他外部時間源來設定才能有用。不幸的是,我們獲取時鐘的方法並不像你所希望的那樣可靠或準確 —— 硬體時鐘和 NTP 可能會變幻莫測。舉幾個例子:
|
||||
|
||||
* 計算機中的石英鐘不夠精確:它會 **漂移**(drifts,即執行速度快於或慢於預期)。時鐘漂移取決於機器的溫度。 Google 假設其伺服器時鐘漂移為 200 ppm(百萬分之一)【41】,相當於每 30 秒與伺服器重新同步一次的時鐘漂移為 6 毫秒,或者每天重新同步的時鐘漂移為 17 秒。即使一切工作正常,此漂移也會限制可以達到的最佳準確度。
|
||||
* 計算機中的石英鐘不夠精確:它會 **漂移**(drifts,即執行速度快於或慢於預期)。時鐘漂移取決於機器的溫度。Google 假設其伺服器時鐘漂移為 200 ppm(百萬分之一)【41】,相當於每 30 秒與伺服器重新同步一次的時鐘漂移為 6 毫秒,或者每天重新同步的時鐘漂移為 17 秒。即使一切工作正常,此漂移也會限制可以達到的最佳準確度。
|
||||
* 如果計算機的時鐘與 NTP 伺服器的時鐘差別太大,可能會拒絕同步,或者本地時鐘將被強制重置【37】。任何觀察重置前後時間的應用程式都可能會看到時間倒退或突然跳躍。
|
||||
* 如果某個節點被 NTP 伺服器的防火牆意外阻塞,有可能會持續一段時間都沒有人會注意到。有證據表明,這在實踐中確實發生過。
|
||||
* NTP 同步只能和網路延遲一樣好,所以當你在擁有可變資料包延遲的擁塞網路上時,NTP 同步的準確性會受到限制。一個實驗表明,當透過網際網路同步時,35 毫秒的最小誤差是可以實現的,儘管偶爾的網路延遲峰值會導致大約一秒的誤差。根據配置,較大的網路延遲會導致 NTP 客戶端完全放棄。
|
||||
@ -311,9 +311,9 @@
|
||||
|
||||
#### 有序事件的時間戳
|
||||
|
||||
讓我們考慮一個特別的情況,一件很有誘惑但也很危險的事情:依賴時鐘,在多個節點上對事件進行排序。 例如,如果兩個客戶端寫入分散式資料庫,誰先到達? 哪一個更近?
|
||||
讓我們考慮一個特別的情況,一件很有誘惑但也很危險的事情:依賴時鐘,在多個節點上對事件進行排序。例如,如果兩個客戶端寫入分散式資料庫,誰先到達? 哪一個更近?
|
||||
|
||||
[圖 8-3](../img/fig8-3.png) 顯示了在具有多主複製的資料庫中對時鐘的危險使用(該例子類似於 [圖 5-9](../img/fig5-9.png))。 客戶端 A 在節點 1 上寫入 `x = 1`;寫入被複制到節點 3;客戶端 B 在節點 3 上增加 x(我們現在有 `x = 2`);最後這兩個寫入都被複制到節點 2。
|
||||
[圖 8-3](../img/fig8-3.png) 顯示了在具有多主複製的資料庫中對時鐘的危險使用(該例子類似於 [圖 5-9](../img/fig5-9.png))。客戶端 A 在節點 1 上寫入 `x = 1`;寫入被複制到節點 3;客戶端 B 在節點 3 上增加 x(我們現在有 `x = 2`);最後這兩個寫入都被複制到節點 2。
|
||||
|
||||
![](../img/fig8-3.png)
|
||||
|
||||
@ -359,7 +359,7 @@ NTP 同步是否能足夠準確,以至於這種不正確的排序不會發生
|
||||
|
||||
我們可以使用同步時鐘的時間戳作為事務 ID 嗎?如果我們能夠獲得足夠好的同步性,那麼這種方法將具有很合適的屬性:更晚的事務會有更大的時間戳。當然,問題在於時鐘精度的不確定性。
|
||||
|
||||
Spanner 以這種方式實現跨資料中心的快照隔離【59,60】。它使用 TrueTime API 報告的時鐘置信區間,並基於以下觀察結果:如果你有兩個置信區間,每個置信區間包含最早和最晚可能的時間戳($A = [A_{earliest}, A_{latest}]$, $B=[B_{earliest}, B_{latest}]$),這兩個區間不重疊(即:$A_{earliest} <A_{latest} <B_{earliest} <B_{latest}$)的話,那麼 B 肯定發生在 A 之後 —— 這是毫無疑問的。只有當區間重疊時,我們才不確定 A 和 B 發生的順序。
|
||||
Spanner 以這種方式實現跨資料中心的快照隔離【59,60】。它使用 TrueTime API 報告的時鐘置信區間,並基於以下觀察結果:如果你有兩個置信區間,每個置信區間包含最早和最晚可能的時間戳($A = [A_{earliest}, A_{latest}]$,$B=[B_{earliest}, B_{latest}]$),這兩個區間不重疊(即:$A_{earliest} <A_{latest} <B_{earliest} <B_{latest}$)的話,那麼 B 肯定發生在 A 之後 —— 這是毫無疑問的。只有當區間重疊時,我們才不確定 A 和 B 發生的順序。
|
||||
|
||||
為了確保事務時間戳反映因果關係,在提交讀寫事務之前,Spanner 在提交讀寫事務時,會故意等待置信區間長度的時間。透過這樣,它可以確保任何可能讀取資料的事務處於足夠晚的時間,因此它們的置信區間不會重疊。為了保持儘可能短的等待時間,Spanner 需要保持儘可能小的時鐘不確定性,為此,Google 在每個資料中心都部署了一個 GPS 接收器或原子鐘,這允許時鐘同步到大約 7 毫秒以內【41】。
|
||||
|
||||
@ -401,9 +401,9 @@ while (true) {
|
||||
* 在虛擬化環境中,可以 **掛起(suspend)** 虛擬機器(暫停執行所有程序並將記憶體內容儲存到磁碟)並恢復(恢復記憶體內容並繼續執行)。這個暫停可以在程序執行的任何時候發生,並且可以持續任意長的時間。這個功能有時用於虛擬機器從一個主機到另一個主機的實時遷移,而不需要重新啟動,在這種情況下,暫停的長度取決於程序寫入記憶體的速率【67】。
|
||||
* 在終端使用者的裝置(如膝上型電腦)上,執行也可能被暫停並隨意恢復,例如當用戶關閉膝上型電腦的蓋子時。
|
||||
* 當作業系統上下文切換到另一個執行緒時,或者當管理程式切換到另一個虛擬機器時(在虛擬機器中執行時),當前正在執行的執行緒可能在程式碼中的任意點處暫停。在虛擬機器的情況下,在其他虛擬機器中花費的 CPU 時間被稱為 **竊取時間(steal time)**。如果機器處於沉重的負載下(即,如果等待執行的執行緒佇列很長),暫停的執行緒再次執行可能需要一些時間。
|
||||
* 如果應用程式執行同步磁碟訪問,則執行緒可能暫停,等待緩慢的磁碟 I/O 操作完成【68】。在許多語言中,即使程式碼沒有包含檔案訪問,磁碟訪問也可能出乎意料地發生 —— 例如,Java 類載入器在第一次使用時惰性載入類檔案,這可能在程式執行過程中隨時發生。 I/O 暫停和 GC 暫停甚至可能合謀組合它們的延遲【69】。如果磁碟實際上是一個網路檔案系統或網路塊裝置(如亞馬遜的 EBS),I/O 延遲進一步受到網路延遲變化的影響【29】。
|
||||
* 如果應用程式執行同步磁碟訪問,則執行緒可能暫停,等待緩慢的磁碟 I/O 操作完成【68】。在許多語言中,即使程式碼沒有包含檔案訪問,磁碟訪問也可能出乎意料地發生 —— 例如,Java 類載入器在第一次使用時惰性載入類檔案,這可能在程式執行過程中隨時發生。I/O 暫停和 GC 暫停甚至可能合謀組合它們的延遲【69】。如果磁碟實際上是一個網路檔案系統或網路塊裝置(如亞馬遜的 EBS),I/O 延遲進一步受到網路延遲變化的影響【29】。
|
||||
* 如果作業系統配置為允許交換到磁碟(頁面交換),則簡單的記憶體訪問可能導致 **頁面錯誤(page fault)**,要求將磁碟中的頁面裝入記憶體。當這個緩慢的 I/O 操作發生時,執行緒暫停。如果記憶體壓力很高,則可能需要將另一個頁面換出到磁碟。在極端情況下,作業系統可能花費大部分時間將頁面交換到記憶體中,而實際上完成的工作很少(這被稱為 **抖動**,即 thrashing)。為了避免這個問題,通常在伺服器機器上禁用頁面排程(如果你寧願幹掉一個程序來釋放記憶體,也不願意冒抖動風險)。
|
||||
* 可以透過傳送 SIGSTOP 訊號來暫停 Unix 程序,例如透過在 shell 中按下 Ctrl-Z。 這個訊號立即阻止程序繼續執行更多的 CPU 週期,直到 SIGCONT 恢復為止,此時它將繼續執行。 即使你的環境通常不使用 SIGSTOP,也可能由運維工程師意外發送。
|
||||
* 可以透過傳送 SIGSTOP 訊號來暫停 Unix 程序,例如透過在 shell 中按下 Ctrl-Z。這個訊號立即阻止程序繼續執行更多的 CPU 週期,直到 SIGCONT 恢復為止,此時它將繼續執行。即使你的環境通常不使用 SIGSTOP,也可能由運維工程師意外發送。
|
||||
|
||||
所有這些事件都可以隨時 **搶佔(preempt)** 正在執行的執行緒,並在稍後的時間恢復執行,而執行緒甚至不會注意到這一點。這個問題類似於在單個機器上使多執行緒程式碼執行緒安全:你不能對時序做任何假設,因為隨時可能發生上下文切換,或者出現並行執行。
|
||||
|
||||
@ -478,7 +478,7 @@ while (true) {
|
||||
|
||||
如果一個節點繼續表現為 **天選者**,即使大多數節點已經宣告它已經死了,則在考慮不周的系統中可能會導致問題。這樣的節點能以自己賦予的權能向其他節點發送訊息,如果其他節點相信,整個系統可能會做一些不正確的事情。
|
||||
|
||||
例如,[圖 8-4](../img/fig8-4.png) 顯示了由於不正確的鎖實現導致的資料損壞錯誤。 (這個錯誤不僅僅是理論上的:HBase 曾經有這個問題【74,75】)假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問,因為如果多個客戶試圖對此寫入,該檔案將被損壞。你嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。
|
||||
例如,[圖 8-4](../img/fig8-4.png) 顯示了由於不正確的鎖實現導致的資料損壞錯誤。(這個錯誤不僅僅是理論上的:HBase 曾經有這個問題【74,75】)假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問,因為如果多個客戶試圖對此寫入,該檔案將被損壞。你嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。
|
||||
|
||||
![](../img/fig8-4.png)
|
||||
|
||||
@ -648,7 +648,7 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
|
||||
|
||||
在本章中,我們也轉換了幾次話題,探討了網路、時鐘和程序的不可靠性是否是不可避免的自然規律。我們看到這並不是:有可能給網路提供硬實時的響應保證和有限的延遲,但是這樣做非常昂貴,且導致硬體資源的利用率降低。大多數非安全關鍵系統會選擇 **便宜而不可靠**,而不是 **昂貴和可靠**。
|
||||
|
||||
我們還談到了超級計算機,它們採用可靠的元件,因此當元件發生故障時必須完全停止並重新啟動。相比之下,分散式系統可以永久執行而不會在服務層面中斷,因為所有的錯誤和維護都可以在節點級別進行處理 —— 至少在理論上是如此。 (實際上,如果一個錯誤的配置變更被應用到所有的節點,仍然會使分散式系統癱瘓)。
|
||||
我們還談到了超級計算機,它們採用可靠的元件,因此當元件發生故障時必須完全停止並重新啟動。相比之下,分散式系統可以永久執行而不會在服務層面中斷,因為所有的錯誤和維護都可以在節點級別進行處理 —— 至少在理論上是如此。(實際上,如果一個錯誤的配置變更被應用到所有的節點,仍然會使分散式系統癱瘓)。
|
||||
|
||||
本章一直在講存在的問題,給我們展現了一幅黯淡的前景。在 [下一章](ch9.md) 中,我們將繼續討論解決方案,並討論一些旨在解決分散式系統中所有問題的演算法。
|
||||
|
||||
|
50
zh-tw/ch9.md
50
zh-tw/ch9.md
@ -84,7 +84,7 @@
|
||||
* $read(x)⇒v$表示客戶端請求讀取暫存器 `x` 的值,資料庫返回值 `v`。
|
||||
* $write(x,v)⇒r$ 表示客戶端請求將暫存器 `x` 設定為值 `v` ,資料庫返回響應 `r` (可能正確,可能錯誤)。
|
||||
|
||||
在 [圖 9-2](../img/fig9-2.png) 中,`x` 的值最初為 `0`,客戶端 C 執行寫請求將其設定為 `1`。發生這種情況時,客戶端 A 和 B 反覆輪詢資料庫以讀取最新值。 A 和 B 的請求可能會收到怎樣的響應?
|
||||
在 [圖 9-2](../img/fig9-2.png) 中,`x` 的值最初為 `0`,客戶端 C 執行寫請求將其設定為 `1`。發生這種情況時,客戶端 A 和 B 反覆輪詢資料庫以讀取最新值。A 和 B 的請求可能會收到怎樣的響應?
|
||||
|
||||
* 客戶端 A 的第一個讀操作,完成於寫操作開始之前,因此必須返回舊值 `0`。
|
||||
* 客戶端 A 的最後一個讀操作,開始於寫操作完成之後。如果資料庫是線性一致性的,它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則讀取處理一定發生在寫入完成之後,因此它必須看到寫入的新值。
|
||||
@ -108,7 +108,7 @@
|
||||
|
||||
在 [圖 9-4](../img/fig9-4.png) 中,除了讀寫之外,還增加了第三種類型的操作:
|
||||
|
||||
* $cas(x, v_{old}, v_{new})⇒r$ 表示客戶端請求進行原子性的 [**比較與設定**](ch7.md#比較並設定(CAS)) 操作。如果暫存器 $x$ 的當前值等於 $v_{old}$ ,則應該原子地設定為 $v_{new}$ 。如果 $x$ 不等於 $v_{old}$ ,則操作應該保持暫存器不變並返回一個錯誤。 $r$ 是資料庫的響應(正確或錯誤)。
|
||||
* $cas(x, v_{old}, v_{new})⇒r$ 表示客戶端請求進行原子性的 [**比較與設定**](ch7.md#比較並設定(CAS)) 操作。如果暫存器 $x$ 的當前值等於 $v_{old}$ ,則應該原子地設定為 $v_{new}$ 。如果 $x$ 不等於 $v_{old}$ ,則操作應該保持暫存器不變並返回一個錯誤。$r$ 是資料庫的響應(正確或錯誤)。
|
||||
|
||||
[圖 9-4](../img/fig9-4.png) 中的每個操作都在我們認為執行操作的時候用豎線標出(在每個操作的條柱之內)。這些標記按順序連在一起,其結果必須是一個有效的暫存器讀寫序列(**每次讀取都必須返回最近一次寫入設定的值**)。
|
||||
|
||||
@ -116,7 +116,7 @@
|
||||
|
||||
![](../img/fig9-4.png)
|
||||
|
||||
**圖 9-4 視覺化讀取和寫入看起來已經生效的時間點。 B 的最後讀取不是線性一致性的**
|
||||
**圖 9-4 視覺化讀取和寫入看起來已經生效的時間點。B 的最後讀取不是線性一致性的**
|
||||
|
||||
[圖 9-4](../img/fig9-4.png) 中有一些有趣的細節需要指出:
|
||||
|
||||
@ -126,9 +126,9 @@
|
||||
|
||||
* 此模型不假設有任何事務隔離:另一個客戶端可能隨時更改值。例如,C 首先讀取 `1` ,然後讀取 `2` ,因為兩次讀取之間的值由 B 更改。可以使用原子 **比較並設定(cas)** 操作來檢查該值是否未被另一客戶端同時更改:B 和 C 的 **cas** 請求成功,但是 D 的 **cas** 請求失敗(在資料庫處理它時,`x` 的值不再是 `0` )。
|
||||
|
||||
* 客戶 B 的最後一次讀取(陰影條柱中)不是線性一致性的。 該操作與 C 的 **cas** 寫操作併發(它將 `x` 從 `2` 更新為 `4` )。在沒有其他請求的情況下,B 的讀取返回 `2` 是可以的。然而,在 B 的讀取開始之前,客戶端 A 已經讀取了新的值 `4` ,因此不允許 B 讀取比 A 更舊的值。再次,與 [圖 9-1](../img/fig9-1.png) 中的 Alice 和 Bob 的情況相同。
|
||||
* 客戶 B 的最後一次讀取(陰影條柱中)不是線性一致性的。該操作與 C 的 **cas** 寫操作併發(它將 `x` 從 `2` 更新為 `4` )。在沒有其他請求的情況下,B 的讀取返回 `2` 是可以的。然而,在 B 的讀取開始之前,客戶端 A 已經讀取了新的值 `4` ,因此不允許 B 讀取比 A 更舊的值。再次,與 [圖 9-1](../img/fig9-1.png) 中的 Alice 和 Bob 的情況相同。
|
||||
|
||||
這就是線性一致性背後的直覺。 正式的定義【6】更準確地描述了它。 透過記錄所有請求和響應的時序,並檢查它們是否可以排列成有效的順序,以測試一個系統的行為是否線性一致性是可能的(儘管在計算上是昂貴的)【11】。
|
||||
這就是線性一致性背後的直覺。正式的定義【6】更準確地描述了它。透過記錄所有請求和響應的時序,並檢查它們是否可以排列成有效的順序,以測試一個系統的行為是否線性一致性是可能的(儘管在計算上是昂貴的)【11】。
|
||||
|
||||
|
||||
> ### 線性一致性與可序列化
|
||||
@ -180,7 +180,7 @@
|
||||
|
||||
計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如 [圖 9-5](../img/fig9-5.png) 所示。
|
||||
|
||||
影象縮放器需要明確的指令來執行尺寸縮放作業,指令是 Web 伺服器透過訊息佇列傳送的(請參閱 [第十一章](ch11.md))。 Web 伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。
|
||||
影象縮放器需要明確的指令來執行尺寸縮放作業,指令是 Web 伺服器透過訊息佇列傳送的(請參閱 [第十一章](ch11.md))。Web 伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。
|
||||
|
||||
![](../img/fig9-5.png)
|
||||
|
||||
@ -204,7 +204,7 @@
|
||||
|
||||
在具有單主複製功能的系統中(請參閱 “[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們 **可能(potential)** 是線性一致性的 [^iv]。然而,實際上並不是每個單主資料庫都是線性一致性的,無論是因為設計的原因(例如,因為使用了快照隔離)還是因為在併發處理上存在錯誤【10】。
|
||||
|
||||
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。 交叉分割槽事務是一個不同的問題(請參閱 “[分散式事務與共識](#分散式事務與共識)”)。
|
||||
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。交叉分割槽事務是一個不同的問題(請參閱 “[分散式事務與共識](#分散式事務與共識)”)。
|
||||
|
||||
從主庫讀取依賴一個假設,你確切地知道領導者是誰。正如在 “[真相由多數所定義](ch8.md#真相由多數所定義)” 中所討論的那樣,一個節點很可能會認為它是領導者,而事實上並非如此 —— 如果具有錯覺的領導者繼續為請求提供服務,可能違反線性一致性【20】。使用非同步複製,故障切換時甚至可能會丟失已提交的寫入(請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”),這同時違反了永續性和線性一致性。
|
||||
|
||||
@ -232,9 +232,9 @@
|
||||
|
||||
在 [圖 9-6](../img/fig9-6.png) 中,$x$ 的初始值為 0,寫入客戶端透過向所有三個副本( $n = 3, w = 3$ )傳送寫入將 $x$ 更新為 `1`。客戶端 A 併發地從兩個節點組成的法定人群( $r = 2$ )中讀取資料,並在其中一個節點上看到新值 `1` 。客戶端 B 也併發地從兩個不同的節點組成的法定人數中讀取,並從兩個節點中取回了舊值 `0` 。
|
||||
|
||||
法定人數條件滿足( $w + r> n$ ),但是這個執行是非線性一致的:B 的請求在 A 的請求完成後開始,但是 B 返回舊值,而 A 返回新值。 (又一次,如同 Alice 和 Bob 的例子 [圖 9-1](../img/fig9-1.png))
|
||||
法定人數條件滿足( $w + r> n$ ),但是這個執行是非線性一致的:B 的請求在 A 的請求完成後開始,但是 B 返回舊值,而 A 返回新值。(又一次,如同 Alice 和 Bob 的例子 [圖 9-1](../img/fig9-1.png))
|
||||
|
||||
有趣的是,透過犧牲效能,可以使 Dynamo 風格的法定人數線性化:讀取者必須在將結果返回給應用之前,同步執行讀修復(請參閱 “[讀修復和反熵](ch5.md#讀修復和反熵)”) ,並且寫入者必須在傳送寫入之前,讀取法定數量節點的最新狀態【24,25】。然而,由於效能損失,Riak 不執行同步讀修復【26】。 Cassandra 在進行法定人數讀取時,**確實** 在等待讀修復完成【27】;但是由於使用了最後寫入勝利的衝突解決方案,當同一個鍵有多個併發寫入時,將不能保證線性一致性。
|
||||
有趣的是,透過犧牲效能,可以使 Dynamo 風格的法定人數線性化:讀取者必須在將結果返回給應用之前,同步執行讀修復(請參閱 “[讀修復和反熵](ch5.md#讀修復和反熵)”) ,並且寫入者必須在傳送寫入之前,讀取法定數量節點的最新狀態【24,25】。然而,由於效能損失,Riak 不執行同步讀修復【26】。Cassandra 在進行法定人數讀取時,**確實** 在等待讀修復完成【27】;但是由於使用了最後寫入勝利的衝突解決方案,當同一個鍵有多個併發寫入時,將不能保證線性一致性。
|
||||
|
||||
而且,這種方式只能實現線性一致的讀寫;不能實現線性一致的比較和設定(CAS)操作,因為它需要一個共識演算法【28】。
|
||||
|
||||
@ -268,11 +268,11 @@
|
||||
* 如果應用需要線性一致性,且某些副本因為網路問題與其他副本斷開連線,那麼這些副本掉線時不能處理請求。請求必須等到網路問題解決,或直接返回錯誤。(無論哪種方式,服務都 **不可用**)。
|
||||
* 如果應用不需要線性一致性,那麼某個副本即使與其他副本斷開連線,也可以獨立處理請求(例如多主複製)。在這種情況下,應用可以在網路問題解決前保持可用,但其行為不是線性一致的。
|
||||
|
||||
[^v]: 這兩種選擇有時分別稱為 CP(在網路分割槽下一致但不可用)和 AP(在網路分割槽下可用但不一致)。 但是,這種分類方案存在一些缺陷【9】,所以最好不要這樣用。
|
||||
[^v]: 這兩種選擇有時分別稱為 CP(在網路分割槽下一致但不可用)和 AP(在網路分割槽下可用但不一致)。但是,這種分類方案存在一些缺陷【9】,所以最好不要這樣用。
|
||||
|
||||
因此,不需要線性一致性的應用對網路問題有更強的容錯能力。這種見解通常被稱為 CAP 定理【29,30,31,32】,由 Eric Brewer 於 2000 年命名,儘管 70 年代的分散式資料庫設計者早就知道了這種權衡【33,34,35,36】。
|
||||
|
||||
CAP 最初是作為一個經驗法則提出的,沒有準確的定義,目的是開始討論資料庫的權衡。那時候許多分散式資料庫側重於在共享儲存的叢集上提供線性一致性的語義【18】,CAP 定理鼓勵資料庫工程師向分散式無共享系統的設計領域深入探索,這類架構更適合實現大規模的網路服務【37】。 對於這種文化上的轉變,CAP 值得讚揚 —— 它見證了自 00 年代中期以來新資料庫的技術爆炸(即 NoSQL)。
|
||||
CAP 最初是作為一個經驗法則提出的,沒有準確的定義,目的是開始討論資料庫的權衡。那時候許多分散式資料庫側重於在共享儲存的叢集上提供線性一致性的語義【18】,CAP 定理鼓勵資料庫工程師向分散式無共享系統的設計領域深入探索,這類架構更適合實現大規模的網路服務【37】。對於這種文化上的轉變,CAP 值得讚揚 —— 它見證了自 00 年代中期以來新資料庫的技術爆炸(即 NoSQL)。
|
||||
|
||||
> #### CAP定理沒有幫助
|
||||
>
|
||||
@ -282,7 +282,7 @@ CAP 最初是作為一個經驗法則提出的,沒有準確的定義,目的
|
||||
>
|
||||
> 在 CAP 的討論中,術語可用性有幾個相互矛盾的定義,形式化作為一個定理【30】並不符合其通常的含義【40】。許多所謂的 “高可用”(容錯)系統實際上不符合 CAP 對可用性的特殊定義。總而言之,圍繞著 CAP 有很多誤解和困惑,並不能幫助我們更好地理解系統,所以最好避免使用 CAP。
|
||||
|
||||
CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一個一致性模型(即線性一致性)和一種故障(網路分割槽 [^vi],或活躍但彼此斷開的節點)。它沒有討論任何關於網路延遲,死亡節點或其他權衡的事。 因此,儘管 CAP 在歷史上有一些影響力,但對於設計系統而言並沒有實際價值【9,40】。
|
||||
CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一個一致性模型(即線性一致性)和一種故障(網路分割槽 [^vi],或活躍但彼此斷開的節點)。它沒有討論任何關於網路延遲,死亡節點或其他權衡的事。因此,儘管 CAP 在歷史上有一些影響力,但對於設計系統而言並沒有實際價值【9,40】。
|
||||
|
||||
在分散式系統中有更多有趣的 “不可能” 的結果【41】,且 CAP 定理現在已經被更精確的結果取代【2,42】,所以它現在基本上成了歷史古蹟了。
|
||||
|
||||
@ -417,7 +417,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
儘管剛才描述的三個序列號生成器與因果不一致,但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳,萊斯利・蘭伯特(Leslie Lamport)於 1978 年提出【56】,現在是分散式系統領域中被引用最多的論文之一。
|
||||
|
||||
[圖 9-8](../img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。 蘭伯特時間戳就是兩者的簡單組合:(計數器,節點 ID)$(counter, node ID)$。兩個節點有時可能具有相同的計數器值,但透過在時間戳中包含節點 ID,每個時間戳都是唯一的。
|
||||
[圖 9-8](../img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。蘭伯特時間戳就是兩者的簡單組合:(計數器,節點 ID)$(counter, node ID)$。兩個節點有時可能具有相同的計數器值,但透過在時間戳中包含節點 ID,每個時間戳都是唯一的。
|
||||
|
||||
![](../img/fig9-8.png)
|
||||
|
||||
@ -432,7 +432,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
只要每一個操作都攜帶著最大計數器值,這個方案確保蘭伯特時間戳的排序與因果一致,因為每個因果依賴都會導致時間戳增長。
|
||||
|
||||
蘭伯特時間戳有時會與我們在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中看到的版本向量相混淆。雖然兩者有一些相似之處,但它們有著不同的目的:版本向量可以區分兩個操作是併發的,還是一個因果依賴另一個;而蘭伯特時間戳總是施行一個全序。從蘭伯特時間戳的全序中,你無法分辨兩個操作是併發的還是因果依賴的。 蘭伯特時間戳優於版本向量的地方是,它更加緊湊。
|
||||
蘭伯特時間戳有時會與我們在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中看到的版本向量相混淆。雖然兩者有一些相似之處,但它們有著不同的目的:版本向量可以區分兩個操作是併發的,還是一個因果依賴另一個;而蘭伯特時間戳總是施行一個全序。從蘭伯特時間戳的全序中,你無法分辨兩個操作是併發的還是因果依賴的。蘭伯特時間戳優於版本向量的地方是,它更加緊湊。
|
||||
|
||||
#### 光有時間戳排序還不夠
|
||||
|
||||
@ -462,9 +462,9 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
> #### 順序保證的範圍
|
||||
>
|
||||
> 每個分割槽各有一個主庫的分割槽資料庫,通常只在每個分割槽內維持順序,這意味著它們不能提供跨分割槽的一致性保證(例如,一致性快照,外來鍵引用)。 跨所有分割槽的全序是可能的,但需要額外的協調【59】。
|
||||
> 每個分割槽各有一個主庫的分割槽資料庫,通常只在每個分割槽內維持順序,這意味著它們不能提供跨分割槽的一致性保證(例如,一致性快照,外來鍵引用)。跨所有分割槽的全序是可能的,但需要額外的協調【59】。
|
||||
|
||||
全序廣播通常被描述為在節點間交換訊息的協議。 非正式地講,它要滿足兩個安全屬性:
|
||||
全序廣播通常被描述為在節點間交換訊息的協議。非正式地講,它要滿足兩個安全屬性:
|
||||
|
||||
* 可靠交付(reliable delivery)
|
||||
|
||||
@ -494,7 +494,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
如 [圖 9-4](../img/fig9-4.png) 所示,線性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有著密切的聯絡 [^x]。
|
||||
|
||||
[^x]: 從形式上講,線性一致讀寫暫存器是一個 “更容易” 的問題。 全序廣播等價於共識【67】,而共識問題在非同步的崩潰 - 停止模型【68】中沒有確定性的解決方案,而線性一致的讀寫暫存器 **可以** 在這種模型中實現【23,24,25】。 然而,支援諸如 **比較並設定(CAS, compare-and-set)**,或 **自增並返回(increment-and-get)** 的原子操作使它等價於共識問題【28】。 因此,共識問題與線性一致暫存器問題密切相關。
|
||||
[^x]: 從形式上講,線性一致讀寫暫存器是一個 “更容易” 的問題。全序廣播等價於共識【67】,而共識問題在非同步的崩潰 - 停止模型【68】中沒有確定性的解決方案,而線性一致的讀寫暫存器 **可以** 在這種模型中實現【23,24,25】。然而,支援諸如 **比較並設定(CAS, compare-and-set)**,或 **自增並返回(increment-and-get)** 的原子操作使它等價於共識問題【28】。因此,共識問題與線性一致暫存器問題密切相關。
|
||||
|
||||
全序廣播是非同步的:訊息被保證以固定的順序可靠地傳送,但是不能保證訊息 **何時** 被送達(所以一個接收者可能落後於其他接收者)。相比之下,線性一致性是新鮮性的保證:讀取一定能看見最新的寫入值。
|
||||
|
||||
@ -508,14 +508,14 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
2. 讀日誌,並等待你剛才追加的訊息被讀回。[^xi]
|
||||
4. 檢查是否有任何訊息聲稱目標使用者名稱的所有權。如果這些訊息中的第一條就是你自己的訊息,那麼你就成功了:你可以提交聲稱的使用者名稱(也許是透過向日志追加另一條訊息)並向客戶端確認。如果所需使用者名稱的第一條訊息來自其他使用者,則中止操作。
|
||||
|
||||
[^xi]: 如果你不等待,而是在訊息入隊之後立即確認寫入,則會得到類似於多核 x86 處理器記憶體的一致性模型【43】。 該模型既不是線性一致的也不是順序一致的。
|
||||
[^xi]: 如果你不等待,而是在訊息入隊之後立即確認寫入,則會得到類似於多核 x86 處理器記憶體的一致性模型【43】。該模型既不是線性一致的也不是順序一致的。
|
||||
|
||||
由於日誌項是以相同順序送達至所有節點,因此如果有多個併發寫入,則所有節點會對最先到達者達成一致。選擇衝突寫入中的第一個作為勝利者,並中止後來者,以此確定所有節點對某個寫入是提交還是中止達成一致。類似的方法可以在一個日誌的基礎上實現可序列化的多物件事務【62】。
|
||||
|
||||
儘管這一過程保證寫入是線性一致的,但它並不保證讀取也是線性一致的 —— 如果你從與日誌非同步更新的儲存中讀取資料,結果可能是陳舊的。 (精確地說,這裡描述的過程提供了 **順序一致性(sequential consistency)**【47,64】,有時也稱為 **時間線一致性(timeline consistency)**【65,66】,比線性一致性稍微弱一些的保證)。為了使讀取也線性一致,有幾個選項:
|
||||
儘管這一過程保證寫入是線性一致的,但它並不保證讀取也是線性一致的 —— 如果你從與日誌非同步更新的儲存中讀取資料,結果可能是陳舊的。(精確地說,這裡描述的過程提供了 **順序一致性(sequential consistency)**【47,64】,有時也稱為 **時間線一致性(timeline consistency)**【65,66】,比線性一致性稍微弱一些的保證)。為了使讀取也線性一致,有幾個選項:
|
||||
|
||||
* 你可以透過在日誌中追加一條訊息,然後讀取日誌,直到該訊息被讀回才執行實際的讀取操作。訊息在日誌中的位置因此定義了讀取發生的時間點(etcd 的法定人數讀取有些類似這種情況【16】)。
|
||||
* 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。 (這是 Zookeeper `sync()` 操作背後的思想【15】)。
|
||||
* 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。(這是 Zookeeper `sync()` 操作背後的思想【15】)。
|
||||
* 你可以從同步更新的副本中進行讀取,因此可以確保結果是最新的(這種技術用於鏈式複製(chain replication)【63】;請參閱 “[關於複製的研究](ch5.md#關於複製的研究)”)。
|
||||
|
||||
#### 使用線性一致性儲存實現全序廣播
|
||||
@ -551,7 +551,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就 ACID 而言,請參閱 “[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止 / 回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為 **原子提交(atomic commit)** 問題 [^xii]。
|
||||
|
||||
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在 **所有** 參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就 **任意一個** 被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞** 原子提交則要比共識更為困難 —— 請參閱 “[三階段提交](#三階段提交)”。
|
||||
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在 **所有** 參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。共識則允許就 **任意一個** 被參與者提出的候選值達成一致。然而,原子提交和共識可以相互簡化為對方【70,71】。**非阻塞** 原子提交則要比共識更為困難 —— 請參閱 “[三階段提交](#三階段提交)”。
|
||||
|
||||
> ### 共識的不可能性
|
||||
>
|
||||
@ -594,7 +594,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
#### 兩階段提交簡介
|
||||
|
||||
**兩階段提交(two-phase commit)** 是一種用於實現跨多個節點的原子事務提交的演算法,即確保所有節點提交或所有節點中止。 它是分散式資料庫中的經典演算法【13,35,75】。 2PC 在某些資料庫內部使用,也以 **XA 事務** 的形式對應用可用【76,77】(例如 Java Transaction API 支援)或以 SOAP Web 服務的 `WS-AtomicTransaction` 形式提供給應用【78,79】。
|
||||
**兩階段提交(two-phase commit)** 是一種用於實現跨多個節點的原子事務提交的演算法,即確保所有節點提交或所有節點中止。它是分散式資料庫中的經典演算法【13,35,75】。2PC 在某些資料庫內部使用,也以 **XA 事務** 的形式對應用可用【76,77】(例如 Java Transaction API 支援)或以 SOAP Web 服務的 `WS-AtomicTransaction` 形式提供給應用【78,79】。
|
||||
|
||||
[圖 9-9](../img/fig9-9.png) 說明了 2PC 的基本流程。2PC 中的提交 / 中止過程分為兩個階段(因此而得名),而不是單節點事務中的單個提交請求。
|
||||
|
||||
@ -604,7 +604,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
> #### 不要把2PC和2PL搞混了
|
||||
>
|
||||
> 兩階段提交(2PC)和兩階段鎖定(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”)是兩個完全不同的東西。 2PC 在分散式資料庫中提供原子提交,而 2PL 提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。
|
||||
> 兩階段提交(2PC)和兩階段鎖定(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”)是兩個完全不同的東西。2PC 在分散式資料庫中提供原子提交,而 2PL 提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。
|
||||
|
||||
2PC 使用一個通常不會出現在單節點事務中的新元件:**協調者**(coordinator,也稱為 **事務管理器**,即 transaction manager)。協調者通常在請求事務的相同應用程序中以庫的形式實現(例如,嵌入在 Java EE 容器中),但也可以是單獨的程序或服務。這種協調者的例子包括 Narayana、JOTM、BTM 或 MSDTC。
|
||||
|
||||
@ -617,7 +617,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了
|
||||
|
||||
#### 系統承諾
|
||||
|
||||
這個簡短的描述可能並沒有說清楚為什麼兩階段提交保證了原子性,而跨多個節點的一階段提交卻沒有。在兩階段提交的情況下,準備請求和提交請求當然也可以輕易丟失。 2PC 又有什麼不同呢?
|
||||
這個簡短的描述可能並沒有說清楚為什麼兩階段提交保證了原子性,而跨多個節點的一階段提交卻沒有。在兩階段提交的情況下,準備請求和提交請求當然也可以輕易丟失。2PC 又有什麼不同呢?
|
||||
|
||||
為了理解它的工作原理,我們必須更詳細地分解這個過程:
|
||||
|
||||
@ -872,7 +872,7 @@ ZooKeeper/Chubby 模型執行良好的一個例子是,如果你有幾個程序
|
||||
|
||||
ZooKeeper、etcd 和 Consul 也經常用於服務發現 —— 也就是找出你需要連線到哪個 IP 地址才能到達特定的服務。在雲資料中心環境中,虛擬機器來來往往很常見,你通常不會事先知道服務的 IP 地址。相反,你可以配置你的服務,使其在啟動時註冊服務登錄檔中的網路端點,然後可以由其他服務找到它們。
|
||||
|
||||
但是,服務發現是否需要達成共識還不太清楚。 DNS 是查詢服務名稱的 IP 地址的傳統方式,它使用多層快取來實現良好的效能和可用性。從 DNS 讀取是絕對不線性一致性的,如果 DNS 查詢的結果有點陳舊,通常不會有問題【109】。 DNS 的可用性和對網路中斷的魯棒性更重要。
|
||||
但是,服務發現是否需要達成共識還不太清楚。DNS 是查詢服務名稱的 IP 地址的傳統方式,它使用多層快取來實現良好的效能和可用性。從 DNS 讀取是絕對不線性一致性的,如果 DNS 查詢的結果有點陳舊,通常不會有問題【109】。DNS 的可用性和對網路中斷的魯棒性更重要。
|
||||
|
||||
儘管服務發現並不需要共識,但領導者選舉卻是如此。因此,如果你的共識系統已經知道領導是誰,那麼也可以使用這些資訊來幫助其他服務發現領導是誰。為此,一些共識系統支援只讀快取副本。這些副本非同步接收共識演算法所有決策的日誌,但不主動參與投票。因此,它們能夠提供不需要線性一致性的讀取請求。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 術語表
|
||||
|
||||
> 請注意,本術語表中的定義簡短而簡單,旨在傳達核心思想,而不是術語的完整細微之處。 有關更多詳細資訊,請參閱正文中的參考資料。
|
||||
> 請注意,本術語表中的定義簡短而簡單,旨在傳達核心思想,而不是術語的完整細微之處。有關更多詳細資訊,請參閱正文中的參考資料。
|
||||
|
||||
|
||||
* **非同步(asynchronous)**
|
||||
@ -151,35 +151,35 @@
|
||||
|
||||
* **規範化(normalized)**
|
||||
|
||||
以沒有冗餘或重複的方式進行結構化。 在規範化資料庫中,當某些資料發生變化時,你只需要在一個地方進行更改,而不是在許多不同的地方複製很多次。 請參閱“[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”。
|
||||
以沒有冗餘或重複的方式進行結構化。在規範化資料庫中,當某些資料發生變化時,你只需要在一個地方進行更改,而不是在許多不同的地方複製很多次。請參閱“[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”。
|
||||
|
||||
* **OLAP(Online Analytic Processing)**
|
||||
|
||||
線上分析處理。 透過對大量記錄進行聚合(例如,計數,總和,平均)來表徵的訪問模式。 請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”。
|
||||
線上分析處理。透過對大量記錄進行聚合(例如,計數,總和,平均)來表徵的訪問模式。請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”。
|
||||
|
||||
* **OLTP(Online Transaction Processing)**
|
||||
|
||||
線上事務處理。 訪問模式的特點是快速查詢,讀取或寫入少量記錄,這些記錄通常透過鍵索引。 請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”。
|
||||
線上事務處理。訪問模式的特點是快速查詢,讀取或寫入少量記錄,這些記錄通常透過鍵索引。請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”。
|
||||
|
||||
* **分割槽(partitioning)**
|
||||
|
||||
將單機上的大型資料集或計算結果拆分為較小部分,並將其分佈到多臺機器上。 也稱為分片。見[第六章](ch6.md)。
|
||||
將單機上的大型資料集或計算結果拆分為較小部分,並將其分佈到多臺機器上。也稱為分片。見[第六章](ch6.md)。
|
||||
|
||||
* **百分位點(percentile)**
|
||||
|
||||
透過計算有多少值高於或低於某個閾值來衡量值分佈的方法。 例如,某個時間段的第95個百分位響應時間是時間t,則該時間段中,95%的請求完成時間小於t,5%的請求完成時間要比t長。 請參閱“[描述效能](ch1.md#描述效能)”。
|
||||
透過計算有多少值高於或低於某個閾值來衡量值分佈的方法。例如,某個時間段的第95個百分位響應時間是時間t,則該時間段中,95%的請求完成時間小於t,5%的請求完成時間要比t長。請參閱“[描述效能](ch1.md#描述效能)”。
|
||||
|
||||
* **主鍵(primary key)**
|
||||
|
||||
唯一標識記錄的值(通常是數字或字串)。 在許多應用程式中,主鍵由系統在建立記錄時生成(例如,按順序或隨機); 它們通常不由使用者設定。 另請參閱次級索引。
|
||||
唯一標識記錄的值(通常是數字或字串)。在許多應用程式中,主鍵由系統在建立記錄時生成(例如,按順序或隨機); 它們通常不由使用者設定。另請參閱次級索引。
|
||||
|
||||
* **法定人數(quorum)**
|
||||
|
||||
在操作完成之前,需要對操作進行投票的最少節點數量。 請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”。
|
||||
在操作完成之前,需要對操作進行投票的最少節點數量。請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”。
|
||||
|
||||
* **再平衡(rebalance)**
|
||||
|
||||
將資料或服務從一個節點移動到另一個節點以實現負載均衡。 請參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”。
|
||||
將資料或服務從一個節點移動到另一個節點以實現負載均衡。請參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”。
|
||||
|
||||
* **複製(replication)**
|
||||
|
||||
@ -187,37 +187,37 @@
|
||||
|
||||
* **模式(schema)**
|
||||
|
||||
一些資料結構的描述,包括其欄位和資料型別。 可以在資料生命週期的不同點檢查某些資料是否符合模式(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),模式可以隨時間變化(請參閱[第四章](ch4.md))。
|
||||
一些資料結構的描述,包括其欄位和資料型別。可以在資料生命週期的不同點檢查某些資料是否符合模式(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),模式可以隨時間變化(請參閱[第四章](ch4.md))。
|
||||
|
||||
* **次級索引(secondary index)**
|
||||
|
||||
與主要資料儲存器一起維護的附加資料結構,使你可以高效地搜尋與某種條件相匹配的記錄。 請參閱“[其他索引結構](ch3.md#其他索引結構)”和“[分割槽與次級索引](ch6.md#分割槽與次級索引)”。
|
||||
與主要資料儲存器一起維護的附加資料結構,使你可以高效地搜尋與某種條件相匹配的記錄。請參閱“[其他索引結構](ch3.md#其他索引結構)”和“[分割槽與次級索引](ch6.md#分割槽與次級索引)”。
|
||||
|
||||
* **可序列化(serializable)**
|
||||
|
||||
保證多個併發事務同時執行時,它們的行為與按順序逐個執行事務相同。 請參閱第七章的“[可序列化](ch7.md#可序列化)”。
|
||||
保證多個併發事務同時執行時,它們的行為與按順序逐個執行事務相同。請參閱第七章的“[可序列化](ch7.md#可序列化)”。
|
||||
|
||||
* **無共享(shared-nothing)**
|
||||
|
||||
與共享記憶體或共享磁碟架構相比,獨立節點(每個節點都有自己的CPU,記憶體和磁碟)透過傳統網路連線。 見[第二部分](part-ii.md)的介紹。
|
||||
與共享記憶體或共享磁碟架構相比,獨立節點(每個節點都有自己的CPU,記憶體和磁碟)透過傳統網路連線。見[第二部分](part-ii.md)的介紹。
|
||||
|
||||
* **偏斜(skew)**
|
||||
|
||||
各分割槽負載不平衡,例如某些分割槽有大量請求或資料,而其他分割槽則少得多。也被稱為熱點。請參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”和“[處理偏斜](ch10.md#處理偏斜)”。
|
||||
|
||||
時間線異常導致事件以不期望的順序出現。 請參閱“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中的關於讀取偏差的討論,“[寫入偏差與幻讀](ch7.md#寫入偏差與幻讀)”中的寫入偏差以及“[有序事件的時間戳](ch8.md#有序事件的時間戳)”中的時鐘偏斜。
|
||||
時間線異常導致事件以不期望的順序出現。請參閱“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中的關於讀取偏差的討論,“[寫入偏差與幻讀](ch7.md#寫入偏差與幻讀)”中的寫入偏差以及“[有序事件的時間戳](ch8.md#有序事件的時間戳)”中的時鐘偏斜。
|
||||
|
||||
* **腦裂(split brain)**
|
||||
|
||||
兩個節點同時認為自己是領導者的情況,這種情況可能違反系統擔保。 請參閱“[處理節點宕機](ch5.md#處理節點宕機)”和“[真相由多數所定義](ch8.md#真相由多數所定義)”。
|
||||
兩個節點同時認為自己是領導者的情況,這種情況可能違反系統擔保。請參閱“[處理節點宕機](ch5.md#處理節點宕機)”和“[真相由多數所定義](ch8.md#真相由多數所定義)”。
|
||||
|
||||
* **儲存過程(stored procedure)**
|
||||
|
||||
一種對事務邏輯進行編碼的方式,它可以完全在資料庫伺服器上執行,事務執行期間無需與客戶端通訊。 請參閱“[真的序列執行](ch7.md#真的序列執行)”。
|
||||
一種對事務邏輯進行編碼的方式,它可以完全在資料庫伺服器上執行,事務執行期間無需與客戶端通訊。請參閱“[真的序列執行](ch7.md#真的序列執行)”。
|
||||
|
||||
* **流處理(stream process)**
|
||||
|
||||
持續執行的計算。可以持續接收事件流作為輸入,並得出一些輸出。 見[第十一章](ch11.md)。
|
||||
持續執行的計算。可以持續接收事件流作為輸入,並得出一些輸出。見[第十一章](ch11.md)。
|
||||
|
||||
* **同步(synchronous)**
|
||||
|
||||
@ -225,28 +225,28 @@
|
||||
|
||||
* **記錄系統(system of record)**
|
||||
|
||||
一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統衍生。 請參閱[第三部分](part-iii.md)的介紹。
|
||||
一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統衍生。請參閱[第三部分](part-iii.md)的介紹。
|
||||
|
||||
* **超時(timeout)**
|
||||
|
||||
檢測故障的最簡單方法之一,即在一段時間內觀察是否缺乏響應。 但是,不可能知道超時是由於遠端節點的問題還是網路中的問題造成的。 請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”。
|
||||
檢測故障的最簡單方法之一,即在一段時間內觀察是否缺乏響應。但是,不可能知道超時是由於遠端節點的問題還是網路中的問題造成的。請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”。
|
||||
|
||||
* **全序(total order)**
|
||||
|
||||
一種比較事物的方法(例如時間戳),可以讓你總是說出兩件事中哪一件更大,哪件更小。 總的來說,有些東西是無法比擬的(不能說哪個更大或更小)的順序稱為偏序。 請參閱“[因果順序不是全序的](ch9.md#因果順序不是全序的)”。
|
||||
一種比較事物的方法(例如時間戳),可以讓你總是說出兩件事中哪一件更大,哪件更小。總的來說,有些東西是無法比擬的(不能說哪個更大或更小)的順序稱為偏序。請參閱“[因果順序不是全序的](ch9.md#因果順序不是全序的)”。
|
||||
|
||||
* **事務(transaction)**
|
||||
|
||||
為了簡化錯誤處理和併發問題,將幾個讀寫操作分組到一個邏輯單元中。 見[第七章](ch7.md)。
|
||||
為了簡化錯誤處理和併發問題,將幾個讀寫操作分組到一個邏輯單元中。見[第七章](ch7.md)。
|
||||
|
||||
* **兩階段提交(2PC, two-phase commit)**
|
||||
|
||||
一種確保多個數據庫節點全部提交或全部中止事務的演算法。 請參閱“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”。
|
||||
一種確保多個數據庫節點全部提交或全部中止事務的演算法。請參閱“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”。
|
||||
|
||||
* **兩階段鎖定(2PL, two-phase locking)**
|
||||
|
||||
一種用於實現可序列化隔離的演算法,該演算法透過事務獲取對其讀取或寫入的所有資料的鎖,直到事務結束。 請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”。
|
||||
一種用於實現可序列化隔離的演算法,該演算法透過事務獲取對其讀取或寫入的所有資料的鎖,直到事務結束。請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”。
|
||||
|
||||
* **無邊界(unbounded)**
|
||||
|
||||
沒有任何已知的上限或大小。 反義詞是邊界(bounded)。
|
||||
沒有任何已知的上限或大小。反義詞是邊界(bounded)。
|
@ -27,7 +27,7 @@
|
||||
|
||||
如果你需要的只是伸縮至更高的 **載荷(load)**,最簡單的方法就是購買更強大的機器(有時稱為 **垂直伸縮**,即 vertical scaling,或 **向上伸縮**,即 scale up)。許多處理器,記憶體和磁碟可以在同一個作業系統下相互連線,快速的相互連線允許任意處理器訪問記憶體或磁碟的任意部分。在這種 **共享記憶體架構(shared-memory architecture)** 中,所有的元件都可以看作一臺單獨的機器 [^i]。
|
||||
|
||||
[^i]: 在大型機中,儘管任意處理器都可以訪問記憶體的任意部分,但總有一些記憶體區域與一些處理器更接近(稱為 **非均勻記憶體訪問(nonuniform memory access, NUMA)**【1】)。 為了有效利用這種架構特性,需要對處理進行細分,以便每個處理器主要訪問臨近的記憶體,這意味著即使表面上看起來只有一臺機器在執行,**分割槽(partitioning)** 仍然是必要的。
|
||||
[^i]: 在大型機中,儘管任意處理器都可以訪問記憶體的任意部分,但總有一些記憶體區域與一些處理器更接近(稱為 **非均勻記憶體訪問(nonuniform memory access, NUMA)**【1】)。為了有效利用這種架構特性,需要對處理進行細分,以便每個處理器主要訪問臨近的記憶體,這意味著即使表面上看起來只有一臺機器在執行,**分割槽(partitioning)** 仍然是必要的。
|
||||
|
||||
共享記憶體方法的問題在於,成本增長速度快於線性增長:一臺有著雙倍處理器數量,雙倍記憶體大小,雙倍磁碟容量的機器,通常成本會遠遠超過原來的兩倍。而且可能因為存在瓶頸,並不足以處理雙倍的載荷。
|
||||
|
||||
@ -53,11 +53,11 @@
|
||||
|
||||
* 複製(Replication)
|
||||
|
||||
在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。 複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。 複製也有助於改善效能。 [第五章](ch5.md) 將討論複製。
|
||||
在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。複製也有助於改善效能。[第五章](ch5.md) 將討論複製。
|
||||
|
||||
* 分割槽 (Partitioning)
|
||||
|
||||
將一個大型資料庫拆分成較小的子集(稱為 **分割槽**,即 partitions),從而不同的分割槽可以指派給不同的 **節點**(nodes,亦稱 **分片**,即 sharding)。 [第六章](ch6.md) 將討論分割槽。
|
||||
將一個大型資料庫拆分成較小的子集(稱為 **分割槽**,即 partitions),從而不同的分割槽可以指派給不同的 **節點**(nodes,亦稱 **分片**,即 sharding)。[第六章](ch6.md) 將討論分割槽。
|
||||
|
||||
複製和分割槽是不同的機制,但它們經常同時使用。如 [圖 II-1](../img/figii-1.png) 所示。
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
理解了這些概念,就可以開始討論在分散式系統中需要做出的困難抉擇。[第七章](ch7.md) 將討論 **事務(Transaction)**,這對於瞭解資料系統中可能出現的各種問題,以及我們可以做些什麼很有幫助。[第八章](ch8.md) 和 [第九章](ch9.md) 將討論分散式系統的根本侷限性。
|
||||
|
||||
在本書的 [第三部分](part-iii.md) 中,將討論如何將多個(可能是分散式的)資料儲存整合為一個更大的系統,以滿足複雜的應用需求。 但首先,我們來聊聊分散式的資料。
|
||||
在本書的 [第三部分](part-iii.md) 中,將討論如何將多個(可能是分散式的)資料儲存整合為一個更大的系統,以滿足複雜的應用需求。但首先,我們來聊聊分散式的資料。
|
||||
|
||||
|
||||
## 索引
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 序言
|
||||
|
||||
如果近幾年從業於軟體工程,特別是伺服器端和後端系統開發,那麼你很有可能已經被大量關於資料儲存和處理的時髦詞彙轟炸過了: NoSQL!大資料!Web-Scale!分片!最終一致性!ACID! CAP 定理!雲服務!MapReduce!實時!
|
||||
如果近幾年從業於軟體工程,特別是伺服器端和後端系統開發,那麼你很有可能已經被大量關於資料儲存和處理的時髦詞彙轟炸過了: NoSQL!大資料!Web-Scale!分片!最終一致性!ACID!CAP 定理!雲服務!MapReduce!實時!
|
||||
|
||||
在最近十年中,我們看到了很多有趣的進展,關於資料庫,分散式系統,以及在此基礎上構建應用程式的方式。這些進展有著各種各樣的驅動力:
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
|
||||
## 參考文獻與延伸閱讀
|
||||
|
||||
本書中討論的大部分內容已經在其它地方以某種形式出現過了 —— 會議簡報、研究論文、部落格文章、程式碼、BUG 跟蹤器、郵件列表以及工程習慣中。本書總結了不同來源資料中最重要的想法,並在文字中包含了指向原始文獻的連結。 如果你想更深入地探索一個領域,那麼每章末尾的參考文獻都是很好的資源,其中大部分可以免費線上獲取。
|
||||
本書中討論的大部分內容已經在其它地方以某種形式出現過了 —— 會議簡報、研究論文、部落格文章、程式碼、BUG 跟蹤器、郵件列表以及工程習慣中。本書總結了不同來源資料中最重要的想法,並在文字中包含了指向原始文獻的連結。如果你想更深入地探索一個領域,那麼每章末尾的參考文獻都是很好的資源,其中大部分可以免費線上獲取。
|
||||
|
||||
|
||||
## O‘Reilly Safari
|
||||
|
Loading…
Reference in New Issue
Block a user