专栏名称: 程序人生
十年漫漫程序人生,打过各种杂,也做过让我骄傲的软件;管理过数十人的团队,还带领一班兄弟姐妹创过业,目前在硅谷一家创业公司担任 VP。关注程序人生,了解程序猿,学做程序猿,做好程序猿,让我们的程序人生精彩满满。
目录
相关文章推荐
程序员小灰  ·  清华+北大《DeepSeek学习手册》(全9册) ·  2 天前  
程序员的那些事  ·  大家有没有发现一个奇特现象:你能在一个公司工 ... ·  2 天前  
程序员小灰  ·  小灰的基金,终于开始赚钱了! ·  2 天前  
OSC开源社区  ·  【直播预约】开源、可定义数据中台AllDat ... ·  4 天前  
51好读  ›  专栏  ›  程序人生

上帝说:要有一门面向未来的语言,于是有了 erlang

程序人生  · 公众号  · 程序员  · 2017-04-13 21:19

正文

今个谈谈 erlang。我之前谈过不少语言:

elixir:灵丹妙药?or 徒有其名?

Javascript: 世纪机器语言?

golang五周岁

这些文章流于表面,更多是简单的介绍。这篇文章不同,因为 erlang 并不是一门新语言,简单介绍它的人不算少,我希望这篇文章能在深度上有所区别。写文章,雅俗共赏是件困难的事情:讲深了,初学者或者外行读着扫兴;说浅了,专家们会觉着浪费时间。所以,我会尽量做到深入浅出。

言归正传。erlang 是约三十年前上帝馈赠给人间的一份礼物。它是如此独特,在构建的过程中揉进了如此多的奇思妙想,在三十年前就试图解决三十年后我们才大规模遇见的问题,在编程语言史上可算是一个自成体系的标杆。要在一篇文章中讲明白 erlang 是件困难的事情 —— 它从一个完整的,独一无二的世界观开始,提供了一个如操作系统般繁杂的 VM,随后又将业界的最佳实践抽象出一套框架(OTP),解决了很多分布式并发系统下复杂的基础问题。没有一门语言,或者一个系统像 erlang 这样不单单关心软件如何撰写,还关心软件如何运行;不仅仅关心软件今天如何运行,还关心软件明日如何升级。所以,本文概括性地谈谈 —— 如同「独孤九剑」的总诀 —— 关于 erlang 的方方面面都提及一些,讲些有意思的内容,但都不会深入到太细节的东西。

我们先从 Joe 老爷子的 worldview,也就是世界观谈起。

世界观

程序语言有其独特的思想。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.

这些朴实无华的,看上去似乎没什么大不了的句子,构成了 erlang 世界的基石。

我来将其分成四个部分解读。

process(进程)

在 erlang 的世界里,万事万物都是 process。process 就像原子一样,组成分子,细胞,组织,器官,最终构成一副机体(见我的文章: 谈谈边界(Boundary) )。那么,process 究竟是什么?

其实它和 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 garbage collector and why it matters )

process 其实很像是 OOP 中的 object:process 之间是隔离的 —— 一个 process 无法访问另一个 process 的内部状态,这是封装的概念,而通过使用 parent/child process 也可完美实现继承和多态。实际上,erlang 算得上是 OOP 始祖 smalltalk 的正统的传承者(另一个渐渐淡出大家视野的传人是 ObjC)。

routing(路由)

当系统中有了无数的独立的 process 后,一个很迫切的问题是如何找到我们需要与之互操作的 process:这是个路由的过程。对于 erlang 来说,我们可以通过唯一的 pid 来定位到目标 process,就像打电话时直播电话号码;我们也可以通过和 pid 一一对应的名字来定位到目标 process,就像你背不下来程序君的手机号但可以从手机地址簿里调出呼叫一样;我们还可以通过呼叫转接的服务,在手机上没有记录我电话号码的情况下,通过第三方呼叫到我 —— 在 erlang 里,这个第三方,叫 name registry。

说两句题外话。

路由在系统设计里是一个很厚重的主题,处处闪现它的影子。我们知道,一切系统工作的基础都是事件触发,而事件触发的本源是路由。你看这篇文章时指尖上下滑动,触发了 tap/swipe 这样的事件,这事件需要被准确无误地路由给其 handler,才能完成正确的操作。DynamoDB 的本质是什么?就是你给我一个 key,我把它路由到 cluster 下面某个 node 下面的某个 value,要读要写,要杀要剐你说了算。至于其使用到的 gossip,consistent hashing,都是为路由服务的手段和算法而已。C++ 里你调用一个纯虚函数,底层通过一张 vtable 将你的调用路由到合适的子类的函数的实现上。Virtual memory 是虚地址到实地址转化的一张「路由表」,VMA 等概念不过是这道路由大餐的提拉米苏。所以我们学习一个系统,先从其路由开始;我们设计一个系统,路由也是核心的一环。子曾经曰过: 参透路由表,走遍天下都不怕。

error handling(错误处理)

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。

golang 的 error handling 没有类似的 link/monitor 的机制,我觉着是其一个不大不小的败笔(当然也可能是为了性能考量之后的妥协,毕竟没有理想的语言,只有为了特定目的而妥协出来的语言)。至于如果 golang 要采用类似的机制 —— 那么,goroutine 和 channel,goroutine 和 goroutine,channel 和 channel 间,是否需要 link/monitor,如何 link/monitor,不在本文讨论的范围之内。

concurrency(并发)

前面讲到,erlang 的并发模型使用了 actor model。actor model 是说每个 actor 和其他 actor 通讯的方式能且仅能通过 message passing 而完成。因而,每个 actor 有其独立的 mailbox,用于接收消息。在 erlang 下,process 就是 actor。为了调度数量庞大的 processes,erlang VM 有自己的 scheduler —— 在 SMP(可以简单理解为多核)环境下,每个 CPU core 有一个 scheduler。和 OS 的 scheduler 类似,它有 run queue 和 wait queue,scheduler 从 run queue 上取出下一个 process,执行其代码。如果 process 没有通过诸如 receive message 这样的动作显式地将自己阻塞并被调度出去,scheduler 会在一个固定的 cycle 后(在 erlang 里叫 reduction)把 process 调度出去。所以从 erlang 程序员的角度来看,erlang 是 preemptive scheduling,并且,这种每个 process 获得的 CPU 时间几乎固定,事件在可预知的时间内得到调度,被称为 soft realtime。鲜有带 GC 的语言能够达到 soft realtime,Java 不行,golang 不行,因为它们的全局 GC 都会有臭名昭著的 STW(stop the world)问题(GC 一工作,世界就清净了,GC 会工作多久,天知道)。

既然提到了 soft realtime,那么,什么事 hard realtime 呢?CPU 的时钟中断是一种 hard realtime。它会在固定的间隔被触发,丝毫不差,风雨无阻。

OK,有了这些最基础的认知,我们可以进入到下一个环节 —— 谈谈 message passing,scheduling,以及 OTP。

Message passing

erlang 的 message passing 在很多文章里被当做是理所当然的,无需深究的事情,就像水啊,电啊这样的东西,谁关心它们是怎么来的?程序君在研习 erlang 时,对此却有好多疑问:

  • message 怎么从一个 process 传递到另一个 process?这中间有数据拷贝么?有锁么?锁是什么粒度?

  • message 会否在传递的过程中丢失?如果丢了,咋办?

  • 那些巨型的,从 TCP/IP stack 上收来的 message,从小明传递给小红,是否也要拷贝?拷贝的话,效率该多低?

  • exit,link,monitor 这些 signal 究竟和 message 是什么样的关系?

  • ...

这些问题我自个也未完全明白(尤其是 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 一起玩完。

小明可能觉得没完没了给小红发「爱老虎油」挺无聊,所以偶尔会学着徐志摩对林徽因那样,写写情书。一封冗长的情书写完后,小明要再誊一份送到小红的 heap 里,浪费时间,很不合算;万一,小明这情书改日要原封不动发给小花呢?小明比较聪明,发一个情书的链接,想看,点链接去看。

这便是 erlang message passing 处理 binary 的方式。大的 binary(通过 binary allocator 分配出来的)放在 share pool 里,发消息时只发一个 reference。对方收到以后,通过 reference 可以直接找到数据,避免无谓的拷贝。我们知道,reference 是有代价的 —— 这个 binary 要记录 refcnt,对「收看」和「看过」的行为要增增减减,保证 binary 不会在 consumer 还未使用前就被释放,同时也要保证 binary 不会在全部使用者使用完了之后还未释放。

调度(scheduling)

调度是 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 饿死。







请到「今天看啥」查看全文