【DDIA笔记】第一章 可靠性、可扩展性和可维护性

可靠性、可扩展性、可维护性

本书为什么以数据系统为主题

数据系统是一种模糊的统称,在信息社会中,一切皆可信息化,或者,某种程度上来说–数字化。这些数据的采集、存储和使用,是构成信息社会的基础。

因此作为IT从业人员,有必要系统性地了解一下现代的、分布式地数据系统。学习本书,能够学习到数据系统地背后的原理、了解其常见的实践,进而将其应用到我们工作的系统设计中。

常见的数据系统有哪些?

  • 数据库:存储数据,以便之后再次使用

  • 缓存:记录一些非常重地操作结果,方便之后加快读取速度

  • 搜索引擎:允许用户以各种关键字搜索、以各种条件过滤数据

  • 流式处理:源源不断地产生数据,并发送给其他进程进行处理

  • 批处理:定时处理累积地大量数据

  • 消息队列:进行消息地传送与分发

数据系统的日益复杂化

这些年来,出现了许多新的数据存储工具与数据处理工具,它们针对不同的应用场景进行优化,因此不再适合生硬地归入传统类别。

  1. Kafka:可以持久化存储一段时间日志数据,可以作为消息队列对数据进行分发,可以作为流式处理组件对数据反复蒸馏

  2. Spark:可以对数据进行批处理,也可以化小批为流,对数据进行流式处理

  3. Redis:可以作为缓存加速对数据库的访问,也可以作为事件中心对消息的发布订阅

我们面临一个新的场景,以某种组合使用这些组件的时候,在某种程度上,便是创建了一个新的数据系统。

下图是一个常见的对用户数据进行采集、存储、查询、旁路等操作的数据系统实例,从其示意图中可以看到各种web services的影子。

在这里插入图片描述

这样的系统在设计的时候可以有很多取舍:

  1. 采用哪种缓存策略,是旁路还是写穿透

  2. 部分组件机器出现问题时,是保证可用性还是保证一致性

  3. 当机器一时难以恢复,如何保证数据的正确性和完整性

  4. 当负载增加时,是增加机器还是提升单机性能

  5. 设计对外API时,是力求简洁还是追求强大。

  6. 当部分系统降级时,如何为用户提供始终如一的良好性能

因此,有必要从根本上思考下如何评价一个好数据系统,如何构建一个好的数据系统,有哪些可以遵循的设计模式,有哪些通常需要考虑的方面。

用三个词来回答就是:可靠性、可扩展性、可维护性

可靠性

如何衡量可靠性呢?

  1. 功能上:

    • 正常情况下,应用行为满足API给出的行为

    • 在用户误输入/误操作时,能够正常处理

  2. 性能上:能够在给定硬件和数据量下满足承诺的性能指标

  3. 安全上:能够阻止未授权、恶意破坏

如果所有这些在一起意味着“正确工作”,那么可以把可靠性粗略理解为“即使出现问题,也能继续正确工作”。

造成这种错误的原因叫故障,能够预料并应对故障的系统特性可称为“容错”或“韧性”。

注意:故障(fault) 不同于 失效(failure)

故障定义为系统的一部分状态偏离标准

失效是系统作为一个整体停止向用户提供服务。

故障的概率不可能降低到0,因此需要设计容错机制来防止因故障而导致失效。

在这类容错系统里,通过故意触发来提高故障率是有意义的,因为这种方法可以确保容错机制不断运行并接受考验,比如Netflix公司的(Home - Chaos Monkey)

硬件故障

在一个大型数据中心中,以下现象是常态:

  1. 网络抖动、不通

  2. 硬盘老化坏道

  3. 内存故障

  4. 机器过热导致CPU出问题

  5. 机房断电

这些都是硬件故障,磁盘的平均无故障时间约为10-15年,因此从数学期望上来讲,在拥有1w个磁盘的存储集群上,平均每天都会有一个磁盘出故障。

那么如何解决系统的故障率呢? 第一反应通常是增加单个硬件的冗余度,例如磁盘可以组件RAID,服务器可能有双路电源和热插拔CPU,数据中心可能有电池和柴油发电机作为后备电源,某个组件挂掉时冗余组件可以立刻接管。

软件错误

我们通常认为硬件故障是随机的、相互独立的,大量硬件组件之间同时发生故障是极其罕见的。另一类错误是内部的系统性错误,这类错误难以预料,而且是跨节点相关的可能会比硬件故障造成更多的系统失效。

以下列举了几个例子:

  1. 不能处理特定输入,导致系统崩溃。

  2. 失控进程(如循环未释放资源)耗尽CPU、内存、磁盘空间和网络资源

  3. 系统依赖的服务变慢,无响应或是开始返回错误的响应

  4. 级联故障

在设计软件时,我们通常有一些环境假设,和一些隐性约束。随着时间的推移、系统的持续运行,如果这些假设不能够继续被满足;如果这些约束被后面维护者增加功能时所破坏;都有可能让一开始正常运行的系统,突然崩溃。

人为错误

系统中最不稳定的是人,因此要在设计层面尽可能消除人对系统影响。根据软件的生命周期,分几个阶段来考虑:

  1. 设计编码

    • 尽量消除所有不必要的假设,提供合理的抽象,仔细设计API

    • 进程间进行隔离,对尤其容易出错的模块使用沙箱机制

    • 对服务依赖进行熔断设计

  2. 测试阶段

    • 尽可能引入第三方成员测试,将测试平台自动化

    • 单元测试、集成测试、e2e测试、混沌测试

  3. 运行阶段

    • 详细的仪表盘、看板

    • 持续自检

    • 报警机制

    • 问题预案

  4. 针对组织

    • 科学的培训和管理

可靠性有多重要

事关用户数据安全、企业声誉,企业存活和做大的基石

可扩展性

可扩展性表示描述系统应对负载增长能力。它很重要,但是在实践中又很难做好,因为存在一个基本矛盾 :只有能存活下来的产品才有资格谈扩展,而过早为扩展设计往往活不下去

但是可以了解一些基本的概念,来应对可能会暴增的负载。

衡量负载

应对负载前,先找到合适的方法来衡量负载,如负载参数

  • 应用的日活、月活

  • 每秒向web服务器发出的请求

  • 数据库的读写比率

  • 聊天室里同时活跃的用户数

  • 缓存命中率

在上述情况里,也许平均情况更重要,也是少数的极端场景才是你的瓶颈

下面以推特在2012年11月发布的数据为例,推特的两个主要业务是:

  • 发布推文用户可以向其粉丝发布新消息,平均4.6k请求/秒,峰值超过12k请求/秒

  • 主页时间线用户可以查询他们关注的人发布的推文,即推荐feed流,300K请求/秒

处理每秒12000次写入(发推文的速率峰值)还是很简单的,但是推特的可扩展性挑战并不是来自推特量,而是来自扇出(fan-out)-- 每个用户关注了许多人,也被很多人关注

大体来说,这一对操作有两种实现方式:

  1. 发布推文时,只需要将新推文插入全局推文集合即可。当一个用户请求自己的推荐feed流主页时间线时,首先查找所有他关注的人,查询这些被关注用户发布的推荐并按时间顺序合并。如下图的关系型数据库里,可以写出来这样的查询:

    在这里插入图片描述

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. 为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱。当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。因为结果已经提前计算好了,所以读取主页时间线的请求开销很小。

在这里插入图片描述

简单来说就是推和拉两种方式:

  1. 拉:每个人查看其首页推荐feed流时,从数据库现拉取所有关注用户的推文,合并后呈现

  2. 推:为每个用户保存一个feed流视图,当用户发推文时,将其插入所有关注者的feed流视图中。

前者是Lazy的,用户只有查看时才会去拉取,不会有无效计算和请求,但每次都需要现算,呈现速度较慢,而且流量一大也扛不住。

后者事先计算出视图,而不管用户看不看,呈现速度较快,但是引入很低无效请求。

后者的效果更好,因为发推频率比查询主页时间线频率几乎低了两个数量级,所以在这种情况下,最好是在写入时做更多的工作,而在读取时做更少的工作。

后者的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345K的写入。但是这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能导致主页时间线缓存的3000万次写入。

在这个例子里,每个用户粉丝数的分布是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载。

最后推特使用了两种方法的混合,大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中,但是少数拥有海量粉丝的用户(名流)会被排除在外。当用户读取主页时间线时,分别获取该用户关注的每位名流的推文,再与用户的主页时间线缓存合并。

描述性能

系统负载是从用户的视角来审视系统,是一种客观指标;系统性能是描述系统的一种实际能力。比如:

  1. 吞吐量:每秒可以处理的单位数据量,通常记为QPS

  2. 响应时间:从用户侧观察到的发出请求到收到回复的时间

  3. 延迟:日常中,延迟经常和响应时间混用指代响应时间,但严格来说,延迟只是把请求过程中排队等休眠时间,虽然其在响应时间中一般占大头;但是只有我们把请求真正处理耗时认为是瞬时,延迟才能等同于响应时间。

响应时间通常用百分位点来衡量,比如p95,p99,p999,它们意味着95%,99%,99.9%的请求都能在该阈值内完成。在实际中,通常使用滑动窗口滚动计算最近一段时间的响应时间分布,并通常以折线图或柱状图进行呈现。

响应时间的高百分位点(也称为 尾部延迟,即 tail latencies)非常重要,因为它们直接影响用户的服务体验。

应对负载的方法

在讨论了用于描述负载的参数和用于衡量性能的指标后,可以讨论可扩展性了:当负载参数增加时,如何保持良好的性能?

一般有下面两种方案:

  • 纵向扩展,也称为垂直扩展:转向更强大的机器

  • 横向扩展,也成为水平扩展:将负载分布到多台小机器

负载的两种扩展方式:

  • 自动:负载不好预测而且变化较多,使用该方式较好。缺点是不容易跟踪负载,容易抖动,造成资源浪费。

  • 手动:如果负载容易预测且不经常变化,最好手动。设计简单而且不容易出错。

针对不同应用场景来说的话:

  • 如果规模很小,尽量先用性能好的机器,可以省去很多麻烦。

  • 可以上云,利用云的可扩展性

  • 自行设计可扩展的分布式架构

两种服务类型:

  • 无状态服务:比较简单,多台机器,外层加一个gateway就行

  • 有状态服务:根据需求场景,如读写负载、存储量级、数据复杂度、响应时间、访问模式来进行取舍,设计合乎需求的架构

没有万金油架构!

可维护性

软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债和添加新的功能。

但是我们可以,也应该以这样一种方式来设计软件:在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则:

  • 可操作性:便于运维团队保持系统平稳运行

  • 简单性:从系统中消除尽可能多的复杂度,使新工程师也能轻松理解系统

  • 可演化性:工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配

可操作性:人生苦短,关爱运维

一个优秀运维团队的典型职责如下:

  • 监控系统的运行状况,并在服务状态不佳时快速恢复服务。
  • 跟踪问题的原因,例如系统故障或性能下降。
  • 及时更新软件和平台,比如安全补丁。
  • 了解系统间的相互作用,以便在异常变更造成损失前进行规避。
  • 预测未来的问题,并在问题出现之前加以解决(例如,容量规划)。
  • 建立部署、配置、管理方面的良好实践,编写相应工具。
  • 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台。
  • 当配置变更时,维持系统的安全性。
  • 定义工作流程,使运维操作可预测,并保持生产环境稳定。
  • 铁打的营盘流水的兵,维持组织对系统的了解。

数据系统可以通过各种方式使日常任务更轻松:

  • 通过良好的监控,提供对系统内部状态和运行时行为的 可见性(visibility)
  • 为自动化提供良好支持,将系统与标准化工具相集成。
  • 避免依赖单台机器(在整个系统继续不间断运行的情况下允许机器停机维护)。
  • 提供良好的文档和易于理解的操作模型(“如果做 X,会发生 Y”)。
  • 提供良好的默认行为,但需要时也允许管理员自由覆盖默认值。
  • 有条件时进行自我修复,但需要时也允许管理员手动控制系统状态。
  • 行为可预测,最大限度减少意外。

简单性:管理复杂度

复杂度的表现:

  • 状态空间的膨胀

  • 组件间的强耦合

  • 纠结的依赖关系

  • 不一致的术语和命名

  • 解决性能问题的hack

  • 需要绕开的特例

过多地引入额外复杂度的原因一般是问题理解的不够本质,写出了流水账式的代码。如果你为一个问题找到了合适的抽象,那么问题就解决了一半,如:高级编程语言是一种抽象,隐藏了机器码、CPU 寄存器和系统调用。 SQL 也是一种抽象,隐藏了复杂的磁盘 / 内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有 直接(directly) 使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。

如何找到合适的抽象?

  1. 从计算机领域常见的抽象中找。
  2. 从日常生活中常接触的概念找。

总之,一个合适的抽象,要么是符合直觉的;要么是和你的读者共享上下文的。

可演化性:降低改变门槛

系统的需求永远不变,基本是不可能的。更可能的情况是,它们处于常态的变化中,例如:

  • 你了解了新的事实

  • 出现意想不到的应用场景

  • 业务优先级发生变化

  • 用户要求新功能

  • 新平台取代旧平台

  • 法律或监管要求发生变化

  • 系统增长迫使架构变化

如何应对?

  • 组织流程方面:敏捷开发

  • 系统设计:简单性与抽象性,合理抽象和封装,对修改关闭,对扩展开放

参考资料
DDIA逐章精读
翻译

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值