(轉)分佈式系統編程,你到哪一級了?

介紹html

當分佈式系統編程成爲你生活中的一部分時,你須要經歷一段學習曲線。這篇文章描述了一下我當前在這個領域大體屬於哪一個層次,並但願能爲你指出足夠多 的錯誤,從別人的錯誤中學習,從而使你能以最優的路徑通向成功。先聲明一下,我在1995年時達到第1級,我如今處於第3級。你本身屬於哪一級呢?java

第0級:徹底一無所知git

每一個程序員都從這一級開始。我不會在此浪費太多口舌,由於這實在沒什麼太多可說的。相反,我會引用一些我曾經經歷過的對話,爲從未接觸過度布式系統的開發者們提供一些建議。程序員

對話1:github

NN:在分佈式系統中,複製是個很容易的操做,你只須要讓全部的結點同時存儲你要複製的東東就好了 算法

另外一段對話(從我記憶深處挖出來的):編程

NN: 「爲了咱們的第一人稱射擊遊戲,咱們得寫一個本身的網絡處理引擎。」api

我:「爲何?」安全

NN: 「雖然已經有一些優秀的商業引擎了,但獲取license的費用很是高昂,咱們不想爲此買單。」服務器

我:「你以前對於分佈式系統有什麼經驗嗎?」

NN:「是的,我以前寫過一個套接字服務器。」

我:「你以爲你要花多久能完成這個網絡引擎?」

NN:「我想2周吧。保險起見,我計劃用4周時間。」

好吧,有時候仍是保持沉默比較好。

第1級:RPC

RMI是一種很是強 大的用來構建大型系統的技術。事實上,這個技術用Java來描述的話,結合一些工做的例子能夠在短短几頁紙內描述清楚。RMI技術很是使人振奮,並且它很 容易使用。你能夠調用你所能綁定到的任何服務器資源,並且你能夠構建出分佈式的網絡對象。過去人們經常爲構建複雜的軟件系統犯難,如今RMI打開了這道大 門。   ——   Peter van der Linden, Just Java(第4版, Sun Microsystems)

我先聲明,我並非說這本書很爛。我清楚的記得這本書讀起來頗有趣(尤爲是章節之間插入的軼聞),我曾經學習Java的時候就是用的這本書(過久以 前了,簡直不像在一個時空裏似的)。通常狀況下,我以爲做者說的挺好。他對RMI的態度就是典型的分佈式系統設計的第1級水平。處於這個等級的人對統一的 對象有共同的見解。事實上,Waldo在他們著名的論文「a note on distributed computing」(1994)上曾深刻描述過,這裏我作下總結:

我所倡導的寫分佈式應用的策略可分爲3個階段。第1階段,寫這個應用時不用擔憂對象 存儲的位置,以及它們之間的通信如何實現。第2階段,經過具體化對象的位置以及通信方法來調整程序性能。第3階段,真槍實彈的測試(網絡隔離、機器宕機等 各類狀況)。這裏的思想就是,無論一個調用是本地的仍是遠程的,對程序的正確性都不會產生任何影響。

一樣仍是這篇論文,隨後進一步挖掘了這個主題並展現了其中的問題。這個觀點是錯誤的,並且已經錯了快20年。無論如何,若是說Java RMI達成了一個目標,那就是:就算你從等式中拿掉傳輸協議、命名、綁定以及序列化,它仍是不成立。能記得起CORBA的老程序員們一樣也會記得它也是很差使的,但他們有一個藉口:CORBA還在同各類底層的問題纏鬥中。Java RMI將全部這些都拋開了,但使剩下的問題變得更爲突出。其中有兩點,第一點純粹就是個麻煩:

網絡不是透明的

讓咱們看看這段簡單的Java RMI代碼示例(一樣取自Just Java一書)

1
2
3
public interface WeatherIntf extends java.rmi.Remote {
     public String getWeather() throws java.rmi.RemoteException;
}

想要使用天氣服務的客戶端須要這樣作:

1
2
3
4
5
6
7
try {
     Remote robj = Naming.lookup(「 //localhost/WeatherServer」);
     WeatherIntf weatherserver = (WeatherInf)robj;
     String forecast = weatherserver.getWeather();
     System.out.println(「The weather will be 「 + forecast);
} catch (Exception e) {
     System.out.println(e.getMessage());

客戶端代碼須要將RemoteExceptions考慮在內。若是你想看看你究竟會遇到什麼樣的異常錯誤,能夠看看那20多個子類的定義。這樣你的代碼就會變得醜陋,好吧,這個咱們就忍了。

局部性錯誤

RMI的真正問題在於這些調用可能會出現局部性失敗的狀況。好比,調用可能會在對其餘層的請求操做執行前失敗,又或者請求成功了,但以後的返回值又不正確。引發這類局部性失敗的緣由很是多。其實,這些故障模式正是分佈式系統特性的明肯定義:

「分佈式系統就是某一臺你根本意識不到其存在的計算機,它的故障會形成你的計算機沒法正常使用。」  ——  Leslie Lamport

若是這個方法只是去檢索天氣預報,出現問題時你能夠簡單的進行重試,但若是你想遞增一個計數器,重試可能會致使產生0到2次的更新,結果就不肯定 了。這個解決方案應該來自冪等操做,但構建這樣的操做並不老是可行的。此外,由於你決定改變方法調用的語義,那你基本上就認可了RMI與本地調用是不一樣 的。而這也就認可了RMI其實是個悖論。

不論什麼狀況下,這種範式都是失敗的。由於網絡的透明度和分佈式系統的架構抽象歷來就是沒法實現的。這也代表了某些軟件所採用的方法比其餘軟件爲此 所受到的影響更多。Scrum的一些變種方法中傾向於作原型化。原型更集中於「好的方面」(happy path),而好的方面一般都不是問題所在之處。這基本上意味着你將永遠停留在第1級的水平。(很差意思,我知道這是個小小的打擊)

那些脫離了第一級水平的人懂得對於須要解決的這個問題,咱們要有足夠的尊重。他們摒棄了網絡透明化的思想,從戰略性的角度來處理局部性失敗的問題。

第2級:分佈式算法 + 異步消息傳遞 + 語言級支持

OK,你已經學習了分佈式計算中的悖論是什麼。你決定吞下這顆子彈,而後對消息傳遞機制建模,以此顯式地控制出現失敗的狀況。你將應用分爲兩個層次,底層負責網絡和消息傳遞,而上層處理消息的到達,以及須要處理的各類請求。

這個上層實現了一種分佈式狀態機,若是你去問設計者這個狀態機是用來作什麼的,他們可能會這樣回答你:這是創建在TCP之上的一個Multi-Paxos算法實現。

明智的開發,這裏用到的策略能夠歸結爲:程序員首先在本地主要採用線程來模擬不一樣的進程來開發這個應用。每一個線程運行分佈式狀態機的一個部分,基本 上就是負責運行一段消息處理的循環。一旦這個應用是本地完整的且運行正確,就能夠在遠端的計算機上用真正的進程來取代線程。到這個階段,除去網絡中可能出 現的問題外,這個分佈式應用已經能夠正常工做了。到容錯階段時,能夠經過配置每一個分佈式實體來正確反映故障的方式來達成,這種方式很直接。(我引述自「A Fault Tolerant Abstraction for Transparent Distributed Programming」)

由於分佈式狀態機的存在,局部性故障能夠經過設計來解決。對於線程,其實也有不少種選擇,但協程(coroutines)更適合(在各類不一樣的編程語言中,協程也被稱爲纖程fiber,輕量級線程,微線程或者就叫線程),由於協程容許咱們對併發行爲有更細粒度的控制。

結合「C代碼並不會使網絡變得更快」的論點,你能夠轉移到在語言級支持這種細粒度併發控制的編程語言中去。流行的選擇以下(排名不分前後)注意,這些編程語言每每都是函數式的:

1.  Mozart

2.  Erlang

3. OCaml

4. Haskell

5. Stackless

6.  Clojure

舉個例子,下面讓咱們看看在Erlang中這種併發控制的代碼看起來是怎樣的(取自Erlang concurrent programming

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- module (tut15)
- export ([start/0, ping/2, pong/0]).
ping(0, Pong_PID ) ->
     Pong_PID ! finished,
     io:format (「ping finished~n」, []);
 
ping(N, Pong_PID )->
     Pong_PID ! {ping, self()},
     receive
         pong ->
         io:format (「 Ping received pong~n」, [])
     end ,
     ping(N – 1, Pong_PID ).
 
pong() ->
     receive
     finished ->
         io:format (「 Pong finished~n」, []);
     {ping, Ping_PID } ->
         io:format (「 Pong received ping~n」, []),
         Ping_PID ! pong,
         pong()
     end .
 
start() ->
     Pong_PID = spawn(tut15, pong, []),
     spawn(tut15, ping, [3, Pong_PID ]).

這看起來絕對是對舊有的RPC機制的一個重大提高。如今你能夠推想一下,若是有消息沒有到達時會發生什麼事情了。Erlang還有附加的超時消息以及一個語言內建的「超時」組件,可使你以一種優雅的方式來處理超時。

如今,你選擇了你要採用的策略,選擇了恰當的分佈式算法以及合適的編程語言,而後就能夠開幹了。你很自信能駕馭分佈式編程這頭野獸了,由於你不再是第一級的水平了。

哎呀,惋惜的是這一路上並不是風平浪靜。過了一段時間,當第一個版本發佈後,你將陷入泥潭之中。人們會告訴你,你的分佈式應用有些問題。問題報告中的 主題全都是和變化有關的。開始時會出現「有時」或者「一次」這樣的表示頻率的詞,以後的描述變成了:系統處於不指望的狀態,卡住不動了。若是夠幸運,你有 足夠的log信息,能夠開始着手檢查這些日誌。稍後,你發現是一系列不幸的事件序列形成了報告中所描述的狀況。確實,這是個新的問題。你歷來沒有考慮過這 些,並且在你作大量的測試和模擬時問題從未出現過。因此,你修改代碼以將這種狀況也歸入考慮範圍。

由於你試着要超前考慮,你決定構建一個「猴子」組件,它以僞隨機的方式讓你的分佈式系統作些愚蠢的事情。「猴子」在籠子裏使勁撲騰着,很快你會發如今不少場景下都會致使出現不指望的狀況,好比系統卡住了,或者甚至更糟糕的狀況:系統出現不一致的狀態,而這在分佈式系統中是永遠也不該該發生的事情。

構建一個「猴子」是很棒的主意,並且它確實能減小遇到那些你從未在這個領域內碰到過的怪事的概率。由於你相信,修改一個bug必須和發現這個bug 的測試用例聯繫起來,如今須要迴歸測試這個用例,以證實bug的消除。你如今只須要再構建一次這個測試用例就能夠了。但是如今的問題在於,若是說並不是不可 能的話,要重現這個錯誤的場景起碼是很困難的。你向上帝祈禱,獲得的啓示是:小心存疑慮時,就使用暴力法吧。所以,你構建一個測試用例,而後讓它跑上無數 次,以此來彌補這極小的失敗機率。這會使你解決bug的過程變得緩慢,並且你的測試套件會變得笨重。經過對你的測試集作分而治之的處理,你不得再也不次作一 些補償。不管如何,通過在時間和精力上的大量投入以後,你終於設法獲得了一個較爲穩定的系統。

你在第2級已經到頂了,若是沒有新的啓示,你將永遠卡在這一級。

第3級:分佈式算法 + 異步消息傳遞 +  純函數式

咱們須要花點時間才能意識到:長時間運行「猴子」以此發現系統中的缺陷而後再結合暴力法來重現它們,這種作法並不可取。使用暴力法重現只會顯示出你 的無知。你須要的關鍵性的啓示之一是,若是你能夠只將等式中的不肯定性拿掉的話,你就能夠完美的對每一種場景作重現了。第2級分佈式編程的一個重大的缺點 是:你的併發模型每每會成爲你代碼庫中的病毒。你但願有細粒度的併發控制,好吧,你獲得了,代碼裏處處都是。所以是併發致使了不肯定性,而不肯定性形成了 麻煩。所以必須得把併發給踢出去。但是你又不能拋棄併發,你須要它。那麼,你必定要禁止把併發和你的分佈式狀態機結合在一塊兒。換句話說,你的分佈式狀態機 必須成爲純函數式的。沒有IO操做,沒有併發,什麼都沒有。你的狀態機特徵看起來應該是這樣的:

1
2
3
4
5
6
module type SM = sig
     type state
     type action
     type msg
     val step: msg -> state -> action * state
end

你傳入一個消息和一個狀態,你獲得一個操做和一個結果狀態。操做基本上就是任何試着改變外部世界的東西,須要必定的時間來完成,嘗試的過程當中可能會失敗。典型的操做有:

  1. 發送一個消息
  2. 安排一次超時
  3. 將數據存儲在持久性的存儲介質內

這裏要意識到的重要部分是:你只能經過一個新的消息來獲得新的狀態,再無其餘。在這種嚴格的規定下所獲得的好處是不少的。完美的控制,完美的重現能力以及完美的可追蹤性。爲此而獲得的開銷也一樣存在,你將被迫使全部的操做都變得具體化。而這些基本上就是爲了減小程序複雜性而附加的一層間接。你還須要將每個你關心的外部世界變化都建模爲一個消息。

相比第2級的分佈式編程,另外一個改變在於控制流。在第2級中,客戶端會嘗試強制更新並動態設置狀態。而在這裏,分佈式狀態機假定有徹底的控制力,而且只有當它準備就緒,能夠作些有用的事情時纔會考慮客戶端的請求。所以這些必須分離開來。

若是你把這些道理解釋給一個2級的分佈式系統架構師聽,他可能或多或少的會把這個當成一種替代方案。然而,你須要經歷足夠多的痛苦以後纔會意識到這是惟一可行的選擇,咱們姑且把這些痛苦稱爲經驗吧。

第4級  對分佈式系統領域的深入理解:快樂,好心態,好好睡一覺

老實說,我如今只是第3級水平,我也不知道在這一級裏有什麼新鮮玩意。我深信,函數式編程和異步消息傳遞是分佈式系統謎題的一部分,但這些還不夠。

請容許我重申我所反對的東西。首先,我但願個人分佈式算法實現可以涵蓋到全部的可能狀況。這對我而言是個大問題,我已經在系統部署的問題上犧牲掉了不少睡眠時間。大部分問題都是PEBKAC類的(Problem Exists Between Keyboard And Chair意指用戶引發的錯誤),但有一些確是真正的問題,這給我形成了一些挫敗感。知道本身實現的健壯性程度是很好的。我應該試試證實一下那些定理嗎?我應該作更詳盡的測試嗎?我不知道。

附帶提一下,GitHub上有一個稱爲baardskeerder的僅用於插入操做的B-樹庫,咱們知道能夠經過詳盡的生成插入/刪除排列並斷言它們的正確性以後,咱們就能夠涵蓋到全部的狀況。但這裏,並無那麼簡單,並且我對於要對整個代碼庫作Coqify處理(Coq是一個正式的證實管理系統,它在一種半交互式的環境下提供了一個正式的語言用來編寫數學定義、可執行的算法和定理,用計算機來作檢查證實,這裏做者生造出了Coqify這個詞)還有些猶豫。

第二,爲了保持清晰和簡單,我決定不去碰其它一些正交性的需求。好比,服務發現、認證、受權、私密性以及性能。

說到性能,咱們也許是幸運的,至少異步消息傳遞彷佛與性能方面並不產生矛盾。安全性則徹底是一個XX(做者真的爆粗口了…),由於它幾乎切斷了全部 你所作的事情。有些人把安全性當作是一種調味醬汁,你只要把它倒在你的應用程序上就能夠保證安全了。哎,在這方面我從未取得過成功,並且如今我也認爲這個 問題須要在設計的最初階段從宏觀的角度策略性的去分析解決。

 

結語

開發出健壯的分佈式系統是個頗爲棘手的問題,實際上根本沒有完美的解決方案,或者說至少沒有讓我以爲徹底滿意的解決方案。我敢確定分佈式系統的重要性將隨着處理器和其它一切事物之間的延遲增長而顯著提升。這一結果使得這種類型的應用程序開發變得愈發繁榮。

至於分佈式編程的第4級,也許我該去問問Peter Van Roy。這麼些年來,我閱讀了不少他寫的論文,這些論文對於我本身的一些錯誤認識給了不少啓示。關於這些啓示的缺點嘛,你經常在大部分時間裏看到別人在重複本身的錯誤,但我沒法說服他們應該換種方式去作。

也許,這是由於我沒法提供他們想要的那種靈丹妙藥。他們就想要RPC,並且他們但願這樣能搞定問題。這是執拗的…就像宗教信仰同樣。

原文:http://www.oschina.net/news/29711/distribute-system-programmer

相關文章
相關標籤/搜索