卷1:第15章 Riak與Erlang/OTP

2018-02-24 15:55 更新

Riak是一個(gè)分布式、容錯(cuò)和開放源代碼的數(shù)據(jù)庫,它展示了如何使用Erlang/OTP來構(gòu)建大型可伸縮系統(tǒng)。Riak提供了一些其他數(shù)據(jù)庫中并不常見的特性,比如高可用性、容量和吞吐量的線性伸縮能力等,很大程度上,這是借由Erlang對(duì)大規(guī)模可伸縮分布式系統(tǒng)的支持實(shí)現(xiàn)的。

要開發(fā)像Riak這樣的系統(tǒng),Erlang/OTP是一個(gè)理想的平臺(tái),因?yàn)樗峁┝丝梢灾苯永玫墓?jié)點(diǎn)間通信、消息隊(duì)列、故障探測和客戶-服務(wù)器抽象等功能。而且,Erlang中大多數(shù)常見的模式都已經(jīng)以庫模塊的形式實(shí)現(xiàn)了,我們一般稱之為OTP behaviors。其中包括了用于并發(fā)和錯(cuò)誤處理的通用代碼框架,可以簡化并發(fā)編程,也能避免開發(fā)者陷入一些常見的陷阱。Behaviors由管理者負(fù)責(zé)監(jiān)管,而管理者本身也是behavior,這樣就組成了一個(gè)監(jiān)管樹。通過將監(jiān)管樹打包到應(yīng)用程序中,這就創(chuàng)建了一個(gè)Erlang程序的構(gòu)建塊。

一個(gè)完整的Erlang系統(tǒng),如Riak,是由一組松散耦合且相互作用的應(yīng)用組成的。其中有些應(yīng)用是開發(fā)者編寫的,有些是標(biāo)準(zhǔn)Erlang/OTP發(fā)布包中的,還有一些可能是其他的開源組件。這些應(yīng)用由一個(gè)boot腳本按順序加載并啟動(dòng),而該腳本是從應(yīng)用清單和版本信息中生成的。

系統(tǒng)之間的區(qū)別在于,啟動(dòng)的發(fā)布版本中的應(yīng)用有所不同。在標(biāo)準(zhǔn)的Erlang發(fā)行版中,boot文件會(huì)啟動(dòng)Kernel和StdLib(Standard Library,標(biāo)準(zhǔn)庫)等應(yīng)用。而在有些安裝版本中,還會(huì)啟動(dòng)SASL(Systems Architecture Support Library,系統(tǒng)架構(gòu)支持庫)應(yīng)用。SASL中包含了帶有日志功能的發(fā)布和軟件更新工具。對(duì)Riak而言,除了啟動(dòng)其特定的應(yīng)用以及運(yùn)行時(shí)依賴(其中包括Kernel、StdLib和SASL)之外,并沒有什么不同。一個(gè)完整的、準(zhǔn)備好運(yùn)行的Riak構(gòu)建版本,實(shí)際上將Erlang/OTP發(fā)行包中的這些標(biāo)準(zhǔn)元素都嵌入其中了,當(dāng)在命令行調(diào)用riak start?時(shí),它們會(huì)一同啟動(dòng)。Riak由很多復(fù)雜的應(yīng)用組成,所以本章不應(yīng)看做一個(gè)完整的指南。倒是可以把本章看做以Riak源代碼為例,針對(duì)OTP的入門指南。圖片和數(shù)字主要是為了闡明設(shè)計(jì)意圖,故有所簡化。

15.1 Erlang簡介

Erlang是一個(gè)并發(fā)的函數(shù)式編程語言,用它編寫的程序會(huì)編譯為字節(jié)代碼并運(yùn)行在虛擬機(jī)上。程序中互相調(diào)用的函數(shù)經(jīng)常會(huì)產(chǎn)生副作用,如進(jìn)程間消息傳遞,I/O和數(shù)據(jù)庫操作等。而Erlang變量是單賦值的,也就是說,一旦變量被給定了一個(gè)值,就再也不能修改了。從下面的計(jì)算階乘的例子可以看出,Erlang中大量使用了模式匹配:

-module(factorial).
-export([fac/1]).
fac(0) -> 1;
fac(N) when N>0 ->
   Prev = fac(N-1),
   N*Prev.

在這段代碼中,第一個(gè)子句(clause)給出了0的階乘,第二個(gè)字句計(jì)算正數(shù)的階乘。每一個(gè)子句的主體部分都是一個(gè)表達(dá)式序列,主體部分中最后一個(gè)表達(dá)式就是這個(gè)子句的計(jì)算結(jié)果。調(diào)用這個(gè)函數(shù)的時(shí)候如果傳入一個(gè)負(fù)數(shù)會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤,因?yàn)闆]有一個(gè)子句能匹配負(fù)數(shù)的模式。不處理這種情況的做法是非防御式(non-defensive)編程的一個(gè)例子,這種做法也是Erlang中鼓勵(lì)的做法。

在模塊之中,函數(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)。元組由花括號(hào)包圍起來,例如{ok,37}。在元組中,通過元素的位置訪問元素。記錄(record)是另一種數(shù)據(jù)類型;在記錄中可以保存固定數(shù)目的元素,這些元素可以通過名字訪問和操作。例如這樣的語法可以定義一個(gè)記錄:-record(state, {id, msg_list=[]})。通過表達(dá)式Var = #state{id=1}可以創(chuàng)建一個(gè)實(shí)例,然后通過這樣的表達(dá)式可以查看實(shí)例中的內(nèi)容:Var#state.id。如果要使用可變數(shù)目的元素,那么我們可以使用列表,列表通過方括號(hào)定義,例如[23,34]。[X|Xs]的表達(dá)方式匹配一個(gè)非空的列表,其中X匹配頭,Xs匹配尾。用小寫字母開頭的標(biāo)識(shí)符表示一個(gè)原子(atom),原子就是一個(gè)表示自己的字符串;例如,元組{ok,37}中的ok就是一個(gè)原子。通常通過這種方式使用原子來表示函數(shù)的結(jié)果,例如除了ok結(jié)果之外,還可以有{error, "Error String"}這種形式的結(jié)果。

Erlang系統(tǒng)中的進(jìn)程在獨(dú)立的內(nèi)存中并發(fā)運(yùn)行,以消息傳遞的方式進(jìn)行相互通信。進(jìn)程可以應(yīng)用于大量的應(yīng)用,其中包括數(shù)據(jù)庫的網(wǎng)關(guān),協(xié)議棧的處理程序,以及管理從其他進(jìn)程發(fā)送來的跟蹤消息的日志。雖然這些進(jìn)程處理不同的請(qǐng)求,但是進(jìn)程處理請(qǐng)求的方式卻是有相似之處的。

因?yàn)檫M(jìn)程只存在于虛擬機(jī)中,一個(gè)VM可以同時(shí)運(yùn)行成千上萬個(gè)進(jìn)程,Riak就大量使用了這一特性。例如,對(duì)數(shù)據(jù)的每一個(gè)請(qǐng)求——讀、寫和刪除——都采用獨(dú)立進(jìn)程處理的模型,這種方式對(duì)于大多數(shù)采用操作系統(tǒng)級(jí)線程的實(shí)現(xiàn)而言都是不可能的。

進(jìn)程是通過進(jìn)程標(biāo)識(shí)符識(shí)別的,進(jìn)程標(biāo)識(shí)符稱為PID;此外,進(jìn)程還可以通過別名注冊(cè),不過注冊(cè)別名的方式應(yīng)該只用于長時(shí)間運(yùn)行的“靜態(tài)”進(jìn)程。如果一個(gè)進(jìn)程注冊(cè)了一個(gè)別名,那么其他進(jìn)程就可以在不知道這個(gè)進(jìn)程PID的情況下給這個(gè)進(jìn)程發(fā)送消息。進(jìn)程的創(chuàng)建通過內(nèi)建函數(shù)(built-in function,BIF)?spawn(Module, Function, Arguments)完成。BIF是集成在虛擬機(jī)中的函數(shù),用于完成純Erlang不可能實(shí)現(xiàn)或?qū)崿F(xiàn)很慢的功能。spawn/3這個(gè)BIF接受一個(gè)Module、一個(gè)Function和一個(gè)Arguments作為參數(shù)。這個(gè)BIF的調(diào)用返回新創(chuàng)建的進(jìn)程的PID,并且產(chǎn)生一個(gè)副作用,就是創(chuàng)建了一個(gè)新的進(jìn)程以之前傳入的參數(shù)執(zhí)行模塊中的函數(shù)。

我們通過Pid ! Msg這種寫法將消息Msg發(fā)送給進(jìn)程Pid。一個(gè)進(jìn)程可以通過調(diào)用BIF?self來得到其PID,之后該進(jìn)程可以將PID發(fā)送給其他進(jìn)程,這樣別的進(jìn)程就能夠利用它與原來的進(jìn)程通信了。假設(shè)一個(gè)進(jìn)程期望接收{ok, N}{error, Reason}這種形式的消息。這個(gè)進(jìn)程可以通過receive語句處理這些消息:

receive
   {ok, N} ->
      N+1;
   {error, _} ->
      0
end

這條語句的結(jié)果是由模式匹配語句確定的數(shù)值。如果在模式匹配中并不需要某個(gè)變量的值,可以像上面例子中那樣用下劃線來代替。

進(jìn)程之間的消息傳遞是異步的,進(jìn)程接收到的消息會(huì)按照其到達(dá)順序放在其信箱中。假設(shè)現(xiàn)在正在執(zhí)行的就是上面的receive表達(dá)式:如果信箱中的第一個(gè)元素是{ok, N}{error, Reason},那就可以返回相應(yīng)結(jié)果。如果第一個(gè)元素并非這兩種形式之一,那它會(huì)繼續(xù)保留在信箱之中,然后以類似的方式處理第二個(gè)消息。如果沒有消息能匹配成功,receive會(huì)繼續(xù)等待,直到接收到一個(gè)匹配的消息。

進(jìn)程終止有兩種原因。如果沒有更多的代碼要執(zhí)行了,它們會(huì)以原因normal退出。如果進(jìn)程遇到了運(yùn)行時(shí)錯(cuò)誤,它會(huì)以非normal的原因退出。進(jìn)程的終止只會(huì)對(duì)和其“鏈接”在一起的進(jìn)程產(chǎn)生影響。進(jìn)程可以通過BIF?link(Pid)鏈接在一起,也可以在調(diào)用spawn_link(Module, Function, Arguments)的時(shí)候鏈接在一起。如果一個(gè)進(jìn)程終止了,那么這個(gè)進(jìn)程會(huì)對(duì)其鏈接集合中的所有進(jìn)程發(fā)送一個(gè)EXIT信號(hào)。如果終止原因不是normal,那么收到這個(gè)信號(hào)的進(jìn)程會(huì)終止自己,并且進(jìn)一步傳播EXIT信號(hào)。如果調(diào)用BIF?process_flag(trap_exit, true),那么進(jìn)程收到EXIT信號(hào)之后不會(huì)終止,而是以Erlang消息的方式將EXIT信號(hào)放在進(jìn)程的信箱中。

Riak通過EXIT信號(hào)監(jiān)視輔助進(jìn)程的健康狀況,這些輔助進(jìn)程負(fù)責(zé)執(zhí)行由請(qǐng)求驅(qū)動(dòng)的有限狀態(tài)機(jī)發(fā)起的非關(guān)鍵性的工作。當(dāng)這些輔助進(jìn)程異常終止的時(shí)候,父進(jìn)程可以通過EXIT信號(hào)決定忽略錯(cuò)誤或重新啟動(dòng)進(jìn)程。

15.2. 進(jìn)程框架

我們前面引入了這一概念,即不管進(jìn)程是出于什么目的創(chuàng)建的,它們總要遵從一個(gè)共同的模式。作為開始,我們必須創(chuàng)建一個(gè)進(jìn)程,然后可以為它注冊(cè)一個(gè)別名,當(dāng)然后者是可選的。對(duì)于新創(chuàng)建的進(jìn)程而言,它的第一個(gè)動(dòng)作是初始化進(jìn)程循環(huán)數(shù)據(jù)。循環(huán)數(shù)據(jù)一般通過在進(jìn)程初始化時(shí)傳給內(nèi)置函數(shù)spawn的參數(shù)得到。循環(huán)數(shù)據(jù)保存在叫做進(jìn)程狀態(tài)的變量中。狀態(tài)(一般保存在一個(gè)記錄中)會(huì)被傳遞給接收-求值函數(shù),該函數(shù)是一個(gè)循環(huán),負(fù)責(zé)接收消息,處理消息,更新狀態(tài),之后將狀態(tài)作為參數(shù)傳給一個(gè)尾遞歸調(diào)用。如果處理到了‘stop’消息,接收進(jìn)程會(huì)清理自身數(shù)據(jù),然后退出。

不管進(jìn)程要執(zhí)行什么任務(wù),這都是進(jìn)程之間反復(fù)出現(xiàn)的一種機(jī)制。記住這一點(diǎn)之后,我們?cè)賮砜匆幌?,遵守這一模式的進(jìn)程之間又有何不同:

  • 創(chuàng)建不同的進(jìn)程時(shí)傳入BIF?spawn的參數(shù)會(huì)有不同
  • 在創(chuàng)建一個(gè)進(jìn)程的時(shí)候,要考慮是否為這個(gè)進(jìn)程注冊(cè)一個(gè)別名,如果需要的話,還要考慮別名是什么。
  • 初始化進(jìn)程狀態(tài)的函數(shù)要執(zhí)行的動(dòng)作依進(jìn)程執(zhí)行任務(wù)的不同而不同。
  • 無論哪種情況,系統(tǒng)的狀態(tài)都用循環(huán)數(shù)據(jù)表示,但不同進(jìn)程的循環(huán)數(shù)據(jù)會(huì)有不同。
  • 在接收-求值循環(huán)體中,不同的進(jìn)程接收的消息是不一樣的,而且處理的方式也五花八門。
  • 最后,在進(jìn)程結(jié)束的時(shí)候,清理動(dòng)作也隨進(jìn)程而異。

所以,即使存在一個(gè)通用的動(dòng)作框架,它們?nèi)匀恍枰c具體任務(wù)相關(guān)的各種動(dòng)作來補(bǔ)充。以該框架為模板,程序員能夠創(chuàng)建不同的進(jìn)程,用以承擔(dān)服務(wù)器、有限狀態(tài)機(jī)、事件處理程序和監(jiān)督者等不同職責(zé)。但是我們不必每次都重新實(shí)現(xiàn)這些模式,它們已經(jīng)作為行為模式放在類庫中了。它們是OTP中間件的一部分。

15.3. OTP行為

開發(fā)Riak的核心開發(fā)者團(tuán)隊(duì)分布在十幾個(gè)不同的地點(diǎn)。如果沒有非常緊密的合作和可操作的模板,那么最終可能會(huì)得到各種不同的客戶端/服務(wù)器實(shí)現(xiàn),這些實(shí)現(xiàn)可能還不能處理特殊的邊界條件和并發(fā)相關(guān)的錯(cuò)誤。此外,可能還無法形成一種處理客戶端和服務(wù)器崩潰的統(tǒng)一方法,而且也無法保證來自于一個(gè)請(qǐng)求的應(yīng)答是一個(gè)合法應(yīng)答而不只是某條服從內(nèi)部消息協(xié)議的任意消息。

OTP指的是一組Erlang庫和設(shè)計(jì)模式,宗旨是為開發(fā)健壯系統(tǒng)提供一組現(xiàn)成的工具。其中很多模式和庫都以“行為”(behavior)的形式提供。

OTP行為提供了一些實(shí)現(xiàn)了最常見并發(fā)設(shè)計(jì)模式的庫模塊,從而解決了上述問題。在幕后,這些庫模塊可以確保以一致的方式處理錯(cuò)誤和特殊情況,而程序員并不需要意識(shí)到這些。因此,OTP行為提供了一組標(biāo)準(zhǔn)化的構(gòu)建單元,利用這些構(gòu)建單元可以設(shè)計(jì)和構(gòu)建工業(yè)強(qiáng)度的系統(tǒng)。

15.3.1. OTP行為簡介

OTP行為是通過stdlib應(yīng)用程序中的一些庫模塊提供的,而后者是Erlang/OTP發(fā)行版中的一部分。由程序員編寫的具體代碼放在獨(dú)立的模塊中,這些代碼通過每一個(gè)行為中預(yù)定義的一組標(biāo)準(zhǔn)回調(diào)函數(shù)調(diào)用。這個(gè)回調(diào)模塊要包含實(shí)現(xiàn)某個(gè)功能所需要的所有具體代碼。

OTP行為中包含工作進(jìn)程,負(fù)責(zé)實(shí)際的處理工作,還包含監(jiān)督者進(jìn)程,負(fù)責(zé)監(jiān)視工作進(jìn)程和其他監(jiān)督進(jìn)程。工作進(jìn)程(worker)行為包括服務(wù)器、事件處理程序和有限狀態(tài)機(jī),在圖中通常使用圓圈表示。監(jiān)督者(supervisor)負(fù)責(zé)監(jiān)視其子進(jìn)程,既包含工作進(jìn)程也包含其他監(jiān)督者,在圖中通常用方框表示,工作者和監(jiān)督者共同組成了監(jiān)督樹(supervision tree)。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)