第一章:可靠、可扩展与可维护的应用系统
这一章是全书的总纲。Kleppmann 不讨论任何具体技术,而是先确立评价一切数据系统的三个基本维度:可靠性、可扩展性、可维护性。后续每一章的技术权衡,最终都可以归结为在这三者之间的取舍。
一、为什么需要这三个维度
现代应用系统早已不是"一台机器跑一个程序"的形态。一个典型系统可能包含:
- 数据库(存储状态)
- 缓存(加速读取)
- 搜索索引(支持全文检索)
- 消息队列(异步解耦)
- 批处理框架(离线分析)
这些组件通过网络连接,运行在多台机器上。系统一旦复杂,就会面临三类根本问题:
- 出错时怎么办?→ 可靠性
- 数据量/流量增长时怎么办?→ 可扩展性
- 长期维护和演化怎么办?→ 可维护性
这三个问题,构成了全书所有讨论的出发点。
二、可靠性(Reliability)
1. 定义
系统在出现故障(fault)时,仍能继续正确工作。
这里有一个关键区分:
| 术语 | 含义 | 例子 |
|---|---|---|
| 故障(fault) | 系统的一部分偏离正常状态 | 一块硬盘坏了、一个进程崩溃 |
| 失效(failure) | 系统整体停止提供服务 | 整个数据库不可用 |
可靠性高的系统,不是不会出故障,而是故障不会升级为失效。这就是所谓的"容错"(fault-tolerant)或"韧性"(resilient)。
2. 故障的三种来源
Kleppmann 把故障分为三类,这个分类贯穿全书:
(1)硬件故障
- 硬盘损坏、内存位翻转、电源故障、网络中断
- 传统做法:冗余(RAID、双电源、备用机器)
- 现实:在大规模集群中,硬件故障是常态而非例外。Google 的统计显示,同时有数千块硬盘在坏。
- 趋势:从"单机高可靠"转向"软件层面容忍多机故障"
(2)软件故障
- 系统性 bug、配置错误、级联失效
- 特点:跨节点相关——一个 bug 可能同时击垮所有副本
- 例子:2012 年某公司因一个配置错误导致全网瘫痪;闰秒 bug 导致多系统同时崩溃
- 难点:软件故障没有"冗余"能直接解决,因为所有副本跑的是同一份代码
(3)人为错误
- 运维误操作、配置失误、代码逻辑错误
- 统计上,人为错误是导致系统失效的首要原因
- 应对策略:
- 设计能最小化犯错机会的接口(如"安全默认值")
- 提供沙箱环境测试
- 快速回滚机制
- 完善的监控与遥测
3. 可靠性的价值
- 对用户:数据不丢失、服务不中断
- 对企业:声誉、收入、合规
- 对工程师:可靠性是可以被设计的,而不是靠运气
三、可扩展性(Scalability)
1. 定义
系统应对负载增长的能力。
关键点:"可扩展"不是一个二元属性,而是一个动态问题。不能简单说"系统 X 可扩展",而要说"系统 X 在负载从 A 增长到 B 时,用什么手段应对"。
2. 第一步:描述负载
在讨论扩展之前,必须先量化负载。不同系统的负载参数不同:
- Web 服务:每秒请求数(RPS)
- 数据库:读写比例、缓存命中率
- 社交网络:粉丝数分布(幂律分布,少数用户有海量粉丝)
- 聊天系统:消息发送速率、在线用户数
关键洞察:平均值会骗人。必须看分布,尤其是尾部。
3. 第二步:描述性能
两个核心指标:
(1)吞吐量(Throughput)
- 单位时间处理的请求数/数据量
- 适合批处理、消息队列等场景
(2)响应时间(Response Time)
- 从发出请求到收到响应的时间
- 注意:响应时间不是一个固定值,而是一个分布
百分位数(Percentile)——本章最重要的概念之一:
- p50(中位数):一半请求快于此值
- p95、p99、p999:尾部延迟
- 为什么关注尾部?
- 尾部延迟直接影响用户体验(一个用户可能同时发多个请求,只要一个慢就整体慢)
- 尾部延迟往往由排队引起,而排队是系统过载的早期信号
- 尾延迟放大(Tail Latency Amplification):如果一个用户请求需要调用 100 个后端服务,只要 1 个慢,整体就慢。所以 p99 的 1% 会变成用户侧的 63%。
实践建议:
- 监控 p95/p99,而非只看平均值
- 用直方图而非平均值来聚合延迟
- 在高负载下,尾部延迟会急剧恶化
4. 第三步:应对负载增长
两种基本策略:
(1)垂直扩展(Scaling Up)
- 换更强的机器(更多 CPU、内存、磁盘)
- 优点:简单,无需改代码
- 缺点:成本非线性增长,有物理上限
(2)水平扩展(Scaling Out)
- 增加更多机器,分布式处理
- 优点:理论上无上限,成本线性
- 缺点:复杂性剧增(分区、复制、一致性)
Kleppmann 的核心观点:
没有万能的"可扩展架构"。适合 10 万用户的架构,未必适合 1000 万用户。可扩展性是针对特定负载的,需要根据实际访问模式来设计。
例子:
- 读多写少 → 加缓存、加读副本
- 写多读少 → 分区、LSM-Tree 存储
- 复杂查询 → 列式存储、预聚合
四、可维护性(Maintainability)
1. 定义
系统在长期运行中,能被团队高效地理解、修改、扩展和运维。
Kleppmann 指出:软件的大部分成本不在初始开发,而在持续维护。维护包括:
- 修复 bug
- 适应新需求
- 迁移到新平台
- 应对新负载
- 修复技术债务
2. 可维护性的三个设计原则
(1)可运维性(Operability)
让运维团队能轻松保持系统运行。
具体包括:
- 提供良好的监控与日志
- 支持自动化部署与配置
- 避免依赖特定机器的"雪花服务器"
- 提供清晰的文档与操作手册
- 支持滚动升级、回滚
(2)简单性(Simplicity)
降低系统的复杂度,让新工程师能快速理解。
复杂性的表现:
- 状态空间爆炸
- 隐式依赖
- 命名混乱
- 过度抽象
应对手段:
- 抽象:好的抽象能隐藏实现细节,如 SQL 隐藏了存储引擎
- 消除偶然复杂性:区分"本质复杂性"(问题本身难)和"偶然复杂性"(工具/设计导致的难)
- 模块化、清晰的接口
(3)可演化性(Evolvability)
让系统能适应需求变化。
也称为"可扩展性"(extensibility)或"可修改性"(modifiability)。关键点:
- 需求永远在变,系统必须能跟着变
- 数据模型、接口、协议都要支持演化
- 这与第四章"编码与演化"直接呼应
五、本章的核心思想总结
| 维度 | 核心问题 | 关键概念 | 应对手段 |
|---|---|---|---|
| 可靠性 | 出错时能否继续工作? | 故障 vs 失效 | 冗余、隔离、快速恢复 |
| 可扩展性 | 负载增长时能否应对? | 百分位数、尾延迟 | 垂直/水平扩展、针对性设计 |
| 可维护性 | 长期能否高效维护? | 可运维、简单、可演化 | 抽象、自动化、良好接口 |
三个贯穿全书的底层判断:
- 没有免费的午餐:可靠性、可扩展性、可维护性之间经常需要权衡。
- 没有万能方案:所有技术选型都必须结合具体负载和场景。
- 复杂性是最大的敌人:好的系统设计,本质上是不断对抗复杂性的过程。
六、这一章在全书中地位
第一章确立了全书的"价值坐标系":
- 后面讲复制、分区、事务、共识时,每个技术选择都要回答:“它对可靠性、可扩展性、可维护性分别意味着什么?”
- 例如:单主复制提高了一致性(可靠性),但牺牲了写可扩展性;无主复制提高了可用性,但增加了冲突处理的复杂性(可维护性下降)。
一句话概括本章:
数据系统的设计,本质上是在可靠性、可扩展性、可维护性这三个维度上,针对具体负载和场景,做出清醒的、有意识的权衡。