歡迎光臨
每天分享高質量文章

程式員修神之路–

寫在開始
 

        一般來說有兩種策略用來在併發執行緒中進行通訊:共享資料和訊息傳遞。使用共享資料方式的併發程式設計面臨的最大的一個問題就是資料條件競爭。處理各種鎖的問題是讓人十分頭痛的一件事。

   傳統多數流行的語言併發是基於多執行緒之間的共享記憶體,使用同步方法防止寫爭奪,Actors使用訊息模型,每個Actor在同一時間處理最多一個訊息,可以傳送訊息給其他Actor,保證了單獨寫原則。從而巧妙避免了多執行緒寫爭奪。和共享資料方式相比,訊息傳遞機制最大的優點就是不會產生資料競爭狀態。實現訊息傳遞有兩種常見的型別:基於channel(golang為典型代表)的訊息傳遞和基於Actor(erlang為代表)的訊息傳遞。

 

Actor簡介
 

Actor模型(Actor model)首先是由Carl Hewitt在1973定義, 由Erlang OTP 推廣,其 訊息傳遞更加符合面向物件的原始意圖。Actor屬於併發元件模型,透過元件方式定義併發程式設計正規化的高階階段,避免使用者直接接觸多執行緒併發或執行緒池等基礎概念。
 

Actor模型=資料+行為+訊息。
 

        Actor模型是一個通用的併發程式設計模型,而非某個語言或框架所有,幾乎可以用在任何一門程式語言中,其中最典型的是erlang,在語言層面就提供了Actor模型的支援,殺手鐧應用RabbitMQ 就是基於erlang開發的

 

更加面向物件
 

        Actor類似面向物件程式設計(OO)中的物件,每個Actor實體封裝了自己相關的狀態,並且和其他Actor處於物理隔離狀態。舉個遊戲玩家的例子,每個玩家在Actor系統中是Player 這個Actor的一個實體,每個player都有自己的屬性,比如Id,暱稱,攻擊力等,體現到程式碼級別其實和我們OO的程式碼並無多大區別,在系統記憶體級別也是出現了多個OO的實體

 class PlayerActor
    {
        public int Id { getset; }
        public string Name { getset; }
    }
 

無鎖
 

        在使用Java/C# 等語言進行併發程式設計時需要特別的關註鎖和記憶體原子性等一系列執行緒問題,而Actor模型內部的狀態由它自己維護即它內部資料只能由它自己修改(透過訊息傳遞來進行狀態修改),所以使用Actors模型進行併發程式設計可以很好地避免這些問題。Actor內部是以單執行緒的樣式來執行的,類似於redis,所以Actor完全可以實現分散式鎖類似的應用。

 

 非同步
 

        每個Actor都有一個專用的MailBox來接收訊息,這也是Actor實現非同步的基礎。當一個Actor實體向另外一個Actor發訊息的時候,並非直接呼叫Actor的方法,而是把訊息傳遞到對應的MailBox裡,就好像郵遞員,並不是把郵件直接送到收信人手裡,而是放進每家的郵箱,這樣郵遞員就可以快速的進行下一項工作。所以在Actor系統裡,Actor傳送一條訊息是非常快的。

這樣的設計主要優勢就是解耦了Actor,數萬個Actor併發的執行,每個actor都以自己的步調執行,且傳送訊息,接收訊息都不會被阻塞。

 

隔離
 

        每個Actor的實體都維護這自己的狀態,與其他Actor實體處於物理隔離狀態,並非像 多執行緒+鎖 樣式那樣基於共享資料。Actor透過訊息的樣式與其他Actor進行通訊,與OO式的訊息傳遞方式不同,Actor之間訊息的傳遞是真正物理上的訊息傳遞

 

天生分散式
 

        每個Actor實體的位置透明,無論Actor地址是在本地還是在遠端機器上對於程式碼來說都是一樣的。每個Actor的實體非常小,最多幾百位元組,所以單機幾十萬的Actor的實體很輕鬆。如果你寫過golang程式碼,就會發現其實Actor在重量級上很像Goroutine。由於位置透明性,所以Actor系統可以隨意的橫向擴充套件來應對併發,對於呼叫者來說,呼叫的Actor的位置就在本地,當然這也得益於Actor系統強大的路由系統。

生命週期
 

        每個Actor實體都有自己的生命週期,就像C# java 中的GC機制一樣,對於需要淘汰的Actor,系統會銷毀然後釋放記憶體等資源來保證系統的持續性。其實在Actor系統中,Actor的銷毀完全可以手動幹預,或者做到系統自動化銷毀。

容錯
 

        說到Actor的容錯,不得不說還是挺令人意外的。傳統的程式設計方式都是在將來可能出現異常的地方去捕獲異常來保證系統的穩定性,這就是所謂的防禦式程式設計。但是防禦式程式設計也有自己的缺點,類似於現實,防禦的一方永遠不能100%的防禦住所有將來可能出現程式碼缺陷的地方。比如在java程式碼中很多地方充斥著判斷變數是否為nil,這些就屬於防禦式編碼最典型的案例。但是Actor模型的程式並不進行防禦式程式設計,而是遵循“任其崩潰”的哲學,讓Actor的管理者們來處理這些崩潰問題。比如一個Actor崩潰之後,管理者可以選擇建立新的實體或者記錄日誌。每個Actor的崩潰或者異常資訊都可以反饋到管理者那裡,這就保證了Actor系統在管理每個Actor實體的靈活性。

 

劣勢
 

天下無完美的語言,框架/模型亦是如此。Actor作為分散式下併發模型的一種,也有其劣勢。

0
1
由於同一型別的Actor物件是分散在多個宿主之中,所以取多個Actor的集合是個軟肋。比如在電商系統中,商品作為一類Actor,查詢一個商品的串列在多數情況下經過以下過程:首先根據查詢條件篩選出一系列商品id,根據商品id分別取商品Actor串列(很可能會產生一個商品搜尋的服務,無論是用es或者其他搜尋引擎)。如果量非常大的話,有產生網路風暴的危險(雖然機率非常小)。在實時性要求不是太高的情況下,其實也可以獨立出來商品Actor的串列,利用MQ接收商品資訊修改的訊號來處理資料一致性的問題。

0
2
在很多情況下基於Actor模型的分散式系統,快取很有可能是行程內快取,也就是說每個Actor其實都在行程內儲存了自己的狀態資訊,業內通常把這種服務成為有狀態服務。但是每個Actor又有自己的生命週期,會產生問題嗎?呵呵,也許吧。想想一下,還是拿商品作為例子, 如果環境是非Actor併發模型,商品的快取可以利用LRU策略來淘汰非活躍的商品快取,來保證記憶體不會使用過量,如果是基於Actor模型的行程內快取呢,每個actor其實就是快取本身,就不那麼容易利用LRU策略來保證記憶體使用量了,因為Actor的活躍狀態對於你來說是未知的。

0
3
分散式事物問題,其實這是所有分散式模型都面臨的問題,非由於Actor而存在。還是以商品Actor為例,新增一個商品的時候,商品Actor和統計商品的Actor(很多情況下確實被設計為兩類Actor服務)需要保證事物的完整性,資料的一致性。在很多的情況下可以犧牲實時一致性用最終一致性來保證。

0
4
每個Actor的mailBox有可能會出現堆積或者滿的情況,當這種情況發生,新訊息的處理方式是被拋棄還是等待呢,所以當設計一個Actor系統的時候mailBox的設計需要註意。

寫在最後 升華一下
1
透過以上介紹,既然Actor對於位置是透明的,任何Actor對於其他Actor就好像在本地一樣。基於這個特性我們可以做很多事情了,以前傳統的分散式系統,A伺服器如果想和B伺服器通訊,要麼RPC的呼叫(http呼叫不太常用),要麼透過MQ系統。但是在Actor系統中,伺服器之間的通訊都變的很優雅了,雖然本質上也屬於RPC呼叫,但是對於編碼者來說就好像在呼叫本地函式一樣。其實現在比較時興的是Streaming方式。

2
由於Actor系統的執行模型是單執行緒,並且非同步,所以凡是有資源競爭的類似功能都非常適合Actor模型,比如秒殺活動。

3
基於以上的介紹,Actor模型在設計層面天生就支援了負載均衡,而且對於水平擴容支援的非常好。當然Actor的分散式系統也是需要服務註冊中心的。

4
雖然Actor是單執行緒執行模型,並不意味著每個Actor都需要佔用一個執行緒,其實Actor上執行的任務就像Golang的goroutine一樣,完全可以是一個輕量級的東西,而且一個宿主上所有的Actor可以共享一個執行緒池,這就保證了在使用最少執行緒資源的情況下,最大量化業務程式碼。

    贊(0)

    分享創造快樂