程序语言有其独特的思想。Python 有 Zen of Python,Golang 说 Do not communicate by sharing memory; instead, share memory by communicating。然而,它们并未提升到 worldview 的高度。Joe 老爷子,在其博士论文中,却明确提到了他心目中的一门新语言的 worldview:
everything is a process.
process are strongly isolated.
process creation and destruction is a lightweight operation.
message passing is the only way for processes to interact.
processes have unique names.
if you know the name of a process you can send it a message.
processes share no resources.
error handling is non-local.
processes do what they are supposed to do or fail.
其实它和 OS 的 process 概念几乎一样,就是 PCB(Process Control Block),以及围绕 PCB 的 heap,stack,mailbox 等内存空间。只不过,erlang 的 process 的 memory footprint 很小很小,一个刚初始化的 process,也就占用几百个 words(32bit CPU 下,1 word = 4 bytes,64bit CPU,1 word = 8 bytes)。所以,erlang VM 能创建的 process 理论上取决于内存的大小,我们可以轻易创建百万级,甚至千万级的 process(32bit CPU 的 pid 用 28 bit,64 bit CPU 的 pid 用 60 bit,大家可以自行算算 process 的上限)。erlang 的 process 源自 actor model 的思想,它是几种常见的 concurrency model 的一种(其它的 model 请参考我的文章:Concurrency)。process 和 process 通过发送和接收消息来处理各种事务。
erlang 的错误处理机制非常独特 —— error 的 bubble up 是靠 process 之间的 link 完成的。link 是一个作用域双方互相监视的机制,有点像自战国商鞅变法以降,古代中国常有的保甲连坐制度:一人犯法,连坐者也要受刑。两个 link 起来的 process —— 我们暂且赋予他们名称为小明和小红 —— 如果小明挂了,小红会收到 EXIT signal,如若不作处理,正常情况下也会挂掉,而小红之死,会进一步触发和小红 link 起来的其他 processes,从而引发和小明有关的整条利益链上的震荡。这便是著名的 let it crash 思想。这种独特的机制让 error handling 实现了真正意义上的 non-local:一切与其相关的 process,都会受其影响,就像是对待癌细胞一样,除非有对症的法子,否则相关的细胞统统杀死,绝不姑息。反观其他语言的 error handling,与其说是 handling,不如说是 hide —— 就像边关报急,烽火连天,兵部却将其压下,回报圣上天下太平,然后马照跑,舞照跳。
link 是双向的连坐机制,有时候对于一些影响不大的问题,我们并不需要如此强有力的手段,于是有了 monitor。monitor 像是战场上的斥候,监视一个 process 的异动,一旦有变,立即回禀一个消息。收到消息的 process 可以选择按兵不动,或者随机应变。
erlang/OTP 的 supervision tree 就是基于 link/monitor 这样非常简单的机制构建起来的。而之所以 erlang 可以任性的 let it crash,其中一个很大的原因是 process 非常轻量,构建和销毁的代价无非就是创建和回收若干个 C structs。
这些问题我自个也未完全明白(尤其是 link —— 到目前为止,我连它的 source code 在哪都不知道),不过,带着问题学习总是好的。
先说 message 的传递。我们且将 distributed erlang 放在一边,只考虑 single node。在 erlang VM 里,小明发个 message 给小红,其实就是把 message body 从小明的 heap 里拷贝到小红的 heap 里(或者 heap fragment 里,这个区别暂时不需要关心),然后往小红的 mailbox 尾部添加一个指向拷贝后的 message body 的 message control block。如果小红是网红,同一时刻小刚也可能给小红发 message,那么小明就要和小刚竞争,这个时候就需要锁。这把锁是 per process 的,所以很轻。
了解了这个过程,我们就回答了前两个问题。message 不会在传递的过程中丢失,而且这是 exactly once 的 message passing 的场景(我之前的文章 ZeroMQ及其模式 中讲到了分布式系统 exactly once 是不可能的,想想看,这里为什么可以?)。如果小红对小明不理不睬,小明对小红不离不弃,矢志不渝地发消息,会发生什么事?小红的 mailbox 会一直增长下去,直到海枯石烂 —— 或者说内存耗尽。这时 VM crash,所有 process 一起玩完。
调度是 erlang VM 的一个重头戏。那么多 process 如何合理调度以保证 soft realtime?我们知道,谈到调度,就不得不谈 cooperative multitasking 和 preemptive multitasking 这两种操作系统使用的主要的调度方式。
cooperative multitasking 把调度的控制权交给各个 process,相信「人性本善」,每个 process 都是善良的,它们使用一些 CPU 后就会主动做 context switch,这种方式用于一些史前的操作系统:MacOS,Windows 3.X,还有各个公司的 proprietary OS,如 NetScreen 的 ScreenOS。cooperative multitasking 调度的时机是可预知的(因为是 process 自己的代码去 yield),实现起来简单,但很难保证每个 process 对时机把握得得尽善尽美,一旦有不合理实现的 process,通过无休止的大循环占用了过多的 CPU,就会把其他 process 饿死。
preemptive multitasking 是前者的改进,调度的控制权被牢牢把控在 OS 手上,系统为每个 process 的 CPU 使用时间设定一个上限,通过 CPU 时钟中断触发 scheduling,如果当前 process 到达这个上限后会被调度出去。这种方式调度的时机不可预知,可能发生在 process 代码的任何位置,所以实现起来,尤其是 context switch 会复杂一些。但它对应用程序的作者是福音:他不需要操心自己该什么时候该 yield 好让应用程序被调度出去。
erlang VM 的 scheduler,是标准的 scheduler —— 每个 core 上一个逻辑意义上的 scheduler,有多级队列,按照 process 的优先级区隔。max 是 erlang 系统内部保留的优先级,往下是 high / normal / low,按这个顺序依次调度。normal 和 low 尽管在一个队列里,low prio process 要 N 次访问之才还会被调度。scheduler 还会处理其他活儿,比如 ports。ports 是 erlang 和外部世界(file,socket,driver 等)的接口,我们暂且按下不表。
为了更好的 load balance,scheduler 在无事可做时可以去别的 scheduler「偷」 run queue 里的 process 过来运行;同样的,在 run queue 过长时 scheduler 也会主动地试图将自己的 process 迁移(migrate)到别的 scheduler 上。这些细节我们都先按下不表。
erlang 的设计体现着作者对 latency 的重视。golang 在 communication 时才有可能发生 context switch,GHC 据说在内存分配时才发生。这些scheduling 的粒度不小且很不均匀,很难保证稳定地 latency。唯有 erlang,在 VM 层面像工匠般仔细地计量某个 process 的 throughput(reductions),让每个 process 雨露均沾。当然,这些处理是在牺牲局部效率的情况下完成的。老子说:将欲取之,必固与之。妥协是程序语言为达到目的所付出的代价。
OTP
OTP 的全称是 Open Telecom Platform,这个名称其实已经不重要了。基本上,谈到 erlang,我们都会谈到 OTP,它已经成为 erlang 的一部分。
如果把 erlang VM 看做是 OS 的 kernel,那么 OTP 就是这个 kernel 的其他组成部分:stdlib,initd,syslog,OAM,etc. 此外,OTP 还为 erlang 提供了不少影响深远的设计模式:gen_server,gen_event,gen_statem,supervisor,application,以及 release 等。