DDIA 读书计划(一)可靠性,可伸缩性,可维护性

举报
这个昵称不换了 发表于 2022/05/19 10:14:05 2022/05/19
【摘要】 数据密集型应用读书计划
数据系统综述
数据系统(data system)是一种模糊的统称。在信息社会中,一切皆可信息化,或者,某种程度上来说——数字化。这些数据的采集、存储和使用,是构成信息社会的基础。我们常见的绝大部分应用背后都有一套数据系统支撑,比如微信、京东、微博等等。
常见的数据系统有哪些
· 存储数据,以便之后再次使用——数据库
· 记住一些非常“重”的操作结果,方便之后加快读取速度——缓存
· 允许用户以各种关键字搜索、以各种条件过滤数据——搜索引擎
· 源源不断的产生数据、并发送给其他进程进行处理——流式处理
· 定期处理累积的大量数据——批处理
· 进行消息的传送与分发——消息队列
数据系统的日益复杂化
但这些年来,随着应用需求的进一步复杂化,出现了很多新型的数据采集、存储和处理系统,它们不拘泥于单一的功能,也难以生硬的归到某个类别。随便举几个例子:
1. Kafka:可以作为存储持久化一段时间日志数据、可以作为消息队列对数据进行分发、可以作为流式处理组件对数据反复蒸馏等等。
2. Spark:可以对数据进行批处理、也可以化小批为流,对数据进行流式处理。
3. Redis:可以作为缓存加速对数据库的访问、也可以作为事件中心对消息的发布订阅。
我们面临一个新的场景,以某种组合使用这些组件时,在某种程度上,便是创立了一个新的数据系统。书中给了一个常见的对用户数据进行采集、存储、查询、旁路等操作的数据系统示例。从其示意图中可以看到各种 Web Services 的影子。

但就这么一个小系统,在设计时,也有很多取舍:
1. 使用何种缓存策略?是旁路(cache side)还是写穿透?
2. 部分机器组件出现问题时,是保证可用性还是保证一致性?
3. 当机器一时难以恢复,如何保证数据的正确性和完整性?
4. 当负载增加时,是增加机器还是提升单机性能?
5. 设计对外的API时,是力求简洁还是追求强大?
因此,有必要从根本上思考下如何评价一个好数据系统,如何构建一个好的数据系统,有哪些可以遵循的设计模式?有哪些通常需要考虑的方面?
书中用了三个词来回答:可靠性(Reliability)、可伸缩性(Scalability)、可维护性(Maintainability)
可靠性(Reliablity)
如何衡量可靠性?
功能上:
1.正常情况下,应用行为满足API给出的行为
2.在用户误输入/误操作时,能够正常处理
性能上:
在给定硬件和数据量下,能够满足承诺的性能指标
安全上:
能够阻止未授权、恶意破坏。
可用性也是可靠性的一个侧面,云服务通常以多少个 9 来衡量可用性。
两个易混淆的概念:Fault(系统出现问题) and Failure(系统不能提供服务)
不能进行 Fault-tolerance(容错) 的系统,积累的 fault 多了,就很容易 Failure。
如何预防?混沌测试:如 Netflix 的 chaosmonkey
硬件故障
在一个大型数据中心中,这是常态:
1. 网络抖动、不通
2. 硬盘老化坏道
3. 内存故障
4. 机器过热导致 CPU 出问题
5. 机房断电
数据系统中常见的需要考虑的硬件指标:
· MTTF mean time to failure
单块盘 平均故障时间 5 ~10 年,如果你有 1w+ 硬盘,则均匀期望下,每天都有坏盘出现。当然事实是硬盘会一波一波坏。
解决办法,增加冗余度:
机房多路供电,双网络等等。
对于数据:
单机:可以做RAID 冗余。如:EC 编码(一种编码方式,提供校验码支持数据准确性校验)。
多机:多副本 or EC 编码。
软件错误
相比硬件故障的随机性,软件错误的相关性(关联性)更高:
· 接受特定的错误输入,便导致所有应用服务器实例崩溃的BUG。例如2012年6月30日的闰秒,由于Linux内核中的一个错误,许多应用同时挂掉了。
· 失控进程会占用一些共享资源,包括CPU时间、内存、磁盘空间或网络带宽。
· 系统依赖的服务变慢,没有响应,或者开始返回错误的响应。
· 级联故障,一个组件中的小故障触发另一个组件中的故障,进而触发更多的故障
在设计软件时,我们通常有一些环境假设,和一些隐性约束。随着时间的推移、系统的持续运行,如果这些假设不能够继续被满足;如果这些约束被后面维护者增加功能时所破坏;都有可能让一开始正常运行的系统,突然崩溃。
虽然软件中的系统性故障没有速效药,但我们还是有很多小办法,例如:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现差异(discrepancy)时报警。
人为错误
系统中最不稳定的是人,因此要在设计层面尽可能消除人对系统影响。依据软件的生命周期,分几个阶段来考虑:
  • 设计编码阶段
  • 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。(尽可能消除所有不必要的假设,提供合理的抽象,仔细设计API)
  • 将人们最容易犯错的地方与可能导致失效的地方解耦(decouple)。特别是提供一个功能齐全的非生产环境沙箱(sandbox),使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验(进程间进行隔离,对尤其容易出错的模块使用沙箱机制,对服务依赖进行熔断设计)
  • · 测试阶段
  • 在各个层次进行彻底的测试,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的边缘场景(corner case)[尽可能引入第三方成员测试,尽量将测试平台自动化;单元测试、集成测试、e2e 测试、混沌测试]
  • 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。 例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)
  • · 运行阶段
  • 配置详细和明确的监控,比如性能指标和错误率。 在其他工程学科中这指的是遥测(telemetry)。 (一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的。)监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。(详细的仪表盘&持续自检&报警机制&问题预案)
  • · 针对组织
  • 科学的培训和管理
可靠性有多重要?
事关用户数据安全,事关企业声誉,企业存活和做大的基石。
可伸缩性(Scalability)
可伸缩性,即系统应对负载增长的能力。它很重要,但在实践中又很难做好,因为存在一个基本矛盾:只有能存活下来的产品才有资格谈伸缩,而过早为伸缩设计往往活不下去
但仍是可以了解一些基本的概念,来应对可能会暴增的负载。
衡量负载
应对负载之前,要先找到合适的方法来衡量负载,如负载参数(load parameters)
· 应用日活月活
· 每秒向Web服务器发出的请求(QPS)
· 数据库中的读写比率
· 聊天室中同时活跃的用户数量
书中以 Twitter 2012年11 披露的信息为例进行了说明:
1. 识别主营业务:发布推文、首页 Feed 流。
2. 确定其请求量级:发布推文(平均 4.6k请求/秒,峰值超过 12k请求/秒),查看其他人推文(300k请求/秒)

单就这个数据量级来说,无论怎么设计都问题不大。但 Twitter 需要根据用户之间的关注与被关注关系来对数据进行多次处理。常见的有推拉两种方式(面试可能会问如何设计):
1. 。每个人查看其首页 Feed 流时,从数据库现拉取所有关注用户推文,合并后呈现。
2. 。为每个用户保存一个 Feed 流视图,当用户发推文时,将其插入所有关注者 Feed 流视图中。
处理每秒12,000次写入(发推文的速率峰值)还是很简单的。然而推特的扩展性挑战并不是主要来自推特量,而是来自扇出(fan-out, 在事务处理系统中,我们使用它来描述为了服务一个传入请求而需要执行其他服务的请求数量。——每个用户关注了很多人,也被很多人关注。
使用拉的方式,是基于时间线实现的。发布推文时,只需将新推文插入全局推文集合即可。当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。在如下图所示的关系型数据库中,可以编写这样的查询:

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的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的3000万次写入!及时完成这种操作是一个巨大的挑战 —— 推特尝试在5秒内向粉丝发送推文。
在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。
推特轶事的最终转折:现在已经稳健地实现了方法2,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能。
描述性能
注意和系统负载区分,系统负载是从用户视角来审视系统,是一种客观指标。而系统性能则是描述的系统的一种实际能力。比如:
1. 吞吐量(throughput):每秒可以处理的单位数据量,通常记为 QPS。
2. 响应时间(response time):从用户侧观察到的发出请求到收到回复的时间。(在线系统更加关注的点)
3. 延迟(latency):日常中,延迟经常和响应时间混用指代响应时间;但严格来说,延迟只是只请求过程中排队等休眠时间,虽然其在响应时间中一般占大头;但只有我们把请求真正处理耗时认为是瞬时,延迟才能等同于响应时间。
响应时间通常以百分位点来衡量,比如 p95,p99和 p999,它们意味着95%,99%或 99.9% 的请求都能在该阈值内完成。在实际中,通常使用滑动窗口滚动计算最近一段时间的响应时间分布,并通常以折线图或者柱状图进行呈现。[百分位点通常用于服务级别目标(SLO, service level objectives)服务级别协议(SLA, service level agreements),即定义服务预期性能和可用性的合同。 SLA可能会声明,如果服务响应时间的中位数小于200毫秒,且99.9百分位点低于1秒,则认为服务工作正常(如果响应时间更长,就认为服务不达标)。这些指标为客户设定了期望值,并允许客户在SLA未达标的情况下要求退款。]
应对负载
在有了描述和定义负载、性能的手段之后,终于来到正题,如何应对负载的不断增长,即使系统具有可伸缩性。
1. 纵向伸缩(scaling up)or 垂直伸缩(vertical scaling):换具有更强大性能的机器。e.g. 大型机机器学习训练。
2. 横向伸缩(scaling out)or 水平伸缩(horizontal scaling):“并联”很多廉价机,分摊负载。 e.g. 马斯克造火箭。
负载伸缩的两种方式:
· 自动如果负载不好预测且多变,则自动较好。坏处在于不易跟踪负载,容易抖动,造成资源浪费。
· 手动如果负载容易预测且不长变化,最好手动。设计简单,且不容易出错。
针对不同应用场景:
首先,如果规模很小,尽量还是用性能好一点的机器,可以省去很多麻烦。
其次,可以上云,利用云的可伸缩性。甚至如 Snowflake 等基础服务提供商也是 All In 云原生。
最后,实在不行再考虑自行设计可伸缩的分布式架构。
两种服务类型:
· 无状态服务(stateless service)比较简单,多台机器,外层罩一个 gateway 就行。
· 有状态服务根据需求场景,如读写负载、存储量级、数据复杂度、响应时间、访问模式,来进行取舍,设计合乎需求的架构。
不可能啥都要,没有万金油架构!但同时:万变不离其宗,应用的问题可能是读取量、写入量、要存储的数据量、数据的复杂度、响应时间要求、访问模式或者所有问题的大杂烩。组成不同架构的原子设计模式是有限的,这也是本书稍后要论述的重点。
可维护性(Maintainability)
从软件的整个生命周期来看,维护阶段绝对占大头。
但大部分人都喜欢挖坑,不喜欢填坑。因此有必要,在刚开就把坑开的足够好。有三个原则:
· 可运维性(Operability)便于运维团队无痛接手。
· 简洁性(Simplicity)便于新手开发平滑上手:这需要一个合理的抽象,并尽量消除各种复杂度。如,层次化抽象。
· 可演化性(Evolvability) 便于后面需求快速适配:避免耦合过紧,将代码绑定到某种实现上。也称为可扩展性(extensibility)可修改性(modifiability)可塑性(plasticity)
可运维性(Operability):人生苦短,关爱运维
有效的运维绝对是个高技术活:
1. 紧盯系统状态,出问题时快速恢复。
2. 恢复后,复盘问题,定位原因。
3. 定期对平台、库、组件进行更新升级。
4. 了解组件间相互关系,避免级联故障。
5. 建立自动化配置管理、服务管理、更新升级机制。
6. 执行复杂维护任务,如将存储系统从一个数据中心搬到另外一个数据中心。
7. 配置变更时,保证系统安全性。
系统具有良好的可维护性,意味着将可定义的维护过程编写文档和工具以自动化,从而解放出人力关注更高价值事情:
1. 友好的文档和一致的运维规范。
2. 细致的监控仪表盘、自检和报警。
3. 通用的缺省配置。
4. 出问题时的自愈机制,无法自愈时允许管理员手动介入。
5. 将维护过程尽可能的自动化。
6. 避免单点依赖,无论是机器还是人。
简洁性(Simplicity):复杂度管理
复杂度表现:
1. 状态空间的膨胀。
2. 组件间的强耦合。
3. 不一致的术语和命名
4. 为了提升性能的 hack。
5. 随处可见的补丁( workaround)。
需求很简单,但不妨碍你实现的很复杂 😉:过多的引入了额外复杂度(accidental complexity )——非问题本身决定的,而由实现所引入的复杂度。
通常是问题理解的不够本质,写出了“流水账”(没有任何抽象,abstraction)式的代码。
如果你为一个问题找到了合适的抽象,那么问题就解决了一半,如:
1. 高级语言隐藏了机器码、CPU 和系统调用细节。
2. SQL 隐藏了存储体系、索引结构、查询优化实现细节。
如何找到合适的抽象?
1. 从计算机领域常见的抽象中找。
2. 从日常生活中常接触的概念找。
总之,一个合适的抽象,要么是符合直觉的;要么是和你的读者共享上下文的。
本书之后也会给出很多分布式系统中常用的抽象。
可演化性:降低改变门槛
系统需求没有变化,说明这个行业死了。
否则,需求一定是不断在变,引起变化的原因多种多样:
1. 对问题阈了解更全面
2. 出现了之前未考虑到的用例
3. 商业策略的改变
4. 客户爸爸要求新功能
5. 依赖平台的更迭
6. 合规性要求
7. 体量的改变
应对之道:
· 项目管理上敏捷开发
· 系统设计上依赖前两点。合理抽象,合理封装,对修改关闭,对扩展开放。
数据系统综述
数据系统(data system)是一种模糊的统称。在信息社会中,一切皆可信息化,或者,某种程度上来说——数字化。这些数据的采集、存储和使用,是构成信息社会的基础。我们常见的绝大部分应用背后都有一套数据系统支撑,比如微信、京东、微博等等。
常见的数据系统有哪些
  • 存储数据,以便之后再次使用——数据库
  • 记住一些非常“重”的操作结果,方便之后加快读取速度——缓存
  • 允许用户以各种关键字搜索、以各种条件过滤数据——搜索引擎
  • 源源不断的产生数据、并发送给其他进程进行处理——流式处理
  • 定期处理累积的大量数据——批处理
  • 进行消息的传送与分发——消息队列
数据系统的日益复杂化
但这些年来,随着应用需求的进一步复杂化,出现了很多新型的数据采集、存储和处理系统,它们不拘泥于单一的功能,也难以生硬的归到某个类别。随便举几个例子:
  1. Kafka:可以作为存储持久化一段时间日志数据、可以作为消息队列对数据进行分发、可以作为流式处理组件对数据反复蒸馏等等。
  2. Spark:可以对数据进行批处理、也可以化小批为流,对数据进行流式处理。
  3. Redis:可以作为缓存加速对数据库的访问、也可以作为事件中心对消息的发布订阅。
我们面临一个新的场景,以某种组合使用这些组件时,在某种程度上,便是创立了一个新的数据系统。书中给了一个常见的对用户数据进行采集、存储、查询、旁路等操作的数据系统示例。从其示意图中可以看到各种 Web Services 的影子。
但就这么一个小系统,在设计时,也有很多取舍:
  1. 使用何种缓存策略?是旁路(cache side)还是写穿透?
  2. 部分机器组件出现问题时,是保证可用性还是保证一致性?
  3. 当机器一时难以恢复,如何保证数据的正确性和完整性?
  4. 当负载增加时,是增加机器还是提升单机性能?
  5. 设计对外的API时,是力求简洁还是追求强大?
因此,有必要从根本上思考下如何评价一个好数据系统,如何构建一个好的数据系统,有哪些可以遵循的设计模式?有哪些通常需要考虑的方面? 书中用了三个词来回答:可靠性(Reliability)、可伸缩性(Scalability)、可维护性(Maintainability)
可靠性(Reliablity)
如何衡量可靠性? 功能上: 1.正常情况下,应用行为满足API给出的行为 2.在用户误输入/误操作时,能够正常处理 性能上: 在给定硬件和数据量下,能够满足承诺的性能指标 安全上: 能够阻止未授权、恶意破坏。 可用性也是可靠性的一个侧面,云服务通常以多少个 9 来衡量可用性。
两个易混淆的概念:Fault(系统出现问题) and Failure(系统不能提供服务) 不能进行 Fault-tolerance(容错) 的系统,积累的 fault 多了,就很容易 Failure。 如何预防?混沌测试:如 Netflix 的 chaosmonkey
硬件故障
在一个大型数据中心中,这是常态:
  1. 网络抖动、不通
  2. 硬盘老化坏道
  3. 内存故障
  4. 机器过热导致 CPU 出问题
  5. 机房断电
数据系统中常见的需要考虑的硬件指标:
  • MTTF mean time to failure
单块盘 平均故障时间 5 ~10 年,如果你有 1w+ 硬盘,则均匀期望下,每天都有坏盘出现。当然事实是硬盘会一波一波坏。
解决办法,增加冗余度: 机房多路供电,双网络等等。 对于数据: 单机:可以做RAID 冗余。如:EC 编码(一种编码方式,提供校验码支持数据准确性校验)。 多机:多副本 or EC 编码。
软件错误
相比硬件故障的随机性,软件错误的相关性(关联性)更高:
  • 接受特定的错误输入,便导致所有应用服务器实例崩溃的BUG。例如2012年6月30日的闰秒,由于Linux内核中的一个错误,许多应用同时挂掉了。
  • 失控进程会占用一些共享资源,包括CPU时间、内存、磁盘空间或网络带宽。
  • 系统依赖的服务变慢,没有响应,或者开始返回错误的响应。
  • 级联故障,一个组件中的小故障触发另一个组件中的故障,进而触发更多的故障
在设计软件时,我们通常有一些环境假设,和一些隐性约束。随着时间的推移、系统的持续运行,如果这些假设不能够继续被满足;如果这些约束被后面维护者增加功能时所破坏;都有可能让一开始正常运行的系统,突然崩溃。 虽然软件中的系统性故障没有速效药,但我们还是有很多小办法,例如:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现差异(discrepancy)时报警。
人为错误
系统中最不稳定的是人,因此要在设计层面尽可能消除人对系统影响。依据软件的生命周期,分几个阶段来考虑:
  • 设计编码阶段
  • 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。(尽可能消除所有不必要的假设,提供合理的抽象,仔细设计API)
  • 将人们最容易犯错的地方与可能导致失效的地方解耦(decouple)。特别是提供一个功能齐全的非生产环境沙箱(sandbox),使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验(进程间进行隔离,对尤其容易出错的模块使用沙箱机制,对服务依赖进行熔断设计)
  • 测试阶段
  • 在各个层次进行彻底的测试,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的边缘场景(corner case)[尽可能引入第三方成员测试,尽量将测试平台自动化;单元测试、集成测试、e2e 测试、混沌测试]
  • 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。 例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)
  • 运行阶段
  • 配置详细和明确的监控,比如性能指标和错误率。 在其他工程学科中这指的是遥测(telemetry)。 (一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的。)监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。(详细的仪表盘&持续自检&报警机制&问题预案)
  • 针对组织
  • 科学的培训和管理
可靠性有多重要?
事关用户数据安全,事关企业声誉,企业存活和做大的基石。
可伸缩性(Scalability)
可伸缩性,即系统应对负载增长的能力。它很重要,但在实践中又很难做好,因为存在一个基本矛盾:只有能存活下来的产品才有资格谈伸缩,而过早为伸缩设计往往活不下去。 但仍是可以了解一些基本的概念,来应对可能会暴增的负载。
衡量负载
应对负载之前,要先找到合适的方法来衡量负载,如负载参数(load parameters)
  • 应用日活月活
  • 每秒向Web服务器发出的请求(QPS)
  • 数据库中的读写比率
  • 聊天室中同时活跃的用户数量
书中以 Twitter 2012年11 披露的信息为例进行了说明:
  1. 识别主营业务:发布推文、首页 Feed 流。
  2. 确定其请求量级:发布推文(平均 4.6k请求/秒,峰值超过 12k请求/秒),查看其他人推文(300k请求/秒)

单就这个数据量级来说,无论怎么设计都问题不大。但 Twitter 需要根据用户之间的关注与被关注关系来对数据进行多次处理。常见的有推拉两种方式(面试可能会问如何设计):
  1. 。每个人查看其首页 Feed 流时,从数据库现拉取所有关注用户推文,合并后呈现。
  2. 。为每个用户保存一个 Feed 流视图,当用户发推文时,将其插入所有关注者 Feed 流视图中。
处理每秒12,000次写入(发推文的速率峰值)还是很简单的。然而推特的扩展性挑战并不是主要来自推特量,而是来自扇出(fan-out, 在事务处理系统中,我们使用它来描述为了服务一个传入请求而需要执行其他服务的请求数量。——每个用户关注了很多人,也被很多人关注。 使用拉的方式,是基于时间线实现的。发布推文时,只需将新推文插入全局推文集合即可。当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。在如下图所示的关系型数据库中,可以编写这样的查询:

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的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的3000万次写入!及时完成这种操作是一个巨大的挑战 —— 推特尝试在5秒内向粉丝发送推文。 在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。 推特轶事的最终转折:现在已经稳健地实现了方法2,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能。
描述性能
注意和系统负载区分,系统负载是从用户视角来审视系统,是一种客观指标。而系统性能则是描述的系统的一种实际能力。比如:
  1. 吞吐量(throughput):每秒可以处理的单位数据量,通常记为 QPS。
  2. 响应时间(response time):从用户侧观察到的发出请求到收到回复的时间。(在线系统更加关注的点)
  3. 延迟(latency):日常中,延迟经常和响应时间混用指代响应时间;但严格来说,延迟只是只请求过程中排队等休眠时间,虽然其在响应时间中一般占大头;但只有我们把请求真正处理耗时认为是瞬时,延迟才能等同于响应时间。
响应时间通常以百分位点来衡量,比如 p95,p99和 p999,它们意味着95%,99%或 99.9% 的请求都能在该阈值内完成。在实际中,通常使用滑动窗口滚动计算最近一段时间的响应时间分布,并通常以折线图或者柱状图进行呈现。[百分位点通常用于服务级别目标(SLO, service level objectives)服务级别协议(SLA, service level agreements),即定义服务预期性能和可用性的合同。 SLA可能会声明,如果服务响应时间的中位数小于200毫秒,且99.9百分位点低于1秒,则认为服务工作正常(如果响应时间更长,就认为服务不达标)。这些指标为客户设定了期望值,并允许客户在SLA未达标的情况下要求退款。]
应对负载
在有了描述和定义负载、性能的手段之后,终于来到正题,如何应对负载的不断增长,即使系统具有可伸缩性。
  1. 纵向伸缩(scaling up)or 垂直伸缩(vertical scaling):换具有更强大性能的机器。e.g. 大型机机器学习训练。
  2. 横向伸缩(scaling out)or 水平伸缩(horizontal scaling):“并联”很多廉价机,分摊负载。 e.g. 马斯克造火箭。
负载伸缩的两种方式:
  • 自动如果负载不好预测且多变,则自动较好。坏处在于不易跟踪负载,容易抖动,造成资源浪费。
  • 手动如果负载容易预测且不长变化,最好手动。设计简单,且不容易出错。
针对不同应用场景: 首先,如果规模很小,尽量还是用性能好一点的机器,可以省去很多麻烦。 其次,可以上云,利用云的可伸缩性。甚至如 Snowflake 等基础服务提供商也是 All In 云原生。 最后,实在不行再考虑自行设计可伸缩的分布式架构。 两种服务类型:
  • 无状态服务(stateless service)比较简单,多台机器,外层罩一个 gateway 就行。
  • 有状态服务根据需求场景,如读写负载、存储量级、数据复杂度、响应时间、访问模式,来进行取舍,设计合乎需求的架构。
不可能啥都要,没有万金油架构!但同时:万变不离其宗,应用的问题可能是读取量、写入量、要存储的数据量、数据的复杂度、响应时间要求、访问模式或者所有问题的大杂烩。组成不同架构的原子设计模式是有限的,这也是本书稍后要论述的重点。
可维护性(Maintainability)
从软件的整个生命周期来看,维护阶段绝对占大头。 但大部分人都喜欢挖坑,不喜欢填坑。因此有必要,在刚开就把坑开的足够好。有三个原则:
  • _可运维性(Operability)_便于运维团队无痛接手。
  • _简洁性(Simplicity)_便于新手开发平滑上手:这需要一个合理的抽象,并尽量消除各种复杂度。如,层次化抽象。
  • 可演化性(Evolvability) 便于后面需求快速适配:避免耦合过紧,将代码绑定到某种实现上。也称为可扩展性(extensibility)可修改性(modifiability)可塑性(plasticity)
可运维性(Operability):人生苦短,关爱运维
有效的运维绝对是个高技术活:
  1. 紧盯系统状态,出问题时快速恢复。
  2. 恢复后,复盘问题,定位原因。
  3. 定期对平台、库、组件进行更新升级。
  4. 了解组件间相互关系,避免级联故障。
  5. 建立自动化配置管理、服务管理、更新升级机制。
  6. 执行复杂维护任务,如将存储系统从一个数据中心搬到另外一个数据中心。
  7. 配置变更时,保证系统安全性。
系统具有良好的可维护性,意味着将可定义的维护过程编写文档和工具以自动化,从而解放出人力关注更高价值事情:
  1. 友好的文档和一致的运维规范。
  2. 细致的监控仪表盘、自检和报警。
  3. 通用的缺省配置。
  4. 出问题时的自愈机制,无法自愈时允许管理员手动介入。
  5. 将维护过程尽可能的自动化。
  6. 避免单点依赖,无论是机器还是人。
简洁性(Simplicity):复杂度管理
复杂度表现:
  1. 状态空间的膨胀。
  2. 组件间的强耦合。
  3. 不一致的术语和命名
  4. 为了提升性能的 hack。
  5. 随处可见的补丁( workaround)。
需求很简单,但不妨碍你实现的很复杂 :过多的引入了额外复杂度(accidental complexity )——非问题本身决定的,而由实现所引入的复杂度。 通常是问题理解的不够本质,写出了“流水账”(没有任何抽象,abstraction)式的代码。 如果你为一个问题找到了合适的抽象,那么问题就解决了一半,如:
  1. 高级语言隐藏了机器码、CPU 和系统调用细节。
  2. SQL 隐藏了存储体系、索引结构、查询优化实现细节。
如何找到合适的抽象?
  1. 从计算机领域常见的抽象中找。
  2. 从日常生活中常接触的概念找。
总之,一个合适的抽象,要么是符合直觉的;要么是和你的读者共享上下文的。 本书之后也会给出很多分布式系统中常用的抽象。
可演化性:降低改变门槛
系统需求没有变化,说明这个行业死了。 否则,需求一定是不断在变,引起变化的原因多种多样:
  1. 对问题阈了解更全面
  2. 出现了之前未考虑到的用例
  3. 商业策略的改变
  4. 客户爸爸要求新功能
  5. 依赖平台的更迭
  6. 合规性要求
  7. 体量的改变
应对之道:
  • 项目管理上敏捷开发
  • 系统设计上依赖前两点。合理抽象,合理封装,对修改关闭,对扩展开放。
【版权声明】本文为华为云社区用户翻译文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容, 举报邮箱:cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。