Riak是一個分布式、容錯和開放源代碼的數(shù)據(jù)庫,它展示了如何使用Erlang/OTP來構(gòu)建大型可伸縮系統(tǒng)。Riak提供了一些其他數(shù)據(jù)庫中并不常見的特性,比如高可用性、容量和吞吐量的線性伸縮能力等,很大程度上,這是借由Erlang對大規(guī)??缮炜s分布式系統(tǒng)的支持實現(xiàn)的。
要開發(fā)像Riak這樣的系統(tǒng),Erlang/OTP是一個理想的平臺,因為它提供了可以直接利用的節(jié)點間通信、消息隊列、故障探測和客戶-服務(wù)器抽象等功能。而且,Erlang中大多數(shù)常見的模式都已經(jīng)以庫模塊的形式實現(xiàn)了,我們一般稱之為OTP behaviors。其中包括了用于并發(fā)和錯誤處理的通用代碼框架,可以簡化并發(fā)編程,也能避免開發(fā)者陷入一些常見的陷阱。Behaviors由管理者負責(zé)監(jiān)管,而管理者本身也是behavior,這樣就組成了一個監(jiān)管樹。通過將監(jiān)管樹打包到應(yīng)用程序中,這就創(chuàng)建了一個Erlang程序的構(gòu)建塊。
一個完整的Erlang系統(tǒng),如Riak,是由一組松散耦合且相互作用的應(yīng)用組成的。其中有些應(yīng)用是開發(fā)者編寫的,有些是標(biāo)準(zhǔn)Erlang/OTP發(fā)布包中的,還有一些可能是其他的開源組件。這些應(yīng)用由一個boot腳本按順序加載并啟動,而該腳本是從應(yīng)用清單和版本信息中生成的。
系統(tǒng)之間的區(qū)別在于,啟動的發(fā)布版本中的應(yīng)用有所不同。在標(biāo)準(zhǔn)的Erlang發(fā)行版中,boot文件會啟動Kernel和StdLib(Standard Library,標(biāo)準(zhǔn)庫)等應(yīng)用。而在有些安裝版本中,還會啟動SASL(Systems Architecture Support Library,系統(tǒng)架構(gòu)支持庫)應(yīng)用。SASL中包含了帶有日志功能的發(fā)布和軟件更新工具。對Riak而言,除了啟動其特定的應(yīng)用以及運行時依賴(其中包括Kernel、StdLib和SASL)之外,并沒有什么不同。一個完整的、準(zhǔn)備好運行的Riak構(gòu)建版本,實際上將Erlang/OTP發(fā)行包中的這些標(biāo)準(zhǔn)元素都嵌入其中了,當(dāng)在命令行調(diào)用riak start
?時,它們會一同啟動。Riak由很多復(fù)雜的應(yīng)用組成,所以本章不應(yīng)看做一個完整的指南。倒是可以把本章看做以Riak源代碼為例,針對OTP的入門指南。圖片和數(shù)字主要是為了闡明設(shè)計意圖,故有所簡化。
Erlang是一個并發(fā)的函數(shù)式編程語言,用它編寫的程序會編譯為字節(jié)代碼并運行在虛擬機上。程序中互相調(diào)用的函數(shù)經(jīng)常會產(chǎn)生副作用,如進程間消息傳遞,I/O和數(shù)據(jù)庫操作等。而Erlang變量是單賦值的,也就是說,一旦變量被給定了一個值,就再也不能修改了。從下面的計算階乘的例子可以看出,Erlang中大量使用了模式匹配:
-module(factorial).
-export([fac/1]).
fac(0) -> 1;
fac(N) when N>0 ->
Prev = fac(N-1),
N*Prev.
在這段代碼中,第一個子句(clause)給出了0的階乘,第二個字句計算正數(shù)的階乘。每一個子句的主體部分都是一個表達式序列,主體部分中最后一個表達式就是這個子句的計算結(jié)果。調(diào)用這個函數(shù)的時候如果傳入一個負數(shù)會導(dǎo)致運行時錯誤,因為沒有一個子句能匹配負數(shù)的模式。不處理這種情況的做法是非防御式(non-defensive)編程的一個例子,這種做法也是Erlang中鼓勵的做法。
在模塊之中,函數(shù)以正常的方式調(diào)用;而在模塊之外,函數(shù)名之前應(yīng)該加上模塊名,如factorial:fac(3)
。允許定義同名但是參數(shù)數(shù)目不同的函數(shù)——函數(shù)的參數(shù)數(shù)目稱為函數(shù)的元數(shù)(arity)。在factorial
模塊的export指令中,元數(shù)為1的fac
函數(shù)通過fac/1
表示。
Erlang支持元組(tuple,也稱為乘積類型(product type))和列表(list)。元組由花括號包圍起來,例如{ok,37}
。在元組中,通過元素的位置訪問元素。記錄(record)是另一種數(shù)據(jù)類型;在記錄中可以保存固定數(shù)目的元素,這些元素可以通過名字訪問和操作。例如這樣的語法可以定義一個記錄:-record(state, {id, msg_list=[]})
。通過表達式Var = #state{id=1}
可以創(chuàng)建一個實例,然后通過這樣的表達式可以查看實例中的內(nèi)容:Var#state.id
。如果要使用可變數(shù)目的元素,那么我們可以使用列表,列表通過方括號定義,例如[23,34]
。[X|Xs]
的表達方式匹配一個非空的列表,其中X匹配頭,Xs匹配尾。用小寫字母開頭的標(biāo)識符表示一個原子(atom),原子就是一個表示自己的字符串;例如,元組{ok,37}
中的ok
就是一個原子。通常通過這種方式使用原子來表示函數(shù)的結(jié)果,例如除了ok
結(jié)果之外,還可以有{error, "Error String"}
這種形式的結(jié)果。
Erlang系統(tǒng)中的進程在獨立的內(nèi)存中并發(fā)運行,以消息傳遞的方式進行相互通信。進程可以應(yīng)用于大量的應(yīng)用,其中包括數(shù)據(jù)庫的網(wǎng)關(guān),協(xié)議棧的處理程序,以及管理從其他進程發(fā)送來的跟蹤消息的日志。雖然這些進程處理不同的請求,但是進程處理請求的方式卻是有相似之處的。
因為進程只存在于虛擬機中,一個VM可以同時運行成千上萬個進程,Riak就大量使用了這一特性。例如,對數(shù)據(jù)的每一個請求——讀、寫和刪除——都采用獨立進程處理的模型,這種方式對于大多數(shù)采用操作系統(tǒng)級線程的實現(xiàn)而言都是不可能的。
進程是通過進程標(biāo)識符識別的,進程標(biāo)識符稱為PID;此外,進程還可以通過別名注冊,不過注冊別名的方式應(yīng)該只用于長時間運行的“靜態(tài)”進程。如果一個進程注冊了一個別名,那么其他進程就可以在不知道這個進程PID的情況下給這個進程發(fā)送消息。進程的創(chuàng)建通過內(nèi)建函數(shù)(built-in function,BIF)?spawn(Module, Function, Arguments)
完成。BIF是集成在虛擬機中的函數(shù),用于完成純Erlang不可能實現(xiàn)或?qū)崿F(xiàn)很慢的功能。spawn/3
這個BIF接受一個Module
、一個Function
和一個Arguments
作為參數(shù)。這個BIF的調(diào)用返回新創(chuàng)建的進程的PID,并且產(chǎn)生一個副作用,就是創(chuàng)建了一個新的進程以之前傳入的參數(shù)執(zhí)行模塊中的函數(shù)。
我們通過Pid ! Msg
這種寫法將消息Msg
發(fā)送給進程Pid
。一個進程可以通過調(diào)用BIF?self
來得到其PID,之后該進程可以將PID發(fā)送給其他進程,這樣別的進程就能夠利用它與原來的進程通信了。假設(shè)一個進程期望接收{ok, N}
和{error, Reason}
這種形式的消息。這個進程可以通過receive語句處理這些消息:
receive
{ok, N} ->
N+1;
{error, _} ->
0
end
這條語句的結(jié)果是由模式匹配語句確定的數(shù)值。如果在模式匹配中并不需要某個變量的值,可以像上面例子中那樣用下劃線來代替。
進程之間的消息傳遞是異步的,進程接收到的消息會按照其到達順序放在其信箱中。假設(shè)現(xiàn)在正在執(zhí)行的就是上面的receive表達式:如果信箱中的第一個元素是{ok, N}
或{error, Reason}
,那就可以返回相應(yīng)結(jié)果。如果第一個元素并非這兩種形式之一,那它會繼續(xù)保留在信箱之中,然后以類似的方式處理第二個消息。如果沒有消息能匹配成功,receive會繼續(xù)等待,直到接收到一個匹配的消息。
進程終止有兩種原因。如果沒有更多的代碼要執(zhí)行了,它們會以原因normal退出。如果進程遇到了運行時錯誤,它會以非normal的原因退出。進程的終止只會對和其“鏈接”在一起的進程產(chǎn)生影響。進程可以通過BIF?link(Pid)
鏈接在一起,也可以在調(diào)用spawn_link(Module, Function, Arguments)
的時候鏈接在一起。如果一個進程終止了,那么這個進程會對其鏈接集合中的所有進程發(fā)送一個EXIT信號。如果終止原因不是normal,那么收到這個信號的進程會終止自己,并且進一步傳播EXIT信號。如果調(diào)用BIF?process_flag(trap_exit, true)
,那么進程收到EXIT信號之后不會終止,而是以Erlang消息的方式將EXIT信號放在進程的信箱中。
Riak通過EXIT信號監(jiān)視輔助進程的健康狀況,這些輔助進程負責(zé)執(zhí)行由請求驅(qū)動的有限狀態(tài)機發(fā)起的非關(guān)鍵性的工作。當(dāng)這些輔助進程異常終止的時候,父進程可以通過EXIT信號決定忽略錯誤或重新啟動進程。
我們前面引入了這一概念,即不管進程是出于什么目的創(chuàng)建的,它們總要遵從一個共同的模式。作為開始,我們必須創(chuàng)建一個進程,然后可以為它注冊一個別名,當(dāng)然后者是可選的。對于新創(chuàng)建的進程而言,它的第一個動作是初始化進程循環(huán)數(shù)據(jù)。循環(huán)數(shù)據(jù)一般通過在進程初始化時傳給內(nèi)置函數(shù)spawn
的參數(shù)得到。循環(huán)數(shù)據(jù)保存在叫做進程狀態(tài)的變量中。狀態(tài)(一般保存在一個記錄中)會被傳遞給接收-求值函數(shù),該函數(shù)是一個循環(huán),負責(zé)接收消息,處理消息,更新狀態(tài),之后將狀態(tài)作為參數(shù)傳給一個尾遞歸調(diào)用。如果處理到了‘stop’消息,接收進程會清理自身數(shù)據(jù),然后退出。
不管進程要執(zhí)行什么任務(wù),這都是進程之間反復(fù)出現(xiàn)的一種機制。記住這一點之后,我們再來看一下,遵守這一模式的進程之間又有何不同:
spawn
的參數(shù)會有不同所以,即使存在一個通用的動作框架,它們?nèi)匀恍枰c具體任務(wù)相關(guān)的各種動作來補充。以該框架為模板,程序員能夠創(chuàng)建不同的進程,用以承擔(dān)服務(wù)器、有限狀態(tài)機、事件處理程序和監(jiān)督者等不同職責(zé)。但是我們不必每次都重新實現(xiàn)這些模式,它們已經(jīng)作為行為模式放在類庫中了。它們是OTP中間件的一部分。
開發(fā)Riak的核心開發(fā)者團隊分布在十幾個不同的地點。如果沒有非常緊密的合作和可操作的模板,那么最終可能會得到各種不同的客戶端/服務(wù)器實現(xiàn),這些實現(xiàn)可能還不能處理特殊的邊界條件和并發(fā)相關(guān)的錯誤。此外,可能還無法形成一種處理客戶端和服務(wù)器崩潰的統(tǒng)一方法,而且也無法保證來自于一個請求的應(yīng)答是一個合法應(yīng)答而不只是某條服從內(nèi)部消息協(xié)議的任意消息。
OTP指的是一組Erlang庫和設(shè)計模式,宗旨是為開發(fā)健壯系統(tǒng)提供一組現(xiàn)成的工具。其中很多模式和庫都以“行為”(behavior)的形式提供。
OTP行為提供了一些實現(xiàn)了最常見并發(fā)設(shè)計模式的庫模塊,從而解決了上述問題。在幕后,這些庫模塊可以確保以一致的方式處理錯誤和特殊情況,而程序員并不需要意識到這些。因此,OTP行為提供了一組標(biāo)準(zhǔn)化的構(gòu)建單元,利用這些構(gòu)建單元可以設(shè)計和構(gòu)建工業(yè)強度的系統(tǒng)。
OTP行為是通過stdlib
應(yīng)用程序中的一些庫模塊提供的,而后者是Erlang/OTP發(fā)行版中的一部分。由程序員編寫的具體代碼放在獨立的模塊中,這些代碼通過每一個行為中預(yù)定義的一組標(biāo)準(zhǔn)回調(diào)函數(shù)調(diào)用。這個回調(diào)模塊要包含實現(xiàn)某個功能所需要的所有具體代碼。
OTP行為中包含工作進程,負責(zé)實際的處理工作,還包含監(jiān)督者進程,負責(zé)監(jiān)視工作進程和其他監(jiān)督進程。工作進程(worker)行為包括服務(wù)器、事件處理程序和有限狀態(tài)機,在圖中通常使用圓圈表示。監(jiān)督者(supervisor)負責(zé)監(jiān)視其子進程,既包含工作進程也包含其他監(jiān)督者,在圖中通常用方框表示,工作者和監(jiān)督者共同組成了監(jiān)督樹(supervision tree)。
更多建議: