在复杂性的海洋中导航
设计一个计算机系统与设计一个算法截然不同。Lampson 指出,系统设计的需求更模糊、复杂且易变;内部结构和接口繁多;成功的标准也远不清晰。
设计师常在可能性的海洋中挣扎。因此,比寻找“最佳”方案更重要的,是避免糟糕的选择,并在各部分之间明确划分职责。
免责声明 (Disclaimer)
“这些不是新颖的理论、万无一失的秘诀、系统设计的定律、精确的公式,也非始终适用或保证有效。它们仅仅是提示 (Hints)。”
Lampson 的罗盘:设计框架
为了组织这些设计智慧,Lampson 构建了一个二维框架,将设计提示按照其目的(Why)和应用领域(Where)进行分类。
功能性 (Functionality)
系统能工作吗?关注正确性、简单性和接口设计。
速度 (Speed)
系统够快吗?关注性能优化、资源管理和权衡。
容错性 (Fault-tolerance)
系统能持续工作吗?关注可靠性、恢复能力和一致性。
这些目标应用于设计的三个层面:完整性 (Completeness)、接口 (Interface) 和 实现 (Implementation)。
核心枢纽:交互式设计矩阵 (Figure 1)
* 点击矩阵单元格查看详细内容
层面 (Where) ↓
箴言详情
请选择左侧矩阵中的一个单元格,以探索该分类下的具体设计原则。
一、功能性:它能工作吗?
核心在于定义良好的接口和保持简单性。
原则:保持简单 (KISS)
做好一件事 (Do one thing well)
接口应捕获抽象的最小本质。当接口试图做太多时,实现会变得庞大、缓慢和复杂。
不要过度泛化 (Don't generalize)
泛化通常是错误的。不要承诺提供实现者无法交付的功能,特别是只有少数客户需要的功能。
确保正确 (Get it right)
抽象和简单性都不能替代正确性。警惕抽象可能带来的危险(例如不恰当的抽象可能导致性能灾难)。
“完美不是无以复加,而是无可删减。” — 圣埃克苏佩里
原则:接口是契约
定义接口是系统设计中最重要也最困难的部分,因为它必须满足三个冲突的要求:简单、完整,且允许足够小和快的实现。
不要隐藏能力 (Don't hide power)
抽象的目的是隐藏不希望看到的属性;理想的属性不应被隐藏。如果底层能快速完成某事,高层抽象不应将其埋没。
留给客户 (Leave it to the client)
接口只解决一个问题,其余留给客户。这能结合简单性、灵活性和高性能(如 Unix 哲学)。
保持基础接口稳定
接口体现了系统多个部分共享的假设。改变公共接口的代价巨大,系统必须通过多年稳定的接口进行关联。
原则:务实的实现
计划扔掉一个原型
如果系统有新意,第一个实现必须完全重做才能达到满意。有计划地构建原型成本更低。(源自《人月神话》)
保守实现的秘密
秘密是客户程序不允许依赖的实现假设(即可变的部分)。接口定义了不变的部分。这有助于系统的演进。
区分常态和最坏情况
常态必须快,最坏情况必须有进展。为常态优化(如缓存)是值得的,但也要为危机预留资源以确保系统不会死锁。
二、速度:它足够快吗?
关注性能优化、资源管理和负载控制的策略。
性能的现实:关注重点
“让它快,而不是通用或强大。(Make it fast, rather than general or powerful.)”
提供快速的基础操作远胜于缓慢的强大操作。程序大部分时间都在做简单的事情(加载、存储、测试、加一)。
通常 80% 的时间消耗在 20% 的代码上。但直觉往往无法找到这 20%,因此必须使用测量工具来定位热点。
核心技术:缓存与提示
两者都是保存计算结果以提高性能,但本质不同。
缓存 (Cache)
存储昂贵计算的结果。缓存是真相的副本。
- 必须正确:数据变化时必须更新或失效。
- 通常通过关联查找访问。
- 例子:硬件内存缓存、Web缓存。
提示 (Hint)
也是保存的结果,用于加速常规执行。提示是真相的“猜测”。
- 可能错误:在采取不可恢复的操作前,必须检查其正确性。
- 不一定通过关联查找访问。
- 例子:网络路由表、文件系统磁盘地址映射。
执行策略
分割资源 (Split resources)
优先固定分割资源,而非共享。专用资源通常访问更快,行为更可预测(如寄存器 vs 内存)。
使用蛮力 (Use brute force)
随着硬件成本下降,直接但计算量大的方案优于复杂但难以分析的方案。
后台计算与批处理
将工作推迟到后台空闲时间处理(如垃圾回收)。如果可能,使用批处理,因其通常比增量处理成本更低。
资源管理哲学
安全第一 (Safety first)
努力避免灾难(如系统颠簸 Thrashing),而不是追求最优。通用系统无法优化资源使用。应提供过剩容量。
减载 (Shed load)
通过减载来控制需求,而不是允许系统过载。方法包括拒绝新用户、限制服务或丢弃数据包。
三、容错性:它能持续工作吗?
“可靠性的必然代价是简单性。” (C. Hoare)
黄金法则:端到端 (End-to-End)
对于一个可靠的系统,应用级的错误恢复是绝对必要的。任何其他的错误检测或恢复在逻辑上都不是必需的,而严格来说是为了性能。
示例:文件传输
要确信文件正确到达目标磁盘 B,必须从 B 读取并与源 A 校验。中间环节(网络、内存)的检查是不够的,因为数据可能在其他地方被破坏。
中间检查只是为了减少重试的工作量(性能问题),与最终可靠性无关。
机制:原子性与日志
使操作原子化 (Make actions atomic)
原子操作(事务)要么完成,要么完全无效(All or Nothing)。
这极大地简化了故障恢复,因为无需处理操作的中间状态。实现通常需要操作是可重启的(幂等的)。
记录更新日志 (Log updates)
日志是一种简单、可靠的数据结构(仅追加),用于记录对象状态的真相。
通过重放日志(记录了操作和参数),可以在崩溃后恢复状态。这要求更新过程是函数式的。
历久弥新的智慧
尽管这篇论文发表于 1983 年,但它所探讨的关于简单性、接口设计、性能权衡和可靠性的核心思想,在今天的云计算、分布式系统和微服务架构时代依然具有极高的指导价值。这些提示是跨越技术周期的系统设计基石。
“我本人至少违反过这些规则中的大多数一次,并且几乎总是后悔不已。”