第 2 章 定义非功能性需求
互联网运转得太好了,以至于多数人把它当作太平洋那样的天然资源,而不是人造之物。我们上一次见到这种规模、又这么少出错的技术是什么时候?
——Alan Kay,接受 Dr. Dobb's Journal 采访(2012)
如果你正在打造一个应用,往往会有一份贯穿始终的需求清单。清单顶端最可能是应用必须提供的功能:要有哪些界面、哪些按钮、每个操作应当做什么,以达成这一软件的目的。这些是你的功能性需求(functional requirements)。
除此之外,你大概还有一些非功能性需求(nonfunctional requirements):例如,应用要快、要可靠、要安全、要合规,且易于维护。这些需求或许并未被明确写出——因为它们看起来似乎不言自明——但它们与功能同等重要:一个慢得让人抓狂或不可靠的应用,等同于不存在。
许多非功能性需求(如安全性)超出本书范围。但本书会讨论其中几项,本章也将帮助你为自己的系统把它们清楚地表达出来。具体来说,我们会看:
- 如何定义并测量一个系统的性能;
- 一个服务"可靠"意味着什么——也就是即便出错也能继续正确工作;
- 如何通过高效地增加计算容量让系统具备可扩展性,以应对负载增长;
- 如何让系统长期来看更易维护。
本章引入的术语在后续章节中也很有用——届时我们会深入数据密集型系统具体如何实现的细节。然而抽象的定义可能颇为枯燥;为了让概念更具体,本章先以一个社交网络服务的案例研究开篇,借此提供性能与可扩展性方面的实用范例。
案例研究:社交网络主页时间线
设想我们被指派去实现一个类似 X(前身 Twitter)风格的社交网络:用户可以发布消息,并关注其他用户。这与真实服务的运作方式相比是一个巨大的简化 [1, 2, 3],但它能帮助说明大规模系统会出现的一些问题。
我们假设用户每天总共发布 5 亿条帖子,平均下来每秒 5,800 条。偶尔这一速率会飙到每秒 150,000 条 [4]。我们再假设普通用户关注 200 人,并被 200 人关注(实际上分布范围非常广:大多数人只有少量关注者,而极少数名人——比如 Barack Obama——拥有上亿关注者)。
表示用户、帖子与关注关系
我们把所有数据放在一个关系型数据库里,如图 2-1 所示:一个 users 表、一个 posts 表、一个 follows 关系表。

图 2-1. 用户可彼此关注的社交网络的简化关系模式
图示文字描述: 图示左侧的 follows 表(含 follower_id、followee_id 两列,示例行 17055506→12,"当前登录用户:17055506"),中间是 posts 表(id=20,sender_id=12,text="just setting up my twttr",timestamp=1142974214),右侧是 users 表(id=12,screen_name=jack,profile_image=1234567.jpg),箭头表示 follows.followee_id → users.id 与 users.id → posts.sender_id 的引用关系。
假设我们这个社交网络必须支持的主要读取操作是主页时间线——它显示该用户所关注者最近发布的帖子(为简化起见,先忽略广告、未关注者的推荐帖子和其他扩展功能)。我们可以用如下 SQL 查询,为某个用户取得主页时间线:
SELECT posts.*, users.* FROM posts
JOIN follows ON posts.sender_id = follows.followee_id
JOIN users ON posts.sender_id = users.id
WHERE follows.follower_id = current_user
ORDER BY posts.timestamp DESC
LIMIT 1000执行该查询时,数据库将通过 follows 表找出 current_user 关注的所有人,再查询这些人最近的帖子,并按时间戳排序得到这些被关注账户中最新的 1,000 条。
为保证帖子的时效性,我们假设:当某人发出一条帖子后,希望其关注者能在 5 秒内看到。一种办法是:当用户在线时,让其客户端每 5 秒重复运行上面的查询(这种方法称为轮询,polling)。如果同一时刻有 1000 万用户在线并已登录,意味着该查询每秒要被运行 200 万次。即便降低轮询频率,这个量级仍然惊人。
而且这个查询本身就相当昂贵:若一名用户关注了 200 人,查询就需要为这 200 人分别获取最新帖子列表,再合并这些列表。每秒 200 万次时间线查询乘以平均 200 个被关注账户,意味着每秒 4 亿次查找——是一个极大的数字。这还只是平均情况。有些用户关注了几万个账户,对他们来说这个查询代价巨大,很难做得快。
物化与更新时间线
我们怎么才能做得更好?首先,与其轮询,不如让服务器主动把新帖子推送给在线的关注者。其次,我们应预先计算查询结果,让用户对自己主页时间线的请求可以直接由一份缓存提供。
设想:对每一位用户,我们都存一个数据结构来承载其主页时间线(即所关注者最近的帖子)。每当某用户发出一条帖子,我们就查找其全部关注者,并把这条帖子插入到每位关注者的主页时间线里——就像把一封信投入邮箱。这样当某位用户登录时,我们只要把这份预先计算好的主页时间线交给他即可。此外,要在自己的时间线收到任何新帖子的通知,用户客户端只需订阅"被加进自己时间线的帖子流"即可。
这种做法的代价是:每当用户发出一条帖子,我们都得做更多工作,因为主页时间线是一种需要更新的派生数据。整个过程见图 2-2。当一次原始请求引发了若干次下游请求时,我们用术语*扇出(fan-out)*来描述请求数被放大的倍数。

图 2-2. 扇出:把新帖子分发给该发帖用户的每一位关注者
图示文字描述: 图示左侧"User makes post",箭头进入"All posts"队列(T8 T7 T6 T5 T4 T3 T2 T1);中部"Fan-out: deliver post to each follower"将该帖分发到右侧三位 recipient 的子队列(Posts for recipient 1/2/3);右侧用户通过"Get home timeline (website, API)"读取属于自己的时间线。
按每秒 5,800 条帖子的速率计算,若平均一条帖子触达 200 位关注者(即扇出系数为 200),我们每秒需做略多于 100 万次的主页时间线写入。这个数字虽然不小,但相比"如果不这样做、就要做的每秒 4 亿次按发件人查帖",已是一项巨大的节省。
如果突发事件让发帖速率激增,我们也不必立即完成所有时间线投递——可以把它们排入队列,允许帖子稍晚一些再出现在关注者的时间线里。即便在这种负载尖峰下,时间线读取仍然很快,因为我们只是从缓存中读取。
这种"预先计算并更新查询结果"的过程称为物化(materialization),时间线缓存就是一个*物化视图(materialized view)*的例子(这个概念在后续章节会进一步讨论)。物化视图加快了读取,但代价是写入时要做更多工作。多数用户的写入成本不大,但社交网络还需要考虑某些极端情形:
- 若一名用户关注了大量账户,且这些账户发帖频繁,那么写入这位用户物化时间线的速率会相当高。然而这位用户多半不会读完时间线里的所有帖子,所以可以简单地丢弃部分时间线写入,只向其展示其关注账户帖子的一个抽样 [5]。
- 当一个名人账户(关注者数量极多)发出一条帖子时,我们必须做大量工作,把这条帖子插入到数百万关注者中每个人的主页时间线。这种情况下,丢弃部分写入就不合适了。一种解决办法是把名人帖子与普通用户帖子分开处理:把名人帖子单独存放,避免写入数百万条时间线,等到时间线被读取时再与物化时间线合并。即便经过这样的优化,处理社交网络上的名人仍可能需要大量基础设施 [6]。
描述性能
大多数关于软件性能的讨论会考量两类主要指标:
响应时间(Response time) :从用户发起请求到收到所请求结果之间所经过的时间。其计量单位是秒(或毫秒、微秒)。
吞吐量(Throughput) :每秒处理的请求数,或每秒处理的数据量。在给定的硬件资源分配下,存在一个可处理的最大吞吐量。其计量单位是"每秒若干个事物"。
在前面的社交网络案例中,"每秒帖子数"和"每秒时间线写入数"是吞吐量指标,而"加载主页时间线所需时间"和"帖子送达关注者所需时间"则是响应时间指标。
吞吐量与响应时间常常彼此关联。图 2-3 画出了一个在线服务中这种关系的示意。当请求吞吐量较低时,服务的响应时间也低;但负载增大后,响应时间会随之上升。这是*排队(queueing)*导致的:当一个请求到达高负载系统时,CPU 很可能正在处理之前的请求,因此新到来的请求必须等先前的请求完成才能被处理。当吞吐量逼近硬件能承受的最大值时,排队延迟会急剧增加。

图 2-3. 当服务的吞吐量逼近其容量时,由于排队,响应时间剧烈上升
图示文字描述: 图示横轴为 Throughput(吞吐量),纵轴为 Response time(响应时间)。曲线在低吞吐量段几乎水平,对应"未受负载时的服务时间";当吞吐量逼近"硬件能处理的最大值"虚线时,曲线急剧上扬。
当过载系统无法自行恢复时
如果一个系统接近过载、吞吐量被推到极限附近,它有时会陷入一个恶性循环:效率变得更低,因此愈加过载。例如,若有大量请求在队列中等待处理,响应时间可能拉长到让客户端超时并重发请求的程度。这会让请求速率进一步升高、让问题更糟——形成所谓的重试风暴(retry storm)。即便后来负载下降了,这种系统也可能仍卡在过载状态,直到重启或被人工干预。这种现象被称为亚稳态失效(metastable failure),可能在生产系统中引发严重事故 [7, 8, 9]。
要避免重试压垮服务,你可以让客户端在两次重试之间增加并随机化时间间隔(即指数退避,exponential backoff [10, 11]);并在某项服务最近返回错误或超时后,借助熔断器(circuit breaker) [12, 13] 或令牌桶(token bucket) [14] 算法暂停向其发送请求。服务端也可以检测自身是否接近过载,并主动开始拒绝请求(负载丢弃,load shedding [15]),或在响应中告知客户端慢下来(背压,backpressure [1, 16])。排队与负载均衡算法的选择也会带来差别 [17]。
就性能指标而言,用户最关心的通常是响应时间,而吞吐量则决定所需的计算资源(例如你需要多少台服务器),从而决定服务某种工作负载的成本。如果吞吐量预计会超过当前硬件能承载的容量,就需要扩充容量;如果通过添加计算资源就能让一个系统的最大吞吐量显著提高,我们就说这个系统是可扩展的(scalable)。
本节我们主要聚焦响应时间,吞吐量与可扩展性留到"可扩展性"(第 49 页)再讨论。
延迟与响应时间
"延迟(latency)"和"响应时间(response time)"有时被互换使用,但本书中我们对二者及若干相关术语作如下特定区分(图 2-4 演示):
- 响应时间是客户端所看到的:它包括系统中任何环节产生的所有延迟。
- *服务时间(service time)*是服务实际处理客户端请求所花费的时长。
- *排队延迟(queueing delays)*可能发生在数据流的多个环节——例如,请求被收到后,可能要等 CPU 空闲才能处理;又或者响应数据包可能要缓冲一阵,等同机器上其他任务通过出口网卡把大量数据发完,才轮到它发送。
- *延迟(latency)是一个泛指术语,指请求未被实际处理的那段时间——即它处于潜伏(latent)*状态的时间。特别地,*网络延迟(network latency)或网络延时(network delay)*指请求与响应在网络中往返所花的时间。

图 2-4. 响应时间、服务时间、网络延迟与排队延迟
图示文字描述: 图示从左到右是时间轴。Client 与 Service 各为一条水平线。Client 发出 Request 经过 Network latency → Queueing delay → Service 在 Processing → Service time → Queueing → Network latency → Client 收到 Response。整段为 Response time。
在图 2-4 中,时间从左流向右;每个通信节点显示为一条水平线,请求或响应消息以从一节点到另一节点的粗对角箭头表示。这种风格的图在本书中你会经常看到。
即便你反复发起同一个请求,响应时间也可能在不同请求之间显著变化。许多因素会引入随机延迟——例如切换到后台进程的上下文、网络包丢失与 TCP 重传、垃圾回收暂停、缺页中断引发的磁盘读取,乃至服务器机架上的机械震动 [18]。我们将在"超时与无界延迟"(第 352 页)中更详尽地讨论这一话题。
排队延迟通常是响应时间波动的主要来源。由于一台服务器只能并行处理少量任务(例如受其 CPU 核心数限制),只需少量慢请求就能拖住后续请求的处理——这种效应被称为队头阻塞(head-of-line blocking)。即使后续请求本身的服务时间很快,由于要排队等前面那个请求完成,客户端看到的整体响应时间也会变慢。排队延迟并不属于服务时间,因此在客户端测量响应时间显得尤为重要。
平均、中位数与百分位
由于响应时间因请求而异,我们需要把它视为一种分布而非单一数值。在图 2-5 中,每根灰色竖条代表一次到达服务的请求,其高度表示该请求耗时多久。绝大多数请求相当快,但偶尔有离群值耗时长得多。网络延迟的这种波动也称为抖动(jitter)。

图 2-5. 演示均值与百分位:对一项服务的 100 次请求样本的响应时间
图示文字描述: 图示横轴为 Requests(请求),纵轴为 Response time(响应时间);众多灰色竖条参差不齐;从下到上画出虚线:Median (p50)、Mean (average)、95th percentile、99th percentile。
通常人们会报告一项服务的平均响应时间(严格说是算术平均值:把所有响应时间相加再除以请求数)。平均响应时间对估计吞吐量的极限很有用 [19]。但若你想知道你的"典型"响应时间,平均值并不太好用——因为它无法告诉你究竟有多少用户实际经历了那种延迟。
更好的做法通常是使用百分位(percentiles)。把响应时间从快到慢排序,中位数(median)是排在中间的那个值——例如若中位响应时间是 200 ms,则一半请求在 200 毫秒内返回,另一半超过 200 毫秒。中位数因此是一个不错的指标,能告诉你"用户通常要等多久"。中位数也称第 50 百分位,有时缩写为 p50。
要了解离群值有多严重,可以看更高的百分位:第 95、第 99、第 99.9 百分位——分别缩写为 p95、p99、p999——很常见。例如,若 p95 响应时间是 1.5 秒,意味着 100 个请求中有 95 个在 1.5 秒内返回,5 个超过 1.5 秒。如图 2-5 所示。
高响应时间百分位——又称长尾延迟(tail latencies)——很重要,因为它们直接影响用户对服务的体验。例如 Amazon 用第 99.9 百分位描述其内部服务的响应时间要求,即便这只影响千分之一的请求。原因在于:经历最慢请求的客户,往往恰恰是账户上数据最多的客户——因为他们购买了大量商品,也就是最有价值的客户 [20]。让网站在这些客户面前依然飞快,对留住他们至关重要。
把第 99.99 百分位(最慢的万分之一请求)作为优化目标,则被认为代价过高,对 Amazon 而言也未带来足够收益。降低极高百分位的响应时间本身就很困难,因为它们极易受你无法控制的随机事件影响,且收益递减。
响应时间对用户的影响
"服务越快用户体验越好"听起来不言自明 [21]。然而,要拿到可靠数据来量化延迟对用户行为的影响出人意料地难。
一些常被引用的统计并不可靠。例如 2006 年 Google 报告称:搜索结果加载从 400 ms 慢到 900 ms 与流量及收入下降 20% 相关 [22]。然而 Google 在 2009 年的另一项研究报告说,延迟增加 400 ms 仅导致每天搜索数下降 0.6% [23];同年 Bing 发现页面加载时间增加两秒会让广告收入下降 4.3% [24]。这两家公司更新的数据似乎未公开。
较近的 Akamai 研究称:响应时间增加 100 ms 会让电商网站的转化率下降多达 7% [25]。但仔细一看,同一研究又显示:非常快的页面加载时间也与较低的转化率相关!这一看似悖论的结果可解释为:加载最快的页面通常是没什么有用内容的页面(如 404 错误页)。但由于该研究并未把页面内容的影响与加载时间的影响分离开,其结论恐怕意义有限。
次年 Yahoo 的一项研究比较了快速加载与慢速加载的搜索结果页面的点击率,并控制了搜索结果的质量 [26]。研究报告:当快慢响应间差距达到 1.25 秒及以上时,快搜索的点击数比慢搜索多 20% – 30%。
响应时间指标的用途
在那些"作为一次终端用户请求处理的一部分会被多次调用"的后端服务里,高百分位尤其重要。即便你以并行方式发起这些调用,整个请求依然要等其中最慢的那一次并行调用完成。如图 2-6 所示,仅一次慢调用就能让整个终端用户请求变慢。即便只有一小部分后端调用慢,多次后端调用累积下来,至少出现一次慢调用的概率也会增大——因此终端用户请求中变慢的比例会更高(这种效应称为尾延迟放大,tail latency amplification [27])。

图 2-6. 当为某请求处理而需多次后端调用时,仅一次慢调用就会拖慢整个终端用户请求
图示文字描述: 图示顶部为 End-user request 总耗时(约 487 ms 起绿色条形);中部 Web 应用同时调用 7 个 Backend,各自耗时 92/76/103/143/86/487/133 ms;终端总耗时由最慢的 Backend 6 决定。
百分位常被用于*服务等级目标(SLO)与服务等级协议(SLA)*中,作为定义服务预期性能与可用性的方式 [28]。例如,一个 SLO 可能为某服务设定如下目标:中位响应时间小于 200 毫秒、第 99 百分位小于 1 秒,且至少 99.9% 的有效请求获得非错误响应。SLA 则是一份合同,规定当 SLO 未达成时如何处理(例如客户可能有权获得退款)。这只是基本思想;在实践中,为 SLO 与 SLA 制定恰当的可用性指标并不简单 [29, 30]。
计算百分位
若要把响应时间百分位加进你服务的监控仪表板,你需要持续高效地计算它们。例如,你或许想为最近 10 分钟的响应时间维持一个滚动窗口;每分钟在该窗口内计算中位数与各百分位,并把它们绘到图表上。
最简单的实现是为时间窗口内的所有请求保留一个响应时间列表,每分钟做一次排序。如果对你而言这种实现太低效,有些算法可以以极低的 CPU 与内存代价给出百分位的良好近似。开源的百分位估计库包括 HdrHistogram [31]、t-digest [32, 33]、OpenHistogram [34] 和 DDSketch [35]。
注意:把百分位本身做平均(如为降低时间分辨率或合并多机数据)在数学上没有意义。聚合响应时间数据的正确方式是把直方图相加 [36]。
可靠性与容错
每个人对什么算"可靠"或"不可靠"都有一种直觉。对软件而言,典型期望包括:
- 应用做用户期望它做的事。
- 应用能容忍用户的失误或非预期的使用方式。
- 在预期负载与数据量下,性能足以满足所需用例。
- 系统能阻止任何未经授权的访问与滥用。
如果上述各点合起来意味着"正确工作",那么我们可以把可靠性(reliability)粗略理解为"即便出错也能继续正确工作"。为了把"出错"说得更精确,我们将区分故障(fault)与失效(failure) [37, 38, 39]:
故障 :当系统的某个部件停止正确工作时即发生故障——例如某块硬盘出现故障、单台机器崩溃,或系统所依赖的某项外部服务发生中断。
失效 :当系统作为整体停止向用户提供所需服务时发生失效——也就是说,它不再满足 SLO。
故障与失效的区分可能令人困惑:它们其实是同一件事,只是层级不同。例如若某块硬盘宕了,我们说该硬盘失效;若系统只由这块硬盘构成,那它便停止提供所需服务,因而也失效。然而,若系统由多块硬盘组成,从更大系统的视角看,单块硬盘的失效只是一次故障,更大的系统也许能通过在另一块硬盘上保留数据副本来容忍这一故障。
容错
如果一个系统在某些故障发生时仍能继续向用户提供所需服务,我们称之为容错(fault-tolerant)。如果系统无法容忍某个特定部件出问题,我们就称该部件为单点故障(single point of failure,SPOF)——因为该部件的故障会上升为整个系统的失效。
例如在我们的社交网络案例研究中,可能发生的故障是:参与更新物化时间线的某台机器在扇出过程中崩溃或不可用。要让这一过程容错,我们需要确保另一台机器能接管这一任务,既不漏发应送达的帖子,也不重复投递(这一思想称为恰好一次语义(exactly-once semantics),第 12 章会详细讨论)。
容错总是局限在一定数量、一定类型的故障范围内。例如,一个系统可能可以容忍最多两块硬盘同时失效,或三个节点中崩溃一个。容忍任意多的故障没有意义:若所有节点都崩溃,谁都帮不上忙。如果整个地球(连同其上的所有服务器)被黑洞吞噬,要容忍这种故障就得在太空中托管——祝你拿到这笔预算批准。
反直觉的是,对此类容错系统而言,故意提高故障发生率反而合理——例如不打招呼就随机杀掉若干进程。这种做法称为故障注入(fault injection)。许多关键 bug 实际上是糟糕的错误处理引起的 [40];通过故意诱发故障,你能确保容错机制不断被演练与测试,从而在真实故障发生时更有把握。*混沌工程(chaos engineering)*这一学科正是通过故意注入故障的实验来提高对容错机制的信心 [41]。
虽然我们一般倾向于"容忍故障"而非"防止故障",但在某些情形下"防"比"治"更好(因为已经无法治)。安全方面就是这样:若攻击者已侵入系统、窃取了敏感数据,这件事便无法撤销。然而本书主要关注那些可以"治"的故障,下文几节会展开讨论。
硬件与软件故障
谈到系统失效的原因,最先浮现脑海的往往是硬件故障:
- 大约每年 2% 至 5% 的磁性硬盘会失效 [42, 43];在一个 10,000 块硬盘的存储集群中,平均每天应预计有一块磁盘失效。近期数据表明硬盘越来越可靠,但失效率仍然可观 [44]。
- 大约每年 0.5% 至 1% 的固态硬盘(SSD)失效 [45]。少量的位错误会被自动纠正 [46],但每个驱动器每年大约会出现一次不可纠正的错误,即便驱动器还相对新(即损耗很小)也是如此。这一错误率高于磁性硬盘 [47, 48]。
- 其他硬件部件(如电源、RAID 控制器与内存模组)也会失效,只是频率低于硬盘 [49, 50]。
- 大约每 1,000 台机器中有 1 台的某个 CPU 核心会偶尔算错结果,多半是制造缺陷所致 [51, 52, 53]。有时这种错误计算会导致崩溃,但有时它只是让程序默默返回错误结果。
- RAM 中的数据会损坏——可能源于宇宙射线等随机事件,也可能是永久性的物理缺陷。即便使用了纠错码(ECC),在一年里仍有超过 1% 的机器会遇到不可纠正的错误,通常导致机器崩溃,并需要更换对应的内存模组 [54]。此外,某些病态的内存访问模式能以高概率翻转比特位 [55]。
- 整座数据中心可能不可用(例如断电或网络配置错误),甚至被永久毁坏(例如火灾、洪水或地震)[56]。当太阳释放大量带电粒子时,会在长程导线中感应出大电流的太阳风暴,可能损坏电网与海底网络电缆 [57]。这类大规模失效虽然罕见,但若服务无法承受失去一座数据中心,影响可能是灾难性的 [58]。
这些事件在小型系统中往往罕见到只要损坏的硬件能轻易替换,你就不必太担心。但在大规模系统中,硬件故障发生得足够频繁,以致它们成了系统正常运行的一部分。
通过冗余来容忍硬件故障
我们应对不可靠硬件的第一手段,通常是给硬件部件加上冗余,以降低系统失效率。磁盘可以组成 RAID 配置(把数据分散到同一机器内的多块磁盘上,使得一块磁盘失效不会引起数据丢失),服务器可以配双电源与可热插拔的 CPU,数据中心可以配电池与柴油发电机做备用电力。这种冗余常能让一台机器无中断运转数年。
冗余在故障彼此独立时最有效——也就是说,一次故障的发生不会改变另一次故障发生的可能性。然而经验表明,部件失效之间存在显著相关性 [43, 59, 60]:整机柜或整座数据中心不可用的情况,仍比我们期望的更常发生。
硬件冗余提高了单台机器的正常运行时间;然而正如"分布式系统 vs 单节点系统"(第 19 页)所讨论的,使用分布式系统本身也有其优势,例如能容忍一整座数据中心宕掉。正因如此,云系统更少强调单机可靠性,而是在软件层面容忍出故障的节点,以提供高可用服务。云提供商使用*可用区(availability zones)*来标识哪些资源在物理上紧邻:处于同一地点的资源比地理上分隔的资源更可能在同一时刻一起失效。
本书讨论的容错技术旨在容忍整机、整机柜或整可用区的失效。它们通常这样工作:当一座数据中心里的机器失效或不可达时,由另一座数据中心里的机器接手任务。第 6、10 章及书中其他若干处会讨论这些容错技术。
能容忍整机损失的系统还带来运维优势:单服务器系统若需重启(例如打操作系统安全补丁)就要计划停机,而多节点容错系统可以一次只重启一个节点,对面向用户的服务毫无影响。这种做法称为滚动升级(rolling upgrade),第 5 章会进一步讨论。
软件故障
虽然硬件失效彼此之间相关性不强(至少在一段时间内基本独立——例如某机器上一块硬盘失效,其他硬盘大概率没事),但软件故障往往高度相关,因为常常多个节点跑的是同一份软件,因而拥有同样的 bug [61, 62]。这类故障比彼此独立的硬件故障更难预料,且往往会造成更多的系统失效 [49]。例子包括:
- 一个软件 bug 导致所有节点在某种特定情况下同时失效。例如 2012 年 6 月 30 日的闰秒,Linux 内核中的一个 bug 让许多 Java 应用同时挂起,从而拖垮多个互联网服务 [63]。又因某固件 bug,某些型号的 SSD 在精确运行 32,768 小时(不到 4 年)后突然全部失效,导致其上数据无法恢复 [64]。
- 失控进程吃光某种共享的有限资源——CPU 时间、内存、磁盘空间、网络带宽或线程 [65]。例如,处理大请求的某进程因占用过多内存而被操作系统杀死,或客户端库的 bug 让请求量比预期大得多 [66]。
- 系统所依赖的某项服务变慢、不响应,或开始返回损坏的响应。
- 不同系统之间的相互作用产生孤立测试时不会出现的"涌现行为" [67]。
- 级联失败(cascading failures):一个部件的问题让另一个部件过载、变慢,进而又拖垮第三个部件 [68, 69]。
引发这类软件故障的 bug 常常长期潜伏,直到某种异常情形把它触发。届时人们会发现:软件对其所处环境作了某种假设——这种假设通常成立,但出于某种原因终究会不再成立 [70, 71]。
软件中系统性故障的问题没有速效解药。许多小事都能帮上忙:仔细思考系统中的假设与交互;彻底测试;保证进程隔离;允许进程崩溃后重启;避免重试风暴这类反馈环路(参见"当过载系统无法自行恢复时",第 38 页);在生产环境中度量、监控与分析系统行为。
人与可靠性
软件系统由人设计与构建,让其持续运行的运维人员同样是人。与机器不同,人并不只是照规则行事;其长处之一就在于能凭创造力与适应力把工作做成。然而这一特性也带来不可预测性:即便出于好意,也有可能造成失误。例如,对大型互联网服务的一项研究发现:运维人员的配置变更是中断的首要原因,而硬件故障(服务器或网络)只在 10%–25% 的事故中起了作用 [72]。
把这类问题贴上"人为错误"的标签、想通过更严的流程与合规更好地控制人的行为来解决——很有诱惑力。然而把责任全推给犯错的人是适得其反的。我们所说的"人为错误"并不是事故真正的原因,而是某种社会-技术系统在让人们尽力做好工作时出问题的症状 [73]。复杂系统常有"涌现行为",部件之间的意外交互也可能引发失效 [74]。
各类技术手段都有助于降低人为失误的影响:包括彻底的测试(手写测试与对大量随机输入做性质测试,property testing)[40]、用于快速回滚配置变更的机制、对新代码做渐进式发布、详尽清晰的监控、用于诊断生产问题的可观测性工具(参见"分布式系统的麻烦",第 20 页),以及精心设计、鼓励"做对的事"、让"做错的事"更难做的接口。
然而上述一切都需要投入时间与金钱。在日常业务的现实压力下,组织常把"创造收入"的活动置于"提升系统对失误的韧性"之上。当不得不在更多功能与更多测试之间取舍时,许多组织选择前者,这并不难理解。一旦本可避免的错误最终发生,把责任推给犯错的人没有道理——问题在于组织的优先级。
越来越多组织正在采用*无责事后回顾(blameless postmortems)*的文化:事故发生后,鼓励涉事者无惧惩罚地分享全部细节,从而让组织里其他人学习如何在将来防止类似问题 [75]。这一过程可能揭示出:需要调整业务优先级、补足被忽视的领域、调整相关人员的激励机制,或把另一个系统性问题摆到管理层面前。
一般而言,调查事故时应对过分简单的答案保持警惕。"Bob 部署该变更时本该更小心"无济于事,"我们必须用 Haskell 重写后端"也一样。管理者应抓住这一机会,从那些每天与系统打交道的人的视角,去理解这套社会-技术系统是如何运作的,并依据他们的反馈采取改进措施 [73]。
可靠性有多重要?
可靠性并非只为核电站与空中交通管制而存在;更平凡的应用同样被期望可靠运行。商业应用中的 bug 会造成生产力损失(若数字报错还会带来法律风险);电商网站的中断则会以失去的营收和声誉损害的形式造成巨大代价。
在许多应用中,几分钟乃至几小时的临时中断尚可承受 [76],但永久性的数据丢失或损坏将是灾难性的。设想一位父母把孩子所有的照片与视频都存在你的相册应用里 [77]:若这个数据库突然坏了,他们会作何感受?他们知道怎么从备份中恢复吗?
再举一个不可靠软件如何伤人的例子:英国的"邮政局 Horizon 丑闻"。1999 至 2019 年间,英国数百名管理邮局分局的人因会计软件显示账目短缺而被判盗窃或欺诈罪。直到最终人们才弄清,许多账目短缺其实是软件 bug 所致,许多定罪因此被推翻 [78]。这场很可能是英国史上最大规模的司法不公,根源在于英国法律的一个假设:除非有相反证据,否则计算机被推定为正常运作(因此其产生的证据是可靠的)[79]。软件工程师可能会嘲笑"软件能没有 bug"这种想法,但对那些因不可靠的计算机系统被错判入狱、宣告破产、乃至自杀的人来说,这种嘲笑并不是什么安慰。
在某些情形下,我们或许会为降低开发成本而选择牺牲一些可靠性(例如为尚未验证的市场开发原型产品)——但务必清醒地意识到自己何时正在抄近路,并把潜在后果时时记在心里。
可扩展性
即便一个系统今天能可靠工作,也不代表它将来必然如此。性能退化的常见原因之一是负载增长。也许系统从 10,000 个并发用户成长到 100,000 个,或从 100 万个到 1,000 万个;又或者它处理的数据量比以前大得多。
*可扩展性(scalability)*是我们用来描述系统应对负载增长能力的术语。有时讨论可扩展性时人们会说:"你又不是 Google 或 Amazon。别再为规模操心了,用关系型数据库吧。"这句话适不适用于你,取决于你正在打造的应用类型。
如果你正在做一款新产品,目前用户量较少(可能在创业阶段),首要的工程目标通常是把系统保持得尽可能简单、灵活,以便随着对客户需求的了解去修改和调整产品功能 [80]。在这种环境里,去为或许将来才需要的"假想规模"操心,反而会适得其反。最好的情况下,那些对可扩展性的投资是被浪费的精力与过早的优化;最坏的情况下,它们把你锁进一种僵化的设计,让应用更难演进。
可扩展性不是一维标签——说"X 可扩展"或"Y 不可扩展"没有意义。讨论可扩展性意味着考虑这些问题:
- 如果系统按某种方式增长,我们有哪些应对增长的选项?
- 我们如何添加计算资源以处理额外负载?
- 基于当前增长预期,我们何时会触及当前架构的极限?
如果你成功让应用流行起来,并因此承载越来越多负载,你会发现性能瓶颈在哪里、需要沿哪些维度扩展。到那时,就要开始为"可扩展性技术"操心了。
理解负载
首先,你需要清晰理解系统当前的负载。只有这样,才能讨论关于增长的问题("如果负载翻倍会发生什么?")。这通常是某种吞吐量指标——例如每秒到达服务的请求数、每天新增数据的 GB 数,或每小时的购物车结账数。有时你关心的是某个可变量的峰值——例如我们案例研究中同时在线的用户数。
负载的其他统计特征也会影响访问模式,从而影响可扩展性需求。例如你可能需要知道:数据库中读与写的比例、缓存命中率,或每位用户的数据条目数(在我们的案例里是关注者数)。你或许更关心平均情形;又或许你的瓶颈被极少数极端情形主导。这都取决于你应用的具体情况。
一旦了解了系统的负载,你就可以研究当负载增加时会发生什么。可以从两个角度看:
- 当负载以某种方式增加,而系统资源(CPU、内存、网络带宽等)保持不变时,系统的性能会受到怎样的影响?
- 当负载以某种方式增加时,要保持性能不变,需要把资源增加多少?
通常目标是在保持性能符合 SLA 要求(参见"响应时间指标的用途",第 41 页)的同时,尽量降低运行系统的成本。所需的计算资源越多,成本越高。某些类型的硬件可能比其他更具成本效益,且这一情况会随新硬件的出现而变化。
如果将资源加倍,便能处理双倍负载又保持性能不变,我们就说你拥有线性可扩展性(linear scalability)——这通常被视为好事。偶尔也可能在不到双倍资源的情况下处理双倍负载,这得益于规模经济或更好的峰值负载分布 [81, 82]。更常见的情况是:成本增长比线性更快。原因可能有很多;例如当你拥有大量数据时,即便请求大小相同,处理一次写请求要做的工作也可能比数据量小时更多。
共享内存、共享磁盘与无共享架构
为一项服务增加硬件资源最简单的方式,是把它搬到一台更强的机器上。单核 CPU 的速度已不再显著提升,但你可以购买(或在云中租用)一台拥有更多 CPU 核心、更多 RAM 与更多磁盘空间的机器。这种做法称为纵向扩展(vertical scaling)或向上扩展(scaling up)。
通过使用多进程或多线程,你可以在单台机器上获得并行性。归属同一进程的所有线程都能访问同一份 RAM,因此这种做法也称为共享内存架构(shared-memory architecture)。共享内存方法的问题在于:成本增长比线性更快——一台高端机器拥有比低端机器多一倍的硬件资源,价格通常远高于两倍。而且由于存在瓶颈,那台机器实际上未必能处理两倍负载。
另一种做法是共享磁盘架构(shared-disk architecture):使用若干台拥有独立 CPU 与 RAM 的机器,但把数据存在一组共享磁盘阵列中——这些磁盘通过快速网络连接:网络附属存储(NAS)或存储区域网络(SAN)。该架构传统上用于本地数据仓库工作负载,但其锁的争用与开销限制了共享磁盘方法的可扩展性 [83]。
相对地,无共享架构(shared-nothing architecture) [84]——也叫横向扩展(horizontal scaling)或向外扩展(scaling out)——是一个分布式系统,包含多个节点,每个节点拥有自己的 CPU、RAM 与磁盘。节点间的任何协调都在软件层面通过常规网络完成。
这种方法近年来日趋流行,其好处在于:它有潜力做到线性扩展、可以使用价格/性能比最优的硬件(云中尤为如此)、可以更轻松地随负载增减而调整硬件资源,并能通过把系统分布到多个数据中心与区域以实现更强的容错。其代价是:要做显式分片(sharding,参见第 7 章),并要承担分布式系统的全部复杂度(第 9 章会讨论)。
某些云原生数据库系统使用独立的服务来分别承担存储与事务执行(参见"存储与计算的分离",第 16 页),多个计算节点共享对同一存储服务的访问。这种模型与共享磁盘架构有些相似,但避开了老系统的可扩展性问题。它没有提供文件系统(NAS)或块设备(SAN)抽象,而是提供专为数据库具体需求而设计的专用 API [85]。
可扩展性的若干原则
在大规模运行的系统,其架构通常高度针对应用而定。并不存在通用的、一刀切的可扩展架构(这种东西俗称神奇扩展秘方)。例如,为每秒处理 100,000 个 1 KB 请求而设计的系统,与为每分钟处理 3 个 2 GB 请求而设计的系统,看起来截然不同——尽管两者的数据吞吐量相同(100 MB/秒)。
而且,适合某一负载水平的架构不太可能应付高出 10 倍的负载。如果你做的服务增长很快,那么每经历一个数量级的负载增长,都很可能需要重新考量架构。由于应用的需求也会演化,预先规划超出一个数量级的扩展通常并不划算。
可扩展性的一项良好通用原则,是把系统拆成可以在很大程度上彼此独立运行的较小组件。这是微服务(参见"微服务与无服务器",第 21 页)、分片(第 7 章)、流处理(第 12 章)以及无共享架构背后的根本原则。挑战在于:要知道"应该放在一起"和"应该分开"之间该如何划线。微服务的设计指南可在其他书中找到 [86],第 7 章会讨论无共享系统的分片。
另一条好原则是:不要把事情做得超出必要。如果一台单机数据库就够用,那就比复杂的分布式部署更可取。自动扩缩系统(按需自动添加或移除资源)虽然听起来很酷,但若负载相当可预测,手工扩缩的系统反而更少出现运维上的意外(参见"运维:自动 vs 手动再平衡",第 264 页)。一个由 5 个服务组成的系统,比由 50 个服务组成的系统简单得多。优秀的架构通常是若干种做法的务实组合。
可维护性
软件不会磨损,也不会发生材料疲劳,所以它不会像机械物体那样以相同方式损坏。但应用的需求经常演化,软件运行的环境也在变(例如其依赖与底层平台),还会有需要修复的 bug。
人们普遍认识到:软件的大部分成本并不在最初的开发,而在于持续的维护——修 bug、维持其运行、调查失效、适配到新平台、为新用例做修改、偿还技术债、增添新特性 [87, 88]。
维护可能非常复杂,对遗留系统尤其如此。一个长期成功运行的系统很可能使用了今天少有工程师懂的过时技术(如大型机与 COBOL 代码);关于"系统当初为什么这么设计"的机构知识,也可能随人离开组织而丢失。修复别人遗留下来的错误也可能在所难免。由于计算机系统常与其所支持的人事组织彼此交织,维护此类系统既是一个技术问题,也是一个"人"的问题 [89]。
我们今天构建的每一个系统,只要有足够的价值能存活足够久,终有一天都会成为遗留系统。要尽量减少未来维护我们软件的人的痛苦,我们应在设计时就把维护放在心上。虽然不能总是预测哪些决策会带来未来的麻烦,但本书将关注几条广为适用的原则:
可运维性(Operability) :让组织能轻松地保持系统平稳运行。
简单性(Simplicity) :通过使用众所周知、一致的模式与结构,并避免不必要的复杂度,把系统实现得让新工程师容易理解。
可演化性(Evolvability) :让工程师将来能方便地修改系统,随需求变化将其调整或扩展到未曾预料的用例。
可运维性:让运维工作轻松
我们之前在"云时代的运营"(第 17 页)讨论过运维角色,并看到对可靠运维而言,人的流程至少和软件工具同等重要。事实上,有人认为"良好的运维往往能绕开糟糕(或不完整)软件的种种限制,但糟糕运维下的软件即便很好也无法可靠运行"[62]。
在由数千台机器组成的大型系统中,手工维护成本会高得不可接受,因此自动化必不可少。然而自动化是一柄双刃剑。总会有一些边缘情况(如罕见的失效场景)需要运维团队人工介入;又因为不能被自动处理的情况往往是最复杂的,更高的自动化反而需要更熟练的运维团队来解决这些问题 [90]。
此外,自动化系统出问题时往往比依赖运维人员手工操作的系统更难排障。正因如此,更高的自动化对可运维性并不总是更好。但一定程度的自动化仍然重要——最佳折中点取决于你具体的应用与组织。
良好的可运维性意味着让日常任务变得简单,让运维团队能把精力放在高价值的工作上。数据系统可以通过以下做法提供帮助 [91]:
- 让监控工具能够检查系统的关键指标,并配套提供可观测性工具(参见"分布式系统的麻烦",第 20 页),让运维人员能洞察系统的运行时行为。商业与开源工具都可以在此提供帮助 [92]。
- 避免对个别机器的依赖(让机器在停机维护时,整个系统仍能不间断地继续运行)。
- 提供良好的文档与易于理解的运维心智模型("如果我做 X,就会发生 Y")。
- 提供良好的默认行为,但也给管理员在需要时覆盖默认值的自由。
- 在适当之处自我修复,但同样在需要时让管理员对系统状态拥有手工控制权。
- 表现出可预测的行为,把意外降到最低。
简单性:管理复杂度
小型软件项目的代码可以简洁、富有表达力,令人愉悦;但项目一旦变大,常会变得非常复杂、难以理解。这种复杂度会让所有需要为该系统工作的人都慢下来,进一步抬升维护成本。深陷复杂度泥潭的软件项目有时被形容为"大泥球(big ball of mud)" [93]。
当复杂度让维护变难时,预算与排期常会失控。在复杂软件中,做一次变更而引入 bug 的风险也更大;当系统让开发者更难理解与推理时,隐含假设、非预期后果与意外交互更易被忽视 [71]。反过来,降低复杂度能极大改善软件的可维护性,因此简单性应是我们构建系统时的关键目标。
简单的系统更容易理解,所以应尽可能用最简单的方式去解决问题。可惜说易做难——是否简单常常是主观判断,没有客观的"简单度"标准 [94]。例如某系统可能在简洁接口背后藏着复杂实现;另一个则可能实现简单,却把更多内部细节暴露给使用者——哪个更简单?
为复杂度建模的一种做法把它分为两类:本质(essential)与偶然(accidental)。其想法是:本质复杂度是应用问题域固有的,而偶然复杂度只因我们工具的局限而出现 [95]。但这一区分本身也有缺陷——本质与偶然之间的边界会随我们工具的演化而漂移 [96]。
我们用以管理复杂度的最好工具之一是抽象(abstraction)。良好的抽象能在干净、易于理解的外观背后藏起大量实现细节,并能用于一系列不同的应用。这种复用不仅比一次次重新实现类似的东西更高效,也带来了更高质量的软件——因为对抽象组件的质量改进会让所有使用它的应用都受益。
例如,高级编程语言是隐藏了机器代码、CPU 寄存器与系统调用的抽象。SQL 是一个抽象,藏起了磁盘上与内存中复杂的数据结构、其他客户端的并发请求,以及崩溃后的不一致。当然,用高级语言编程时我们仍然在使用机器代码,只是不直接使用它——因为编程语言的抽象省去了我们直接思考它的负担。
旨在降低应用代码复杂度的抽象,可以借助设计模式(design patterns) [97] 与领域驱动设计(DDD) [98] 等方法学构建出来。本书并不讨论这类应用专属的抽象,而是关注那些通用的抽象——你可以在它们之上构建应用,例如数据库事务、索引与事件日志。如果你想使用 DDD 等技术,可以在本书所描述的这些基础之上实现它们。
可演化性:让变更易于进行
你的系统需求一直保持不变的可能性极小。它们更可能处在持续变化中:你了解到新的事实、未曾预料的新用例出现、业务优先级改变、用户提出新的功能请求、新平台取代旧平台、法律或法规要求变化、系统增长迫使架构调整等等。
在组织流程方面,Agile(敏捷)工作方式提供了一个适应变化的框架。敏捷社区也已开发出一系列在频繁变化的环境中构建软件时有用的技术工具与流程,如测试驱动开发(TDD)与重构。本书要寻找的是:在由若干具有不同特征的应用或服务构成的系统层面上,提高敏捷性的途径。
修改一个数据系统、让它适配不断变化的需求,这件事的难易程度与系统的简单性及其抽象密切相关。松耦合、简单的系统通常比紧耦合、复杂的系统更容易修改。鉴于这一思想至关重要,我们专门用一个词来指称数据系统层面的敏捷性:可演化性(evolvability) [99]。
让大型系统中的变更变得困难的一个主要因素是不可逆性(irreversibility) [100]。例如,假设你正在从一个数据库迁移到另一个。如果新系统出问题时你无法切回老系统,那么这一选择的赌注就远大于"能够轻松回退"的情形。因此不可逆动作要非常小心地对待。降低不可逆性可以提高灵活性。
小结
本章我们考察了几种非功能性需求的例子:性能、可靠性、可扩展性与可维护性。借助这些主题,我们也接触了贯穿本书后文的若干原则与术语。
我们以"在社交网络中实现主页时间线"的案例研究开篇,演示了大规模场景下出现的一些挑战。然后讨论了如何度量性能(如使用响应时间百分位)以及如何度量系统的负载(如使用吞吐量指标),并讨论了这些指标如何被用于 SLA。可扩展性是一个密切相关的概念:它聚焦于在负载增长时让性能保持不变。我们看了若干可扩展性的通用原则,例如把任务拆成可以独立运作的较小部分;后续章节会更深入地讲解可扩展性技术。
要实现可靠性,你可以使用容错技术——它们让系统在某个组件(如磁盘、机器或某项服务)出故障时也能继续提供服务。我们看了一些可能出现的硬件故障示例,并把它们与软件故障作了区分;软件故障常常更难处理,因为彼此之间相关性较强。实现可靠性的另一面是建立对人为失误的韧性,我们也看了"无责事后回顾"作为从事故中学习的一种技术。
最后,我们考察了可维护性的几个方面,包括支持运维团队的工作、管理复杂度,以及让应用功能能随时间演化。要达成这些目标没有简单的答案,但一种有帮助的做法是:用众所周知、提供有用抽象的构件来构建应用。本书余下的部分将介绍若干在实践中被证明有价值的构件。
参考文献
[1] Mike Cvet. "How We Learned to Stop Worrying and Love Fan-in at Twitter." At QCon San Francisco, December 2016.
[2] Raffi Krikorian. "Timelines at Scale." At QCon San Francisco, November 2012. 归档于 perma.cc/V9G5-KLYK
[3] Twitter. "Twitter's Recommendation Algorithm." blog.x.com, March 2023. 归档于 perma.cc/L5GT-229T
[4] Raffi Krikorian. "New Tweets per Second Record, and How!" blog.x.com, August 2013. 归档于 perma.cc/6JZN-XJYN
[5] Jaz Volpert. "When Imperfect Systems Are Good, Actually: Bluesky's Lossy Timelines." jazco.dev, February 2025. 归档于 perma.cc/2PVE-L2MX
[6] Samuel Axon. "3% of Twitter's Servers Dedicated to Justin Bieber." mashable.com, September 2010. 归档于 perma.cc/F35N-CGVX
[7] Nathan Bronson, Abutalib Aghayev, Aleksey Charapko, Timothy Zhu. "Metastable Failures in Distributed Systems." At Workshop on Hot Topics in Operating Systems (HotOS), May 2021.
[8] Marc Brooker. "Metastability and Distributed Systems." brooker.co.za, May 2021. 归档于 perma.cc/7FGJ-7XRK
[9] Lexiang Huang 等. "Metastable Failures in the Wild." At 17th USENIX Symposium on Operating Systems Design and Implementation (OSDI), July 2022.
[10] Marc Brooker. "Exponential Backoff and Jitter." aws.amazon.com, March 2015. 归档于 perma.cc/R6MS-AZKH
[11] Marc Brooker. "What Is Backoff For?" brooker.co.za, August 2022. 归档于 perma.cc/PW9N-55Q5
[12] Michael T. Nygard. Release It!, 2nd edition. Pragmatic Bookshelf, 2018. ISBN: 9781680502398
[13] Frank Chen. "Slowing Down to Speed Up—Circuit Breakers for Slack's CI/CD." slack.engineering, August 2022. 归档于 perma.cc/5FGS-ZPH3
[14] Marc Brooker. "Fixing Retries with Token Buckets and Circuit Breakers." brooker.co.za, February 2022. 归档于 perma.cc/MD6N-GW26
[15] David Yanacek. "Using Load Shedding to Avoid Overload." Amazon Builders' Library, aws.amazon.com. 归档于 perma.cc/9SAW-68MP
[16] Matthew Sackman. "Pushing Back." wellquite.org, May 2016. 归档于 perma.cc/3KCZ-RUFY
[17] Dmitry Kopytkov, Patrick Lee. "Meet Bandaid, the Dropbox Service Proxy." dropbox.tech, March 2018. 归档于 perma.cc/KUU6-YG4S
[18] Haryadi S. Gunawi 等. "Fail-Slow at Scale: Evidence of Hardware Performance Faults in Large Production Systems." At 16th USENIX Conference on File and Storage Technologies, February 2018.
[19] Marc Brooker. "Is the Mean Really Useless?" brooker.co.za, December 2017. 归档于 perma.cc/U5AE-CVEM
[20] Giuseppe DeCandia 等. "Dynamo: Amazon's Highly Available Key-Value Store." At 21st ACM Symposium on Operating Systems Principles (SOSP), October 2007.
[21] Kathryn Whitenton. "The Need for Speed, 23 Years Later." nngroup.com, May 2020. 归档于 perma.cc/C4ER-LZYA
[22] Greg Linden. "Marissa Mayer at Web 2.0." glinden.blogspot.com, November 2005. 归档于 perma.cc/V7EA-3VXB
[23] Jake Brutlag. "Speed Matters for Google Web Search." services.google.com, June 2009. 归档于 perma.cc/BK7R-X7M2
[24] Eric Schurman, Jake Brutlag. "Performance Related Changes and Their User Impact." Talk at Velocity 2009.
[25] Akamai Technologies, Inc. "The State of Online Retail Performance." akamai.com, April 2017. 归档于 perma.cc/UEK2-HYCS
[26] Xiao Bai, Ioannis Arapakis, B. Barla Cambazoglu, Ana Freire. "Understanding and Leveraging the Impact of Response Latency on User Behaviour in Web Search." ACM Transactions on Information Systems, volume 36, issue 2, article 21, April 2018.
[27] Jeffrey Dean, Luiz André Barroso. "The Tail at Scale." Communications of the ACM, volume 56, issue 2, pages 74–80, February 2013.
[28] Alex Hidalgo. Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error Budgets. O'Reilly Media, 2020. ISBN: 9781492076813
[29] Jeffrey C. Mogul, John Wilkes. "Nines Are Not Enough: Meaningful Metrics for Clouds." At 17th Workshop on Hot Topics in Operating Systems (HotOS), May 2019.
[30] Tamás Hauer 等. "Meaningful Availability." At 17th USENIX Symposium on Networked Systems Design and Implementation (NSDI), February 2020.
[31] Gil Tene. "HdrHistogram: A High Dynamic Range Histogram." hdrhistogram.github.io/HdrHistogram
[32] Ted Dunning. "The t-digest: Efficient Estimates of Distributions." Software Impacts, volume 7, article 100049, February 2021.
[33] David Kohn. "How Percentile Approximation Works (and Why It's More Useful than Averages)." timescale.com, September 2021. 归档于 perma.cc/3PDP-NR8B
[34] Heinrich Hartmann, Theo Schlossnagle. "Circllhist—A Log-Linear Histogram Data Structure for IT Infrastructure Monitoring." arXiv:2001.06561, January 2020.
[35] Charles Masson, Jee E. Rim, Homin K. Lee. "DDSketch: A Fast and Fully-Mergeable Quantile Sketch with Relative-Error Guarantees." Proceedings of the VLDB Endowment, volume 12, issue 12, pages 2195–2205, August 2019.
[36] Baron Schwartz. "Why Percentiles Don't Work the Way You Think." solarwinds.com, November 2016. 归档于 perma.cc/469T-6UGB
[37] Walter L. Heimerdinger, Charles B. Weinstock. "A Conceptual Framework for System Fault Tolerance." Technical Report CMU/SEI-92-TR-033, Software Engineering Institute, Carnegie Mellon University, October 1992. 归档于 perma.cc/GD2V-DMJW
[38] Felix C. Gärtner. "Fundamentals of Fault-Tolerant Distributed Computing in Asynchronous Environments." ACM Computing Surveys, volume 31, issue 1, pages 1–26, March 1999.
[39] Algirdas Avižienis 等. "Basic Concepts and Taxonomy of Dependable and Secure Computing." IEEE Transactions on Dependable and Secure Computing, volume 1, issue 1, pages 11–33, January 2004.
[40] Ding Yuan 等. "Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems." At 11th USENIX Symposium on Operating Systems Design and Implementation (OSDI), October 2014.
[41] Casey Rosenthal, Nora Jones. Chaos Engineering. O'Reilly Media, 2020. ISBN: 9781492043867
[42] Eduardo Pinheiro, Wolf-Dietrich Weber, Luiz Andre Barroso. "Failure Trends in a Large Disk Drive Population." At 5th USENIX Conference on File and Storage Technologies (FAST), February 2007.
[43] Bianca Schroeder, Garth A. Gibson. "Disk Failures in the Real World: What Does an MTTF of 1,000,000 Hours Mean to You?" At 5th USENIX Conference on File and Storage Technologies (FAST), February 2007.
[44] Andy Klein. "Backblaze Drive Stats for Q2 2021." backblaze.com, August 2021. 归档于 perma.cc/2943-UD5E
[45] Iyswarya Narayanan 等. "SSD Failures in Datacenters: What? When? And Why?" At 9th ACM International on Systems and Storage Conference (SYSTOR), June 2016.
[46] Alibaba Cloud Storage Team. "Storage System Design Analysis: Factors Affecting NVMe SSD Performance (1)." alibabacloud.com, January 2019. 归档于 archive.org
[47] Bianca Schroeder, Raghav Lagisetty, Arif Merchant. "Flash Reliability in Production: The Expected and the Unexpected." At 14th USENIX Conference on File and Storage Technologies (FAST), February 2016.
[48] Jacob Alter, Ji Xue, Alma Dimnaku, Evgenia Smirni. "SSD Failures in the Field: Symptoms, Causes, and Prediction Models." At International Conference for High Performance Computing, Networking, Storage and Analysis (SC), November 2019.
[49] Daniel Ford 等. "Availability in Globally Distributed Storage Systems." At 9th USENIX Symposium on Operating Systems Design and Implementation (OSDI), October 2010.
[50] Kashi Venkatesh Vishwanath, Nachiappan Nagappan. "Characterizing Cloud Computing Hardware Reliability." At 1st ACM Symposium on Cloud Computing (SoCC), June 2010.
[51] Peter H. Hochschild 等. "Cores That Don't Count." At Workshop on Hot Topics in Operating Systems (HotOS), June 2021.
[52] Harish Dattatraya Dixit 等. "Silent Data Corruptions at Scale." arXiv:2102.11245, February 2021.
[53] Diogo Behrens 等. "Scalable Error Isolation for Distributed Systems." At 12th USENIX Symposium on Networked Systems Design and Implementation (NSDI), May 2015.
[54] Bianca Schroeder, Eduardo Pinheiro, Wolf-Dietrich Weber. "DRAM Errors in the Wild: A Large-Scale Field Study." At 11th International Joint Conference on Measurement and Modeling of Computer Systems (SIGMETRICS), June 2009.
[55] Yoongu Kim 等. "Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors." At 41st Annual International Symposium on Computer Architecture (ISCA), June 2014.
[56] Tim Bray. "Worst Case." tbray.org, October 2021. 归档于 perma.cc/4QQM-RTHN
[57] Sangeetha Abdu Jyothi. "Solar Superstorms: Planning for an Internet Apocalypse." At ACM SIGCOMM Conference, August 2021.
[58] Adrian Cockcroft. "Failure Modes and Continuous Resilience." adrianco.medium.com, November 2019. 归档于 perma.cc/7SYS-BVJP
[59] Shujie Han 等. "An In-Depth Study of Correlated Failures in Production SSD-Based Data Centers." At 19th USENIX Conference on File and Storage Technologies (FAST), February 2021.
[60] Edmund B. Nightingale, John R. Douceur, Vince Orgovan. "Cycles, Cells and Platters: An Empirical Analysis of Hardware Failures on a Million Consumer PCs." At 6th European Conference on Computer Systems (EuroSys), April 2011.
[61] Haryadi S. Gunawi 等. "What Bugs Live in the Cloud? A Study of 3000+ Issues in Cloud Systems." At 5th ACM Symposium on Cloud Computing (SoCC), November 2014.
[62] Jay Kreps. "Getting Real About Distributed System Reliability." blog.empathybox.com, March 2012. 归档于 perma.cc/9B5Q-AEBW
[63] Nelson Minar. "Leap Second Crashes Half the Internet." somebits.com, July 2012. 归档于 perma.cc/2WB8-D6EU
[64] Hewlett Packard Enterprise. "Support Alerts—Customer Bulletin a00092491en_us." support.hpe.com, November 2019. 归档于 perma.cc/S5F6-7ZAC
[65] Lorin Hochstein. "Awesome Limits." github.com, November 2020. 归档于 perma.cc/3R5M-E5Q4
[66] Caitie McCaffrey. "Clients Are Jerks: AKA How Halo 4 DoSed the Services at Launch & How We Survived." caitiem.com, June 2015. 归档于 perma.cc/MXX4-W373
[67] Lilia Tang 等. "Fail Through the Cracks: Cross-System Interaction Failures in Modern Cloud Systems." At 18th European Conference on Computer Systems (EuroSys), May 2023.
[68] Mike Ulrich. "Addressing Cascading Failures." In Betsy Beyer 等编辑,Site Reliability Engineering: How Google Runs Production Systems. O'Reilly Media, 2016. ISBN: 9781491929124
[69] Harri Faßbender. "Cascading Failures in Large-Scale Distributed Systems." blog.mi.hdm-stuttgart.de, March 2022. 归档于 perma.cc/K7VY-YJRX
[70] Richard I. Cook. "How Complex Systems Fail." Cognitive Technologies Laboratory, April 2000. 归档于 perma.cc/RDS6-2YVA
[71] David D. Woods. "STELLA: Report from the SNAFUcatchers Workshop on Coping with Complexity." snafucatchers.github.io, March 2017. 归档于 archive.org
[72] David Oppenheimer, Archana Ganapathi, David A. Patterson. "Why Do Internet Services Fail, and What Can Be Done About It?" At 4th USENIX Symposium on Internet Technologies and Systems (USITS), March 2003.
[73] Sidney Dekker. The Field Guide to Understanding "Human Error", 3rd edition. CRC Press, 2017. ISBN: 9781472439055
[74] Sidney Dekker. Drift into Failure: From Hunting Broken Components to Understanding Complex Systems. CRC Press, 2011. ISBN: 9781315257396
[75] John Allspaw. "Blameless PostMortems and a Just Culture." etsy.com, May 2012. 归档于 perma.cc/YMJ7-NTAP
[76] Itzy Sabo. "Uptime Guarantees—A Pragmatic Perspective." world.hey.com, March 2023. 归档于 perma.cc/F7TU-78JB
[77] Michael Jurewitz. "The Human Impact of Bugs." jury.me, March 2013. 归档于 perma.cc/5KQ4-VDYL
[78] Mark Halper. "How Software Bugs Led to 'One of the Greatest Miscarriages of Justice' in British History." Communications of the ACM, volume 68, issue 3, pages 12–14, January 2025.
[79] Nicholas Bohm 等. "The Legal Rule That Computers Are Presumed to Be Operating Correctly—Unforeseen and Unjust Consequences." Briefing note, benthamsgaze.org, June 2022. 归档于 perma.cc/WQ6X-TMW4
[80] Dan McKinley. "Choose Boring Technology." mcfunley.com, March 2015. 归档于 perma.cc/7QW7-J4YP
[81] Andy Warfield. "Building and Operating a Pretty Big Storage System Called S3." allthingsdistributed.com, July 2023. 归档于 perma.cc/7LPK-TP7V
[82] Marc Brooker. "Surprising Scalability of Multitenancy." brooker.co.za, March 2023. 归档于 perma.cc/ZZD9-VV8T
[83] Ben Stopford. "Shared Nothing vs. Shared Disk Architectures: An Independent View." benstopford.com, November 2009. 归档于 perma.cc/7BXH-EDUR
[84] Michael Stonebraker. "The Case for Shared Nothing." IEEE Database Engineering Bulletin, volume 9, issue 1, pages 4–9, March 1986. perma.cc/P9YL-C4PS
[85] Panagiotis Antonopoulos 等. "Socrates: The New SQL Server in the Cloud." At ACM International Conference on Management of Data (SIGMOD), June 2019.
[86] Sam Newman. Building Microservices, 2nd edition. O'Reilly Media, 2021. ISBN: 9781492034025
[87] Nathan Ensmenger. "When Good Software Goes Bad: The Surprising Durability of an Ephemeral Technology." At The Maintainers Conference, April 2016. 归档于 perma.cc/ZXT4-HGZB
[88] Robert L. Glass. Facts and Fallacies of Software Engineering. Addison-Wesley Professional, 2002. ISBN: 9780321117427
[89] Marianne Bellotti. Kill It with Fire. No Starch Press, 2021. ISBN: 9781718501188
[90] Lisanne Bainbridge. "Ironies of Automation." Automatica, volume 19, issue 6, pages 775–779, November 1983.
[91] James Hamilton. "On Designing and Deploying Internet-Scale Services." At 21st Large Installation System Administration Conference (LISA), November 2007.
[92] Dotan Horovits. "Open Source for Better Observability." horovits.medium.com, October 2021. 归档于 perma.cc/R2HD-U2ZT
[93] Brian Foote, Joseph Yoder. "Big Ball of Mud." At 4th Conference on Pattern Languages of Programs (PLoP), September 1997. 归档于 perma.cc/4GUP-2PBV
[94] Marc Brooker. "What Is a Simple System?" brooker.co.za, May 2022. 归档于 perma.cc/U72T-BFVE
[95] Frederick P. Brooks. "No Silver Bullet—Essence and Accident in Software Engineering." In The Mythical Man-Month, Anniversary edition, Addison-Wesley, 1995. ISBN: 9780201835953
[96] Dan Luu. "Against Essential and Accidental Complexity." danluu.com, December 2020. 归档于 perma.cc/H5ES-69KC
[97] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional, 1994. ISBN: 9780201633610
[98] Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley Professional, 2003. ISBN: 9780321125217
[99] Hongyu Pei Breivold, Ivica Crnkovic, Peter J. Eriksson. "Analyzing Software Evolvability." At 32nd Annual IEEE International Computer Software and Applications Conference (COMPSAC), July 2008.
[100] Enrico Zaninotto. "From X Programming to the X Organisation." At XP Conference, May 2002. 归档于 perma.cc/R9AR-QCKZ