前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《数据密集型应用系统设计》读书笔记(一)

《数据密集型应用系统设计》读书笔记(一)

作者头像
口仆
发布2020-12-08 11:10:39
1.8K0
发布2020-12-08 11:10:39
举报
文章被收录于专栏:用户2133719的专栏

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

目前许多的新型应用都属于「数据密集型」(data-intensive),而不是计算密集型(compute-intensive),对于这些应用,CPU 的处理能力并不是第一限制性因素,关键在于数据量、数据的复杂度及数据的快速多变性。

近年来出现了许多用于数据存储和处理的新工具,它们可以针对各种不同的应用场景进行优化,不适合再归为数据库、队列、高速缓存等不同类型,因此我们将其统称为「数据系统」(data system)。此外,越来越多的应用系统需求广泛,单个组件无法满足所有数据处理与存储需求。因此需要将任务分解,每个组件负责高效完成其中一部分,多个组件依靠应用代码有机衔接起来。例如,许多应用系统包括以下模块:

  • 「数据库」:用来存储数据,以方便应用再次访问
  • 「高速缓存」:缓存那些复杂或操作代价昂贵的结果,以加快下一次访问
  • 「索引」:允许用户按关键字搜索数据并进行各种过滤
  • 「流式处理」:持续发送消息到另一个进程,并采用异步方式处理
  • 「批处理」:定期处理大量的累积数据

下图给出了一个包含多个不同组件的数据系统架构的示例。其组合使用了多个组件来提供服务,而对外提供服务的界面或 API 会隐藏很多内部实现细节。这样基本上我们基于一个个较小的、通用的组件,构建了一个全新的、专用的数据系统。

影响数据系统设计的因素有很多,本章将专注于对大多数系统都极为重要的三个问题:

  • 「可靠性」(Reliability)。当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转(虽然性能可能有所降低,但确保功能正确)
  • 「可扩展性」(Scalability)。随着规模的增长(如数据量、流量或复杂性),系统应以合理的方式来匹配这种增长
  • 「可维护性」(Maintainability)。随着时间的推移,许多新的人员参与到系统开发和运维,以维护现有功能或适配新场景等,系统都应高效运转。

1.1 可靠性

系统的可靠性意味着即使发生了某些错误,系统仍可以继续正常工作。此处定义可能出错的事情为「故障」(faults),而系统可应对的错误称为「容错」(fault-tolerant),注意系统不可能容忍所有的故障,因此容错总是指容忍「特定类型」的故障。

我们还需要注意区分「故障」「失效」(failure)这两个概念。故障通常定义为组件偏离其正常规格,而失效则意味着系统作为一个整体停止,无法向用户提供所需的服务。失效要比故障更加严重,我们希望通过设计容错机制来避免从故障引发系统失效。下面将介绍三种主要的故障类型:硬件故障、软件错误与人为失误。这些故障的共同点是其产生的影响可以被消除(因此我们倾向于容忍而不是预防,与安全问题不同)。

1.1.1 硬件故障

常见的硬件故障包括硬盘崩溃、内存故障、电网停电等。通常减少这种故障的方法是为硬件添加「冗余」,例如对磁盘配置 RAID、服务器配置双电源等。当一个组件发生故障,冗余组件可以快速接管,之后再更换失效的组件。

硬件冗余方案可以使得单台机器完全失效的概率降为非常低的水平,而近年来随着数据量和应用计算需求的增加,更多的应用可以运行在大规模机器之上,随之而来的硬件故障率呈线性增长。此时系统强调的是总体的灵活性与弹性而非单台机器的可靠性,因此通过「软件容错」的方式来容忍多机失效逐渐成为新的手段。

1.1.2 软件错误

硬件故障大多是相互独立的,而发生在系统内的软件错误往往是相互关联的,例如系统内核 bug、应用进程失控、级联故障等。软件错误通常会长时间处于引而不发的状态,直到碰到特定的触发条件。这意味着软件其实对使用环境存在某种假设,这种假设通常为真,由于某些原因可能会不再成立。

软件的系统问题通常没有快速的解决办法,只能仔细考虑更多细节,包括认真检查系统中的假设与交互、进行全面的测试、进程隔离、允许进程崩溃并自动重启、评估监测并分析生产环节的行为表现等。

1.1.3 人为失误

除了上述两种错误,人为的失误(包括软件开发、运维等环节)也常常会引发系统故障,下面列举一些可以提升系统可靠性,减少人为失误影响的方法:

  • 以最小出错的方式来设计系统,如精心设计的抽象层、API 和管理界面
  • 想办法分离最容易出错的地方、容易引发故障的接口
  • 充分的测试,从各单元测试到全系统集成测试以及手动测试
  • 出现人为失误时,提供快速的恢复机制以尽量减少故障影响
  • 设置详细而清晰的监控子系统,包括性能指标和错误率

1.2 可扩展性

可扩展性是用来描述系统应对负载增加能力的术语。我们首先会介绍定量描述负载与性能的方法,然后给出应对负载增加的可能方法。

1.2.1 描述负载

负载可以用称为「负载参数」(load parmeters)的若干数字来描述。参数的最佳选择取决于系统的体系结构,常见的选择有:

  • Web 服务器的每秒请求处理次数
  • 数据库的写入率
  • 聊天室的同时活跃用户数量
  • 缓存的命中率

有时平均值很重要,而有时少数的峰值更加重要。原文这里给出了一个 Twitter 的例子来说明负载,Twitter 的两个典型业务操作是:

  • 「发布推特」:一个用户可以发布一条新消息到其所有的关注者(平均 4.6k 请求/秒,峰值 12k 请求/秒)
  • 「主页时间线」:一个用户可以查看其关注对象发布的推特(平均 300k 请求/秒)

上述操作的难点在于巨大的「扇出」(fan-out)结构,即每个用户会关注很多人,也会被很多人圈粉。Twitter 给出了如下图所示的两种处理方案:

方法 1 是将发送的新推特插入到全局的推特集合中,当用户查看时间线时,首先查找其所有的关注对象,列出这些人的所有推特,最后以时间为序进行合并。在关系型数据库中,可以通过如下查询语句实现:

代码语言:javascript
复制
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

方法 2 则是对每个用户的时间线维护一个缓存,当用户发布新推特时,查询其关注者,将该推特插入到每个关注者的时间线缓存中。由于已经预先计算了时间线,所以访问时间线的性能会非常快。

Twitter 在最初的版本中使用了方法 1,但随着主页时间线的读负载压力的与日俱增,开始切换为方法 2,因为时间线浏览的压力要比发布推特高的多,所以在发布时多完成一些事情可以加速读性能。然而,方法 2 也存在着一定缺陷,如图中所示,假定每名用户平均 75 个关注者,则需要每秒约 345k 次写入缓存,而由于关注者数量的偏差,对于某些超级用户来说,其发布一条推特可能会导致数千万次的写入发生,这对写性能的要求极大。目前 Twitter 正在考虑将两种方法结合起来,大部分用户发布推特时采用方法 2,以一对多写入时间线,而部分超级用户则才用类似方法 1 的方法,其推特被单独提取,当读取时才和用户的时间线合并。

在本例中,每个用户关注者的分布情况时可扩展性的关键负载参数,其决定了扇出数,在不同的应用中存在着类似的关键负载参数,需要根据具体情况进行判断。

1.2.2 描述性能

描述负载之后,我们需要进一步了解负载增加系统将会发生什么变化,此时我们就需要对系统性能进行定量描述。与负载的描述类似,在不同的系统中关心的性能指标有所不同:

  • 在批处理系统如 Hadoop 中,通常关心「吞吐量」(throughput),即每秒可处理的记录条数,或在固定大小数据集上运行作业所需的总时间
  • 在线上系统中,更看重服务的「响应时间」(response time),即客户端从发送请求到接收响应之间的间隔。

对于响应时间来说,其包括处理请求时间、以及各种延迟(网络延迟、排队延迟)。即使反复发送、处理相同的请求,每次都可能会产生略微不同的响应时间。因此我们最好将响应时间视为一种可度量的数据分布。

如下图所示,每个灰色条代表一个服务请求,高度表示该请求的响应时间。我们可以考察服务请求的「平均响应时间」(一般为算术平均值),但其有时会掩盖一些信息;因此更好的选择是使用「百分位数」,包括中位数、95百分位数、99百分位数等。百分位数常用于描述、定义服务质量指标和服务质量协议,较高的响应时间百分位数会直接影响用户的总体服务体验(请求最慢的客户往往购买了更多的商品)。

1.2.3 应对负载增加的方法

在描述完负载与性能的相关参数后,我们关心的问题是:当负载参数增加时,应如何保持良好性能?这里原书给出了一系列的 tips,现归纳如下:

  • 针对特定级别负载而设计的架构不太可能应付超出预设目标 10 倍的实际负载
  • 扩展一般分为「垂直扩展」(升级到更强大的机器)与「水平扩展」(将负载分布到多个更小的机器)
    • 水平扩展不可避免,不过单机性能同样不能忽视
  • 如果负载高度不可预测,自动「弹性」系统会更加高效,其可以自动检测负载增减,然后自动添加更多计算资源
  • 将无状态服务扩展的至多机器相对容易,而将有状态服务从单个节点扩展到分布式多机环境的复杂性会大大增加
    • 随着分布式系统的发展,未来分布式数据系统将成为标配
  • 超大规模的系统往往针对特定应用而高度定制,很难有一种通用的架构

1.3 可维护性

软件的大部分成本在于开发完成后的持续维护,包括 bug 修复、保证系统正常运行、故障排查、新平台适配、增加新功能等。本节将关注软件系统中保持高可维护性的三个设计原则:

  • 「可操作性」(Operability):方便运维团队来保持系统平稳运行
  • 「简单性」(Simplicity):简化系统复杂性,使新工程师能够轻松理解系统(注意与用户界面的简单性区分)
  • 「可演化性」(Evolvability):使贡献是能够轻松地对系统进行更改,根据需求变化将其适配到非典型场景(也称为可延伸性、易修改性或可塑性)

1.3.1 可操作性

良好的操作性可以使得日常的工作变得简单,使运维团队能够专注于高附加值的任务。数据系统设计可以在这方面做很多事情,例如:

  • 提供对系统运行时行为和内部的可观测性,方便监控
  • 提供良好的自动化支持与标准工具集成
  • 避免依赖特定的机器(允许机器停机维护而系统不间断运行)
  • 提供良好的文档和易于理解的操作模式
  • 提供良好的默认配置,允许管理员方便地修改默认值

1.3.2 简单性

系统的复杂性有各种各样的表现方式:状态空间的膨胀、模块的紧耦合、不一致的命名和术语、特殊处理等。复杂性会使得维护变得越来越困难,降低复杂性可以大大提高软件的可维护性。

简化系统设计并不意味着减少系统功能,而主要意味着消除「意外」(accidental)方面的复杂性,其并非软件固有,而是实现本身所衍生出来的问题。

消除意外复杂性的最好方法之一就是「抽象」(abstraction)。一个好的设计抽象可以隐藏大量的实现细节,并对外提供干净、易懂的接口;一个好的抽象也可以用于不同的应用程序,提升开发效率与软件质量。

1.3.3 可演化性

一成不变的系统需求几乎不存在,我们使用可演化性来指代「数据系统层面的敏捷性」。可演化性的目标在于可以轻松地修改数据系统,使其适应不断变化的需求。这一目标与简单性密切相关:简单易懂的系统通常比复杂的系统更容易修改。

在组织流程方面,「敏捷」(Agile)开发模式为适应变化提供了很好的参考。不过当前大部分的敏捷开发技术还只是针对小规模、本地模式环境。在更大的数据系统层面上提高敏捷性的方法还有待探索。

思维导图

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 口仆 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第 1 章 可靠、可扩展与可维护的应用系统
    • 1.1 可靠性
      • 1.1.1 硬件故障
      • 1.1.2 软件错误
      • 1.1.3 人为失误
    • 1.2 可扩展性
      • 1.2.1 描述负载
      • 1.2.2 描述性能
      • 1.2.3 应对负载增加的方法
    • 1.3 可维护性
      • 1.3.1 可操作性
      • 1.3.2 简单性
      • 1.3.3 可演化性
    • 思维导图
    相关产品与服务
    关系型数据库
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档