专业书籍精读 · DDIA · 第 1 章
Designing Data-Intensive Applications · Ch 1 · Martin Kleppmann · 2017
你每天刷的淘宝、微信、B 站,背后都是一套存数据、取数据的系统。DDIA(《数据密集型应用设计》)第 1 章不讲某个具体技术,而是先问一个更根本的问题:一套数据系统,怎样才算「好」?作者给了三把尺子——可靠、可扩展、可维护,整本书都在围着它们转。
把数据系统想成一家餐厅:可靠是「就算今天有个厨师病了、坏了一个灶台,客人照样能吃上饭」;可扩展是「今天来 10 桌能接住,下个月火了来 100 桌也别乱套」;可维护是「后厨动线清爽,换个新人、加道新菜都不折腾」。这一章就是教你把这三件模糊的「好」,变成能量化、能讨论的东西。
大多数人衡量快慢爱看平均值:平均响应 100 毫秒,听着不错。但平均值会骗人——100 个请求里 99 个很快、1 个卡了 5 秒,平均下来还挺好看,可那个等 5 秒的,偏偏常是你最重要的大客户(他数据多、请求重)。
这一章教你换个问法:把 100 个请求按快慢排队,看排在第 50、第 95、第 99 的那个有多慢。第 99 位那个(叫「p99」)代表「最倒霉那批人的体验」——真正决定口碑的往往是它,而不是平均值。
零件坏了叫故障,整个系统对外停摆叫失效。好系统的诀窍不是「零件永不坏」(做不到),而是让某个零件坏了也别拖垮整体——多备几份、坏一个顶上另一个。Netflix 甚至养了只「捣乱猴子」(Chaos Monkey),故意随机关掉线上机器,逼工程师把系统练成「坏一台没人慌」。
读完这一章,你手里就有了一套通用的评判语言:谈可靠,就谈「故障别变失效」;谈快慢,就谈 p95/p99 而不是平均;谈扩展,就先问「压力到底涨在哪」。后面每一章讲的复制、分片、事务,本质上都是在这三把尺子之间做取舍——没有最好,只有最合适。
一套数据系统好不好,就看三把尺子:可靠(零件坏了也别整体停)、可扩展(压力涨了也扛得住)、可维护(好改好管)。而且别再用「平均值」骗自己,要盯「最慢那一批人」(p99)的体验。
想进到具体机制、记号和示意图? → 切到精读版
DDIA 第 1 章不教任何一种具体技术,而是搭起全书的思考框架:一套「数据密集型」系统的好坏,落在三个词上——可靠性(reliability)、可扩展性(scalability)、可维护性(maintainability)。它教你把这三件模糊的事变成能量化、能权衡的工程指标,尤其是用响应时间百分位而非平均值来谈性能,为后面 11 章的所有取舍立好标尺。
p50 就是中位数。作者 Martin Kleppmann(剑桥研究者、前 LinkedIn 工程师)。本章是全书 Part I「数据系统的基石」的开篇,也是整本书的总纲:它不接续某个具体技术,而是先立标尺;后面每一章——复制、分区、事务、共识、批 / 流处理——都是在这三把尺子之间做具体取舍。可以说,这一章是读懂后面所有权衡的「坐标系」。
工程师张口就说系统要高可用、高性能、好扩展,但这些词太滑:多可靠算可靠?「快」是平均快还是最慢也快?「扩展」到底扩什么?没有统一的量化语言,团队就会各说各话、拍脑袋决策。这一章的任务,就是把三个口号拆成能测量、能对话、能权衡的具体概念——这样后面讨论「用单主复制还是无主复制」时,大家才有共同的评判基准。
可靠性的定义很朴素:出了岔子,系统还能继续正确工作。关键是分清两个词:故障(fault)是单个组件偏离了它该有的表现(一块盘坏了、一个进程崩了、网络抖了);失效(failure)是系统作为整体停止对外提供服务。故障几乎不可避免,但可以设法不让它升级成失效——这就叫容错(fault-tolerant)。
作者把故障分三类。硬件故障:磁盘平均无故障时间约 10–50 年,机器一多,几乎每天都有盘坏——传统解法是加冗余(RAID、双电源、热备),趋势是往软件层容错靠。软件故障更阴险,因为常是相关联的、成批爆发的系统性错误(某个触发条件下所有节点同时崩、一次闰秒 bug 放倒一整片)。人为故障——配置错误是线上事故的头号原因。对策不是苛求「别出错」,而是在设计上容错:解耦、快速回滚、充分监控,以及像 Netflix 的 Chaos Monkey 那样主动往生产环境注入故障,逼出脆弱点。
表 1 · 三类故障的性格与对策(DDIA 分类)
| 故障类型 | 典型例子与量级 | 是否相关 | 主要对策 |
|---|---|---|---|
| 硬件故障 | 磁盘损坏、内存出错、断电。磁盘 MTTF 约 10–50 年 → 一个 1 万 块盘的集群平均每天坏 ~1 块 | 大多相互独立 | 冗余(RAID、双电源、多机热备);趋势转向软件层容错 |
| 软件故障 | 某触发条件下所有节点同时崩、一次闰秒 bug 放倒一整片、级联失效 | 高度相关、成批爆发 | 解耦、限流、隔离;充分测试与监控;进程可自愈 |
| 人为故障 | 配置错误——大型互联网服务停机的头号原因 | 相关(一次误操作波及多处) | 设计上容错:解耦、快速回滚、灰度发布、充分监控、演练 |
可扩展性不是一个「有 / 没有」的标签,而是要具体回答:当负载增长时,系统靠什么手段维持性能?分两步走。
第一步描述负载:用几个负载参数(load parameters)刻画压力——每秒请求数、读写比、同时在线用户、缓存命中率。书里的经典例子是 Twitter 首页时间线:真正的负载参数不是「发推速率」,而是每个用户的粉丝数分布——名人一条推要推送给几千万人,「扇出(fan-out)」才是难点所在。DDIA 引的实测量级很能说明问题:发推平均 4.6k 请求/秒、峰值 12k+,而首页时间线读取高达 300k 请求/秒——读写比约 65:1,压力压在读侧,且每条推都要扇出给作者的所有粉丝。
表 2 · Twitter 首页时间线:两种扇出的量级对比(数字为 DDIA 所引 2012 年前后 Twitter 实测)
| 写时扇出 fan-out on write | 读时合并 fan-out on read | |
|---|---|---|
| 发一条推 | 写入每个粉丝的收件箱缓存 | 只写作者自己一次 |
| 读时间线 | 直接读自己收件箱,最快 | 现查所有关注对象、合并排序,较慢 |
| 写放大 | 一条推 × 平均 75 粉丝 ≈ 34.5 万次写/秒(4.6k×75) | 无写放大 |
| 名人一条推 | 3000 万+ 次写入,还得及时送达 | 1 次写入,代价转嫁到读 |
| 适用 | 粉丝不多的普通用户 | 被千万人关注的大 V |
Twitter 的最终答案是混合:绝大多数用户走写时扇出,极少数超大 V 走读时合并、在读时把两条来源拼起来——用负载参数(粉丝数分布)本身来分流。这正是「可扩展性没有通用答案、只能针对具体负载定制」的活样板。
第二步描述性能,这里有本章最重要的一课:别用平均值,用百分位。响应时间不是一个数、而是一个分布——大多数请求快、少数很慢。平均值会被少数极端值带偏,也掩盖了「谁在受苦」。正确姿势是看百分位:p50(中位数,一半用户比它快)、p95、p99、p99.9。尾部(p99 及以后)常常最要紧——因为请求最慢的用户,往往正是数据最多、最有价值的那批。亚马逊就用 p99.9 而非平均来定服务目标。
表 3 · 响应时间百分位怎么读、代价几何
| 百分位 | 含义 | 典型用途 / 代价 |
|---|---|---|
p50 中位数 | 一半用户比它快 | 「典型」体验 |
p95 | 每 20 个请求最慢的 1 个 | 常见 SLA 目标 |
p99 | 每 100 个最慢的 1 个 | 重点盯的尾部起点 |
p99.9 | 每 1000 个最慢的 1 个,常是数据最多、最有价值的用户 | 亚马逊用它定内部服务 SLA |
p99.99 | 每 1 万 个最慢的 1 个 | 亚马逊认为再往这优化不划算——收益递减、成本陡增 |
尾延迟为什么难缠?一是排队延迟常主导尾部:服务器能并行处理的请求数有限,一个慢请求会把后面排队的都拖慢(队头阻塞 head-of-line blocking)。二是尾延迟放大(tail latency amplification):若一个页面要并行调用多个后端、必须等最慢的那个才能返回,那么后端只要有一点慢请求,页面级的「慢」就会被放大。算个具体量级:一次用户请求并行访问 100 个后端、每个各有 1% 的概率变慢,则整页被至少一个慢调用拖慢的概率高达 1 − 0.99¹⁰⁰ ≈ 63%——单个后端看着很健康,页面级尾部却已惨不忍睹(这正是 Google 的 Jeff Dean 与 Luiz Barroso 在《The Tail at Scale》里量化的现象)。所以度量要在客户端侧、按百分位持续观测,而不是只看服务端平均。
软件成本的大头不在开发、在长期维护。作者拆成三条:可运维性(operability)——让运维容易把系统跑好(好的监控、文档、自动化);简单性(simplicity)——管住复杂度,用好的抽象(abstraction)消除偶发复杂度(accidental complexity,指并非问题本身固有、而是实现方式带来的复杂度),别让系统烂成一团「大泥球(big ball of mud)」;可演化性(evolvability)——让需求变化时容易改。三者共同决定一套系统能不能被人长期、低痛地养着。
表 4 · 纵向扩展 vs 横向扩展
| 纵向 scale up | 横向 scale out | |
|---|---|---|
| 做法 | 换更强的单机(更多 CPU / 内存、更快的盘) | 一堆普通机器组成无共享 shared-nothing 集群 |
| 扩展上限 | 有硬天花板——单机再强也有极限 | 近乎线性可扩,能到极大规模 |
| 成本曲线 | 高端机器价格超线性上涨 | 普通机器堆量,单位成本低 |
| 复杂度 | 低——应用几乎不用改 | 高——引入复制 / 分区 / 一致性等分布式全部难题(Part II) |
| 适用 | 负载不极端、图省事 | 负载大 / 要高可用,愿承担分布式复杂度 |
现实里常两者混用:几台够强的机器往往比大量小机器更简单、也更划算。没有万能的扩展魔法——架构永远是针对具体应用的负载特征定制的。
这一章之所以被无数后端工程师奉为「面试与架构的通用底本」,是因为它给了一套跨技术的评判语言:无论你用 PostgreSQL、Cassandra、Kafka 还是自研系统,谈可靠都能落到「故障是否被容错边界挡住」,谈性能都能落到「p99 是多少、尾延迟从哪来」,谈扩展都能落到「负载参数是什么、怎么加机器」。百分位延迟(p99/p99.9)今天已是各大公司 SLA 与监控面板的标准语言;混沌工程(Chaos Engineering)也从 Netflix 扩散成一门通行实践。后面每一章的技术,都可以放回这三把尺子上评判。
p99.9(而非平均)定内部服务的 SLA,理由是「最慢的请求往往来自数据最多、最有价值的客户」——验证了本章「看尾部、别看平均」。Dynamo 论文, SOSP 2007 ↗① 一句话:全书总纲——用可靠 / 可扩展 / 可维护三把尺子,把「系统好不好」变成可量化、可权衡的工程问题。
② 可靠性:分清故障(组件出问题)与失效(整体停摆),目标是容错——别让故障升级成失效;故障分硬件 / 软件 / 人为,人为配置错误最常见。
③ 可扩展性两步走:先用负载参数描述压力(Twitter 例中是粉丝数分布 / 扇出),再描述性能。
④ 性能度量的核心一课:用百分位、别用平均——p50/p95/p99/p99.9,尾延迟最要紧,因为最慢的用户常最有价值。
⑤ 尾延迟难缠源于排队延迟 / 队头阻塞与尾延迟放大;要在客户端按百分位持续观测。
⑥ 扩展手段:纵向(换强机、有上限)vs 横向(无共享、扩得大但引入分布式麻烦);没有万能扩展魔法。
⑦ 可维护性 = 可运维 + 简单(治偶发复杂度、别成大泥球)+ 可演化。
⑧ 意义:给了一套跨技术的评判语言,是读懂后面复制 / 分区 / 事务 / 共识等所有取舍的坐标系。