Skip to content

Latest commit

 

History

History
127 lines (72 loc) · 11.1 KB

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

File metadata and controls

127 lines (72 loc) · 11.1 KB

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

本书将专注于对大多数软件系统都极为重要的三个问题:

  • 可靠性(Reliability)

    当出现意外情况如硬件、 软件故障、 人为失误等,系统应可以继续正常运转,虽然性能可能有所降低,但确保功能正确。

  • 可扩展性(Scalability)

    随着规模的增长,例如数据量 、流量或复杂性,系统应以合理的方式来匹配这种增长。

  • 可维护性(Maintainability)

    随着时间的推移,许多新的人员参与到系统开发和运维,以维护现有功能或适配新场景等,系统都应高效运转。

可靠性

可能出错的事情称为错误(faults)或故障,系统可应对错误则称为容错(fault tolerant)或者弹性(resilient)。

注意,故障(fault)与失效(failure)不完全一致。故障通常被定义为组件偏离其正常规格,而失效意味系统作为一个整体停止,无法向用户提供所需的服务。我们不太可能将故障概率降低到零,因此通常设计容错机制来避免从故障引发系统失效。本书将介绍在不可靠组件基础上构建可靠性系统的相关技术。

硬件故障

有研究证明硬盘的平均无故障时间(MTTF)约为 10-50 年。因此,在一个包括 10 000 个磁盘的存储集群中,我们应该预期平均每天有一个磁盘发生故障。

我们的第一反应通常是为硬件添加冗余来减少系统故障率。直到最近,采用硬件冗余方案对于大多数应用场景还是足够的,它使得单台机器完全失效的概率降为非常低的水平。但是,随着数数据和应用计算需求的增加,更多的应用可以运行在大规模机器之上,随之而来的硬件故障率呈线性增长。

因此,通过软件容错的方式来容忍多机失效成为新的手段,或者至少成为硬件容错的有力补充。

软件错误

另一类故障则是系统内的软件问题,这些故障事先更加难以预料,而且因为节点之间是由软件关联的,因而往往会导致更多的系统故障。

软件系统问题有时没有快速解决办法,而只能仔细考虑很多细节,包括认真检查依赖的假设条件与系统之间交互,进行全面的测试,进程隔离,允许进程崩溃并自动重启,反复评估,监控并分析生产环节的行为表现等。

人为失误

如果我们假定入是不可靠的,那么该如何保证系统的可靠性呢?可以尝试结合以下多种方法:

  • 以最小出错的方式来设计系统。例如,精心设计的抽象层 、API 以及管理界面,使做正确的事情很轻松,但搞坏很复杂。
  • 想办法分离最容易出错的地方、容易引发故障的接口。
  • 充分的测试:从各单元测试到全系统集成测试以及手动测试。自动化测试已被广泛使用,对于覆盖正常操作中很少出现的边界条件等尤为重要。
  • 当出现人为失误时,提供快速的恢复机制以尽最减少故障影响。
  • 设置详细而清晰的监控子系统,包括性能指标和错误率。

可扩展性

可扩展性是用来描述系统应对负载增加能力的术语。 但是请注意,它并不是衡量一个系统的一维指标,谈论 ”X 可扩展“ 或 ”Y 不可扩展“ 没有太大意义。相反,讨论可扩展性通常要考虑这类问题:”如果系统以某种方式增长,我们应对增长的措施有哪些我们该如何添加计算资源来处理额外的负载“。

描述负载⭐

负载可以用称为负载参数的若干数字来描述,参数的最佳选择取决于系统的体系结构,它可能是 Web 服务器的每秒请求处理次数、数据库中写入的比例、聊天室的同时活动用户数量、缓存命中率等。有时平均值很重要,有时系统瓶颈来自于少数峰值。

我们以 Twitter 为例,使用其 2012 年 11 月发布的数据。Twitter 的两个典型业务操作是:

  • 发布 tweet 消息:用户可以快速推送新消息到所有的关注者,平均大约 4.6k request/sec,峰值约 12k requests/sec。
  • 主页时间线(Home timeline)浏览:平均 300k request/sec 查看关注对象的最新消息。

仅仅处理峰值约 12k 的消息发送听起来并不难,但是,Twitter 扩展性的挑战重点不在于消息大小,而在于巨大的扇出(fan-out)结构:每个用户会关注很多人, 也会被很多人圈粉。 此时大概有两种处理方案:

  1. 将发送的新 tweet 插入到全局的 tweet 集合中。当用户查看时间线时,首先查找所有的关注对象,列出这些人的所有 tweet,最后以时间为序来排序合并。 如果以图 1-2 的关系型数据模型,可以执行下述的 SQL 查询语句:
SELECT tweets.*, users.* FROM tweets
JOIN users ON tweets.sender_id = users.id
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_user

image-20220105001726944

  1. 对每个用户的时间线维护一个缓存,如图 1-3 所示,类似每个用户一个 tweet 邮箱。当用户推送新 tweet 时,查询其关注者,将 tweet插入到每个关注者的时间线缓存中。因为已经预先将结果取出,之后访问时间线性能非常快。

    image-20220105001853877

Twitter 在其第一个版本使用了方法一,但发现主页时间线的读负载压力与日俱增,系统优化颇费周折,因此转而采用第二种方法。实践发现这样更好,因为时间线浏览 tweet 的压力几乎比发布 tweet 要高出两个数量级,基于此,在发布时多完成一些事情可以加速读性能。

然而,方法 2 的缺点也很明显,在发布 tweet 时增加了大批额外的工作。考虑平均 75 个关注者和每秒 4.6k 的 tweet, 则需要每秒 4.6 x 75 = 345k 速率写入缓存。但是,75 这个平均关注者背后还隐藏其他事实,即关注者其实偏差巨大,例如某些用户拥有超过 3000 万的追随者。这就意味着峰值情况下一个 tweet 会导致3000 万笔写入!而且要求尽量快,Twitter 的设计目标是 5s 内完成,这成为一个巨大的挑战。

描述性能⭐

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

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

这两个问题都会关注性能指标,所以我们先简要介绍一下如何描述系统性能。

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

即使是反复发送、处理相同的请求,每次可能都会产生略微不同的响应时间。实际情况往往更复杂,由于系统要处理各种不同的请求,响应时间可能变化很大。因此,==最好不要将响应时间视为一个固定的数字,而是可度量的一种数值分布==。

image-20220105003023592

我们经常考察的是服务请求的平均响应时间,然而,如果想知道更典型的响应时间,平均值并不是合适的指标,因为它掩盖了一些信息,无法告诉有多少用户实际经历了多少延迟。

因此最好使用百分位数(percentiles),如果已经搜集到了响应时间信息,将其从最快到最慢排序,中位数(median)就是列表中间的响应时间。例如,如果中位数响应时间为 200 ms,那意味着有一半的请求响应不到 200 ms,而另一半请求则需要更长的时间。

为了弄清楚异常值有多槽糕,需要关注更大的百分位数如常见的第 95、99 和 99.9(缩写为 p95、p99 和 p999)值。

采用较高的响应时间百分位数(tail latencies,尾部延迟或==长尾效应==)很重要,因为它们直接影响用户的总体服务体验。

排队延迟往往在高百分数响应时间中影响很大。 由于服务器并行处理的请求有限(例如 CPU 内核数的限制),正在处理的少数请求可能会阻挡后续请求,这种情况有时被称为==队头阻塞(head-of-line blocking)==。即使后续请求可能处理很简单,但它阻塞在等待先前请求的完成,客户端将会观察到极慢的响应时间。因此,很重要的一点是要在客户端来测量响应时间。

应对负载增加的方法

现在谈论更多的是如何在==垂直扩展==(scaling up,即升级到更强大的机器)和==水平扩展==( scaling out,即将负载分布到多个更小的机器)之间做取舍。在多台机器上分配负载也被称为无共享(==shared-nothing==)体系结构。在单台机器上运行的系统通常更简单,然而高端机器可能非常昂贵,且扩展水平有限,最终往往还是无法避免需要水平扩展。

把无状态服务分布然后扩展至多台机器相对比较容易,而有状态服务从单个节点扩展到分布式多机环境的复杂性会大大增加。出于这个原因,直到最近通常的做法一直是:将数据库运行在一个节点上(采用垂直扩展策略),直到高扩展性或高可用性的要求迫使不得不做水平扩展。

超大规模的系统往往针对特定应用而高度定制,很难有一种通用的架构。背后取舍因素包括数据读取量 、写入量 、待存储的数据量、数据的复杂程度 、响应时间要求、访问模式等,或者更多的是上述所有因素的叠加,再加上其他更复杂的问题。

例如,即使两个系统的数据吞吐量折算下来是一样的,但是为每秒处理 100000 次请求(每个大小为 1KB)而设计的系统,与为每分钟 3 个请求(每个大小 2GB)设计的系统会大不相同。

可维护性

我们将特别关注软件系统的三个设计原则:

  • 可运维性 方便运营团队来保持系统平稳运行。
  • 简单性 简化系统复杂性,使新工程师能够轻松理解系统。注意这与用户界面的简单性并不一样。
  • 可演化性 后续工程师能够轻松地对系统进行改进,并根据需求变化将其适配到非典型场景,也称为可延伸性、易修改性或可塑性。

消除意外复杂性最好手段之一是抽象。一个好的设计抽象可以隐藏大量的实现细节,并对外提供干净、易懂的接口。一个好的设计抽象可用于各种不同的应用程序。 这样,复用远比多次重复实现更有效率;另一方面,也带来更高质量的软件,而质量过硬的抽象组件所带来的好处,可以使运行其上的所有应用轻松获益。