DDIA-设计数据密集型应用
DDIA 这本书在存储/分布式领域的排名差不多是Top1的存在, 之前一直没有机会好好研读, 也因为欠缺了不少前置知识和储备, 可能也不太容易理解, 现在时机已到, 开始阅读, 并记录下其中的扩展笔记和疑问知识点.
书差不多读完了, 但因为买了实体书, 所以没有特意在这边做笔记了..懒
0x00.前言
A. 怎么读书
本书阅读后/中强烈建议配合 MIT 6.824
(分布式系统) 课程来完成加深巩固, 分布式系统的知识繁多, 有许多需要注意的小点都很难从文字里发现, 因此结合代码实践以及提问才能真正入门, 关于 6.824
的课程内容, 我在单独的文档里会细说, 大家可以参考
然后主要来说一下书籍本身, 读书要有目的有目标的去读, 那么笔记就聚焦在:
- 书的核心脉络, 让其他人能够很快了解书籍在讲什么, 重点是什么
- 书的关键结论, 以及经典的描述, 图表 (可能部分是自己来画)
- 结合自己的实际学习体验, 对提出的概念加以扩展(带有“补”字样, 以及一些思索 + 补充阅读
PS: 需要提醒的是, 原书自然是英文版, 网上先有一版 Github
版, 后有官方中文版, 三个版本都会参考, 然后我会对比较拗口的翻译进行一定转化, 避免绕名词, 也尽量让描述更好理解.
B. 章节
书分为三大部分, 对应12个章节, 书写的顺序大概按自底向上的逻辑在编写, 先讲基础概念, 再讲存储, 最后讲计算.
- 基本概念
- 可靠性 + 扩展性 + 可维护性
- 数据模型 & 查询语言
- 存储 & 检索
- 编码
- 分布式存储
- 复制 (replica?)
- 分区 (partition)
- 事务 (transaction)
- 分布式系统疑难点
- 一致性 & 共识
- 衍生数据
- 批处理 (batch process)
- 流处理 (stream process)
- 展望未来
C. 序章
首先来说说书名, DDIA(Design Data Intensive App)
这本书的名字不长, 但直译过来比较拗口, 以至于我很一段时间都没记住到底是啥, 在序章里作者首先讲了一下分类方式, 他把大数据领域分为两大块:
- 数据密集型 (I/O)
- DB
- 消息队列
- 缓存
- 索引 (?)
- 计算密集型 (Compute intensive)
这样看就很容易想起, 我们在 OS(操作系统) 中对进程/线程做的事的分类也是如此, 由此可知, 更简单来说, 还是按 “存储 + 计算” 两个大类来划分, 那显然本书的侧重应该是在存储方面, 并结合 Design
的含义, 那就能推测出, 书应该是着重在说 “如何设计一个存储系统” (待后续验证)
然后序言里作者希望大家阅读此书的效果是, 尽可能透过现象, 理解本质, 从而应对日新月异的名词和变化, 并找到自己的探索方向, 书里讲述具体的案例使用经典的 3W原则
:
- What (它是什么)
- Why (为什么需要它)
- How (它如何运转)
最后的地方, 作者告诉大家, 本书侧重描述存储系统的架构(Architecture)部分, 其它会略过, 并阐明了一些自己的态度, 比如他并不喜欢用 “Big Data” (大数据)这样空泛, 易滥用的名词, 会更细致描述具体的技术点, 并且作者更喜欢开源.
D. 概念
这里存放一些常见, 不好理解的概念全称, 完整表详见原书-术语表.
首先一个比较主要的转换就是 data
(数据) 这个词, 我基本都转为了I/O, 或者 “存储”, 因为 “数据系统” 的说法让我很难明白是在说啥..
0x01. 可靠性 + 可扩展性 + 可维护性
正式进入第一章, 书的每一章开头都配有一副中世纪的黑白插画, 颇有几分传奇色彩, 第一章的配图是一个船长的掌舵指南针, 如下图:
它进一步解释了标题的三个特性的要点:
- 可靠性 (Reliability)
- 软/硬件容错性 (Tolerating)
- 人为失误
- 可扩展性 (Scalability)
- 负载情况
- 性能指标 [ 延迟 (Latency) + 吞吐量 (Throughput) ]
- 可维护性 (Maintainability)
- 维护难度 (Operability)
- 易用性 (Simplicity)
- 持续迭代 (Evolvability)
注意上面不少翻译我都做了改变, 大家可以自行对照英文理解. 下面进入1.1小节
1.0 消息队列 [补]
在作者引入的几个典型的存储系统组件里, 消息队列我最为陌生, 所以先带着几个关键问题:
- 什么是消息队列
- 它是做什么的
- 为什么需要它
首先, Message Queue
. 先看数据结构, 是个队列, 那就符合普通先进先出的有序特性, 然后消息这里就可以当做一个请求, 发送请求方可以认为是生产者, 处理消息/请求的一方认为是消费者, 是最典型的“生产-消费”模型的实现, 也就是说, 用一个链表就能实现最简的消息队列了.
生产消费模型的好处不言而喻, 最经典的特性是异步 + 解耦, 生产和消费端可以各干各的, 互相不需要感知, 那么消息队列自然也有这样的好处, 不过除了继承P-C模型的优点外, 自然也继承了队列的应用场景—- 降峰/限流 (排队). 例如秒杀系统里, 最原始的做法就是大家一起请求服务器扛不住, 那我直接让超额的请求全部阻塞排队, 避免把server完全打挂, 另外在我关注的分布式事务 + RPC调用中也会用到 (后续补充)
类似于程序语言内置的 map
哈希表也能实现K-V存储的缓存功能, 为什么又需要 Redis
呢, 最大的好处我觉得是在于分布式管理多机的缓存, 并且进一步解耦. 那普通的链队, 之于单独的消息队列组件 (类似Kafaka
), 我想主要也是在于分布式和定制化吧.
1.1 存储系统の思考
其实一般来说, DB (Database) 作为存储界的核心, 它本身是可以做到集大成的, 比如普通的数据库, 自身也有缓存层, 也可以自己实现二级索引, 包括消息队列的功能, 只不过缺点是要做的事太多, 可能就不精, 并且耦合重, 也存在许多单点的可靠性问题, 所以数据量/请求量大, 一般都会用拆分独立组件的方式, 例如 DB + Cache + Index + MQ
. 然后书中给出了一个这样模型的图示:
上图我简化加理解一下是这样:
graph TD A(User) --request--> B(Core Server) --Hit Cache--> C((Cache Server)) B --Miss Cache--> D((DB Server)) --Hit Index-->E[Index Server] D--Miss Index --> F[DB File] B --async-put--> B2((Message Quque)) B3(..Consumers..) --async-get-->B2 subgraph Async B2 B3 end subgraph Main B C D E F end
上面的模型里很显然的包含了一个主流程, 和一个异步操作的任务队列处理其他事, 并且拥有独立的缓存, 索引, 和DB功能, 看起来是符合各司其职, 重度解耦的, 这也是目前常见的Web/后端服务里常用的设计方式
个人思考:
其实在早些时候的图数据库JanusGraph架构的设计, 也是非常类似这样的结构, 各个组件各司其职, 由一个中心点(图server)来调度, 但是这样的缺点也是很明显的:
- 某个子组件的故障, 很容易导致全局的故障, 要避免这样的情况, 得在中心(server)做很好的设计, 并不容易
- 对于一个细分领域的产品来说, 你过多依赖了外部组件, 维护成本非常高
- 解耦必然带来性能损失, 对于强调读写性能的组件来说, 可能影响较大
- 除开存储内部的解耦, 另外存储和计算的解耦会更多, 带来许多不通用和性能影响.
0x02. 可靠性
第一节提供了一个经典的主流后端服务设计方式(组件), 第二节来详细介绍对可靠性的定义, 以及它衍生出的概念(错误, 容错, 混沌测试等)
先说什么是软件领域的常见可靠定义:
- 程序执行的结果, 符合用户预期
- 系统就像一座城堡, 有明确的访问途径和边界(承载量 + 权限管理)
- 具有一定的容错性, 例如轮船破了一个洞不会导致马上沉船
然后这里引入了一个混沌(chaos)测试的概念, 简单说就是随机去破坏系统的某一部分(比如杀掉一个工作中的进程), 看看系统是否还能保持正常对外服务, 最后作者引出了一个分布式系统中常见的做法, “处理/容忍故障, 而不是试图避免故障”, 也就是说复杂系统应该假设问题无处不在, 主要是出了问题之后该如何应对, 而不是期望写出一个能避免绝大多数故障的系统 (虽然那听起来很美好).
基本定义了可靠性和对应的错误处理理念之后, 下面书分三大类问题来细说:
A. 硬件故障
如果一块企业级机械硬盘的可靠工作时间能到10~50年(书中引用)的话, 1万台节点的集群里, 每天都至少有1个磁盘出现硬件故障, 而类似的, CPU, 内存, GPU, 网卡, 交换机等常见硬件也都有自己的使用寿命, 它们中任一出现或多或少的问题, 都可能明显的影响系统的正常运行, 那么它们自然就是不可靠因素. 并且你很难避免(保证使用不会出现故障的磁盘, 或者能确切知道每块磁盘什么时候会坏…)
那么按之前提出的理念, 不能避免我们就去尽可能容忍它们, 最简单/经典的思路就是冗余 ——不把鸡蛋在一个篮子, 多CPU, 防止停电的备用电池(电源), 热拔插的替换组件等. 这类方案就像汽车的备胎, 让你在野外爆胎的时候, 能使得这个问题不至于导致灾难性的后果, 这类思想广泛运用在自然界和计算机中, 但是它有两个主要缺点:
- 有短暂的不可用时间, 你换胎需要熄火停车.
- 故障短时内不能过多. 比如你的汽车如果一次性爆了两个胎, 你仅有的一个备胎可能就不够补上
那么在计算机体系里, 这个问题在极端情况下就更为明显, 而且还有一个问题是, 这种方式明显缺乏弹性, 如果是短时间的突增和骤减, 你就需要频繁的增减硬件设备, 这会导致系统的可靠性大幅降低 (或者是成本大幅增加). 最后就引出另一个避免这种问题的名词 (滚动升级)
B. 软件故障
一般来说硬件的故障相比软件而言其实是少和可预知性强很多的, 就比如书中提到的, 硬件一般不会成批的同时出现问题, 但是软件里可能经常出现严重且难以预见的连锁问题, 导致“雪崩”, 并且很多坑就是不小心埋下的定时炸弹, 引爆之前没有人知晓. 这里作者给出了处理软件问题常见的方式 —– 监控报警
C. 人为失误
根据书中引用的大互联网公司(应该是FLAG类)统计, 人为失误(比如配错了配置文件, 误操作)占比是第一位, 硬件故障如上一节推测的, 仅占10%~25%
, 可见如同人月神话的理念一样, 在大的系统/公司中, “人” 才是最不可靠因素. 那么如何应对呢, 书中提出几个方式:
- 更严格的权限控制 + 流程规范 (但是会限制易用性, 需要找平衡)
- 独立的测试环境, 不在生产环境做测试.
- 尽可能完善全面的测试和监控 (特别注意corner case)
- 提供快速回滚的机制, 有严重问题可以回退
0x03. 可扩展性
首先这节的开篇作者给可了扩展性一个典型例子, 当系统的请求量从1万/s变成了100万/s, 如何保证系统仍然可用可靠, 或者说成本多少, 可扩展性大部分情况下不是一个非黑即白的概念, 而是讨论扩展的代价 + 效率问题. (比如线性增长)
A. 负载情况
这里原文翻译是描述负载, 总之意思是说, 在描述扩展性之前, 先要说一下当前的系统负载情况, 然后它用推特(类国内weibo)举例, 有两个典型case:
- 用户发推: 准确说, 这里是说每个用户发推后推送给关注者的平均速度, 大约
5k/秒
不到, 峰值最大会达到1万/秒
出头 - 查看推特: 这里是指全网查看推特主页的平均请求总数/s, 大概是
30万/秒
首先这里其实描述我没太读懂, 我理解推特的读取速度最大值应该是热点推文, 还有它这里的扇出描述也有点拗口, 应该想表达的就是超级顶点(大V)的遍历会很重, 接着它说了两个方案:
不算复杂的跨表查询
1
2
3
4
5# 要查询当前用户 tom 的推特关注主页
select tweets.*, users.* from tweets # 最终从推特表查询
join users on tweets.sender_id = users.id
join follows on follows.followee_id = user.id
where follows.follower_id = "tom" # 1. 先从用户关注表找到tom
B. 性能指标
这里说的相对比较泛一些, 性能相关问题的确很重要, 也很体系, 建议看火焰图的作者的相关书籍体系学习.
更新: DDIA 整本看完之后, 个人觉得最好的部分还是在前半部分, 后半部分讲计算或趋势不知道是翻译还是时间原因, 倒是有些印象不深, 然后同期深入理解 JVM 的作者周志华前辈更新了一本个人觉得更适合入门普及的架构书 “凤凰架构“(且开源) , 几乎适合所有人阅读, 而且写的的确很好, 以后阅读 DDIA 之前, 感觉都可以先阅读它.
参考资料:
- DDIP-CN (原书官方中文版)
- DDIP-EN (原书英文影印版)
- Gtihub - 书中引用文献汇总
- 其他人的阅读心得 (TODO)