Erlang

Table of Contents

1. 错误处理哲学

如果你来自c这样的语言,这听起来会非常奇怪。在c里我们被教导要编写预防性代码,程序应当检查它们的参数以避免崩溃。在c里这么做很有必要:编写多进程代码极其困难,而且绝大多数应用程序只有一个进程,所以如果这个进程让整个应用程序崩溃,麻烦可就大了。这意味着需要大量的错误检查代码,他们会和非错误检查代码交织在一起。

在erlang,我们所做的恰恰相反。我们会把应用程序构建成两个部分:一部分负责解决问题,另外一部分负责在错误时纠正他们。负责解决问题的部分会尽可能的少用防御性代码,并假设函数的所有参数都是正确的,程序也会正常运行。纠正错误的部分往往是通用的,因此同一段错误纠正代码可以用在许多不同的应用程序里。举一个例子,如果数据库的某个事物出了错,就应当简单地中止该事务,让系统把数据库恢复到出错之前的状态。如果操作系统里某个进程崩溃了,就让操作系统关闭所有打开的文件或套接字,然后让系统恢复到某个稳定状态。

这么做让任务有了清楚的区分,编写解决问题的代码和错误修复的代码,让两者不会交织在一起,代码的体积可能会因此显著变小。

让程序在出错时立即崩溃,通常是一个很好的主意,事实上他有不少优点:

  1. 不必编写防御性代码来防止错误,直接崩溃就好。
  2. 不必思考应对措施,而是选择直接崩溃,别人会来修复这个错误。
  3. 不会使错误恶化,因为无需在知道出错后进行额外的计算。
  4. 如果在错误发生后,第一时间举旗示意,就能得到非常好的错误诊断,在错误发生后继续运行,经常会导致更多错误发生,让调试变得更加困难。
  5. 并且错误恢复代码时,不用担心崩溃的原因,只需要把注意力放在事后清理上。
  6. 它简化了系统架构,这样我们就能把应用程序和错误恢复当成两个独立的问题来思考,而不是一个交叉的问题。

找其他人修复: 别人来修复某个错误,而不是自己动手,是个不错的主意,它能够促进专业化。如果我需要做手术,就会去找大夫,而不是尝试自己操作。如果我的汽车出了点小问题,车上的控制电脑会尝试修复它,如果修复失败,问题会变得更加棘手,就必须把车拉到修理厂,让其他的人来修理。如果erlang进程出了点小问题,可以尝试用try/catch来修复它,但如果修复失败,就应该直接崩溃,让其他进程来修复这个错误。

2. 《面对软件错误构建可靠的分布式系统》

2.1. 进程通信和名称

因此,获取进程的名字是安全性的关键因素。因为进程名是不可仿造的,所以只要我们能够将关于进程名字的知识限制在可信进程的范围内,我们的系统就肯定是安全的。在许多古老的宗教信仰中,人们都相信人类可以通过灵魂的真名来支配灵魂,以获得超越灵魂的力量。一旦获知了灵魂的真名,就可以获得超越它的力量,并且可以用这个真名来驱使灵魂去做很多事。COPL采用的是相同的思想。

2.2. 远程错误处理

远程错误处理有许多好处:

  1. 错误处理代码和出错代码运行在不同的控制线程中。
  2. 解决问题的代码不会被处理异常的代码扰乱。
  3. 该方法可以用于分布式系统,所以一个单节点系统的代码移植到一个分布式系统中只需对错误处理代码做很少的修改。
  4. 系统可以在单一节点系统上构建与测试,然后无需进行大的修改就可以部署到多节点系统上。

2.3. 错误处理和操作系统

错误恢复、运行时修改系统的代码是许多真实系统需要的两项典型的非功能特性。通常的编程语言和系统对编写已经定义好的功能行为的代码提供了强力的支持,但是对程序的非功能性部分的支持却很贫乏。

在大多数的编程语言中,编写纯的函数(其值确定地依赖于函数的输入)是容易的2,但是要做到修改运行时系统的代码,或以一种通用的方式处理错误,或保护我们的代码不受系统部分发生的故障的影响这一类事情,却要困难得多,有时甚至是不可能的。因此,程序员运用了操作系统提供的服务——操作系统通常以进程的面貌提供了保护区域、并发机制等等。

从某种意义上讲,操作系统提供了“被编程语言设计者遗忘了的东西”。但是在Erlang这样的编程语言中,操作系统是几乎不需要的。OS真正提供给Erlang的只是一些设备驱动程序,而OS提供的诸如进程、消息传递、调度、内存管理等等机制都不需要。

用OS的机制来弥补编程语言的不足所带来的问题是,操作系统的低层机制不能够轻易地被改变。例如操作系统中关于什么是进程的概念以及进程间调度的策略都不能修改。

通过给程序员提供轻量级的进程和关于错误检测和处理的基本机制,应用程序的编写者就很容易地设计和实现他们自己的应用操作系统,这种应用操作系统是专为他们的特定的问题的特征而特别设计的。OTP系统——用Erlang编写的一个应用程序——便是此中一例。

2.4. 运行时检查错误

关于如何将软件模块化是有颇多争议的。从开始Burroughs的Espol语言到后来的Mesa、Ada语言,编译器编写者们总是把硬件系统想得很完美,并主张由他们通过静态的编译时类型检查来提供良好的隔离性。与编译器编写者们相反,操作系统设计者们则主张运行时检查,并主张将进程作为保护单位和故障单位。

尽管编译器检查和由编程语言提供的异常处理确实有用,但是从历史上看,人们似乎更偏向于用运行时检查加进程的方式来达到故障封闭的目标。因为这种方式具有简单性这一优势——一旦一个进程或它的处理器出错,只管停下它!这种方式中进程就充当了一种干净的模块单位、服务单位、容错单位、出错单位的角色。

故障被限制在速错的软件模块之内。

2.5. 故障单元隔离

进程连接对于建立进程群组(group)是有用的,进程群组中的一个进程出错,所有进程都将死掉。通常我们把属于一个应用的进程连接起来,并且让其中的一个进程充当“监视者”的角色。监视者被设定来捕获退出信号。如果进程群组中有任何一个进程出错了,群组中除了监视者以外的其它所有进程都将死掉,而由监视者来接收群组中的进程的出错消息,这些出错消息描述了故障原因。

这些结论并不新奇。早在20多年前Jim Gray就得出了非常类似的结论,他曾经在《Why do computers stop and what can be done about it》这篇非常通俗的文章中描述过。他说: 与硬件系统一样,软件的容错性关键在于把大的系统逐级分解成模块,每一个模块既是提供服务的最小单位,也是发生故障的最小单位,一个模块的故障不会传播到模块之外。……进程要想达到容错性,就不能与其他进程有共享状态;它与其他进程的唯一联系就是由内核消息系统传递的消息。

各个软件部件不能很好地彼此隔离,是许多流行的编程语言不能够用来构建健壮的软件的主要原因。安全性的本质,在于要能够将互不信任的程序隔离起来,在于要保护基本平台不受这些程序的破坏。隔离在面向对象系统中是相当困难的,因为对象很容易被别名化(aliased)。