erlang/OTP 平台(学习笔记)(三)

分布式 Erlang

借助于语言属性和基于复制的进程通信,Erlang程序天然就可以分布到多台计算机上。要问为什么,且让我们来看两个用Java或C++这类语言写成的进程,它们运作良好并以共享内存为通信手段。假设你已经搞定了锁的问题,一切精准而高效,但就在你试图将其中一个线程挪到另一台机器上时,问题出现了。或许是为了利用更高效的计算能力和内存,或许是为了预防两个线程在硬件故障造成的宕机中同时挂掉,无论如何,这一刻降临时,程序员往往被迫重新设计代码结构,以便配合新的分布式环境中迥异的通信机制。显然,这将耗费大量的开发成本,而且很可能会引入数年才能彻底清除的bug。
Erlang程序却不受这些问题的影响。Erlang规避了数据共享并通过复制进行通信,这使得Erlang代码可以直接分布到多台机器上。在命令式语言里用线程编程时,各部分代码往往会因数据共享引入复杂的依赖关系;这类问题在Erlang中则很少见。今天能跑在你的笔记本上,明天就能跑在集群上。
Erlang应用通常可以直接分布到多个网络节点上,这同时意味着伸缩性问题也简化为一个数量级。你仍然需要考虑好各类进程的职能,每类进程需要运行多少个实例,在哪些机器上运行,怎样均衡负载以及怎样管理数据;但至少以下这类问题不用再劳你费心了:“我到底该怎么切分现有的程序才能搭建出冗余的分布式系统?”,“它们之间该怎么通信?”,还有“我该怎样得体地处理故障?”。

Erlang运行时系统和虚拟机

标准Erlang实现的核心是一个称作Erlang运行时系统(ERTS)的应用:这是一大块用C语言写成的代码,负责Erlang中所有底层的玩意儿。通过它你才能跟文件系统和终端打交道,它还处理内存,实现Erlang进程的也是它。ERTS知道如何将这些进程分布到现有的CPU资源上才能充分发挥计算机硬件的能力。同时,哪怕你只有一个单核CPU它也能实现Erlang进程的并发执行。ERTS还负责处理进程间的消息传递,并使处在不同机器上运行在各自的ERTS中的进程能够像身处同一台机器上一样进行通信。Erlang中所有需要底层支持的东西都由ERTS处理,所以ERTS移植到哪个平台Erlang就能在哪个平台上跑。
ERTS中特别重要的一个部分就是Erlang的虚拟机模拟器:这是执行Erlang程序经编译后产出字节码的地方。这个虚拟机也就是Bogdan Erlang抽象机(BEAM ) ,它非常高效:虽然我们也可以将Erlang程序编译为本地机器码,但一般没有那个必要,因为BEAM模拟器已经够快的了。注意虚拟机和ERTS之间并没有明确的界线;通常人们(包括我们自己)口中的Erlang VM指的就是模拟器加上运行时系统。
运行时系统中有许多有趣的特性,若不在文档中挖地三尺或是长期浸淫于Erlang邮件列表,你是不会知道的。它们正是Erlang能同时处理那么多进程的精要之所在,也是Erlang如此特别的原因之一。Erlang语言的基本哲学加上实现者所采取的务实方案,共同为我们带来了异常高效、面向生产的稳定系统。

我们将讨论促成了Erlang的强大和高效的3个重要方面:

调度器——处理运行中的Erlang进程,令所有就绪的进程共享可用的CPU资源,并在新消息到达或发生超时的时候唤醒相应的睡眠中的进程;

IO模型——′防止系统在进程与外部设备通信时阻塞,令系统平稳运行;

垃圾回收器——回收不再使用的内存。
 

1.调度器

经过多年的演进,ERTS的进程调度器提供了其他平台无法比拟的灵活性。它最初的设计目标是在单CPU上并发运行轻量级Erlang进程,而不是关心底层用什么操作系统。ERTS运行的时候通常就是单个操作系统进程(在操作系统的进程列表中一般名为beam或werl),就跑着管理所有Erlang进程的调度器。
随着线程在大多数操作系统中的普及,ERTS也有所变化,开始将I/O系统这类东西从运行Erlang进程的线程中拿出来,放到独立的线程中去,但完成主体工作的线程仍然只有一个。如果你用的是多核系统,就必须在同一台机器上运行多个ERTS实例。Erlang/OTP第11版中增加了对称多处理器( SMP)支持。这是一项重大突破,令Erlang运行时系统可以在内部使用不止一个进程调度器,每个占用一个独立的操作系统线程。
这意味着现在Erlang进程可以以n :m的方式映射到操作系统线程。每个调度器处理一个进程池。可并行运行的Erlang进程最多能有m个(每个调度器线程执行一个),但同一池内的进程仍像之前所有进程共用一个调度器那样分时运行。在此基础之上,进程可以在进程池之间迁移以便维持可用调度器上的负载均衡。在最新的Erlang/OTP发布版中,甚至可以根据机器上CPU的拓扑情况将进程绑定到特定的调度器上,从而更好地利用硬件的缓存架构。这意味着,大多数时候,作为一名Erlang程序员你不用担心手头有多少CPU或有多少个核:你只要中规中矩地写程序,并尽量将程序切分为尺寸适中的并行任务就好,负载均衡之类的事情就让Erlang运行时系统去操心吧。不管是单核还是128核——都一样,只会更快。

2.I/O与调度

很多并发语言都有的一个毛病就是它们没怎么拿I/O当回事儿。单个进程进行IO时,它们几乎都存在整个系统或大半系统阻塞的问题。这真是既恼人又没有必要,尤其是Erlang早在二十年前就已经解决了这个问题。在前一节,我们曾讨论过Erlang的进程调度器。除了处理进程调度,调度器还替系统优雅地处理了IO问题。在系统的最底层,Erlang以事件驱动的方式处理所有IO,当数据进出系统时,程序可以以非阻塞方式完成数据处理。这降低了连接建立和断开的频次,还避免了OS层面上的加锁开销和上下文切换。
这是一种高效的IO处理方法。可惜,程序员往往难以分析和理解这种技术,这也是为什么只有在明确要求高可靠性和低延迟的系统中才能见到这种技术。早在2001年,Dan Kegel就在他的论文The C10K Problem中描述过这个问题,虽然现在已经略显过时,但这篇文章仍然很值得一读。它针对这个问题及可能的解决方案给出了良好的综述。这些方案实现起来全都既复杂又痛苦,·这正是Erlang运行时系统替你包办这些问题的原因。Erlang在进程调度器中整合了基于事件的I/O系统。事实上,你一点儿都不用操心就能享受一切便利。这让用Erlang/OTP构建高可靠性系统变得轻松了很多。

3.进程隔离与垃圾回收器

虽然实现相对简单,Erlang程序却不太会像其他语言开发的系统那样在GC时遭受停顿。这主要因为Erlang进程之间的隔离:每个进程所使用的内存都是自己的,随进程的创建和结束而分配和释放。听起来好像没什么要紧,实则不然。首先,这意味着垃圾回收器可以在不影响其他进程运行的前提下单独暂停目标进程。其次,单个进程占用的内存通常较小,遍历可以快速完成。(也有内存占用量大的进程,但这些进程一般不用做出快速响应。)再次,调度器知道每个进程最后一次运行的时间,如果某个进程自上次垃圾回收后什么也没干,调度器会跳过它。正是这些因素让Erlang既可以轻松使用垃圾回收器,又可以保证较短的停顿时间。除此以外,有时候进程自派生到完工,再到退出,根本就没有触发过垃圾回收。这种情况下,进程的作用相当于一块昙花一现的内存,除自动分配和释放外,没有任何额外的开销。

本节所描述的运行时系统的特性使Erlang程序能够充分利用可用的CPU来运行大量进程、执行IO操作,并自动回收内存,与此同时还能维持软实时响应能力。了解了平台这些方面的知识,便可更好地理解自己的系统自启动后的各种行为。
 

你可能感兴趣的:(erlang,学习,笔记)