简单之美——系统设计黄金法则
最近多次看到系统设计与实现的文章与讨论,再加上以前读过的其他资料以及自己的一些实践教训,让我觉得应该把这些资料汇总整理一下。如果要从讨论不同系统的众多资料中总结一条黄金法则的话,那只有一个词——“简单”;如果用一个英语单词来表达的话,那就是——KISS (Keep It Simple, Stupid!)。
麻省理工方法与新泽西方法(MIT Approach vs. New Jersey Approach)
这个观点来自一篇很经典的文章,Richard Gabriel 在 1989 年写的文章中的一节“The Rise of ‘Worse is Better’”。说来惭愧,我是直到 2011 年 5 月在 IBM T.J. Watson 实验室听报告才第一次听说,当时便印象深刻。后来上普林斯顿的高级系统设计课程,发现这篇文章也在 Reading List 中,要求所有学生阅读然后在课上讨论。
“The Rise of ‘Worse is Better”对比了以 LISP 系统为代表的麻省理工方法和以 Unix/C为代表的新泽西(贝尔实验室)方法。Gabriel 发现相比于 LISP/CLOS 系统完美的设计,Unix/C只是一味追求实现简单,但事实却证明 Unix/C 像终极计算机病毒那样快速蔓延,奠定了今天计算机系统的基础。
让我们来看看这两种不同的设计哲学。
1)MIT Approach
简单性:设计必须简单,这既是对实现的要求,也是对接口的要求。接口的简单要比实现的简单更加重要。
正确性:设计在任何值得注意的方面都要保证正确。不正确是绝对不允许的。
一致性:设计必须保持一致兼容。设计可以允许轻微少量的不简单和不完整,来避免不一致。一致性和正确性同等重要。
完整性:设计必须覆盖到实际应用的各种重要场景。所有可预料到的情况都必须覆盖到。简单性不能过度的损害完整性。
2)New Jersey Approach
简单性:设计必须简单,这既是对实现的要求,也是对接口的要求。实现的简单要比接口的简单更加重要。简单是设计中需要第一重视的因素。
正确性:设计在任何值得注意的方面都要求正确。为了简单性,正确性可以做轻微的让步。
一致性:设计不能过度不兼容一致。为了简单,一致性可以在某些方面做些牺牲,但与其允许设计中的这些处理不常见情况的部分去增加实现的复杂性和不一致性,不如丢掉它们。
完整性:设计必须覆盖到实际应用的各种重要场景。所有可预料到的情况都应该覆盖到。为了保证其它几种特征的品质,完整性可以作出牺牲。事实上,一旦简单性受到危害,完整性必须做出牺牲。一致性可以为实现的完整性作出牺牲;最不重要的是接口上的一致性。
如果觉得这种哲学描述太抽象的话,原文中有一个关于 Unix 中断处理的例子,非常生动。一位 MIT 的教授一直困恼于 Syscall 处理时间过长出现中断时如何保护用户进程某些状态,从而让用户进程能继续执行。他问新泽西人,Unix 是怎么处理这个问题。新泽西人说,Unix 只支持大多数 Syscall 处理时间较短的情况,如果时间太长出现中断 Syscall 不能完成,那就会返回一个错误码,让用户重新调用 Syscall。但 MIT 人不喜欢这个解决方案,因为这不是“正确的做法”。
Unix/C开发于 1970 年前后,那时离 1964 年刚推出的 IBM System/360 没几年,软件刚摆脱硬件束缚,能移植到不同的机器上,从而变成了一种可单独出售的产品。就是这样的一个软件产业的萌芽期,这种“实现简单”的理念被证明是更有效的。那么在今天的互联网时代,这种理念还有效吗?我们再来看下一篇文章。
来自互联网巨头们的教训
这是最近看到的一篇文章,作者从 High Scalability Blog 上总结了几大互联网在设计后台数据中心所遇到的教训(这篇文章总结的非常好,强烈推荐大家读一下)。文章开头就总结了七个互联网公司(Google, YouTube, Twitter, Amazon, eBay, Facebook and Instagram)都提到的 6 点教训:
Keep it simple – complexity will come naturally over time.
Automate everything, including failure recovery.
Iterate your solutions – be prepared to throw away a working component when you want to scale it up to the next level.
Use the right tool for the job, but don’t be afraid to roll your own solution.
Use caching, where appropriate.
Know when to favor data consistency over data availability, and vice versa.
第一点就是“简单”,但和 New Jersey Approach 的原因和内涵有所不同。不同于 Unix 时代相对简单的单机系统,互联网时代的大公司的系统往往都是成千上万台机器,在这样的系统上部署、管理服务(软件)是一项非常有挑战的任务。而为大规模用户提供的一项服务往往会涉及到众多模块、若干步骤。此时“简单”就是要求每个阶段、每个步骤、每个子任务尽量采用最简单的解决方案,这是由于大规模系统内在的不确定性导致的复杂性决定的。
即使做到了每个环节最简单,但由于不确定性的存在,整个系统还是会出现不可控的复杂性。比如,Google 的 Jeff Dean 最近在 UC Berkeley 有个报告介绍他们努力缓解大规模数据中心中的 Long-Tail Latency 难题。问题简单描述如下:假设一台机器处理请求的平均响应时间为 1ms,只有1% 的请求处理时间会大于 1s (99th-Percentile)。如果一个请求需要由 100 个这样的节点一起处理,那么就会出现 63% 的请求响应时间大于 1s,这样的系统完全是不可接受的。面对这个复杂的不确定性问题,Google 他们做了很多工作,权衡各种 Tradeoff,具体请看这个报告。
大规模数据中心,看起来似乎和我们普通的开发人员离得比较远。但最近看 Paul Graham 写的《Hackers and Painters》这本介绍硅谷创业公司的书,发现 Graham 也在多处强调“简单”。
Paul Graham 的《Hackers and Painters(黑客与画家)》
Paul Graham 被称为“硅谷创业之父”。他在 1995 年和 MIT 的 Robert Morris 教授创办了 Viaweb,于 1998 年被 Yahoo!以 4900 万美元收购。2005年,他又创办了Y Combinator 创业孵化器公司,帮助 80 多家创业公司成长起来,其中包括 Dropbox (市值大于 40 亿美元)、Airbnb (市值大于 13 亿美元)等。显然,Graham 有丰富的创业经验。
Graham 在“设计者的品味”一章中写到,“好的设计是简单的”、“简单就是美,正如漂亮的数学证明往往是简短而巧妙的那种”。他提到,有些创业者希望第一版就能推出功能齐全的产品,满足所有的用户需求,但这种想法是致命的。在硅谷创业最忌讳的就是“Premature Optimization”。因为一方面用户需求是多样的,不同人群都有不同的需求;另一方面开发者想象的需求往往和真实的用户需求有偏差。所以,Graham 推崇那种有用户参与反馈的迭代优化的方式。
无独有偶,最近至少听到两个报告提到了 Facebook 的开发模式。当 Facebook 开发一个新的服务,会先让一个小用户群使用,根据用户的反馈来修改功能,同时可以调试程序中的 bug。然后下一版让更大一些的用户群使用,收集用户反馈继续修改程序。如此反馈几次,最后再推向所有用户。这种模式要求再最初设计时尽量简单,从而只需几个月的时间就能推出一个新的功能,然后再不断地优化完善。
到目前为止,谈的工业界偏多一些,但其实在系统领域的学术研究,“简单”法则同样适用。
李凯教授:KISS 原则
普林斯顿大学计算机系的李凯教授是“KISS”原则的坚决贯彻者。几乎每次和李凯老师讨论,他都会强调“Keep it Simple”。李凯老师的做事方式是——只抓住大方向,其他问题尽量简化。
但真正要做到 KISS 原则其实并不容易。我在遇到问题时,往往会从各个方面去考虑问题,其中难免包含了各种细枝末节,这种方式导致问题经常会变得非常复杂。之前讲过这个例子,在移植 TCP/IP 协议栈到用户态时,我觉得有约 10 个功能需要考虑。和李老师讨论,他让我把那些功能分成两类:“必须有(Must Have)”和“可以有(Nice-to-Have)”。当我试了这种方法,发现原来 Must-Have 的功能其实也不过2~3个而已。而最近的例子则是在要设计一个功能让 TCP/IP 连接的 Server 在模拟器中、Client 在真实机器。我考虑是尽量减少模拟器上 OS 的开销,所以打算采用自己写一个设备、然后让用户态程序 Bypass Kernel 直接访问该设备的方案。但李凯老师在了解 OS 开销以后,认为容忍开销、尽量直接使用模拟器自己带的功能,让开发更简单。
这些教训也让我不断地去思考为什么要用 KISS 原则。慢慢地我体会到,KISS 原则目的其实是——“快速推进、逐步优化”。我们设计一个算法,往往可以在大脑中预先思考好,然后直接编程写出来。但是,我们设计实现一个系统,当系统的复杂度超出我们大脑的工作记忆容量时,就无法在大脑中去“模拟”每一个细节。此时,我们应该用最快的速度去把系统建起了,然后再对各个环节进行优化。
这个 KISS 理念并不是计算机系统领域特有的,最早是来源于研制飞机时提出的设计理念。而在其他领域,如果一个任务涉及多个步骤,也同样有效,比如生物研究。我去年前曾看过施一公教授写的一篇文章中也提到了这一点。
施一公教授:”耗费时间的完美主义阻碍创新进取 “
2011年 9 月清华大学施一公教授在科学网上发布了一篇博客《如何做一名优秀的博士生:(二)方法论的转变》,其中介绍到完美主义的危害,其实是从另一个角度来强调“简单”。
施教授讲了他博士后期间的一个故事。一次他的任务是纯化一个蛋白。两天下来,虽然纯化了,但是产量只有 20%。
他不好意思地对导师说,“产率很低,我计划继续优化蛋白的纯化方法,提高产率”。
但导师反问:“你为什么想提高产率?已有的蛋白不够你做初步的结晶实验吗?”
他回敬:“我有足够的蛋白做结晶筛选,但我需要优化产率以得到更多的蛋白。”
导师不客气地打断:“不对。产率够高了,你的时间比产率重要。请尽快开始结晶。”
实践最后证明他导师的建议是对的。
对于这个故事,施一公教授总结如下:
“在大刀阔斧进行创新实验的初期阶段,对每一步实验的设计当然要尽量仔细,但一旦按计划开始后对其中间步骤的实验结果不必追求完美,而是应该义无反顾地把实验一步步推到终点,看看可否得到大致与假设相符的总体结果。如果大体上相符,你才应该回过头去仔细地再改进每一步的实验设计。如果大体不符,而总体实验设计和操作都没有错误,那你的假设(或总体方向)很可能是有大问题的。
这个方法论在每一天的实验中都会用到。比如,结构生物学中,第一次尝试纯化一种新的蛋白不应该追求每一步的产率,而应该尽量把所有纯化步骤进行到底,看看能否拿到适于结晶的蛋白。第一次尝试 limited proteolysis,不应该刻意确定 protease 浓度或追求蛋白纯度,而是要关注结果中是否有 protease-resistant core domain。从 1998 年开始自己的独立实验室到现在,我告诉所有学生:切忌一味追求完美主义。
我把这个方法论推到极限:只要一个实验还能往前走,一定要做到终点,尽量看到每一步的结果,之后需要时再回头看,逐一解决中间遇到的问题。 ”
结语
我想从各个角度去阐释“简单之美”,但到最后感觉这篇文章就是一个大杂烩。既然如此,那我就再加一点料。
Elon Musk 是现实世界中的钢铁侠,他先后创办了网络支付公司 PayPal、电动汽车公司 Tesla 以及空间探索公司 Space X。目前 Space X 研制的“猎鹰”火箭已成功试飞,并得到 NASA 16 亿美元的合同。为了减低成本和提供可靠性,Space X 设计的火箭也到处渗透着“简单”:“在他们的猎鹰 1 号运载火箭上,并没有很多专利,科学家们不在乎,只要火箭能飞就行。火箭用的主发动机也不是 21 世纪的最新设计,而是 1960 年代的老古董,只有一个燃料喷射器。它很老,但很可靠。”
参考资料
1、The Rise of ‘Worse is Better’
3、Scalability Lessens from Google, YouTube, Twitter, Amazon, eBay, Facebook and Instagram
4、Achieving Rapid Response Times in Large Online Services, Jeff Dean, Google.