Erlang/Elixir 为什么要选 Erlang 来做消息总线

DavidAlphaFox · 发布于 2018年01月09日 · 431 次阅读
84794b

为什么写这个

近期Erlang语言的排名上升了,算事可喜可贺的事情,国内知名的MQTT产品EMQ也受到了广大厂家和研发的认可。但是有人就提出了疑问,为什么EMQ选择使用Erlang而非Java作为开发?虽然并不了解作者本人的想法,但是本篇将从Java虚拟机和Erlang运行时的层面上说明下Erlang为什么更适合消息总线类产品。

宏观比较

Erlang运行时

Erlang是一种函数类型语言,使用Actor模型。在Erlang中一个Actor就是一个Erlang进程,Actor通过彼此之间传递消息来完成通信。Erlang运行时采用执行beam文件中的bytecode来完成工作。

JVM

Java是一门面向对象的语言,JVM是通过执行Java原文件编译后class文件中的bytecode来完成相应的工作,这让Java具备的一次编译只要JVM版本兼容就可以多处运行。同时JVM会通过JIT技术,将class文件中的bytecode进行即时编译生成高性能的本地代码以提高执行速度。

线程使用

Erlang运行时

Erlang会为创建和系统中安装CPU数量相同的OS线程,这些线程被称为调度器。每个调度器会管理大量前面提到的Erlang进程。为了最大限度的利用调度器,Erlang会使用任务秘取相关技术,让Erlang进程自动在多个调度器中进行负载均衡。
这些调度器通过reduction机制严格限制了每个Erlang进程执行的时间,同时使用任务优先级机制可以让高优先级的进程优先执行,从而实现Erlang进程软实时特性。
由于Erlang的进程并非真正的OS线程或OS进程,只是Erlang运行时中的一组数据结构,因此非常轻量级,可以在创建成千上万个Erlang进程。

JVM

JVM将Java中的线程直接影射到OS级别的线程,因此Java中的线程调度直接依赖于操作系统的线程调度机制。同时,由于是OS级别的线程,因此是非常消耗资源的。JVM也提供了线程池的封装,用来调度异步任务,但是这些异步任务是无法做到软实时的。
每个Java线程都会包含下面信息:

  1. 一个程序计数器PC,用来保存当前指令地址
  2. 一个栈用来保存栈帧
  3. 如果使用native方法还会创建一个native栈

内存管理

Erlang运行时

内存分配

Erlang运行时会大量使用内存池,根据使用目的不同划分多个内存池。
由于Erlang本身并没有变量,每个Erlang的进程都拥有自己的堆和栈。Erlang进程的堆和栈共享一段内存空间,堆向上生长,栈向下生长,因此非常容易检测到堆栈溢出。
Erlang在消息传递的过程中会使用内存复制,对于大于64bytes的二进制在Erlang运行时中会独立内存池进行分配,当在进程间传递这种消息的时候只是传递指向该二进制的指针。

垃圾回收

对于超过64bytes的二进制使引用计数来进行垃圾回收。对于每个进程则采用内存分代垃圾回收。Erlang运行时的垃圾回收,是针对Erlang进程的堆栈进行的,当一个Erlang进程结束运行,可以将该进程所有内存直接回收,因此无需进行stop the world。

JVM

内存分配

JVM将内存分为堆内存和非堆内存,堆内存保存了所有的Java对象,整个JVM共享这个堆。非堆内存,用来保存编译过的Java代码,以及JIT代码等。

垃圾回收

JVM堆内存使用分代垃圾回收,并且可以使用多种垃圾回收算法。但是对于堆内存回收不管何种算法都需要stop the world阶段。

并发

Erlang运行时

Erlang使用了Actor模型,在Erlang语言层面上Erlang进程是不存在互锁的机制,所有的通讯都是通过消息传递进行的。
Erlang运行时中,当Erlang进程互发消息的时候,会使用off_heap机制,尽可能减少Erlang进程执行的时候需要加锁的次数

JVM

由于Java线程是OS线程,所以可以只是使用OS线程锁的相关元语。

网络IO

涉及到消息系统,一定会涉及到网络IO,因此在这里面单独讨论下。

Erlang运行时

Erlang默认优先使用高级IO多路复用的API,在没有这个API的情况下,默认使用select作为IO多路复用。整个Erlang运行时共享一个IO多路复用。所有的调度器使用leader/follower模式,为了确保IO的实效和Erlang进程的软实时性,Erlang调度器会使用reduction机制,在执行一定量Erlang进程后强制检查IO是否有触发。但是与leader/follower模式稍有不同的是,如果一个Erlang调度器发现已经有另一个调度器在检查IO,会立刻放IO多路复用的锁去调取可执行的Erlang进程来执行。

socket处理模式

因为Erlang进程的轻量性,因此在Erlang中socket处理方案是为每个socket创建一个Erlang进程,socket断开连接后,可以选择Erlang进程直接退出。和socket绑定的Erlang进程可以直接进行业务处理,而不会影响IO复用器进行IO复用。

JVM

JVM封装了多种IO多路复用,并且在支持AIO。但是JVM并不提供类似Erlang运行时的IO任务管理机制,需要研发人员自行开发。

netty框架

Java中开发网络IO,netty是一个非常完善的Java网络框架,并经过大量的实践检验,可以在netty代码中看到大量针对JVM的IO子系统的bug进行针对性优化的代码。将IO处理,任务处理和编解码进行非常完整的规划,并提供了大量的基础设施。
netty使用的是IO Per Thread模型,在一般服务器开发中又使用acceptor和worker线程分开的方案,acceptor和worker实用的是boss/worker模式,当acceptor完成socket的接入流程就通过worker的wakup机制,将socket交付给worker的IO复用器进行复用。
需要注意的是,worker在进行业务处理的时候,会直接占用worker线程直到业务结束,在此期间和该worker绑定的IO复用器是无法进行IO复用的。因此在netty开发的是有,需要经常使用netty的future线程池,将耗时的业务交给专用的线程池进行处理。

总结

选择Erlang进行消息系统开发有下面的优势:

  1. Erlang本身就是Actor模式,无需设计复杂的线程间消息交换机制(锁力度,队列管理等等)
  2. Erlang的GC不存在stop the world,因此内存效率相对较高,可以有效的防止大链接量下因为GC抖动而引起的socket接入失败(句柄不足依然会引起socket接入失败)
  3. 软实时性,每个socket一个Erlang进程,所有的业务逻辑只需要线性思考(特殊的业务除外,依然需要进行异步思考),Erlang运行时自动完成IO,任务切换,不会因为线性思考而影响IO复用
  4. 自带分布式,在一定量级前无需过度思考机器间RPC
  5. Erlang的binary操作可以进行二进制位操作,进行位级别的编解码非常简单

当然选择Erlang进行消息系统开发也有下面一些劣势:

  1. Erlang语法是函数式,受众群体小
  2. 单IO复用器(R21 有待解决)

总体来讲,Erlang开发消息系统的时候,除了语法外,心智负担比较小。

暂无回复。
需要 登录/注册 后方可回复, 如果你还没有账号请点击这里 注册