ch1 review & rollback & improvement

This commit is contained in:
Vonng 2018-03-15 21:02:03 +08:00
parent cd5e3b03cf
commit 9766f0ad13

54
ch1.md
View File

@ -2,7 +2,7 @@
![](img/ch1.png)
> 互联网做得太棒了,以至于多数人将它看作像海洋这样的天然资源,而不是什么人工产物。 你还记得上一次出现这种大规模并且零失误的技术是什么时候吗?
> 互联网做得太棒了,以至于多数人将它看作像海洋这样的天然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术, 你还记得是什么时候吗?
>
> ——阿兰·凯在接受Dobb博士杂志采访时说2012年
@ -12,7 +12,7 @@
现今很多应用程序都是**数据密集型data-intensive**的,而非**计算密集型compute-intensive**的。因此CPU很少成为这类应用的瓶颈更大的问题通常来自数据量、数据复杂性、以及数据的变更速度。
数据密集型应用通常由提供了很多通用功能的标准组件构成,例如
数据密集型应用通常由标准组件构建而成,标准组件提供了很多通用的功能;例如,许多应用程序都需要
***数据库database***
@ -34,9 +34,9 @@
定期压缩累积的大批量数据
如果这些功能听上去平淡无奇,那是因为这些**数据系统data system**成功地融入到我们的生活中:我们对此司空见惯,只因为我们一直用着它们。绝大多数工程师不会想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。
如果这些功能听上去平淡无奇,那是因为这些**数据系统data system**是非常成功的抽象,我们一直不假思索地使用它们并习以为常。绝大多数工程师不会想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。
但事实并没有这么简单。因为不同的应用有不同的需求,所以数据库系统也是百花齐放,有着各式各样的特性。我们有很多种手段可以实现缓存,也有好几种方法可以搞定搜索索引,诸如此类。因此在开发应用前,我们有必要先弄清楚最适合手头工作的工具和方法。而且当单个工具解决不了你的问题时,你会发现组合使用这些工具还是挺有难度的。
但事实并没有这么简单。不同的应用有不同的需求,所以数据库系统也是百花齐放,有着各式各样的特性。我们有很多种手段可以实现缓存,也有好几种方法可以搞定搜索索引,诸如此类。因此在开发应用前,我们有必要先弄清楚最适合手头工作的工具和方法。而且当单个工具解决不了你的问题时,你会发现组合使用这些工具还是挺有难度的。
本书将是一趟关于数据系统原理、实践与应用的旅程,并讲述了设计数据密集型应用的方法。我们将探索不同工具之间的共性与特性,以及各自的实现原理。
@ -46,11 +46,11 @@
## 关于数据系统的思考
我们通常认为,数据库、消息队列、缓存等工具分属于几个差异显著的类别。虽然数据库和消息队列表面上有一些相似性——它们都会存一段时间的数据——但它们有迥然不同的访问模式,这意味着迥异的性能特征和实现手段。
我们通常认为,数据库、消息队列、缓存等工具分属于几个差异显著的类别。虽然数据库和消息队列表面上有一些相似性——它们都会存一段时间的数据——但它们有迥然不同的访问模式,这意味着迥异的性能特征和实现手段。
那为什么我们现在要把这些东西放在**数据系统data system**这个总称之下相提并论呢?
我们为什么要把这些东西放在**数据系统data system**的总称之下混为一谈呢?
近些年来出现了许多新的数据存储工具与数据处理工具。它们针对不同应用场景进行优化因此不再适合生硬地归入传统类别【1】。现在,类别之间的界限变得越来越模糊例如数据存储可以被当成消息队列用Redis消息队列则带有类似数据库的持久保证Apache Kafka
近些年来出现了许多新的数据存储工具与数据处理工具。它们针对不同应用场景进行优化因此不再适合生硬地归入传统类别【1】。类别之间的界限变得越来越模糊例如数据存储可以被当成消息队列用Redis消息队列则带有类似数据库的持久保证Apache Kafka
其次,越来越多的应用程序有着各种严格而广泛的要求,单个工具不足以满足所有的数据处理和存储需求。取而代之的是,总体工作被拆分成一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来。
@ -74,7 +74,7 @@
***可扩展性Scalability***
有合理的办法应对系统的增长(数据量、流量、复杂性)(参阅“可伸缩性”)
有合理的办法应对系统的增长(数据量、流量、复杂性)(参阅“[可扩展性](#可扩展性)”)
***可维护性Maintainability***
@ -91,12 +91,12 @@
* 应用程序表现出用户所期望的功能。
* 允许用户犯错,允许用户以出乎意料的方式使用软件。
* 在预期的负载和数据量下,性能仍然满足要求。
* 系统能防止未经授权的访问和滥用。
* 在预期的负载和数据量下,性能满足要求。
* 系统能防止未经授权的访问和滥用。
如果所有这些在一起意味着“正确工作”,那么可以把可靠性粗略理解为“即使出现问题,也能继续正确工作”。
造成错误的原因叫做**故障fault**,能预料并应对故障的系统特性可称为**容错fault-tolerant**或**韧性resilient**。“容错”一词可能会产生误导,因为它暗示着系统可以容忍所有可能的错误,但在实际中这是不可能的。比方说,如果整个地球(及其上的所有服务器)都被黑洞吞噬了,想要容下这种错就需要把网络托管到太空里——这种预算能不能批准就祝你好运了。所以在讨论容错时,只有谈论**特定类型certain types**下的错误才有意义。
造成错误的原因叫做**故障fault**,能预料并应对故障的系统特性可称为**容错fault-tolerant**或**韧性resilient**。“**容错**”一词可能会产生误导,因为它暗示着系统可以容忍所有可能的错误,但在实际中这是不可能的。比方说,如果整个地球(及其上的所有服务器)都被黑洞吞噬了,想要容忍这种错误,需要把网络托管到太空中——这种预算能不能批准就祝你好运了。所以在讨论容错时,只有谈论特定类型的错误才有意义。
注意**故障fault**不同于**失效failure**【2】。**故障**通常定义为系统的一部分状态偏离其标准,而**失效**则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因**故障**而导致**失效**。本书们将介绍几种用不可靠的部件构建可靠系统的技术。
@ -106,17 +106,17 @@
### 硬件故障
当想到系统失效的原因时,**硬件故障hardware faults**总会第一个进入脑海。硬盘崩溃、内存出错、机房断电、有人拔错网线……任何与大型数据中心打过交道的人都会告诉你:一旦你拥有很多机器,这些事情**总**会发生!
当想到系统失效的原因时,**硬件故障hardware faults**总会第一个进入脑海。硬盘崩溃、内存出错、机房断电、有人拔错网线……任何与大型数据中心打过交道的人都会告诉你:一旦你拥有很多机器,这些事情**总**会发生!
据报道称,硬盘的**平均无故障时间MTTF, mean time to failure**约为10到50年【5】【6】。因此从数学期望上讲在拥有10000个磁盘的存储集群上平均每天会有1个磁盘出故障。
为了减少系统的故障率第一反应通常都是增加单个硬件的冗余度例如磁盘可以组建RAID、服务器可能有双路电源和热插拔CPU、数据中心可能有电池和柴油发电机作为后备电源等等。并且当某个组件不幸挂掉时立即把它换下来并由冗余组件取而代之。这种方法虽然不能完全防止由硬件问题导致的系统失效,但它简单易懂,通常也足以让机器不间断运行很多年。
为了减少系统的故障率第一反应通常都是增加单个硬件的冗余度例如磁盘可以组建RAID服务器可能有双路电源和热插拔CPU数据中心可能有电池和柴油发电机作为后备电源某个组件挂掉时冗余组件可以立刻接管。这种方法虽然不能完全防止由硬件问题导致的系统失效,但它简单易懂,通常也足以让机器不间断运行很多年。
直到最近,硬件冗余对于大多数应用来说已经足够了,它使单台机器完全失效变得相当罕见。只要你能快速地把备份恢复到新机器上,故障停机时间对大多数应用而言都算不上灾难性的。只有少量高可用性至关重要的应用才会要求有多套硬件冗余。
但是随着数据量和应用计算需求的增加,越来越多的应用开始大量使用机器,这会相应地增加硬件故障率。此外在一些云平台(**如亚马逊网络服务AWS, Amazon Web Services**虚拟机实例不可用却没有任何警告也是很常见的【7】因为云平台的设计就是优先考虑**灵活性flexibility**和**弹性elasticity**[^i],而不是单机可靠性。
如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障的道路上就更进一步了。这样的系统也有运维上的便利,例如:如果需要重启机器(例如应用操作系统安全补丁),单服务器系统就需要计划停机。而允许机器失效的系统则可以一次修复一个节点,并不需要整个系统停机。
如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障的道路上就更进一步了。这样的系统也有运维上的便利,例如:如果需要重启机器(例如应用操作系统安全补丁),单服务器系统就需要计划停机。而允许机器失效的系统则可以一次修复一个节点,无需整个系统停机。
[^i]: 在[应对负载的方法](#应对负载的方法)一节定义
@ -160,7 +160,7 @@
## 可扩展性
系统今天能可靠运行,并不意味未来也能可靠运行。服务**降级degradation**的一个常见原因是负载增加,也就是说现在处理的数据量级比过去大得多,例如,系统负载已经从一万个并发用户增长到十万个并发用户,或者从一百万增长到一千万。
系统今天能可靠运行,并不意味未来也能可靠运行。服务**降级degradation**的一个常见原因是负载增加,例如:系统负载已经从一万个并发用户增长到十万个并发用户,或者从一百万增长到一千万。也许现在处理的数据量级要比过去大得多。
**可扩展性Scalability**是用来描述系统应对负载增长能力的术语。但是请注意这不是贴在系统上的一维标签说“X可扩展”或“Y不可扩展”是没有任何意义的。相反讨论可扩展性意味着考虑诸如“如果系统以特定方式增长有什么选项可以应对增长”和“如何增加计算资源来处理额外的负载”等问题。
@ -205,7 +205,7 @@
推特的第一个版本使用了方法1但系统很难跟上主页时间线查询的负载。所以公司转向了方法2方法2的效果更好因为发推频率比查询主页时间线的频率几乎低了两个数量级所以在这种情况下最好在写入时做更多的工作而在读取时做更少的工作。
然而方法2的缺点是发推现在需要大量的额外工作。平均来说一条推文会发往约75个关注者所以每秒4.6k的发推写入变成了对主页时间线缓存每秒345k的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实一些用户有超过3000万粉丝这意味着一条推文就可能会导致主页时间线缓存的3000万次写入及时完成这种操作是一个巨大的挑战——推特尝试在5秒内向粉丝发送推文。
然而方法2的缺点是发推现在需要大量的额外工作。平均来说一条推文会发往约75个关注者所以每秒4.6k的发推写入变成了对主页时间线缓存每秒345k的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实一些用户有超过3000万粉丝这意味着一条推文就可能会导致主页时间线缓存的3000万次写入及时完成这种操作是一个巨大的挑战——推特尝试在5秒内向粉丝发送推文。
在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是讨论可扩展性的关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以使用相似的原则来考虑你的负载。
@ -216,7 +216,7 @@
一旦系统的负载可以被描述,就可以研究当负载增加会发生什么。我们可以从两种角度来看:
* 增加负载参数并保持系统资源CPU、内存、网络带宽等不变时系统性能将有什么影响
* 增加负载参数并希望保持性能不变时,系统资源需要增加多少?
* 增加负载参数并希望保持性能不变时,需要增加多少系统资源
这两个问题都需要性能数据,所以让我们简单地看一下如何描述系统性能。
@ -226,18 +226,18 @@
> #### 延迟和响应时间
>
> **延迟latency**和**响应时间response time**通常当成同义词用,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间(**服务时间service time**)之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**也就是说它是在这段时间内**休眠地latent**等待服务的【17】。
> **延迟latency**和**响应时间response time**通常当成同义词用,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间(**服务时间service time**)之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**在此期间它处于**休眠latent**状态,并等待服务【17】。
>
即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的**分布distribution**,而不是单个数值。
在[图1-4](img/fig1-4.png)中,每个灰条表代表一次对服务的请求,其高度表示请求花费了多长时间。大多数请求是相当快的,但偶尔会出现需要更长的时间的异常值。这也许是因为缓慢的请求实质上开销更大,例如它们可能会处理更多的数据。但即使所有请求都花费相同时间的情况下,随机的附加延迟也会导致结果变化,例如:上下文切换到后台进程、网络数据包丢失与TCP重传、垃圾收集暂停、强制从磁盘读取的页面错误、服务器机架中的震动【18】或者其他原因。
在[图1-4](img/fig1-4.png)中,每个灰条表代表一次对服务的请求,其高度表示请求花费了多长时间。大多数请求是相当快的,但偶尔会出现需要更长的时间的异常值。这也许是因为缓慢的请求实质上开销更大,例如它们可能会处理更多的数据。但即使(你认为)所有请求都花费相同时间的情况下,随机的附加延迟也会导致结果变化,例如:上下文切换到后台进程网络数据包丢失与TCP重传垃圾收集暂停强制从磁盘读取的页面错误服务器机架中的震动【18】还有很多其他原因。
![](img/fig1-4.png)
**图1-4 展示了一个服务100次请求响应时间的均值与百分位数**
通常报表都会展示服务的平均响应时间。 (严格来讲“平均”一词并不指代任何特定公式,但实际上它通常被理解为**算术平均值arithmetic mean**给定n个值加起来除以n。然而如果你想知道“**典型typical**”响应时间,那么平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。
通常报表都会展示服务的平均响应时间。 (严格来讲“平均”一词并不指代任何特定公式,但实际上它通常被理解为**算术平均值arithmetic mean**给定n个值加起来除以n。然而如果你想知道“**典型typical**”响应时间,那么平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。
通常使用**百分位点percentiles**会更好。如果将响应时间列表按最快到最慢排序,那么**中位数median**就在正中间举个例子如果你的响应时间中位数是200毫秒这意味着一半请求的返回时间少于200毫秒另一半比这个要长。
@ -271,9 +271,9 @@
现在我们已经讨论了用于描述负载的参数和用于衡量性能的指标。可以开始认真讨论可扩展性了:当负载参数增加时,如何保持良好的性能?
适应某个级别负载的架构不太可能应付10倍于此的负载。如果你正在开发一个快速增长的服务那么每次负载发生数量级的增长时你可能都需要重新考虑架构——或者比这思考地更频繁。
适应某个级别负载的架构不太可能应付10倍于此的负载。如果你正在开发一个快速增长的服务那么每次负载发生数量级的增长时你可能都需要重新考虑架构——或者更频繁。
人们经常讨论**纵向扩展scaling up****垂直扩展vertical scaling**,转向更强大的机器)和**横向扩展scaling out****水平扩展horizontal scaling**,将负载分布到多台小机器上)之间的对立。跨多台机器分配负载也称为“**无共享shared-nothing**”架构。可以在单台机器上运行的系统通常更简单,但高端机器可能非常贵,所以非常密集的负载通常无法避免地需要横向扩展。现实世界中的优秀架构需要将这两种方法务实地结合,因为使用几台足够强大的机器可能比大量的小型虚拟机更简单也更便宜。
人们经常讨论**纵向扩展scaling up****垂直扩展vertical scaling**,转向更强大的机器)和**横向扩展scaling out****水平扩展horizontal scaling**,将负载分布到多台小机器上)之间的对立。跨多台机器分配负载也称为“**无共享shared-nothing**”架构。可以在单台机器上运行的系统通常更简单,但高端机器可能非常贵,所以非常密集的负载通常无法避免地需要横向扩展。现实世界中的优秀架构需要将这两种方法务实地结合,因为使用几台足够强大的机器可能比使用大量的小型虚拟机更简单也更便宜。
有些系统是**弹性elastic**的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动扩展(人工分析容量并决定向系统添加更多的机器)。如果负载**极难预测highly unpredictable**,则弹性系统可能很有用,但手动扩展系统更简单,并且意外操作可能会更少(参阅“[重新平衡分区](ch6.md#分区再平衡)”)。
@ -281,7 +281,7 @@
随着分布式系统的工具和抽象越来越好,至少对于某些类型的应用而言,这种常识可能会改变。可以预见分布式数据系统将成为未来的默认设置,即使对不处理大量数据或流量的场景也如此。本书的其余部分将介绍多种分布式数据系统,不仅讨论它们在可扩展性方面的表现,还包括易用性和可维护性。
大规模的系统架构通常是应用特定的,也就是说世界上并没有一招鲜吃遍天的通用可扩展架构(不正式的叫法:**万金油magic scaling sauce** )。应用的问题可能是读取量、写入量、要存储的数据量、数据的复杂度、响应时间要求、访问模式或者所有问题的大杂烩。
大规模的系统架构通常是应用特定的—— 没有一招鲜吃遍天的通用可扩展架构(不正式的叫法:**万金油magic scaling sauce** )。应用的问题可能是读取量、写入量、要存储的数据量、数据的复杂度、响应时间要求、访问模式或者所有问题的大杂烩。
举个例子用于处理每秒十万个请求每个大小为1 kB的系统与用于处理每分钟3个请求每个大小为2GB的系统看上去会非常不一样尽管两个系统有同样的数据吞吐量。
@ -295,7 +295,7 @@
众所周知,软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债、添加新的功能等等。
不幸的是,许多从事软件系统行业的人不喜欢维护所谓的**遗留legacy**系统,因为这也许涉及修复其他人的错误、和过时的平台打交道,或者系统被迫使用于一些份外工作。每一个遗留系统都以自己的方式让人不爽,所以很难给出一个通用的建议来和它们打交道。
不幸的是,许多从事软件系统行业的人不喜欢维护所谓的**遗留legacy**系统,——也许因为涉及修复其他人的错误、和过时的平台打交道,或者系统被迫使用于一些份外工作。每一个遗留系统都以自己的方式让人不爽,所以很难给出一个通用的建议来和它们打交道。
但是我们可以,也应该以这样一种方式来设计软件:在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则:
@ -344,19 +344,19 @@
### 简单性:管理复杂度
小型软件项目可以使用简单讨喜的、富表现力的代码,但随着项目越来越大,代码往往变得难以理解地复杂。这种复杂度拖慢了所有系统相关人员,进一步增加了维护成本。一个陷入复杂泥潭的软件项目有时被描述为**烂泥a big ball of mud**【30】。
小型软件项目可以使用简单讨喜的、富表现力的代码,但随着项目越来越大,代码往往变得非常复杂,难以理解。这种复杂度拖慢了所有系统相关人员,进一步增加了维护成本。一个陷入复杂泥潭的软件项目有时被描述为**烂泥a big ball of mud**【30】。
**复杂度complexity**有各种可能的症状例如状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等现在已经有很多关于这个话题的讨论【31,32,33】。
因为复杂度导致维护困难时,预算和时间安排通常会超支。在复杂的软件中进行变更,引入错误的风险也更大:当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。相反,降低复杂度能极大地提高软件的可维护性,因此简单性应该是构建系统的一个关键目标。
简化系统并不一定意味着减少功能;它也可以意味着消除**额外的accidental**的复杂度。 Moseley和Marks【32】把**额外复杂度**定义为:并非因功能需求而产生,而是仅从工程实现中产生的复杂度。
简化系统并不一定意味着减少功能;它也可以意味着消除**额外的accidental**的复杂度。 Moseley和Marks【32】把**额外复杂度**定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
用于消除**额外复杂度**的最好工具之一是**抽象abstraction**。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。
例如高级编程语言是一种抽象隐藏了机器码、CPU寄存器和系统调用。 SQL也是一种抽象隐藏了复杂的磁盘/内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有**直接directly**使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。
虽然抽象可以帮助我们将系统的复杂度控制在可管理的水平,但是找到好的抽象是非常困难的。在分布式系统领域虽然有许多好的算法,但我们并不清楚它们应该打包成什么样抽象。
抽象可以帮助我们将系统的复杂度控制在可管理的水平,不过,找到好的抽象是非常困难的。在分布式系统领域虽然有许多好的算法,但我们并不清楚它们应该打包成什么样抽象。
本书将紧盯那些允许我们将大型系统的部分提取为定义明确的、可重用的组件的优秀抽象。