有关"非正常终止"
多年以前在 USENIX HotOS 2003 论文集中看到了 一篇 关于 “Crash only software” 的论文,当时有一些想法,但很多没有认真地实践。最近做的东西用到的这方面的设计方法比较多,总结一下写出来。
在设计系统时,很多时候我们会花很多的精力去考虑正常终止的情况,对于希望尽可能少出现 downtime 的系统来说,这很可能是不必要的。
简而言之,程序有两种终止方式,正常的和不正常的;正常的终止是可预期的,非正常终止不一定发生在什么时候—-因为程序设计缺陷、系统管理员杀掉、硬件故障如电源出现问题,等等。
很多时候处理正常终止所需的代价要比非正常终止来的更大。例如在正常的 OS 关机过程中,内核需要通知所有的服务进程退出(这一点目前还是必要的,因为不是所有的程序都设计了从非正常停机中恢复)、通知其他节点自己下线了、甚至某些早期的 Windows 版本还会关闭网络连接,等等,最后还要把内存中到目前为止还没写盘的数据刷到磁盘上,然后再停止运行。而恢复时,则往往会有一些技术来帮助系统尽快地恢复到可以运行的状态,许多时候,在崩溃后引导系统和在正常关闭后引导系统所需的时间是非常接近的。
这造成了一个很奇怪的现象:一方面,我们花费了相当多的时间精力去撰写正常关机所需的代码,而另一方面,这些代码的存在往往会导致系统恢复的时间变得更慢一些。(这篇论文写于 2003 年,其中举了一些实际的例子,如 RedHat 8 正常重启所需要的时间是104秒,而非正常重启则只需75秒;JBoss 3.0分别是47秒对39秒,Windows XP则是61秒对48秒)。还有就是,增加的这些复杂的代码对于长时间运行的系统实际上是很少用到的,系统能够正常地持续运行的时间越长,这些代码就越少会执行到。
因此,对于很多互联网应用来说,既然有那么多的可能性导致异常终止没办法避免,更好的设计就应该是程序中只去考虑非正常终止的问题。程序把启动过程也作为异常终止之后的恢复来实现,而正常终止直接让程序崩溃就可以了,主要的精力应该放到"让恢复过程变得更快和更可靠"以及"让程序运行的过程中任何一个点发生的异常终止都不会导致恢复出现问题"两件事上。
如何实现
原文中列举了一些实现策略,下面是我自己的一些总结:
- 需要持久保存的状态由专用的组件来管理,避免在应用逻辑中直接操作;
- 持久化组件只考虑存储的一致性,可以采取 Copy-on-write 和异步写操作之前先同步写日志等方法来改善恢复速度;
- 分布式系统中,对方未确认数据已经到了可靠的地方,就不要认为处理已经完成;远程调用皆设超时并做对应处理;调用者同时也作为监督者,在认为服务出现问题时可发起重启服务的请求;
- 组件之间尽量隔离(当然这也是面向对象设计的相当基本的要求),避免接口以外的直接交互;
- 对资源使用进行限制,例如通过 OS 限制进程能够使用的内存和 CPU 时间以及实际运行的最长时间等等;
做到这些以后直接将正常停止改为 kill -9 即可。当然,尽量避免程序 bug 导致的崩溃仍然是非常重要的。