首页 > 技术文章 > 【DDIA】设计数据密集型应用读书笔记 Ch1

wAther 2021-01-15 22:10 原文

本书主要讨论三个问题:可靠性(Reliability),可扩展性(Scalability),可维护性(Maintainability)。

  • 可靠性:系统在处于困境状态下,仍然可以正常工作
  • 可扩展性:系统面对数据量、复杂性上升之后的进一步拓展
  • 可维护性:许多不同的人在不同时间段对于系统可以高效进行维护。

可靠性

故障 (Fault)会造成错误,能够预料并及时应对故障的系统特性称为容错(Fault-Tolerant)。系统在产生故障之后,就会失效(Failure),整个系统就会对外停止提供服务。故障通常可能是系统的某个部件产生,但是失效则侧重于整个系统都会停止对外服务。

如果我们需要处理故障,那么我们会对可能出现的故障有潜在的估计并设计应对方法,但是故障出现的原因可能会有很多,所以在讨论容错的时候,需要首先明确讨论特定的错误类型。针对系统而言,有计划的提升故障率,产生新的故障来进行系统测试是有意义的,因为这样可以模拟现实场景,此类有计划的生成并对系统进行测试的方法称为混沌测试 (Chaos test)。

书中讨论的故障主要有几种类型:

  • 硬件故障:包括但不限于硬盘崩溃,内存出错,机房断电,网线连接错误等。当维护的机器数量增大,那么对于整个系统而言,出现某块硬盘故障的可能性也在大大增加。传统意义上面对此类问题都是通过硬件冗余的方式进行,增加硬件的冗余度,让硬件出现错误的时候,可以有其他的硬件及时进行更换。
  • 软件错误:硬件故障通常是随机并且独立的,一块硬盘出现问题并不会导致其他硬盘同样出现问题。但是当系统内部出现系统性问题(systematic error)的时候,这类问题通常会让许多服务一起崩溃。这种系统性的问题通常不会被触发并且有很长的潜伏期,只有当极少数异常状况被触发时才会显现。(例如2012年6月30日的闰秒,导致了 Linux 内核中的一个错误。)
  • 人为错误:系统的设计和维护都是由人进行的,那么人类由于粗心等不可靠因素导致系统出现崩溃也是正常的。那么如何让人类的维护和系统自身进行解耦,更好的维护系统,有几种方法:
    • 用最小化犯错机会的方式进行设计系统。
    • 将人类最容易犯错的地方与可能导致失效的地方进行解耦(decouple),sandbox 提供了一个可以让开发者在不影响真实用户体验的情况下, 用真实数据进行探索和实验,避免了直接影响线上环境。
    • 对每个层次都进行彻底测试,从单元测试、全系统测试到手动测试,尽量使用自动化测试进行。
    • 允许在发生人为错误的时候快速进行系统回滚恢复。例如:快速回滚配置,分批次的进行更新代码。
    • 部署具有监控功能的系统,对于某些系统指标进行监控,并有实时日志记录。

可扩展性

可扩展性讨论的是系统的负载能力增长的能力,聚焦的问题是「当系统以某种特定的方式进行增长,有什么方式应对这种增长」,「如何增加计算资源来应对额外的负载资源」

首先需要明确系统的负载是什么,这些负载可能会对系统的哪些服务产生额外的压力。

当明确了系统的负载之后,就可以研究负载增加的时候,系统会发生什么?

  • 增加负载参数并保持系统资源不变的时候,系统的性能会收到什么样的影响?
  • 增加负载参数的时候,如果希望系统的性能仍然保持不变,需要额外增加多少系统资源?

对于系统性能,我们需要一些指标(metric)去描述系统性能,常用的指标有吞吐量,服务响应时间(客户端发送请求到接受响应之间的时间)。响应时间除了系统本身提供服务所需要的时间之外,还包括了数据进行网络传输所花费的时间,由于服务本身可能需要进行等待所需要的等待时间。

响应时间是描述一次请求所产生的响应时间,但是现实情况非常复杂,我们需要使用一连串的观测进行描述。通常使用平均响应时间来描述系统的性能,但是并不是一个好用的指标,通常使用百分位点(百分之x 的数据比此时要好)表述,中位数,p95,p99。

尾部延迟通常是我们需要关注的地方,但这种数据在平均响应时间中并不会体现,但是尾部延迟在商业中可能非常重要。这是因为通常影响较大的用户可能是数据最多的用户,也是最有价值的用户。但如果我们过多的关注优化 99.99%,则付出的代价同样也很大,甚至不可控。

在实践中,如果是多重调用的后段服务,那么某一个非常慢的服务可能会严重拖累整个用户请求。即使是有部分后段调用速度较慢,那么最终用户请求需要多个后段调用,则获得较慢调用的机会也会增加,最多较高比例的最终用户请求速度都会变慢。

当负载发生快速增长的时候,在某个级别表现良好的系统架构可能在负载有数量级扩张之后无法应对。所以需要重新考虑架构设计。

扩展(Scale)有两个方向的扩展,纵向扩展(Scaling Up)和横向扩展(Scaling Out),纵向扩展选择使用更加强大的单机处理机器,横向扩展则将负载分布到更多的机器上。非常密集的负载请求通常使用横向扩展。

某些系统可以是具有弹性(elastic)的,它们可以在检测到负载增加的时候自动增加计算资源。也有系统是手动扩展,需要人工分析然后进行容量扩展。如果负载本身极难预测估算,则弹性系统可能更有用,因为避免了人力频繁进行估计和扩展。

从系统提供的服务本身而言,对于无状态的服务,进行横向多机扩展可能更加简单,但是对于有状态的服务,从单节点扩展到分布式的多机配置可能引入额外的复杂度。所以数据库这类有状态的服务通常放在单机中,直到无可避免的时候才扩展到多机服务。

对于大规模的系统架构,严重依赖于提供的服务本身。没有一种可通用的大型扩展架构,系统的架构设计取决于服务本身的特性,通常会涉及到读取量,写入量,存储的数据量,数据的复杂度,响应的时间要求,访问模式等参数。

一个可扩展的系统架构通常都有一些预定义好的假设,这些假设指导我们进行系统涉及,但同时,如果假设预估与实际访问有偏差,那么系统架构很可能并不适合当前的模式。


可维护性

软件工程中讲,对于一个软件从开发到后续的维护整个过程,软件的后续维护所需要的精力往往占到整个过程的一半以上,所以系统需要具有一定良好的维护性,但这往往很难达到。维护的角度包括很多,不仅包括修复潜在的问题,保证系统正常运转,还有添加新的功能,适配新的业务,适配新的平台等等。

为了让后续维护系统变得更加的方便,减轻维护期间的痛苦,我们需要关注系统涉及的三个原则:

  • 可操作性 (Operability):让运维团队系统保持平稳运行
  • 简单性(Simplicity):让系统尽可能消除复杂度,使得新人也可以轻松接手。
  • 可演化性(evolability):让工程师轻松对系统进行更改,可以为新的应用场景做适配。

可操作性:运维团队可以更好的维护整个系统。

为了让运维更加的轻松,数据系统应该具有以下的功能:

  • 拥有良好的监控系统,可以对系统内部状态和运行时的行为进行监控。
  • 可以更好的进行自动化,系统和标准化工具相互集成。
  • 避免依赖单个机器。
  • 拥有 well-written 的文档和说明。
  • 行为可以预测。

复杂性:整个系统的复杂度会随着功能和模块的增多而大大提升,也会表现出各种症状,例如状态空间激增,模块间紧密耦合,依赖关系复杂,性能问题出现波动,边界条件增多等。

简化系统的复杂度并不代表着简化了系统的功能,它代表着消除掉额外的复杂度(由具体实现中出现,而不是系统提供服务本身具有的复杂度)

消除系统复杂度的最好方法是抽象,高级抽象思维可以让我们更好的处理整个系统,将复杂的实现细节隐藏在一个干净的外观下面。好的抽象还可以广泛的应用在不同的系统中,成为 wheel。

可演化性:由于业务一直处在变化之中,系统的需求永远是在变化。敏捷开发提供了一种新的框架,可以适用于一直变化的系统开发流程中。还有一些技巧可以使用:TDD重构

推荐阅读