分散式系統程式設計,你到哪一級了?

bigship發表於2013-03-31

介紹

當分散式系統程式設計成為你生活中的一部分時,你需要經歷一段學習曲線。這篇文章描述了一下我當前在這個領域大致屬於哪個層次,並希望能為你指出足夠多的錯誤,從別人的錯誤中學習,從而使你能以最優的路徑通向成功。先宣告一下,我在1995年時達到第1級,我現在處於第3級。你自己屬於哪一級呢?

第0級:完全一無所知

每個程式設計師都從這一級開始。我不會在此浪費太多口舌,因為這實在沒什麼太多可說的。相反,我會引用一些我曾經經歷過的對話,為從未接觸過分散式系統的開發者們提供一些建議。

對話1:

NN:在分散式系統中,複製是個很容易的操作,你只需要讓所有的結點同時儲存你要複製的東東就行了

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

NN: “為了我們的第一人稱射擊遊戲,我們得寫一個自己的網路處理引擎。”

我:“為什麼?”

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一書)

想要使用天氣服務的客戶端需要這樣做:

客戶端程式碼需要將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

這看起來絕對是對舊有的RPC機制的一個重大提升。現在你可以推想一下,如果有訊息沒有到達時會發生什麼事情了。Erlang還有附加的超時訊息以及一個語言內建的“超時”元件,可以使你以一種優雅的方式來處理超時。

現在,你選擇了你要採用的策略,選擇了恰當的分散式演算法以及合適的程式語言,然後就可以開幹了。你很自信能駕馭分散式程式設計這頭野獸了,因為你再也不是第一級的水平了。

哎呀,可惜的是這一路上並非風平浪靜。過了一段時間,當第一個版本釋出後,你將陷入泥潭之中。人們會告訴你,你的分散式應用有些問題。問題報告中的主題全都是和變化有關的。開始時會出現“有時”或者“一次”這樣的表示頻率的詞,之後的描述變成了:系統處於不期望的狀態,卡住不動了。如果夠幸運,你有足夠的log資訊,可以開始著手檢查這些日誌。稍後,你發現是一系列不幸的事件序列造成了報告中所描述的情況。確實,這是個新的問題。你從來沒有考慮過這些,而且在你做大量的測試和模擬時問題從未出現過。所以,你修改程式碼以將這種情況也納入考慮範圍。

因為你試著要超前考慮,你決定構建一個“猴子”元件,它以偽隨機的方式讓你的分散式系統做些愚蠢的事情。“猴子”在籠子裡使勁撲騰著,很快你會發現在很多場景下都會導致出現不期望的情況,比如系統卡住了,或者甚至更糟糕的情況:系統出現不一致的狀態,而這在分散式系統中是永遠也不應該發生的事情。

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

你在第2級已經到頂了,如果沒有新的啟示,你將永遠卡在這一級。

第3級:分散式演算法 + 非同步訊息傳遞 +  純函式式

我們需要花點時間才能意識到:長時間執行“猴子”以此發現系統中的缺陷然後再結合暴力法來重現它們,這種做法並不可取。使用暴力法重現只會顯示出你的無知。你需要的關鍵性的啟示之一是,如果你可以只將等式中的不確定性拿掉的話,你就可以完美的對每一種場景做重現了。第2級分散式程式設計的一個重大的缺點是:你的併發模型往往會成為你程式碼庫中的病毒。你希望有細粒度的併發控制,好吧,你得到了,程式碼裡到處都是。因此是併發導致了不確定性,而不確定性造成了麻煩。因此必須得把併發給踢出去。可是你又不能拋棄併發,你需要它。那麼,你一定要禁止把併發和你的分散式狀態機結合在一起。換句話說,你的分散式狀態機必須成為純函式式的。沒有IO操作,沒有併發,什麼都沒有。你的狀態機特徵看起來應該是這樣的:

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

  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,而且他們希望這樣能搞定問題。這是固執的…就像宗教信仰一樣。

相關文章