第一章 可靠性,可扩展性,可维护性
最后更新于:2022-04-02 04:18:22
[TOC]
## 概述
现今很多应用程序都是 **数据密集型data-intensive**的,而非 **计算密集型compute-intensive**的。因此CPU很少成为这类应用的瓶颈,更大的问题通常来自数据量、数据复杂性、以及数据的变更速度。
数据型应用常见场景:
* 存储数据,以便自己或其他应用程序之后能再次找到 (**数据库(database)**)
* 记住开销昂贵操作的结果,加快读取速度(**缓存(cache)**)
* 允许用户按关键字搜索数据,或以各种方式对数据进行过滤(**搜索索引(search indexes)**)
* 向其他进程发送消息,进行异步处理(**流处理(stream processing)**)
* 定期处理累积的大批量数据(**批处理(batch processing)**)
## 可靠性(Reliability)
1. 注意**故障(fault)**不同于**失效(failure)**。**故障**通常定义为系统的一部分状态偏离其标准,而**失效**则是系统作为一个整体停止向用户提供服务
1. 故障的概率不可能降到零,因此最好设计容错机制以防因**故障**而导致**失效**
在这类容错系统中,通过故意触发来**提高**故障率是有意义的,例如:
- 在没有警告的情况下随机地杀死单个进程。许多高危漏洞实际上是由糟糕的错误处理导致的,因此我们可以通过故意引发故障来确保容错机制不断运行并接受考验,从而提高故障自然发生时系统能正确处理的信心
- Netflix公司的*Chaos Monkey*就是这种方法的一个例子
### 硬件故障
如:硬盘崩溃、内存出错、机房断电、有人拔错网线
- 据报道称,硬盘的**平均无故障时间(MTTF mean time to failure)**约为10到50年,因此从数学期望上讲,在拥有10000个磁盘的存储集群上,平均每天会有1个磁盘出故障
- 为了减少系统的故障率,第一反应通常都是增加单个硬件的冗余度,例如:磁盘可以组建RAID
### 软件错误
* 接受特定的错误输入,便导致所有应用服务器实例崩溃的BUG。例如2012年6月30日的闰秒,由于Linux内核中的一个错误,许多应用同时挂掉了。
* 失控进程会占用一些共享资源,包括CPU时间、内存、磁盘空间或网络带宽。
* 系统依赖的服务变慢,没有响应,或者开始返回错误的响应。
* 级联故障,一个组件中的小故障触发另一个组件中的故障,进而触发更多的故障【10】。
### 人为错误
最好的系统会组合使用以下几种办法:
* 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。
* 将人们最容易犯错的地方与可能导致失效的地方**解耦(decouple)**。特别是提供一个功能齐全的非生产环境**沙箱(sandbox)**,使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验。
* 在各个层次进行彻底的测试【3】,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的**边缘场景(corner case)**。
* 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。 例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。
* 配置详细和明确的监控,比如性能指标和错误率。 在其他工程学科中这指的是**遥测(telemetry)**。 (一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的。)监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。
* 良好的管理实践与充分的培训——一个复杂而重要的方面,但超出了本书的范围。
## 可扩展性(Scalability)
服务**降级(degradation)**的一个常见原因是负载增加
**可扩展性(Scalability)**是用来描述系统应对负载增长能力的术语
负载可以用一些称为**负载参数(load parameters)**的数字来描述,参数的最佳选择取决于系统架构,它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西
### 推特发文实例
方式一:
- 发布推文时,只需将新推文插入全局推文集合即可,当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并,
编写如下查询
```
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
```
方式二:
- 为每个用户的主页时间线维护一个缓存。 当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中
推特的第一个版本使用了方法1,但系统很难跟上主页时间线查询的负载。所以公司转向了方法2,方法2的效果更好,因为发推频率比查询主页时间线的频率几乎低了两个数量级,所以在这种情况下,最好在写入时做更多的工作,而在读取时做更少的工作
然而方法2的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345k的写入
推特最终方法
推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能
### 描述性能
- 对于Hadoop这样的批处理系统,通常关心的是**吞吐量(throughput)**,即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间。
- 对于在线系统,通常更重要的是服务的**响应时间(response time)**,即客户端发送请求到接收响应之间的时间
## 可维护性(Maintainability)
在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则
- 可操作性(Operability)
便于运维团队保持系统平稳运行。
- 简单性(Simplicity)
从系统中消除尽可能多的复杂度(complexity),使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)
- 可演化性(evolability)
使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为可扩展性(extensibility),可修改性(modifiability)或可塑性(plasticity)。
### 可操作性
一个优秀运维团队的典型职责如下
* 监控系统的运行状况,并在服务状态不佳时快速恢复服务
* 跟踪问题的原因,例如系统故障或性能下降
* 及时更新软件和平台,比如安全补丁
* 了解系统间的相互作用,以便在异常变更造成损失前进行规避。
* 预测未来的问题,并在问题出现之前加以解决(例如,容量规划)
* 建立部署,配置、管理方面的良好实践,编写相应工具
* 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台
* 当配置变更时,维持系统的安全性
* 定义工作流程,使运维操作可预测,并保持生产环境稳定。
* 铁打的营盘流水的兵,维持组织对系统的了解。
### 简单性
**复杂度(complexity)** 有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例
简化系统并不一定意味着减少功能;它也可以意味着消除 **额外的(accidental)** 的复杂度
Moseley和Marks把**额外复杂度**定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度
用于消除**额外复杂度**的最好工具之一是**抽象(abstraction)**
比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益
例如
1. 高级编程语言是一种抽象,隐藏了机器码、CPU寄存器和系统调用。
2. SQL也是一种抽象,隐藏了复杂的磁盘/内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性
### 可演化性
';