vlambda博客
学习文章列表

技术 | 被时间“埋没”的高并发王者:Erlang

分布式、fault-tolerant、高并发等等这些名词变得越来越火爆,一个新的框架、数据库、语言如果不跟这几个名词沾边,都不好意思摆在台面上。可是,早在20-30年前的通信领域已经出现了这样的需求,比如 容错、超高并发、在线代码更新、99.9%在线率等等。而互联网领域直到最近10今年才特别关注这种系统。

大概在90年代初,Erlang诞生了,它的设计者正式为了满足通讯领域系统高并发、高容错、分布式的需求设计Erlang和它的虚拟机BEAM。(当然BEAM不是第一代虚拟机,而是经过几轮迭代后被大规模投入使用的版本)。

虽然,很多年轻的程序员可能没有通过Erlang,提起分布式、高并发,他们更多想到的可能是 Golang,Scala/Akka等等。可是Erlang系统就在我们身边:

  • Cisco超过90%的交换机仍然在使用Erlang;

  • 早在2012年,WhatApp的生产系统就实现了在单个Beam虚拟机节点上同时处理超过2百万个TCP/IP连接;

  • RabbitMQ 服务器端代码是Erlang实现的;

  • 电信公司T-Mobile的短信业务也是Erlang实现的;

  • Ericsson用Erlang生态构建新的5G基础设施软件;

  • 等等。

可以说,在分布式、高并发、高容错这个领域,Erlang得到了广泛的应用,多年的使用也证明了Erlang生态在这个领域的可靠性。

我有幸和Erlang最初的几个设计者之一 Robert Virding 学习过一段时间,了解了一些Erlang背后的故事。

Erlang的设计哲学

Erlang不是完美的语言,它的设计具有鲜明的目的性:高并发、高容错、分布式。Erlang的虚拟机BEAM也是为了实现相同的目的,可以说,它只围绕这几个设计目的进行优化,比如它对Binary类型计算进行了优化,而浮点计算则非常缓慢。

Erlang本身属于函数式语言,没有变量,循环,一切都是Immutable等等。而 Erlang 的 Pattern Matching 能力非常优秀且高效。代码精简且清晰,同时为信息传递奠定了基础。最近十几年,工业界也开始慢慢接受、甚至推崇函数式编程,但是Erlang已经在30年前把函数式代码部署在了生产环境。

Erlang的并发模型属于Actor Model,它实现高并发的基石在于非常廉价的Process。这个Process不是操作系统中的进程,而是Beam虚拟机内部的一个抽象。Process非常廉价,每个进程只有 309 字节的foot print,一个Beam节点可以毫无压力的同事运行超过2百万个process。这些Process都是相互独立的,他们不共享任何内存,有独立的堆栈,除非建立连接,一个Process的崩溃完全不会影响到系统的其他进程。

更加有意思的是,这200万个虚拟机进程,仅仅需要运行在一个操作系统线程内。换句话说,BEAM把并发模型从操作系统中抽象出来了。这样做的优势显而易见:如果我们有多核处理器,相同的代码可以毫不费力的通过增加节点分布在不同的内核里,增加处理能力。

进程之间是通过传递信息实现互动,注意这些信息都是不可变的(immutable),同时信息的传递可以跨越节点,即可以实现不同内核中节点的通信、不同物理机器节点的通讯。事实上,代码中几乎不需要区分进程是local还是remote。这种跨节点通讯正式分布式系统的基础。

在BEAM进程(Process)、进程通讯(Message Passing)以及函数式语法(Functional Programming)的基础上,为了进一步增加容错能力、实现99.9%系统在线率,Erlang发展出了非常独特的 异常处理 和 代码更新 模式。BEAM虚拟机可以实现系统在线的情况下修复bug,并仅仅重新部署一部分出问题的系统。

大部分编程模式的异常处理都是基于 try-catch,也就是 Defensive Programming,即尽可能的捕捉异常,阻止程序崩溃。以为绝大部分系统不能接受线程异常,一个线程异常有肯能会导致整个系统崩溃。Erlang系统很不一样,因为所有进程都是独立的,一个进程的崩溃完全不会影响系统,甚至,系统中的大部分进程都不会意识到。所以,Erlang的 异常处理 更加专注于如何让仍然工作的进程修复崩溃的进程,也就是自我恢复,Self-healing。当然,Erlang生态并没有这个名词, 在Erlang 的世界里叫做 supervisor-tree。通常编写业务逻辑的时候,仅仅关注正确的情况,而不去主动捕捉异常,以为异常总是多种多样的,而且一定会发生。通常如果出现异常,进程就会终结,而他的supervisor进程会介入进行合理的操作,比如重启它等等。换句话说,把业务逻辑和异常处理逻辑分离。也正式因为这样的 异常处理和自我修复设计,实现了Erlang系统的高容错性能。

Erlang生态

总结一下 Erlang 实现高并发、高容错、分布式的基石:

  • 函数式语言,Immutability

  • 超轻量线程,Process

  • 自愈模式的异常处理,Supervision tree

  • 在线代码更新,Code Updating

无论是Erlang语言本身,还是他的虚拟机BEAM都是围绕上面那几个特征进行设计的。

其实,很多新的语言和框架都或多或少的在学习上面的特性,比如 go routine 和 channel其实就是轻量级线程和通讯,但却没有Erlang进程那么轻,而且共享内存。Erlang的独特之处在于它的设计目的很单一:高并发。而上面的三个特征是实现可靠的高并发系统必不可少的,Erlang把他们做到了极致,不妥协。

BEAM,高并发的发动机

BEAM,Bogdan's Erlang Abstract Machine,Erlang的设计哲学需要BEAM来实现。就好像Java的成功其实得益于JVM,Erlang的成功也得益于BEAM。不过BEAM属于寄存器机(register machine)而JVM属于堆栈机(stack machine),寄存器机因为没有堆栈结构,直接操作寄存器,一般来讲速度更快,但是开发和维护成本都更大。不过为了支持Erlang数量庞大的轻量级线程,BEAM选择了寄存器机。

在一般的家用电脑上,BEAM 可以在 1 s 内孵化超过 35万个进程。

值得一提的是,BEAM中两个非常重要的部分:scheduler(调度器)和 GC (垃圾回收)。Scheduler负责管理虚拟机中所有的进程,确保他们得到公平的计算资源,不会出现某一个进程独占资源的情况。举个例子,在BEAM里面,即使有一个进程进入了死循环(实际上Erlang中的很多进程就是死循环,即尾递归),scheduler也会确保他不会影响其他进程的资源,这种情况如果发生在JVM中,就可能会抢占资源,影响系统的其他进程。

BEAM的GC是基于每一个进程的,也就是说,GC仅仅按照每一个进程发生,而不会挂起整个系统。正因为如此,GC占用的时间非常短暂,系统永远处于工作状态。而JVM最经常被职责的就是GC会造成系统间歇性的停止。

并不是JVM不好,而是JVM真的不适合超高并发的场景,一次突如其来的GC会阻塞好多信息,而一次没有捕捉的线程异常甚至会让JVM挂起。

写在最后

正确的工具做正确的事情

如今的互联网世界,所有人都在追捧和创造新的技术、新的名词,但是回归到问题本身,新的技术一定好吗?选择合适和可靠的技术才是王道。而一个技术是不是可靠,往往需要多年的大规模生产验证。

Erlang 和 BEAM 就是高并发场景的正确工具,他非常不完美,但是他满足了高并发场景所需要的工程特征,而且经历近30年的生产环境考验。