1 从生活说起
观察一下现实的世界, 我们没有共享“内存”(也就是记忆),各有一个大脑,各有各的记忆,之间并不相连。为了改变你的记忆,我会向你发送一个消息:通过说话,或者挥舞手臂。 你倾听、观察,然后改变了记忆产生行为。但是,如果不问你问题或者观察你的反应,我就无法知道你是否收到了我的消息。
这也就是Erlang进程的工作方式。在Erlang的世界中,并发的基本是进程,每个进程代表一个持续的活动。Erlang进程之间没有共享内存,每个进程都有它自己的内存。要改变其他某个进程的内存,必须向它发送一个消息,并祈祷它能收到并理解这个消息。 要确定另一进程收到了你的消息并改变了它的内存,就必须询问它(通过向它发送一条消息)。
相比而言,其它语言或操作系统的线程喜欢采用共享内存和各种锁来完成并发活动,随之而来的是层出不穷地互相踩踏或无尽的等待;相比而言,Erlang进程模式更加安全:不会有人在周围指手划脚,也不用担心有人在下一个微秒出其不意地篡改自已的数据。
2 Erlang的并发进程
2.1 轻量
和我们通常所见的进程或线程概念不同,Erlang实现并发采用的是非常轻量的进程,每个进程消耗的资源很少,调度很快。若采用一台普通的计算机(2.90 GHz的Intel Core i7双核处理器,8GB内存,运行 Ubuntu操作系统),产生20000个进程,平均需要消耗了3.0微秒的纯CPU时间和3.4微秒的总计运行时间。
2.2 巨量
Erlang系统内设最大可以支持262144个进程,如果需要还可以进一步进行扩展,这足以满足绝大多数的应用需求;如果这还不够,那么你真该考虑出去透透气,散散心了。
2.3 管理简化
前面已经提到,一个Erlang程序会包含几十、几千、甚至几十万个小进程,所有这些进程都是独立运作的,它们通过发送消息来相互交流,每个进程都拥有一块私有内存区域。它们表现得就像是一大群人在一个巨大的房间里喋喋不休, 这使得Erlang程序天生易于管理和扩展。假设现在有10个人(进程),但他们有太多的工作要做,我们可以怎么办?找更多的人过来就行(产生更多进程)。我们要如何管理这群人?很简单,大声把命令告诉他们(广播)就可以了。
2.4 无锁的自由
Erlang进程不共享内存,因此使用内存时无需加锁。有锁的地方就会有钥匙,而钥匙是容易丢失的。当你丢了钥匙会发生什么?会不会有些模块一直等信号量,像在等一个不回家的人?当你在软件系统里丢了钥匙,使用锁结构出现问题时也会如此。 分布式软件系统里只要有锁和钥匙,就总会出问题。 Erlang没有锁,也没有钥匙。有进候,无或许就是最大的有吧。
2.5 临终的呐喊
Erlang 进程就像人类一样,有时会死去。但和人类不同的是,当它们死亡时,会用尽最后一口气喊出导致它们死亡的准确原因。 想象一个挤满人的房间里突然有一个人倒下,就在那一刻,他说“我的心脏病发作了” 或者“我吃得太多,胀死了”,Erlang进程就是这么做的:一个进程可能会在临死时说:“我是因为有人要求我除以零而死的”。
更为巧妙的是,Erlang非常贴心地帮进程预留了处理后事的空间。例如两人通过一种不可见的约定连接在一起,这个约定是如果其中一人死亡, 另一人就会处理一切由此产生的问题。 Erlang的错误检测正是使用的这种方式:进程可以相互连接,如果其中一个进程挂了,另一个进程就会得到一个前者死亡原因的说明性消息,大致就是这么一回事。
2.6 公平
强大的并发支持是Erlang的特色之一,在这一点上常常被其他语言争相模仿。不过,Erlang和模仿者之间有个根本的不同点:公平调度。为了做到公平调度,Erlang可谓“不择手段”, 并做到了“令人发指”的地步。为什么要费劲做这些工作呢?对于一个高并发系统来讲, 软实时、低延时、可响应性往往是渴求的目标,同时也是一项困难的工作。尤其是,在系统过载时,多么希望能具有一致的、可预测的服务降级能力。而公平调度则是达成这些目标的最佳手段,Erlang也是目前唯一在并发上做到公平调度的语言。
3 Erlang的容错机制
3.1 免疫哲学
和大多数编程语言相比,Erlang 在如何处理错误方面显得非常独特。
我们一般会认为,语言、 编程环境以及方法论都应该尽其所能来防止错误出现。所有会导致运行时错误的东西都要被避免;如果无法避免,那么这些东西就会被置于解决方案之外,不予考虑。 程序编写完成,会被部署到情况多变的生产环境中。如果在生产环境中出现错误,就会发布新的程序版本。
Erlang 编程环境采取了和人体免疫系统一样的方法,在这个类比下,其他编程语言则只关心 “卫生”情况,以防止细菌侵入人体。当然,这两种方式都非常重要。几乎所有的编程环境都提供了卫生保证机制(程度可能不同),但是Erlang还提供了免疫系统:可以在运行时处理错误,并视此为一种生存能力。
这个是不是有点像西医和中医的哲学思维不同?
3.2 让崩溃来得更猛烈些
Erlang认为,在系统运行过程中,错误一定会发生,这些错误可能是开发人员, 运维人员引起的,也可能是硬件相关的。要想根除掉程序或者系统中的所有错误,是不实际的,甚至是不可能的。
如果不用千方百计地阻止错误发生,而是能够在错误发生时去处理它们,那么我们就可以用这种方式应对几乎所有的程序未定义行为。 这就是“Let it Crash”概念的来源:因为可以处理错误,那么程序员只需处理那些他们知道如何处理的错误,其他的错误都交给另外一个进程(supervisor)或者虚拟机来处理。 因为大多数bug都是暂态的,因此在碰到错误时,简单地把进程重启到一个已知的稳态, 是一个非常不错的策略。
3.3 错误局部化
大家听说 Erlang,往往是因为其对高并发的良好支持。其实,Erlang的核心特征是容错,从某种程度上讲,并发只是容错这个约束下的一个副产品。容错是Erlang 语言的DNA,也是和其他所有编程语言的本质区别所在。如果把软件系统看作是人体的类比,其他编程语言只关注于环境卫生,防止生病;而Erlang 则提供了免疫系统,允许病毒入侵,通过和病毒的对抗,增强免疫系统,提升生存能力。
这个差别给软件开发带来的影响是根本性的。大家知道,对于大型系统的开发、维护来说, 最怕的就是无法控制改动的影响。我们希望每次改动最好只影响一个地方,我们通过良好的模块化设计和抽象来做到这一点。但是如果这个更改不幸逃过了静态检查和测试,在运行时出了问题,那么即使这个改动在静态层面确实是局部的,照样会造成整个系统的崩溃。而在Erlang中,不仅能做到静态层面的变化隔离,而且也可以做到运行时的错误隔离,让运行时的错误局部化,从而大大降低软件发布、部署的风险。
4 结语
从某种意义上讲,Erlang不仅是一门编程语言,更是一个系统平台。它不仅提供了开发阶段需要的支持,更提供了其他语言所没有的运行阶段的强大支持。
其实,在静态检查和测试阶段发现的问题往往都是些“不那么有趣”的问题,那些逃逸出来的bug才是真正难对付的。 特别是对于涉及并发和分布式的bug,往往难以通过静态检查和测试发现,并且传统的调试手段也无法奏效。而Erlang则提供了强大的运行时问题诊断、调试、解决手段。使用Erlang提供的系列工具、自省机制以及强大的并发和容错支持,我们可以在系统工作时,深入到系统内部,进行问题诊断、跟踪和修正。甚至在需要时在线对其进行“高侵入性”的外科手术。
如果说互联网是目前最庞大的系统,相信没有人会反对。那么这个如此庞大的系统能构建起来的原因是什么呢?显然不是因为静态类型,根本原因在于系统的组织和交互方式。互联网中的每个部件都是彼此间隔离的实体,通过定义良好的协议相互通信,一个部件的失效不会导致其他部件出现问题。这种方式和Erlang 的设计哲学是同构的:即crash‐oriented以及protocol‐oriented,是架构大型系统的最佳方式。