前言

Erlang是具有多重范型的编程语言,具有很多特点,主要的特点有以下几个:

  • 函数式
  • 并发性
  • 分布式
  • 健壮性
  • 软实时
  • 热更新
  • 递增式代码加载
  • 动态类型
  • 解释型

函数式

Erlang是函数式编程语言,函数式是一种编程模型,将计算机中的运算看做是数学中的函数计算,可以避免状态以及变量的概念。

对象是面向对象的第一型,函数式编程语言也是一样,函数是函数式编程的第一型。函数是Erlang编程语言的基本单位,在Erlang里,函数是第一型,函数几乎会被用作一切,包括最简单的计算。所有的概念都是由函数表达,所有额操作也都是由函数操作。

并发性

在上一篇blog中已经说过Erlang编程语言的并发性了,Erlang编程语言可以支持超大量级的并发性,并且不需要依赖操作系统和第三方外部库。Erlang的并发性主要依赖Erlang虚拟机,以及轻量级的Erlang进程。

Erlang进程究竟是怎样轻量?

 1 $ erl
 2 Erlang/OTP 17 [erts-6.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [lock-counting] [dtrace]
 3 
 4 Eshell V6.3  (abort with ^G)
 5 1> Pid = erlang:spawn(fun() -> receive _ -> ok end end).
 6 <0.34.0>
 7 2> erlang:process_info(Pid).
 8 [{current_function,{prim_eval,'receive',2}},
 9  {initial_call,{erlang,apply,2}},
10  {status,waiting},
11  {message_queue_len,0},
12  {messages,[]},
13  {links,[]},
14  {dictionary,[]},
15  {trap_exit,false},
16  {error_handler,error_handler},
17  {priority,normal},
18  {group_leader,<0.25.0>},
19  {total_heap_size,233},
20  {heap_size,233},
21  {stack_size,9},
22  {reductions,17},
23  {garbage_collection,[{min_bin_vheap_size,46422},
24                       {min_heap_size,233},
25                       {fullsweep_after,65535},
26                       {minor_gcs,0}]},
27  {suspending,[]}]
28 3> erlang:process_info(Pid, memory).
29 {memory,8376}

L5,首先,可以使用Erlang内置的API函数创建一个Erlang进程,并返回这个进程的PID。

L7、L28,再使用Erlang的API函数查看Erlang进程的信息,可以看到以默认参数创建的Erlang进程的heap size是233个字节(嗯,单位是words),占用的内存是8376bytes(计量单位就是bytes),这8376bytes的内存主要包括了这个Erlang进程的调用栈、堆、以及一些内部的数据结构。

那么,在Erlang系统中,可以维持多少Erlang进程,就取决于Erlang可以使用多少计算机内存。

当然,需要注意的是,上述是初始化的heap size和内存占用,在Erlang进程的运行中,Erlang调度器会根据实际情况,给Erlang进程分配需要的内存空间,然后根据相关的算法对Erlang进程进行垃圾回收(GC)。

上图是Erlang虚拟机和Erlang库的关系图,从图中,可以看出,不管是Erlang现有的内部库(kernel、stdlib ... )还是可以自己创建的Erlang库(lager、recon ... ),都是运行在Erlang虚拟机上的,Erlang虚拟机是整个Erlang编程语言的核心所在。

 分布式

Erlang的分布式特性是由Erlang在语言层面上支持的,可以使用语言内置的API函数,在远程节点上创建Erlang进程,继而执行指定的模块函数。同样,还可以使用Erlang的RPC模块,调用远程节点的模块函数。

需要注意的一点是,在分布式Erlang系统中,节点指的是,一个可参数分布式Erlang事务的运行着的Erlang系统。

上图是用本地的两个terminal 模拟了两个Erlang节点,一个叫做'test1@127.0.0.1',另一个叫'test2@127.0.0.1'。

首先,ping一下,确认两个节点是可以建立连接,相互通信的。然后,在其中一个节点上,通过rpc模块,在另一个节点上执行相应的模块函数,并函数执行结果。

Erlang节点之间的相互调用,跨节点远程模块函数执行都是异常方便的,Erlang节点之间的通信完全是由Erlang编程语言在语言层面上支持的(重要的事情,再说一遍),Erlang语言有自己的node(节点,不是nodejs)协议,某些语言,也想实现这种方便的方式(如 https://github.com/goerlang)。

健壮性

健壮性是Erlang编程语言一个非常重要的特点,Erlang编程语言的健壮性,主要依赖于以下几点:

  • 进程隔离
  • 完善的错误异常处理
  • 错误处理哲学
  • 监控者进程

关于Erlang进程资源隔离这一点,在上一个blog中也有说到过。在构建可容错的软件系统过程中,要解决的本质问题就是故障隔离,正因为Erlang进程资源隔离的特点,除了几个特殊性的Erlang进程(Erlang系统的主进程如果死掉的话,Erlang系统肯定没法玩了)之外,某个一般性的进程出现错误异常,对整个Erlang系统造成的影响是很小的,因为资源是隔离的,所以某个进程出现的故障具有隔离性,不会导致整个Erlang系统崩溃。

在Erlang系统中,系统提供了一些错误异常处理的方式,体现在API函数上,常用的有

1 1> erlang:exit("test").
2 ** exception exit: "test"
3 2> erlang:throw("test").
4 ** exception throw: "test"
5 3> erlang:error("test").
6 ** exception error: "test"
7 4> 

在Erlang编程语言中,可以使用以上这几个API函数抛出错误异常,这几个API函数都会crash掉调用者进程,这和Erlang的错误处理哲学有关。

(这几个API函数有什么不同,在什么场景下应该用哪个,会在后面的blog中详细介绍)

为了捕获这些错误异常,Erlang同样提供了非常方便的不同的错误异常处理方式,可以使用catch:

1 4> catch 1 + "1".
2 {'EXIT',{badarith,[{erlang,'+',[1,"1"],[]},
3                    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,661}]},
4                    {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,434}]},
5                    {shell,exprs,7,[{file,"shell.erl"},{line,684}]},
6                    {shell,eval_exprs,7,[{file,"shell.erl"},{line,639}]},
7                    {shell,eval_loop,3,[{file,"shell.erl"},{line,624}]}]}}
8 5> 

在上面这个例子中,让1 和 “1” 执行相加操作,系统会爆出异常错误,使用catch来捕获的话,就可以看出错误异常的类型以及调用栈信息,能让码农方便快速的定位究竟是哪里出了问题。

同样,还可以使用try ... catch

5> try 1 + "1" catch Error:Reason -> io:format("Error: ~p, Reason: ~p~n", [Error, Reason]) end.
Error: error, Reason: badarith
ok

try ... catch 这种方式不会显示调用栈信息,和catch 相比的话,overload更小一些。

码农就可以在不同的场景中使用不同的处理方式(如果想知道调用栈信息的话,可以使用catch,如果不关心调用栈信息的话,try ... catch 就OK了),完全自己选择。

至于错误处理哲学,在Erlang系统中,所提倡的方式是,速错,工作进程不成功就成仁,让其他进程来修复错误,尽可能不是用防御式编程(这和Java“有些”不同),这样做,能够让我等码农尽快发现错误异常,避免错误异常真到了生产环境下才被发现(到时候老板扣工资就惨了)。

对于“监控者进程”,Erlang系统提供了link或者是monitor的方式,可以让监控者进程及时发现工作进程的异常故障,进而对异常故障做出相应的处理,速错不是忽略错误异常,而是尽早的发现并修复。在Erlang的OTP框架中,提供了supervisor的behavior,就是基于这种方式的。

 1 6> erlang:process_flag(trap_exit, true).
 2 false
 3 7> erlang:spawn_link(fun() -> 1 + "1" end).
 4 <0.43.0>
 5 8> 
 6 =ERROR REPORT==== 18-Aug-2015::23:53:54 ===
 7 Error in process <0.43.0> with exit value: {badarith,[{erlang,'+',[1,"1"],[]}]}
 8 
 9 
10 8> flush().
11 Shell got {'EXIT',<0.43.0>,{badarith,[{erlang,'+',[1,"1"],[]}]}}
12 ok
13 9> erlang:spawn_monitor(fun() -> 1 + "1" end).
14 
15 =ERROR REPORT==== 18-Aug-2015::23:54:09 ===
16 Error in process <0.46.0> with exit value: {badarith,[{erlang,'+',[1,"1"],[]}]}
17 
18 {<0.46.0>,#Ref<0.0.0.68>}
19 10> flush().
20 Shell got {'DOWN',#Ref<0.0.0.68>,process,<0.46.0>,
21                   {badarith,[{erlang,'+',[1,"1"],[]}]}}
22 ok

L1,先设置当前进程的trap_exit flag,防止link进程死掉牵连当前进程。然后,分别使用spawn_link(L3)和spawn_monitor(L13)两种方式创建进程,并让创建的进程执行肯定会出现错误异常的函数。等被创建的进程异常退出之后,当前进程就能收到相应的消息(L11和L20),然后就能做出相应的处理了,这些错误信息的具体含义也会在后面的blog详细说明。在此主要是为了说明监控这进程的表现形式。

软实时

Erlang软实时的特点主要依赖于:

  • Erlang虚拟机调度机制
  • 内存垃圾回收策略
  • 进程资源隔离

Erlang系统垃圾回收策略是分代回收的,采用递增式垃圾回收方式,基于进程资源隔离的特点,Erlang内存垃圾回收是以单个Erlang进程为单位的,在垃圾回收的过程中,不会stop the world,也就是不会对整个系统造成影响。结合Erlang虚拟机抢占式调度的机制,保证Erlang系统的高可用性和软实时性。

热更新

哇哈哈,很多人提到Erlang都可能会被Erlang的热更新特点所吸引(其他语言也能实现),但是Erlang的热更新是非常方便并且在电信产品中久经考验。Erlang系统,允许程序代码在运行过程中被修改,旧的代码逻辑能够被逐步淘汰而后被新的代码逻辑替换。在此过程中,新旧代码逻辑在系统中是共存的,Erlang“热更新”的特点,能够最大程度的保证Erlang系统的运行,不会因为业务更新造成系统的暂停。

我司的产品(ptengine.com)现在面向的是全球100+个国家地区,覆盖24+时区(嗯,有半时区,还有四分之一时区),也就是,我们几乎没有停服更新的时间窗口,代码程序啥的,就是靠的Erlang的热更新。(当然,也有失败的时候,后面再细说)

递增式代码加载

Erlang的库,包括Erlang现有的库以及码农自己创建的库是运行在Erlang虚拟机外层的(上面有个图)。可以在Erlang系统运行的过程中,被加载,启动,停止以及卸载,这些,都是码农可以去控制的。

比如:

 1 $ erl -pa ./ebin -pa ./deps/*/ebin 
 2 Erlang/OTP 17 [erts-6.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [lock-counting] [dtrace]
 3 
 4 Eshell V6.3  (abort with ^G)
 5 1> application:load(lager).
 6 ok
 7 2> application:ensure_all_started(lager).
 8 {ok,[syntax_tools,compiler,goldrush,lager]}
 9 3> 00:11:23.274 [info] Application lager started on node nonode@nohost
10 
11 3> application:info().     
12 [{loaded,[{goldrush,"Erlang event stream processor","0.1.6"},
13           {kernel,"ERTS  CXC 138 10","3.1"},
14           {lager,"Erlang logging framework","2.0.3"},
15           {syntax_tools,"Syntax tools","1.6.17"},
16           {compiler,"ERTS  CXC 138 10","5.0.3"},
17           {stdlib,"ERTS  CXC 138 10","2.3"}]},
18  {loading,[]},
19  {started,[{lager,temporary},
20            {goldrush,temporary},
21            {compiler,temporary},
22            {syntax_tools,temporary},
23            {stdlib,permanent},
24            {kernel,permanent}]},
25  {start_p_false,[]},
26  {running,[{lager,<0.45.0>},
27            {goldrush,<0.38.0>},
28            {compiler,undefined},
29            {syntax_tools,undefined},
30            {stdlib,undefined},
31            {kernel,<0.9.0>}]},
32  {starting,[]}]
33 4> application:unload(lager).
34 {error,{running,lager}}
35 5> application:stop(lager).
36 
37 =INFO REPORT==== 19-Aug-2015::00:11:57 ===
38     application: lager
39     exited: stopped
40     type: temporary
41 ok
42 6> application:unload(lager).
43 ok

以控制lager库为演示示例,(lager库是Erlang的一个第三方库,是一个应用非常广泛的日志组件)。

L5可以使用application:load(lager)加载lager库,然后使用application:ensure_all_started(lager) 启动lager库以及lager库所以来的库(在start时,Erlang系统的处理方式是,如果还没有load的话,会先load,然后再start,所以实际情况下,load使用的机会是比较少的)。

start之后,可以使用application:info() 函数,去检查是否已经启动成功。确认started了,再去unload(好像有点作,仅仅是为了演示一下),然后发现报错了,是因为lager库正在运行,无法unload,那么就先stop 掉lager库吧。

注意,有可能有些人比较疑惑,运行得好好的,为啥要stop呢?在这里可能有这样一种原因,我们自己创建了一个库,然后上线了,运行了一段时间之后,发现,有一个出现几率很小的bug,想修复一下,这个时候可以用热更,也可以用stop -> unload -> 修改代码/编译 -> load -> start 的方式。如果是我想用其中一个库替换掉这个库,那么这个库就已经没有存在的必要了,就必须stop掉。

动态类型

Erlang既是动态语言,又是动态类型。

动态语言指的是,在系统运行过程中,可以改变代码的结构,现有的函数可以被删除或者是被修改,运行时代码可以根据某些条件改变自身结构。这也是Erlang可以热更新的一个基础。

动态类型值得是,在程序原形期间才会检查数据类型,数据类型的绑定不是在编译阶段,而是延后到运行阶段。

举两个例子:

1 8> F = fun(A, B) ->  io:format("-----------------~n"), A + B  end.
2 #Fun<erl_eval.12.90072148>
3 9> F(1, "1").
4 -----------------
5 ** exception error: an error occurred when evaluating an arithmetic expression
6      in operator  +/2
7         called as 1 + "1"

L1,定义一个函数,先输入一个横线(-----------------),然后执行两个参数的相加操作。在L3处调用该函数,传入的两个参数是1 和 “1”,然后,发生了什么?首先输出了横线,也就是函数已经被执行了,而真正运行到相加操作时,才会检查两个参数的数据类型。

再看一个需要编译的例子:

1 $ cat test.erl 
2 -module(test).
3 -export([start/0]).
4 
5 start() ->
6     add(1, "1").
7 
8 add(A, B) ->
9     A + B.

在这个test模块中,定义了两个函数,第一个是start函数,可以被外部调用,在start函数中,调用了一个内部函数,add,add函数执行的是两个变量的相加操作,而在start函数中,向add函数传入了两个参数,第一个是参数是1,第二个是“1”,这明显是会失败的嘛(其他语言可能不会,但是在Erlang语言中,这是会失败的)。

但是在编译的时候,编译器并没有检查start函数中传给add函数的两个参数的数据类型,这个模块是可以编译通过的。(如何编译模块,会在后面的blog中细说)

但是在运行时,就会出现错误。

1 1> c(test).
2 {ok,test}
3 2> test:start().
4 ** exception error: an error occurred when evaluating an arithmetic expression
5      in function  test:add/2 (test.erl, line 8)

L1是编译Erlang模块文件的一种方式,L2调用了test 模块的start函数,然后就出现错误了。

从上面的两个例子中,可以看出,动态类型存在着一定的弊端,潜在的错误异常,只有在运行阶段才能被发现,无法在编译的时候就尽早的发现潜在的错误异常。

解释型

Erlang编程语言是解释型语言,运行在虚拟机上,具有良好的平台兼容性。

总结

Erlang是函数式编程语言,其核心是Erlang虚拟机。Erlang并发进程不同于操作系统进程,是非常轻量的,Erlang内置的分布式特性,异常方便, Erlang编程语言软实时的特性能够在其错误异常处理机制的保护下更加健壮的运行,其热更新能给我们码农带来诸多的方便。

内置,内置,内置,方便,方便,方便。

参考:

  • Erlang的几本书
  • Erlang官方文档
  • 还是上个blog里提到的那篇论文(殿堂级论文)

 

下一篇blog会对Erlang环境安装一笔带过,重点说一个Wordcount的示例,演示一下Erlang轻量的并发进程以及非常方便好用的分布式特性。

posted on 2015-08-19 00:56  _00  阅读(5520)  评论(2编辑  收藏  举报