前言
Erlang是具有多重范型的編程語言,具有很多特點(diǎn),主要的特點(diǎn)有以下幾個(gè):
函數(shù)式
并發(fā)性
分布式
健壯性
軟實(shí)時(shí)
熱更新
遞增式代碼加載
動(dòng)態(tài)類型
解釋型
函數(shù)式
Erlang是函數(shù)式編程語言,函數(shù)式是一種編程模型,將計(jì)算機(jī)中的運(yùn)算看做是數(shù)學(xué)中的函數(shù)計(jì)算,可以避免狀態(tài)以及變量的概念。
對(duì)象是面向?qū)ο蟮牡谝恍停瘮?shù)式編程語言也是一樣,函數(shù)是函數(shù)式編程的第一型。函數(shù)是Erlang編程語言的基本單位,在Erlang里,函數(shù)是第一型,函數(shù)幾乎會(huì)被用作一切,包括最簡(jiǎn)單的計(jì)算。所有的概念都是由函數(shù)表達(dá),所有額操作也都是由函數(shù)操作。
并發(fā)性
在上一篇blog中已經(jīng)說過Erlang編程語言的并發(fā)性了,Erlang編程語言可以支持超大量級(jí)的并發(fā)性,并且不需要依賴操作系統(tǒng)和第三方外部庫。Erlang的并發(fā)性主要依賴Erlang虛擬機(jī),以及輕量級(jí)的Erlang進(jìn)程。
Erlang進(jìn)程究竟是怎樣輕量?
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內(nèi)置的API函數(shù)創(chuàng)建一個(gè)Erlang進(jìn)程,并返回這個(gè)進(jìn)程的PID。
L7、L28,再使用Erlang的API函數(shù)查看Erlang進(jìn)程的信息,可以看到以默認(rèn)參數(shù)創(chuàng)建的Erlang進(jìn)程的heap size是233個(gè)字節(jié)(嗯,單位是words),占用的內(nèi)存是8376bytes(計(jì)量單位就是bytes),這8376bytes的內(nèi)存主要包括了這個(gè)Erlang進(jìn)程的調(diào)用棧、堆、以及一些內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。
那么,在Erlang系統(tǒng)中,可以維持多少Erlang進(jìn)程,就取決于Erlang可以使用多少計(jì)算機(jī)內(nèi)存。
當(dāng)然,需要注意的是,上述是初始化的heap size和內(nèi)存占用,在Erlang進(jìn)程的運(yùn)行中,Erlang調(diào)度器會(huì)根據(jù)實(shí)際情況,給Erlang進(jìn)程分配需要的內(nèi)存空間,然后根據(jù)相關(guān)的算法對(duì)Erlang進(jìn)程進(jìn)行垃圾回收(GC)。
?
上圖是Erlang虛擬機(jī)和Erlang庫的關(guān)系圖,從圖中,可以看出,不管是Erlang現(xiàn)有的內(nèi)部庫(kernel、stdlib 。。. )還是可以自己創(chuàng)建的Erlang庫(lager、recon 。。. ),都是運(yùn)行在Erlang虛擬機(jī)上的,Erlang虛擬機(jī)是整個(gè)Erlang編程語言的核心所在。
分布式
Erlang的分布式特性是由Erlang在語言層面上支持的,可以使用語言內(nèi)置的API函數(shù),在遠(yuǎn)程節(jié)點(diǎn)上創(chuàng)建Erlang進(jìn)程,繼而執(zhí)行指定的模塊函數(shù)。同樣,還可以使用Erlang的RPC模塊,調(diào)用遠(yuǎn)程節(jié)點(diǎn)的模塊函數(shù)。
需要注意的一點(diǎn)是,在分布式Erlang系統(tǒng)中,節(jié)點(diǎn)指的是,一個(gè)可參數(shù)分布式Erlang事務(wù)的運(yùn)行著的Erlang系統(tǒng)。
?
? ? ? ? ? ??
上圖是用本地的兩個(gè)terminal 模擬了兩個(gè)Erlang節(jié)點(diǎn),一個(gè)叫做‘test1@127.0.0.1’,另一個(gè)叫‘test2@127.0.0.1’。
首先,ping一下,確認(rèn)兩個(gè)節(jié)點(diǎn)是可以建立連接,相互通信的。然后,在其中一個(gè)節(jié)點(diǎn)上,通過rpc模塊,在另一個(gè)節(jié)點(diǎn)上執(zhí)行相應(yīng)的模塊函數(shù),并函數(shù)執(zhí)行結(jié)果。
Erlang節(jié)點(diǎn)之間的相互調(diào)用,跨節(jié)點(diǎn)遠(yuǎn)程模塊函數(shù)執(zhí)行都是異常方便的,Erlang節(jié)點(diǎn)之間的通信完全是由Erlang編程語言在語言層面上支持的(重要的事情,再說一遍),Erlang語言有自己的node(節(jié)點(diǎn),不是nodejs)協(xié)議,某些語言,也想實(shí)現(xiàn)這種方便的方式(如 https://github.com/goerlang)。
健壯性
健壯性是Erlang編程語言一個(gè)非常重要的特點(diǎn),Erlang編程語言的健壯性,主要依賴于以下幾點(diǎn):
進(jìn)程隔離
完善的錯(cuò)誤異常處理
錯(cuò)誤處理哲學(xué)
監(jiān)控者進(jìn)程
關(guān)于Erlang進(jìn)程資源隔離這一點(diǎn),在上一個(gè)blog中也有說到過。在構(gòu)建可容錯(cuò)的軟件系統(tǒng)過程中,要解決的本質(zhì)問題就是故障隔離,正因?yàn)镋rlang進(jìn)程資源隔離的特點(diǎn),除了幾個(gè)特殊性的Erlang進(jìn)程(Erlang系統(tǒng)的主進(jìn)程如果死掉的話,Erlang系統(tǒng)肯定沒法玩了)之外,某個(gè)一般性的進(jìn)程出現(xiàn)錯(cuò)誤異常,對(duì)整個(gè)Erlang系統(tǒng)造成的影響是很小的,因?yàn)橘Y源是隔離的,所以某個(gè)進(jìn)程出現(xiàn)的故障具有隔離性,不會(huì)導(dǎo)致整個(gè)Erlang系統(tǒng)崩潰。
在Erlang系統(tǒng)中,系統(tǒng)提供了一些錯(cuò)誤異常處理的方式,體現(xiàn)在API函數(shù)上,常用的有
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編程語言中,可以使用以上這幾個(gè)API函數(shù)拋出錯(cuò)誤異常,這幾個(gè)API函數(shù)都會(huì)crash掉調(diào)用者進(jìn)程,這和Erlang的錯(cuò)誤處理哲學(xué)有關(guān)。
(這幾個(gè)API函數(shù)有什么不同,在什么場(chǎng)景下應(yīng)該用哪個(gè),會(huì)在后面的blog中詳細(xì)介紹)
為了捕獲這些錯(cuò)誤異常,Erlang同樣提供了非常方便的不同的錯(cuò)誤異常處理方式,可以使用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>
在上面這個(gè)例子中,讓1 和 “1” 執(zhí)行相加操作,系統(tǒng)會(huì)爆出異常錯(cuò)誤,使用catch來捕獲的話,就可以看出錯(cuò)誤異常的類型以及調(diào)用棧信息,能讓碼農(nóng)方便快速的定位究竟是哪里出了問題。
同樣,還可以使用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 這種方式不會(huì)顯示調(diào)用棧信息,和catch 相比的話,overload更小一些。
碼農(nóng)就可以在不同的場(chǎng)景中使用不同的處理方式(如果想知道調(diào)用棧信息的話,可以使用catch,如果不關(guān)心調(diào)用棧信息的話,try 。。. catch 就OK了),完全自己選擇。
至于錯(cuò)誤處理哲學(xué),在Erlang系統(tǒng)中,所提倡的方式是,速錯(cuò),工作進(jìn)程不成功就成仁,讓其他進(jìn)程來修復(fù)錯(cuò)誤,盡可能不是用防御式編程(這和Java“有些”不同),這樣做,能夠讓我等碼農(nóng)盡快發(fā)現(xiàn)錯(cuò)誤異常,避免錯(cuò)誤異常真到了生產(chǎn)環(huán)境下才被發(fā)現(xiàn)(到時(shí)候老板扣工資就慘了)。
對(duì)于“監(jiān)控者進(jìn)程”,Erlang系統(tǒng)提供了link或者是monitor的方式,可以讓監(jiān)控者進(jìn)程及時(shí)發(fā)現(xiàn)工作進(jìn)程的異常故障,進(jìn)而對(duì)異常故障做出相應(yīng)的處理,速錯(cuò)不是忽略錯(cuò)誤異常,而是盡早的發(fā)現(xiàn)并修復(fù)。在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,先設(shè)置當(dāng)前進(jìn)程的trap_exit flag,防止link進(jìn)程死掉牽連當(dāng)前進(jìn)程。然后,分別使用spawn_link(L3)和spawn_monitor(L13)兩種方式創(chuàng)建進(jìn)程,并讓創(chuàng)建的進(jìn)程執(zhí)行肯定會(huì)出現(xiàn)錯(cuò)誤異常的函數(shù)。等被創(chuàng)建的進(jìn)程異常退出之后,當(dāng)前進(jìn)程就能收到相應(yīng)的消息(L11和L20),然后就能做出相應(yīng)的處理了,這些錯(cuò)誤信息的具體含義也會(huì)在后面的blog詳細(xì)說明。在此主要是為了說明監(jiān)控這進(jìn)程的表現(xiàn)形式。
軟實(shí)時(shí)
Erlang軟實(shí)時(shí)的特點(diǎn)主要依賴于:
Erlang虛擬機(jī)調(diào)度機(jī)制
內(nèi)存垃圾回收策略
進(jìn)程資源隔離
Erlang系統(tǒng)垃圾回收策略是分代回收的,采用遞增式垃圾回收方式,基于進(jìn)程資源隔離的特點(diǎn),Erlang內(nèi)存垃圾回收是以單個(gè)Erlang進(jìn)程為單位的,在垃圾回收的過程中,不會(huì)stop the world,也就是不會(huì)對(duì)整個(gè)系統(tǒng)造成影響。結(jié)合Erlang虛擬機(jī)搶占式調(diào)度的機(jī)制,保證Erlang系統(tǒng)的高可用性和軟實(shí)時(shí)性。
熱更新
哇哈哈,很多人提到Erlang都可能會(huì)被Erlang的熱更新特點(diǎn)所吸引(其他語言也能實(shí)現(xiàn)),但是Erlang的熱更新是非常方便并且在電信產(chǎn)品中久經(jīng)考驗(yàn)。Erlang系統(tǒng),允許程序代碼在運(yùn)行過程中被修改,舊的代碼邏輯能夠被逐步淘汰而后被新的代碼邏輯替換。在此過程中,新舊代碼邏輯在系統(tǒng)中是共存的,Erlang“熱更新”的特點(diǎn),能夠最大程度的保證Erlang系統(tǒng)的運(yùn)行,不會(huì)因?yàn)闃I(yè)務(wù)更新造成系統(tǒng)的暫停。
我司的產(chǎn)品(ptengine.com)現(xiàn)在面向的是全球100+個(gè)國家地區(qū),覆蓋24+時(shí)區(qū)(嗯,有半時(shí)區(qū),還有四分之一時(shí)區(qū)),也就是,我們幾乎沒有停服更新的時(shí)間窗口,代碼程序啥的,就是靠的Erlang的熱更新。(當(dāng)然,也有失敗的時(shí)候,后面再細(xì)說)
遞增式代碼加載
Erlang的庫,包括Erlang現(xiàn)有的庫以及碼農(nóng)自己創(chuàng)建的庫是運(yùn)行在Erlang虛擬機(jī)外層的(上面有個(gè)圖)。可以在Erlang系統(tǒng)運(yùn)行的過程中,被加載,啟動(dòng),停止以及卸載,這些,都是碼農(nóng)可以去控制的。
比如:
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的一個(gè)第三方庫,是一個(gè)應(yīng)用非常廣泛的日志組件)。
L5可以使用application:load(lager)加載lager庫,然后使用application:ensure_all_started(lager) 啟動(dòng)lager庫以及l(fā)ager庫所以來的庫(在start時(shí),Erlang系統(tǒng)的處理方式是,如果還沒有l(wèi)oad的話,會(huì)先load,然后再start,所以實(shí)際情況下,load使用的機(jī)會(huì)是比較少的)。
start之后,可以使用application:info() 函數(shù),去檢查是否已經(jīng)啟動(dòng)成功。確認(rèn)started了,再去unload(好像有點(diǎn)作,僅僅是為了演示一下),然后發(fā)現(xiàn)報(bào)錯(cuò)了,是因?yàn)閘ager庫正在運(yùn)行,無法unload,那么就先stop 掉lager庫吧。
注意,有可能有些人比較疑惑,運(yùn)行得好好的,為啥要stop呢?在這里可能有這樣一種原因,我們自己創(chuàng)建了一個(gè)庫,然后上線了,運(yùn)行了一段時(shí)間之后,發(fā)現(xiàn),有一個(gè)出現(xiàn)幾率很小的bug,想修復(fù)一下,這個(gè)時(shí)候可以用熱更,也可以用stop -> unload -> 修改代碼/編譯 -> load -> start 的方式。如果是我想用其中一個(gè)庫替換掉這個(gè)庫,那么這個(gè)庫就已經(jīng)沒有存在的必要了,就必須stop掉。
動(dòng)態(tài)類型
Erlang既是動(dòng)態(tài)語言,又是動(dòng)態(tài)類型。
動(dòng)態(tài)語言指的是,在系統(tǒng)運(yùn)行過程中,可以改變代碼的結(jié)構(gòu),現(xiàn)有的函數(shù)可以被刪除或者是被修改,運(yùn)行時(shí)代碼可以根據(jù)某些條件改變自身結(jié)構(gòu)。這也是Erlang可以熱更新的一個(gè)基礎(chǔ)。
動(dòng)態(tài)類型值得是,在程序原形期間才會(huì)檢查數(shù)據(jù)類型,數(shù)據(jù)類型的綁定不是在編譯階段,而是延后到運(yùn)行階段。
舉兩個(gè)例子:
1 8> F = fun(A, B) -> io:format(“-----------------~n”), A + B end.
2 #Fun
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,定義一個(gè)函數(shù),先輸入一個(gè)橫線(-----------------),然后執(zhí)行兩個(gè)參數(shù)的相加操作。在L3處調(diào)用該函數(shù),傳入的兩個(gè)參數(shù)是1 和 “1”,然后,發(fā)生了什么?首先輸出了橫線,也就是函數(shù)已經(jīng)被執(zhí)行了,而真正運(yùn)行到相加操作時(shí),才會(huì)檢查兩個(gè)參數(shù)的數(shù)據(jù)類型。
再看一個(gè)需要編譯的例子:
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.
在這個(gè)test模塊中,定義了兩個(gè)函數(shù),第一個(gè)是start函數(shù),可以被外部調(diào)用,在start函數(shù)中,調(diào)用了一個(gè)內(nèi)部函數(shù),add,add函數(shù)執(zhí)行的是兩個(gè)變量的相加操作,而在start函數(shù)中,向add函數(shù)傳入了兩個(gè)參數(shù),第一個(gè)是參數(shù)是1,第二個(gè)是“1”,這明顯是會(huì)失敗的嘛(其他語言可能不會(huì),但是在Erlang語言中,這是會(huì)失敗的)。
但是在編譯的時(shí)候,編譯器并沒有檢查start函數(shù)中傳給add函數(shù)的兩個(gè)參數(shù)的數(shù)據(jù)類型,這個(gè)模塊是可以編譯通過的。(如何編譯模塊,會(huì)在后面的blog中細(xì)說)
但是在運(yùn)行時(shí),就會(huì)出現(xiàn)錯(cuò)誤。
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調(diào)用了test 模塊的start函數(shù),然后就出現(xiàn)錯(cuò)誤了。
從上面的兩個(gè)例子中,可以看出,動(dòng)態(tài)類型存在著一定的弊端,潛在的錯(cuò)誤異常,只有在運(yùn)行階段才能被發(fā)現(xiàn),無法在編譯的時(shí)候就盡早的發(fā)現(xiàn)潛在的錯(cuò)誤異常。
解釋型
Erlang編程語言是解釋型語言,運(yùn)行在虛擬機(jī)上,具有良好的平臺(tái)兼容性。
總結(jié)
Erlang是函數(shù)式編程語言,其核心是Erlang虛擬機(jī)。Erlang并發(fā)進(jìn)程不同于操作系統(tǒng)進(jìn)程,是非常輕量的,Erlang內(nèi)置的分布式特性,異常方便, Erlang編程語言軟實(shí)時(shí)的特性能夠在其錯(cuò)誤異常處理機(jī)制的保護(hù)下更加健壯的運(yùn)行,其熱更新能給我們碼農(nóng)帶來諸多的方便。
評(píng)論