数据系统的“不可能三角”——可靠性、可扩展性、可维护性
可靠性、可扩展性、可维护性——这三个词定义了什么是“好”的数据系统,而它们之间的权衡,贯穿了全书每一章。
Alan Kay 说过一句话:“互联网做得太棒了,以至于大多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。”一个技术能做到让用户完全感觉不到它的存在,才是最高的境界。
但作为构建这些系统的人,我们清楚地知道:每一次“无感”的背后,都是一系列精心设计的决策和取舍。
DDIA 的第一章,正是要从最根本的层面回答一个问题:什么样的数据系统,才算是一个“好”系统?
答案是三个词:可靠性(Reliability)、可扩展性(Scalability)、可维护性(Maintainability) 。这三个词构成了全书的方法论基础,后面的每一章——从存储引擎到数据复制,从事务到共识算法——都是在围绕这三个目标展开。
┌─────────────────────────────────────────────┐
│ 一个好的数据系统应该具备 │
├─────────────────────────────────────────────┤
│ │
│ 🔒 可靠性 📈 可扩展性 🛠️ 可维护性 │
│ Reliability Scalability Maintainability │
│ │
│ 故障中仍能 从容应对 让不同的人 │
│ 正常工作 负载增长 都能高效工作 │
│ │
└─────────────────────────────────────────────┘💡 注:这三个维度在业内也被称为系统的“非功能性需求”(Non-functional Requirements)——它们不决定系统“能做什么”,但决定了系统“能做成什么样”。
可靠性(Reliability):不是“不死机”,而是“死得起”
什么是可靠性?
可靠性指的是:系统在困境(硬件故障、软件故障、人为错误)中仍然可以正常工作,正确完成功能,并达到期望的性能水准。
换句话说,可靠性 ≠ 永不故障。世界上不存在永不故障的系统。可靠性的真正含义是:系统能够容忍故障,而不让故障升级为服务失效。
这里有一个关键区分:
- 故障(Fault) :系统的一部分状态偏离了标准——比如一块硬盘坏了、一段代码出了bug、有人配错了参数
- 失效(Failure) :系统作为一个整体停止向用户提供服务
可靠系统的目标是:允许故障发生,但不让它升级为失效。 能预料并应对故障的系统特性,称为容错(Fault-tolerant) 或韧性(Resilient) 。
| 故障(Fault) | 失效(Failure) | |
|---|---|---|
| 定义 | 系统一部分偏离标准 | 系统整体停止服务 |
| 例子 | 一块硬盘坏了、一个服务实例崩溃 | 用户刷不出页面、API 全部超时 |
| 关系 | 故障是原因 | 失效是结果 |
| 目标 | 容忍故障,防止其升级为失效 |
有意思的是,在容错系统中,故意触发故障反而是一种有效的测试手段——Netflix 的 Chaos Monkey 每天随机杀死生产环境中的实例,就是为了验证系统是否真的能在故障发生时自动恢复。
三类故障来源
DDIA 将故障分为三大类:
三类故障来源
├── 硬件故障
│ ├── 硬盘崩溃、内存出错、机房断电
│ ├── 应对:RAID、冗余、热插拔
│ └── 云时代思路:弹性 > 单机可靠性
├── 软件错误
│ ├── 系统性bug、闰秒事件、级联故障
│ ├── 应对:测试、隔离、监控、重启
│ └── 最难排查:潜伏时间长,触发条件罕见
└── 人为错误
├── 配置错误是服务中断的首要原因
├── 应对:简化操作、解耦、灰度发布
└── “删库跑路”不是段子,是真实风险1. 硬件故障(Hardware Faults)
硬盘崩溃、内存出错、机房断电、网线被拔——这些是运维人员最熟悉的“日常”。在一个拥有 10,000 个磁盘的存储集群中,如果磁盘的平均无故障时间是 10-15 年,那么平均每天都会有一个磁盘出故障。这不是小概率事件,是确定事件。
传统的应对方式是增加硬件冗余:RAID 磁盘阵列、双路电源、热插拔 CPU、数据中心的备用电池和柴油发电机。但云时代的思路有所不同——云平台更倾向于优先考虑灵活性和弹性,而不是单机可靠性。与其追求每一台机器都不坏,不如让系统在机器坏了之后能自动恢复。
2. 软件错误(Software Errors)
硬件故障通常是随机的、相互独立的,但软件错误不同——它们是系统性的。一段代码里的bug,可能同时导致所有实例一起崩溃。
书里举了一个经典例子:2012年6月30日的闰秒事件——由于 Linux 内核的一个错误,全球大量服务器在同一时间挂掉了。类似的还有:某个特殊输入导致所有应用实例崩溃、一个失控进程耗尽所有共享资源、一个组件变慢引发级联故障。
这类问题的应对思路包括:仔细审视系统中的假设和交互、彻底的测试、进程隔离、允许进程崩溃并重启、以及持续的生产环境监控。
3. 人为错误(Human Errors)
设计系统的是人,运维系统的也是人。而人是不可靠的。
一项针对大型互联网服务的研究发现:运维配置错误是导致服务中断的首要原因,硬件故障只占 10%-25%。“删库跑路”不只是一个段子——它背后反映的是人为错误的巨大破坏力。
应对思路包括:用最小化犯错机会的方式来设计系统、将容易出错的地方解耦、充分的测试、监控告警、以及持续的培训。
可扩展性(Scalability):10倍流量来了,你有几种姿势?
什么是可扩展性?
可扩展性指的是:系统有合理的办法来应对负载的增长——包括数据量的增长、流量的增长、复杂性的增长。
但“可扩展”不是一个非黑即白的状态,而是一个持续应对变化的能力。要讨论可扩展性,必须先回答两个问题:
- 如果负载增加而系统资源不变,性能会下降多少?
- 如果要保持性能不变,需要增加多少资源?
描述负载:选对负载参数
负载需要用具体的负载参数(Load Parameters) 来量化。不同的系统关注不同的参数:
- Web 服务器:每秒请求数(QPS)
- 数据库:读写比率、缓存命中率
- 聊天室:同时活跃用户数
DDIA 用推特(Twitter) 的例子来说明负载描述的重要性。
2012 年推特峰值时,发推约 12,000 次/秒,刷时间线约 300,000 次/秒。但真正的挑战不在于总流量,而在于扇出(Fan-out) ——一条推文需要被插入到所有关注者的时间线中。一个拥有 3,000 万粉丝的大 V 发一条推文,瞬间就是 3,000 万次写入操作。
推特最初的方案是“发推时写入全局集合,读时间线时做 JOIN”——这种方式写操作轻,但读操作重。后来改为“发推时预计算并插入每个关注者的时间线”——读操作轻了,但写操作(尤其大 V 发推时)变得极重。最终推特采用了两者结合的混合方案。
方案一(读时 JOIN) :
发推 → 写入全局推文表 → 用户刷时间线 → 查关注列表 → JOIN 推文表 → 返回
(写轻,读重)方案二(写时扇出) :
大V发推 → 查3000万粉丝列表 → 写入3000万份时间线缓存 → 用户刷时间线 → 直接读取
(写重,读轻)最终方案:两者结合——普通用户走扇出,大V走读时JOIN
这个例子告诉我们:没有“标准答案”,只有“适合你的负载特征的答案” 。
描述性能:别只看平均值
描述完负载之后,下一个问题是:如何衡量性能?
- 批处理系统:通常关心吞吐量(Throughput) ——每秒处理多少数据
- 在线系统:通常关心响应时间(Response Time) ——请求从发出到完成需要多久
但这里有一个常见的误区:不要只看平均值。
假设一次 API 调用的平均响应时间是 100ms,看起来还不错。但如果 99% 的请求都在 50ms 内完成,而 1% 的请求需要 5 秒——那 1% 的用户体验是灾难性的。
DDIA 强调使用百分位数(Percentiles) 来衡量响应时间:
- 中位数(p50) :50% 的请求比这个快——代表了“典型用户”的体验
- p99:99% 的请求比这个快——代表了“最差 1% 用户”的体验
- p999:99.9% 的请求比这个快——代表了“极端尾部”的体验
请求数
↑
│ ████
│ ██████
│ ████████ ← p50(中位数)
│ ██████████
│████████████ ← p95
│██████████████ ← p99
│████████████████ ← p999
└────────────────────→ 响应时间尾部延迟(Tail Latency) 为什么重要?亚马逊曾经统计过:p99 响应时间每增加 100 毫秒,销售额下降 1% 。在规模面前,最慢的那 1% 用户,代表的可能是数以百万计的真实用户和真金白银的损失。
应对负载增长的策略
当负载增长时,主要有两种应对方式:
- 纵向扩展(Scale Up) :换成更强大的机器——简单直接,但有物理上限,且成本不低
- 横向扩展(Scale Out) :将负载分布到多台小机器上——理论上可以无限扩展,但引入了分布式系统的所有复杂性
另外还有弹性系统(Elastic System) ——检测到负载增加时自动增加计算资源,适合负载波动大的场景。
需要特别注意的是:无状态服务的横向扩展相对简单,但带状态的数据系统(如数据库)从单节点扩展到分布式,会引入大量的额外复杂度。这也是为什么书中说“应该尽量把数据库放在单个节点上”——不是不要分布式,而是要在确实需要的时候才引入它。
可维护性(Maintainability):让接手你代码的人不骂你
什么是可维护性?
软件的生命周期中,开发只占一小部分,维护才是大头。有数据表明,软件 70% 的成本花在了维护阶段。
可维护性指的是:许多不同的人(工程师、运维)在不同的生命周期阶段,都能高效地在系统上工作——让系统保持现有行为,并适应新的应用场景。
DDIA 提出了可维护性的三个设计原则:
1. 可操作性(Operability):让运维不痛苦
系统跑起来只是开始,运维才是持久战。
- 好的可操作性意味着:运维团队能平稳地保持系统运行
- 实践建议:监控先行、一键回滚、文档即代码
一个常见的反例是:系统上线后没有任何监控面板,出问题了只能 SSH 到服务器上翻日志——这种系统运维起来就是一场噩梦。
2. 简单性(Simplicity):让新人不迷茫
这里的“简单”不是指用户界面简单,而是指系统内部的复杂度低。
- 复杂度(Complexity)可能来自:状态空间爆炸、模块之间紧密耦合、诡异的依赖关系、命名不一致……
- 应对方式:合理的抽象——好的抽象可以隐藏大量实现细节,让新人只需要理解抽象层级的逻辑,而不必深入每一行代码
一个简单的系统,新人两周就能上手改bug;一个复杂的系统,老人改一行代码都要战战兢兢。
3. 可演化性(Evolvability):让系统跟得上业务
业务在变,技术在变,系统也必须能变。
- 可演化性意味着:系统能轻松适应变化,而不是每次改需求都要推倒重来
- 实践建议:接口版本化、数据迁移脚本、可插拔架构
DDIA 后续章节中讨论的数据编码与演化(第4章),本质上就是在解决“如何不停机变更 Schema”这个可演化性问题。
三者之间的权衡:没有银弹
读到这里,你可能会问:能不能三者都做到最好?
答案很诚实:很难。
这三个目标之间天然存在张力:
- 追求极致的可靠性(比如同步复制、多机房容灾),可能会牺牲性能和可扩展性
- 追求极致的可扩展性(比如把数据分到 1000 个节点上),可能会牺牲可维护性(系统复杂度飙升)
- 追求极致的可维护性(比如保持架构简单),可能在面对超大规模负载时缺乏可扩展性
可靠性
/ \
/ \
/ 权衡 \
/ \
/____________\
可扩展性 可维护性系统设计本质上就是一系列权衡(Trade-off)的决策。没有“最好”的架构,只有“在当前约束下最合适”的架构。
DDIA 全书的核心方法论就是:帮你理解每个方案的长处和短处,让你在面对具体场景时,能做出有依据的权衡决策。
写在最后
可靠性、可扩展性、可维护性——这三个词看似简单,但它们是评价任何数据系统的元标准。
当你下次面对一个技术选型或者架构决策时,不妨问自己三个问题:
- 这个方案在故障面前表现如何? ——可靠性
- 如果业务量翻 10 倍,这个方案还行吗? ——可扩展性
- 半年后换一个团队来维护,他们搞得定吗? ——可维护性
这三个问题没有标准答案,但问出正确的问题,往往比找到正确的答案更重要。
接下来的章节里,我们会从数据模型与存储引擎开始,一步步深入数据系统的内部——看看数据库到底是怎么把数据存下来、又怎么把它快速找出来的。
下一篇预告:数据模型之战——关系型、文档型、图数据库怎么选?
