旧游无处不堪寻
无寻处,惟有少年心
『数据密集型应用系统设计』读书笔记(一)

这本书一直在我的待读列表,但是一直没有机会拜读,直到最近 2021 年已经快要过去,感觉需要在年末提升一下自己。边读边做一下笔记,留待后用。

数据密集型与计算密集型

对于一个应用系统,如果”数据”是其成败决定性因素,包括数据的规模、数据的复杂度或者数据产生与变化的速率等,我们就可以称为”数据密集型(Data-Intensive)应用系统”。与之对应的是计算密集型(Compute-Intensive),CPU 主频往往是其最大的制约瓶颈。

内容安排


全书分为三大部分:

  1. 主要讨论有关增强数据密集型应用系统所需的若干基本原则
  2. 我们将从单机的数据存储转向跨机器的分布式系统,将依次讨论数据远程复制、数据分区、事务、分布式系统的更多细节以及分布式环境如何达成一致性与共识
  3. 主要针对产生派生数据的系统(所谓派生数据主要指在异构系统中,如果无法用一个数据源来解决所有问题,那么一种自然的方式就是集成多个不同的数据库、缓存模块以及索引模块)

可靠、可扩展与可维护的应用系统


第一章主要介绍相关术语与方法, 这些术语等将贯穿于全书。

  1. 可靠性: 当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转,虽然性能可能有所降低,但确保功能正确
  2. 可扩展性: 随着规模的增长,例如数据量、流量或复杂性,系统应以合理的方式来匹配这种增长
  3. 可维护性: 随着时间的推移,许多新的入员参与到系统开发和运维,以维护现有功能或适配新场景等,系统都应高效运转

可靠性


  1. 应用程序执行用户所期望的功能
  2. 可以容忍用户出现错误或者不正确的软件使用方法
  3. 性能可以应对典型场景、合理负载压力和数据量
  4. 系统可防止任何未经授权的访问和滥用

可能出错的事情称为错误或故障,系统可应对错误则称为容错或弹性。
需要注意: 容错并不是指系统可以容忍各种可能的故降类型,而是指特定类型的故障。

在这种容错系统中,用于测试目的,可以故意提高故障发生概率,例如通过随机杀死某个进程,来确保系统仍保持健壮。通过这种故意引发故障的方式,来持续检验、测试系统的容错机制,增加对真实发生故障时应对的信心。

硬件故障

当我们考虑系统故障时,硬件故障包括: 硬盘崩溃,内存故障,电网停电,甚至误拔网线。应通常是为硬件添加冗余来减少系统故陪率。

软件错误

另一类故障则是系统内的软件问题,这些故障事先更加难以预料,而且因为节点之间是由软件关联的,因而往往会导致更多的系统故障。软件系统问题有时没有快速解决办法,而只能仔细考虑很多细节,包括认真检查依赖的假设条件与系统之间交互,进行全面的测试,进程隔离,允许进程崩溃并自动重启,反复评估,监控并分析生产环节的行为表现等。

人为失误

设计和构建软件系统总是由人类完成,也是由人来运维这些系统。人无法做到万无一失,例如,一项针对大型互联网服务的调查发现,运维者的配置错误居然是系统下线的首要原因。
如果我们假定入是不可靠的,那么该如何保证系统的可靠性呢,可以尝试结合以下多种方法:

  1. 以最小出错的方式来设计系统。例如,精心设计的抽象层、API 以及管理界面
  2. 想办法分离最容易出错的地方、容易引发故障的接口
  3. 充分的测试,从各单元测试到系统集成测试以及手动测试
  4. 当出现人为失误时,提供快速的恢复机制以尽最减少故障影响。例如,快速回滚配置改动,滚动发布新代码等
  5. 设置详细而清晰的监控子系统,包括性能指标和错误率
  6. 推行管理流程并加以培训

可靠性的重要性

很多应用都需要可靠工作,商业软件中的错误会导致效率下降,电子商务网站的暂停会对营收和声誉带来巨大损失。即使在所谓”非关键”应用中,我们也应秉持对用户负责的态度。

可扩展性


即使系统现在工作可靠,并不意味着它将来一定能够可靠运转。发生退化的一个常见原因是负载增加,服务一个客户和服务一万个客户,要处理的数据量也完全是几何级增长。

描述负载

首先,我们需要简洁地描述系统当前的负载,只有这样才能更好地讨论后续增长问题。负载可以用称为负载参数的若干数字来描述。参数的最佳选择取决于系统的体系结构:

  • 可能是Web服务器的每秒请求处理次数
  • 数据库中写入的比例
  • 聊天室的同时活动用户数量
  • 缓存命中率

有时平均值很重要,有时系统瓶颈来自于少数峰值。

描述性能

描述系统负载之后,接下来设想如果负载增加将会发生什么。有两种考虑方式:

  • 负载增加,但系统资源(如 CPU、内存、网络带宽等)保持不变,系统性能会发生什么变化
  • 负载增加,如果要保持性能不变,需要增加多少资源

在批处理系统如 Hadoop 中,我们通常关心吞吐量(throughput),即每秒可处理的记录条数,或者在某指定数据集上运行作业所需的总时间。
而在线系统通常更看重服务的响应时间(response time),即客户端从发送请求到接收响应之间的间隔。

注意: 我们经常考察的是服务请求的平均响应时间,然而,如果想知道更典型的响应时间,平均值并不是合适的指标。最好使用百分位数(percentiles)。如果已经搜集到了响应时间信息,将其从最快到最慢排序,中位数(median)就是列表中间的响应时间。中位数指标非常适合描述多少用户需要等待多长时间:一半的用户请求的服务时间少于中位数响应时间,另一半则多于中位数的时间。因此中位数也称为 50 百分位数,可缩写为 p5O。

当然为了弄清楚异常值有多槽糕,需要关注更大的百分位数如 95、99 和 99.9(缩写为 p95、p99 和 p999)值,作为典型的响应时间阈值。
采用较高的响应时间百分位数(长尾效应)很重要,因为它们直接影响用户的总体服务体验。例如,亚马逊采用 99.9 百分位数来定义其内部服务的响应时间标准,或许它仅影响 1000 个请求中的 1 个。但是考虑到请求最慢的客户往往是购买了更多的商品,因此数据量更大。换言之,他们是最有价值的客户。

对于后台服务,如果一次完整的服务里包含了多次请求调用,此时高百分位数指标尤为重要。即使这些子请求是并行发送、处理,但最终用户仍然需要等待最慢的那个调用完成才行。
最好将响应时间百分位数添加到服务系统监控中,持续跟踪该指标。例如,设置一个 lOmin 的滑动窗口,监控其中响应时间,滚动计算窗口中的中位数和各种百分位数,然后绘制性能图表。

应对负载增加的方法

我们已经讨论了描述负载的参数以及衡最性能的相关指标,接下来讨论可扩展性:即当负载参数增加时,应如何保持良好性能。

现在谈论更多的是如何在垂直扩展(即升级到更强大的机器)和水平扩展(即将负载分布到多个更小的机器)之间做取舍。
在多台机器上分配负载也被称为无共享体系结构。在单台机器上运行的系统通常更简单,然而高端机器可能非常昂贵,且扩展水平有限,最终往往还是无法避免需要水平扩展。
实际上,好的架构通常要做些实际取舍。例如,使用几个强悍的服务器仍可以比大量的小型虚拟机来得更简单、便宜。

某些系统具有弹性特征,它可以自动检测负载增加,然后自动添加更多计算资源,而其他系统则是手动扩展。

可维护性


软件的大部分成本并不在最初的开发阶段,而是在于整个生命周期内持续的投入,这包括维护与缺陷修复。
不幸的是,许多从业人根本不喜欢维护这些所谓的遗留系统,为此,我们可以在软件设计时开始考虑,尽可能较少维护期间的麻烦,避免造出容易过期的系统。我们将特别关注软件系统的三个设计原则:

  1. 可运维性
  2. 简单性
  3. 可演化性

复杂性有各种各样的表现方式: 状态空间的膨胀、模块紧耦合、令入纠结的相互依赖关系、不一致的命名和术语、为了性能而采取的特殊处理、为解决某特定问题而引入的特殊框架等。
复杂性使得维护变得越来越困难,最终会导致预算超支和开发进度滞后。最终开发人员更加难以准确理解、评估或者更加容易忽略相关行为。
消除意外复杂性最好手段之一是抽象。一个好的设计抽象可以隐藏大量的实现细节,并对外提供干净、易懂的接口。一个好的设计抽象可用于各种不同的应用程序。

一成不变的系统需求几乎没有,想法和目标经常在不断变化: 适配新的外部环境、新的用例、业务优先级的变化、用户要求的新功能、新平台取代旧平台、业务增长促使架构的演变等。

总结


知易行难,我们都知道应该使应用程序可靠、可扩展或可维护,但实际实现他们却并不容易。考虑到一些重要的模式和技术在很多不同应用中普遍适用,在接下来的几章中,我们就一些数据密集系统例子,分析它们如何实现上述这些目标。