ddia_read/ch01.md
2023-09-12 00:14:35 +01:00

287 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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