Erlang基礎 -- 介紹 -- Erlang特點


前言

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輕量的並發進程以及非常方便好用的分布式特性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM