赵东炜:Erlang助你迎接多核时代的挑战
——写在《Erlang程序设计》出版之际
另辟蹊径——从容面对容错、分布、并发、多核的挑战。
作为一名程序员,随着工作经验的增长,如果足够幸运的话,终有一日,我们都将会直面大型系统的挑战。最初的手忙脚乱总是难免的,经历过最初的迷茫之后,你会惊讶地发现这是一个完全不同的“生态系统”。要在这样的环境中生存,我们的代码需要具备一些之前我们相当陌生或者闻所未闻的“生存技能”。容错、分布、负载均衡,这些词会频繁出现在搜索列表之中。经历过几轮各种方案的轮番上阵之后,我们会开始反思这一系列问题的来龙去脉,重新审视整个系统架构,寻找瓶颈所在。你可能会和我一样,最终将目光停留在那些之前被认为是无懈可击的优美代码上。开始琢磨:究竟是什么让它们在新的环境中“水土不服”,妨碍其更加有效地利用越来越膨胀的计算资源?
其实,早在多年以前硬件领域上的革命就已经开始,现在这个浪潮终于从高端应用波及常规的计算领域——多核芯片已经量产,单核芯片正在下线——这场革命正在我们的桌面上演。时代已经改变,程序员们再也不能继续稳坐家中装作什么事情也没发生,问题已经自己找上门来了。由单核CPU频率提升带来软件自动加速的时代已经终止,性能的免费大餐已经结束,“生态环境的变化”迫使代码也必须“同步进化”。锁、同步、线程、信号量,这些之前只是在教科书中顺带提及的概念,越来越多地出现在我们日常的编程中,接踵而至的各类问题也开始折磨我们的神经。死锁、竞态,越来越多的锁带来了越来越复杂的问题。
在多核CPU的系统上,程序的性能不增反降,或者暴露出隐藏的错误?
在更强的硬件平台上,程序的并发处理能力却没有得到提升,瓶颈在哪里?
在分布式计算网络中,不得不对程序结构进行重大调整,才能适应新的运行环境?
在系统的关键应用上,不得不为软件故障或者代码升级,进行代价高昂的停机维护?
……
这一系列的问题,归根结底,都是因为主流技术体系在基本模型上存在着与并发计算相冲突的设计。换句话说,问题广泛地存在于我们所写的每一行代码中。在大厦初具规模时,却猛然发现每一块砖石都不牢固,这听来很有一些耸人听闻,但这种事并不罕见。
比如:x = x + n,即使是这个再常见不过的语句,也暗藏着烦恼的根源(你也可以把它称做共享内存陷阱)。从机器指令的角度来思考,这个语句可能做了这么几件事(仅仅只是在概念上)。
(1) mov ax, [bp+x];将寄存器ax赋值为变量x所指示的内存中的数据。
(2) mov bx, n;将寄存器bx赋值为n。
(3) add ax, bx;将寄存器ax加n。
(4) mov [bp+x],ax;将变量x所指示的内存赋值为寄存器ax中的数据。
理论上,这是一个原子操作,在单核CPU下情况也确实如此,但在多核CPU下,就全然不同了。如果在每个核心上都有一个正在执行上述代码的进程(也就是试图对这个代码执行并行计算),问题就出现了。这里的x是在两个进程之间共享的内存。很明显,在第(1)步到第(4)步之间,需要某种机制来保证“某一时刻”只有一个进程正在执行,否则就会破坏数据的完整性。这也就意味着,这样的代码无法充分利用多核CPU的运算能力。也罢,咱们不加速就是了,但更糟糕的是,为了保证不出错,还需要引入锁的机制来避免数据被破坏。现有的主流技术体系全都建立在共享内存的模型之上,像x = x + n这样的代码几乎无处不在,但在多核环境下,每一处这样的代码(逻辑上的或者事实上的)都需要小心地处理锁。更糟糕的是,大量的锁彼此互相影响又会导致更为复杂的问题。这迫使程序员们在实现复杂的功能之余,还要拿出极度的耐心和娴熟的技巧来处理好这些沉闷和易错的锁,且不说是否可能,但这至少也是一个极为繁重的额外负担。
Erlang为了并发而生。20多年前,它的创建者们就已经意识到了这一问题,转而选择了一条与主流语言完全不同的路(还有为数不多的另外几种语言也是如此)。它采用的是消息模型,进程之间并不共享任何数据,因而也就完全地避免了引入锁的必要。对于多核系统而言,完全无锁,也就意味着相同的代码在更多核心的CPU上会很容易具有更高的性能,而对于分布式系统,则意味着尽可能地避免了顺序瓶颈,可以把更多的机器无缝地加入到计算网络中来。甩掉了锁的桎梏,无疑是对程序员们的解放。
不仅如此,Erlang在编程模型上走得更远。它在语言级别提供了一系列的并发原语,通过这些原语,我们可以用进程+消息的模型来建模现实世界中多人协作的场景。一个进程表示一个人,人与人之间并不存在任何共享的内存,彼此之间的协作完全通过消息(说话、打手势、做表情,等等)交互来完成。这正是我们每一个人生而知之的并发模式!软件模拟现实世界协作和交互的场景——这也就是所谓的COP(面向并发编程)思想。
在错误处理上,Erlang也有与众不同的设计决策,这使得实现“容错系统”不再遥不可及。COP假设进程难免会出错——不像其他某些语言一样假设程序不会出错。它假设程序随时可能会出错,如果发生出错的情况,则不要尝试自行处理,而是直接退出,交给更高级的进程来对这种情况进行处理。通过引入“速错”和“进程监控”的概念,我们将错误分层,并由更高层的进程来妥善处理(比如,重启进程,或重启一系列进程)。有了这样的概念作为支撑,构造“容错系统”就会变得易如反掌。在这样的系统之下,软件错误不会导致整个系统的瘫痪,发现错误也无须停机就可直接更新代码,在配置了备份硬件之后,硬件的错误也不会影响服务的正常运行。这么做的结果相当惊人,使用Erlang的电信关键产品,达到了传说中的99.9999999%可用性(即9个9的最高可用性标准)。
Erlang采用虚拟机技术实现,用它编写的程序可以不经修改直接运行在从手机到大型机几乎所有的计算平台之上。这是一项有着20多年历史的成熟技术,有着相当多的成熟库(OTP)和开源软件,这些资产使得它有极高的实用价值。Erlang本身也是开源软件,这扫清了对于语言本身生命力的疑惑。Erlang还是一个充满活力的语言,在它的社区,常常能够见到Joe Armstrong等语言的创建者在回答问题,这一点尤其宝贵。在熟悉了Erlang的思维方法和社区之后,很多人都发出了相见恨晚的感慨。
虽说对于并发而言,Erlang确实是非常好的选择,但这么多年以来,业界对于并发预料之中的增长却一直没有真正发生。此前,这类应用更多地局限在一些相对高端的领域,而Erlang身上浓厚的电信背景,又使得第一眼看来它似乎只适用于电信行业(实际情况远非如此)。长期以来Erlang的使用群体仅局限在一个狭小的技术圈子之内,它处于“非主流语言”的边缘位置已经很久了。这种情况直到最近才有所改观,最近两年,Google的成功使得其引为核心的大规模分布式应用模式广为人知,而多核CPU进入桌面也迫使“工业主流”开始认真对待并发计算。直到此时,解决这类问题最为成熟的Erlang技术,才因为其难以忽视的优势而引起人们的广泛关注。
从历史的眼光来看,在计算机语言的荣誉堂内,上演着一代又一代程序设计语言的繁荣和更替,潮来潮往让人难以捉摸。这与其说是技术,还不如说是时尚。对于Erlang这种有些怪异的小众语言来说,是否真的会成为“下一个Java”?实难预测,而且也不重要。但是有一点已经毫无疑问,那就是“下一代语言”至少也要像Erlang一样,处理好与并发相关的一系列问题(或者做得更好)。也许将来的X++(或X#)语言在吸收了它的精髓之后,又会成为新的工业主流语言。但即便如此,先跟随本书作者开辟的小径信步浏览这些饶有趣味的问题肯定也会大有帮助。
对于想要学习Erlang的读者,虽说语言本身相当简单,但想要运用自如也有一些难度。比如,在适应COP之后会觉得非常自然,但对于有OOP背景的程序员而言,从固有的思维习惯转换到COP和FP上(主要是和自己的思维惯性较劲)需要有一个过程。此外OTP和其他Erlang社区多年积累的财富(这些好比JDK、EJB之于Java)也需要一些时间才能被充分地理解和吸收。但这些有价值的资料大多零星地散落于邮件列表和独立的文档之中,给学习造成了很多不必要的麻烦。现在好了,有了Erlang创建者Joe Armstrong为我们撰写的“官方教程”,这些问题都已迎刃而解。
一般而言,由语言的创建者亲自撰写的教程,常常都是杰作。在翻译的过程中,译者也常常会发出这种赞叹。在本书中,Joe Armstrong不仅全面地讲述了Erlang语言本身,还详细交代了这些语言特性的来龙去脉。为什么要这么设计?除了掌握语言本身之外,能有幸窥见大师精微思辨的轨迹,也是难得的机缘。书中的例子,还会将你为之惊异的那些Erlang特性一一解密。通常是从一个不起眼的小问题开始,从宏观分析到微观实现,层层深入细细道来。问题是什么?要如何建模?该怎么重构?各个版本之间的精微演化全然呈现,但这些微小的改进,最终演化出了那些让人惊喜的特性,整个过程可谓相当精彩。
本书由Erlang中文社区(erlang-china.org)组织翻译。其中,第1章到第14章由金尹翻译,第15章到附录F由赵东炜翻译,全书由赵东炜统稿润色和审校。
由于时间仓促,加之译者水平有限,译文难免会有不足之处,欢迎读者批评指正。
赵东炜
2008年3月于北京