初读《DDIA》思考小记
技术10-12-2023
前言
在203年12月10日,我打开了这本《DDIA》。在即将期末考期间,抽空看看理论书籍,充实一下大脑。
关于可靠性、可扩展性、可维护性我有以下总结:
- 首先可靠用于应对错误,而错误缘于哪?硬件、软件、人为。硬件的故障是随机的,我们可以通过冗余来进行容错。而对于软件、人为,前者不可预测,后者不可靠。只能通过对其培训、检测,从而保证系统可靠。
- 其次,对于可扩展性。可扩展的目的是用于解决负载,我们要先描述好负载,其次根据系统情况,去选择合适的扩展策略。扩展的目的,是通过解决负载问题,提升性能。性能的评估也很重要,要根据实际的业务,去选择合适的评测点。
- 最后是可维护。大白话就是方便后续的运维以及迭代升级。技术层面,开发要做好充足的抽象,而运维应该尽可能的自动化,简单化流程。
现在的数据系统有很多,消息队列、数据库、缓存、搜索引擎,一系列单一的工具,通过代码缝合起来,共同完成任务。数据密集型的主要挑战源于,数据量、数据复杂度、数据变化速度,与之对应的,即计算密集型,处理速度是其瓶颈。缓解瓶颈,提升体验,是我们探讨数据密集型系统设计的目标!
关于数据系统的思考
什么是可靠性、可扩展性、可维护性? 数据密集型的挑战是什么? 为什么可以归类一谈?
一个应用被称为数据密集型的,如果数据是其主要挑战(数据量,数据复杂度、数据变化速度)——与之相对的是计算密集型,即处理器速度是其瓶颈。
软件系统中很重要的三个问题:
- 可靠性(Reliability):系统在困境(硬件故障、软件故障、人为错误)中仍可正常工作
- 可扩展性(Scalability):有合理的办法应对系统的增长(数据量、流量、复杂性)
- 可维护性(Maintainability):许多不同的人在不同的生命周期,都能高效地在系统上工作
单个工具已经不能满足应用系统的需求,总体工作被拆分成一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来。比如一个缓存、索引、数据库协作的例子:
我们通常认为,数据库、消息队列、缓存等工具分属于几个差异显著的类别。虽然数据库和消息队列表面上有一些相似性——它们都会存储一段时间的数据——但它们有迥然不同的访问模式,这意味着迥异的性能特征和实现手段。 近些年来,出现了许多新的数据存储工具与数据处理工具。它们针对不同应用场景进行优化,因此不再适合生硬地归入传统类别。类别之间的界限变得越来越模糊,例如:数据存储可以被当成消息队列用(Redis),消息队列则带有类似数据库的持久保证(Apache Kafka)。 其次,越来越多的应用程序有着各种严格而广泛的要求,单个工具不足以满足所有的数据处理和存储需求。取而代之的是,总体工作被拆分成一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来。
可靠性
要探讨可靠性,首先要进行可靠性的定义?
- 三个点:故障、失效、容错
- 造成错误的原因叫做故障(fault),能预料并应对故障的系统特性可称为容错(fault-tolerant)或者韧性(resilient)。讨论容错时,只有讨论特定类型的错误
- 故障(fault)不同于失效(failure):故障指的是一部分状态偏离标准,而失效则是系统作为一个整体停止向用户提供服务。
- 通常倾向于容忍错误(而不是阻止错误),但也有预防胜于治疗的情况(比如安全问题)
硬件故障
随机性。
- 一般都是增加单个硬件的冗余度
- 云平台的设计是优先考虑灵活性和弹性,而不是单机可靠性。
软件故障
内部问题。
- 这类软件故障的bug 通常潜伏很长时间,直到被异常情况触发为止。往往是某个假设出于某种原因最后不在成立了。
- 解决办法:仔细考虑假设和交互;彻底的测试;重启;监控。
人为故障
人不可靠。
- 人是不可靠的,运维配置错误是导致服务中断的首要原因。
- 解决办法:最小化犯错机会的方式设计系统;容易犯错的地方解耦;测试;监控;培训。
可扩展性
要描述可扩展性,需要描述好负载,因为负载需求,才要求可扩展。
- 描述负载->描述性能->应对负载
- 可扩展性(Scalability)是用来描述系统应对负载增长能力的术语。
描述负载
负载可以用负载参数的数字来描述,取决于系统架构。
- 它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西
一个推拉案例。关于推特博文推送。他的服务扇出大!
需求:用户可以向其粉丝发布新消息(平均 4.6k请求/秒,峰值超过 12k请求/秒),用户可以向其粉丝发布新消息(平均 4.6k请求/秒,峰值超过 12k请求/秒)。
方案一:全局集合,去获取。查询负载压力大。
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
方案二:推文插入到每个关注者的时间线中,「扇出」比较大,当有千万粉丝的大 V 发推压力大
最终方案:推拉结合。
推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并。
描述性能
当负载描述好了,与之对应的便是性能。
- 增加负载参数并保持系统资源不变时,系统性能将受到什么影响?
- 增加负载参数并希望性能不变时,需要增加多少系统资源?
对于不同的系统,我们的关注点不一样!
批处理系统,通常关心吞吐量(throughput);在线系统,通常更关心响应时间(response time)。
选择好的评测点,百分点用通常用于服务级别目标、服务级别协议。
- 对于系统响应时间而言,最好用百分位点,比如中位数、p99 等标识。
- 比如使用中位数,我们可以知道,至少有一半的用户大于百分之50。平均数不合适是因为,他不能体现出比例问题。其次我们百分点位会上调。比如某些情况,用户数据量大,他的时间会上升,但是这种于我们是大客户,我们应该尽可能去优化他的产品体验。
- 测量客户端的响应时间非常重要(而不是服务端),比如会出现头部阻塞、网络延迟等。
- 实践中的百分位点,可以用一个滑动的时间窗口(比如 10 分钟)进行统计。可以对列表进行排序,效率低的话,考虑一下前向衰减,t-digest 等方法近似计算。
注意:对于多个节点的服务情况,其中一个响应过慢,会使得整个服务被拖垮。
应对负载的方法
其实我们更倾向于优先进行纵向扩展。
- 纵向扩展:转向更强大的机器
- 横向扩展:将负载分布到多台小机器上
- 弹性系统:检测到负载增加时自动增加计算资源
- 跨多台机器部署无状态服务比较简单,但是把带状态的数据系统从单节点变成分布式配置则可能引入许多额外复杂度。因此,应该尽量将数据库放在单个节点上。
可维护性
虽然但是,现在很多人都在预防性编程。不过一个合格的软件工程师,应该要懂得如何去做好维护性的系统设计。
- 在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。
- 三个设计原则:
- 可操作性(Operability)便于运维团队保持系统平稳运行。
- 简单性(Simplicity)从系统中消除尽可能多的复杂度(complexity),使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)
- 可演化性(evolability)使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为可扩展性(extensibility),可修改性(modifiability)或可塑性(plasticity)。
可操作性:人生苦短、关爱运维
- 尽量自动化
简单性:管理复杂度
其实就是好被新人接管。
- 消除额外的(accidental)的复杂度
- 消除额外复杂度的最好工具之一是抽象(abstraction)
可演化性:拥抱变化
- 敏捷(agile) 工作模式为适应变化提供了一个框架
- 简单易懂的系统通常比复杂系统更容易修改,即可演化性(evolvability)