DDIA - 第1章 可靠、可扩展与可维护的应用系统

文章目录

  • 第1章 可靠、可扩展与可维护的应用系统
    • 认识数据系统
    • 1 可靠性(Reliability)
      • 1.1 硬件故障
      • 1.2 软件错误
      • 1.3 人为错误
      • 1.4 可靠性的重要性
    • 2 可扩展性(Scalability)
      • 2.1 描述负载
      • 2.2 描述性能
      • 2.3 应对负载增加的方法
    • 3 可维护性(Maintainability)
      • 3.1 可运维性:运维更轻松
      • 3.2 简单性:简化复杂度
      • 3.3 可演化性:易于改变
    • 小结

信息是激发创新的力量

        本章目标: 可靠性、可扩展性与可维护性,如何认识这些问题以及如何达成目标。

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

认识数据系统

        许多应用系统都包含以下模块:数据库、高速缓存、索引、流式处理、批处理。但本书将它们归为一大类即“数据系统”,因为系统之间的界限正在变得模糊,例如Redis既可以用于数据存储也适用于消息队列,Apache Kafka作为消息队列也具备了持久化存储保证。

1 可靠性(Reliability)

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

1.1 硬件故障

        直到最近,采用硬件冗余方案对于大多数应用场景还是足够的,它使得单台机器完全失效的概率将为非常低的水平。只要可以将备份迅速恢复到新机器上,故障的停机时间在大多数应用中并不是灾难性的。而多机冗余则只对少量的关键应用更有意义,对于这些应用,高可用性是绝对必要的。
        通过软件容错的方式来容忍多机失效成为新的手段,或者至少成为硬件容错的有力补充。

1.2 软件错误

        我们通常认为硬件故障之间多是相互独立的:一台机器的磁盘出现故障并不意味着另一台机器的磁盘也要失效。除非存在某种弱相关(例如一些共性原因,如服务器机架中的温度过高),否则通常不太可能出现大量硬件组件同时失效的情况。
        另一类故障则是系统内的软件问题。这些故障实现更加难以预料,而且因为节点之间是由软件关联的,因而往往会导致更多的系统故障。

1.3 人为错误

        设计和构建软件系统总是由人类完成,也是由人来运维这些系统。即使有时意图是好的,但人却无法做到万无一失。例如,一项针对大型互联网服务的调查发现,运维者的配置错误居然是系统下线的首要原因,而硬件问题(服务器或网络)仅在10%~25%的故障中有所影响。

1.4 可靠性的重要性

        即使在所谓“非关键”应用中,我们也应秉持对用户负责的态度。

2 可扩展性(Scalability)

        即使系统现在工作可靠,并不意味着它将来一定能够可靠运转。发生退化的一个常见原因是负载增加,可扩展性是用来描述系统应对负载能力的术语。讨论可扩展性通常要考虑这类问题:“如果系统以某种方式增长,我们应对增长的措施有哪些”,“我们该如何添加计算资源来处理额外的负载”。

2.1 描述负载

        负载可以用称为负载参数的若干数字来描述。参数的最佳选择取决于系统的体系结构,它可能是Web服务器的每秒请求处理次数,数据库中写入的比例,聊天室的同时活动用户数量,缓存命中率等。有时平均值很重要,有时系统瓶颈来自于少数峰值。
        有关Twitter的例子,也是我那进了微信的室友近期在做的事情,但是实在想不到的是Twitter在2012年就已经有了解决方案,并且现在我室友他们的解决方案也如出一辙,不禁感叹国内的技术发展和国外相比还有着很大的距离,自己有点井底之蛙的感觉了。话说回来,对于巨大扇出(fan-out)的结构问题:每个用户会关注很多人,也会被很多人圈粉。解决方案无非是推和拉两种,但两种方法各有利弊,Twitter正在转向结合两种方法。
        大多数用户的tweet在发布时继续以一对多(推)写入时间线,但是少数具有超多关注者(例如那些名人)的用户除外,对这些用户采用类似方案1(拉),其推文被单独提取,在读取时才和用户的时间线主表合并。这种混合方法能够提供始终如一的良好表现。

2.2 描述性能

        在批处理系统如Hadoop中,我们通常关心吞吐量(throughput),即每秒可处理的记录条数,或者在某指定数据集上运行作业所需的总时间;而在线系统通常更看重服务的响应时间(response time),即客户端从发送请求到接收响应之间的间隔。
        延迟与响应时间 延迟(latency)和响应时间(response time)容易混淆使用,但它们并不完全一样。通常响应时间是客户端看到的:除了处理请求时间(服务时间,service time)外,还包括来回网络延迟和各种排队延迟。延迟则是请求花费在处理上的时间。
        最好不要将响应时间视为一个固定的数字,而是可度量的一种数值分布。如果想知道更典型的响应时间,平均值并不是合适的指标,因为它掩盖了一些信息,无法告诉有多少用户实际经历了多少延迟。
        最好使用百分位数(percentiles)。中位数指标非常适合描述多少用户需要等待多长时间,中位数也称为50百分位数,有时缩写为p50.请注意,中位数对应单个请求;这也意味者如果某用户发了多个请求(例如包含在一个完整会话过程中,或者因为某页面包含了多个资源),那么它们中至少一个比中位数慢的概率远远大于50%。为了弄清楚异常值有多糟糕,需要关注更大的百分位数如常见的第95、99和99.9(缩写为p95、p99和p999)值。采用较高的响应时间百分位数(tail latencies,尾部延迟或长尾效应)很重要,因为它们直接影响用户的总体服务体验。
        排队延迟往往在高百分数响应时间中影响很大。由于服务器并行处理的请求有限(例如,CPU内核数的显限制),正在处理的少数请求可能会阻挡后续请求,这种情况有时被称为队头阻塞。即使后续请求可能处理很简单,但它阻塞在等待先前请求的完成,客户端将会观察到极慢的响应时间。因此,很重要的一点是要在客户端来测量响应时间。

2.3 应对负载增加的方法

        讨论可扩展性:即当负载参数增加时,应该如何保持良好的性能?
        首先,针对特定级别负载设计的架构不太可能应付超出预设目标10倍的实际负载。如果目标服务处于快速增长阶段,那么需要认真考虑每增加一个数量级的负载,架构应如何设计。
        对于一次包含多次请求调用的完整服务,即使只有很小百分比的请求缓慢,如果某用户总是频繁产生这种调用,最终总体变慢的概率就会增加(即长尾效应)。
        现在谈论更多的如何在垂直扩展(即升级到更强大的机器)和水平扩展(即将负载分布到多个更小的机器)之间做取舍。在多台机器上分配负载也被称为无共享体系结构。在单台机器上运行的系统通常更简单,然而高端机器可能非常昂贵,且扩展水平有限,最终往往还是无法避免需要水平扩展。实际上,好的架构通常要做些实际取舍,例如,使用几个强悍的服务器仍可以比大量的小型虚拟机来的更简单、便宜。
        把无状态服务分布然后扩展至多台机器相对比较容易,而有状态服务从单个节点扩展到分布式多机环境的复杂性会大大增加。出于这个原因,直到最近通常的做法一直是,将数据库运行一个节点上(采用垂直扩展策略),直到高扩展性或高可用性的要求迫使不得不做水平扩展。
        超大规模的系统往往针对特定应用而高度定制,很难有一种通用的架构。背后取舍因素包括数据读取量、写入量、待存储的数据量、数据的复杂程度、响应时间要求、访问模式等,或者更多的是上述所有因素的叠加,再加上其他更复杂的问题。
        可扩展架构通常都是从通用模块逐步构建而来,背后往往有规律可循。

3 可维护性(Maintainability)

        从软件设计时开始考虑,尽可能减少维护期间的麻烦,甚至避免造出容易过期的系统。特别需要关注软件系统的三个设计原则:可运维性: 方便运行团队来保持系统平稳运行;简单性: 简化系统复杂性,使新工程师能够轻松理解系统。注意这与用户界面的简单性并不一样。可演化性: 后续工程师能够轻松地对系统进行改进,并根据需求变化将其适配到非典型场景,也称为可延伸性、易修改性或可塑性。

3.1 可运维性:运维更轻松

        有人认为,“良好的操作性经常可以化解软件的局限性,而不规范的操作则可以轻松击垮软件”。虽然某些操作可以且应该是自动化的,但最终还是需要人来执行配置并确保正常工作。
        良好的可操作性意味着使日常工作变得简单,是运营团队能够专注于高附加值的任务。

3.2 简单性:简化复杂度

        复杂性有各种各样的表现方式:状态空间的膨胀,模块紧耦合,令人纠结的相互依赖关系,不一致的命名和术语,为了性能而采取的特殊处理,为解决某特定问题而引入的特殊框架等。
        简化系统设计并不意味着减少系统功能,而主要意味着消除意外方面的复杂性,正如Moseley和Marks把复杂性定义为一种“意外”,即它并非软件固有、被用户所见或感知,而是实现本身所衍生出来的问题。
        消除意外复杂性最好手段之一是抽象。一个好的设计抽象可以隐藏大量的实现细节,并对外提供干净、易懂的接口。一个好的设计抽象可用于各种不同的应用程序。这样,复用远比多次重复实现更有效率;另一方面,也带来更高质量的软件,而质量过硬的抽象组件所带来的好处,可以使运行其上的所有应用轻松获益。
        例如,高级编程语言作为一种抽象,可以隐藏机器汇编代码、CPU寄存器和系统调等细节和复杂性。SQL作为一种抽象,隐藏了内部复杂的磁盘和内存数据结构,以及来自多客户端的并发请求,系统崩溃之后的不一致等问题。当然,使用高级编程语言最终并没有脱离机器汇编代码,只是并非直接使用,于汇编代码打交道的事情已经由编程语言抽象为高效接口代我们完成。

3.3 可演化性:易于改变

        一成不变的系统需求几乎没有,想法和目标经常在不断变化:适配新的环境,新的用例,业务优先级的变化,用户要求的新功能,新平台取代旧平台,法律或监管要求的变化,业务增长促使架构的演变等。
        在组织流程方面,敏捷开发模式为适应变化提供了很好的参考。
        我们的目标是可以轻松地修改数据系统,使其适应不断变化的需求,这和简单性与抽象性密切相关;简单易懂的系统往往比复杂的系统更容易修改。这是一个非常重要的理念,我们将采用另一个不同的词来指代数据系统级的敏捷性,即可演化性。

小结

        可靠性意味着即使发生故障,系统也可以正常工作。可扩展性是指负载增加时,有效保持系统性能的相关技术策略。可维护性则意味着许多方面,但究其本质是为了让工程和运营团队更为轻松。

你可能感兴趣的:(DDIA,数据密集型,分布式)