隨著軟體複雜度的增加和使用者規模的增長,分散式系統獲得了廣泛應用。對於軟體開發者而言,掌握分散式系統的相關知識是十分必要的。
但分散式系統包括理論、實踐、專案等多方面內容。這些內容往往交織穿插在一起,給軟體開發者的學習帶來了不少困難,讓許多軟體開發者在學習過程中感到混亂和迷茫。
有鑑於此,本文將從應用結構的演進歷程談起,分析分散式系統是如何產生的,並列出判斷系統是否為分散式系統的依據。
概述
隨著軟體規模、性能要求的不斷提升,分散式系統得到快速發展。分散式系統透過許多低成本節點的協作來完成原本需要龐大單體應用才能實現的功能,在降低硬體成本的基礎上,提升了軟體的可靠性、擴充性、靈活性。
然而,分散式系統在帶來上述優點的同時,也帶來了許多技術問題。
首先,分散式系統的架構和實現需要許多分散式理論和演算法作為基礎,如CAP 定理、BASE 定理、Paxos 演算法、兩階段提交演算法、三階段提交演算法等。如果不能了解這些演算法的具體含義,則會給架構和開發工作帶來困擾。
其次,分散式系統的實現依賴大量的技術方案,如分散式鎖、分散式交易、服務發現、服務呼叫、服務保護、服務閘道等。如果對這些技術的具體實施方案和關鍵點了解不透徹,則可能會在專案中引入漏洞。
最後,分散式系統的部署需要依賴許多中介軟體,如訊息系統中介軟體、分散式協調中介軟體等。如果對這些中介軟體的功能和實現原理不清楚,則可能會導致選型和使用上的錯誤,增加應用的開發成本。
應用的演進歷程
單體應用
單體應用是最簡單和最純粹的應用形式,它就是部署在一台機器上的單一應用。單體應用中可以包含很多模組,模組之間會互相呼叫。這些呼叫都在應用內展開,十分方便。因此,單體應用是一個高度內聚的個體,其內部各個模組間是高度耦合的。
單體應用的開發、維護、部署成本低廉,適合實現一些功能簡單、併發數低、容量小的應用的開發需求。
當應用的功能變得複雜、併發數不斷增高、容量不斷變大時,單體應用的規模也會不斷擴大。這會帶來以下兩個方面的挑戰。
■硬體方面。龐大的單體應用需要與之對應的伺服器提供支援,這種伺服器被稱為「大型主機」,其購買、維護費用都極其高昂。
■軟體方面。單體應用內模組間是高度耦合的,應用規模的增大讓這種耦合變得極為複雜,這使得應用軟體的開發維護變得困難。
因此,當應用的功能足夠複雜、併發數足夠高、容量足夠大時,就需要對單體應用進行拆分,以便於對功能、併發數、容量進行分散。這就演變成了叢集應用。
叢集應用
叢集應用可以對應用的併發數、容量進行分散。叢集應用包含多個同質的應用節點,這些節點組成叢集共同對外提供服務。這裡說的「同質」是指每個應用節點執行同樣的程式、具有同樣的設定,它們像是從一個範本中複製出來的一樣。
為了讓叢集應用中的每個節點都承擔一部分併發數和容量,可以透過反向代理等手段將外界請求分散到應用的多個節點上。叢集應用的結構如圖1所示。
但叢集應用帶來的最明顯的問題是同一個使用者發出的多個請求可能會落在不同的節點上,打破了服務的連貫性。
舉例來說,使用者發出R1、R2兩個請求,且R2的執行要依賴R1的資訊(如R1 觸發一個任務,R2用來查詢任務的執行結果)。如果R1和R2被分配到不同的節點上,則R2的操作可能無法正常執行。
為了解決上述問題,演化出以下幾種叢集方案。
▨ 無狀態的節點叢集
無狀態應用是最容易從單體形式擴充到叢集形式的一類應用。對於無狀態應用而言,假設使用者先後發出R1、R2 兩個請求,則無狀態應用無論是否在之前接收過請求R1,總對請求R2 傳回同樣的結果。即無狀態應用列出的任何一個請求的結果都和該應用之前收到的請求無關。
要想讓應用滿足無狀態,必須保證應用的狀態不會因為介面的呼叫而發生變化。查詢介面能滿足這點,舉例來說,對於使用者而言,一個新聞展示應用是無狀態的。
即使是無狀態的節點叢集,也要面對協作問題。平行喚醒問題就是一個典型的協作問題,舉例來說,一個無狀態節點叢集需要在每天淩晨對外發送一封郵件,我們會發現該叢集中的所有節點會在淩晨同時被喚醒並各自發送一封郵件。
我們希望整個節點叢集對外發送一封郵件而非讓每個節點都發送一封郵件。
在這種情況下,可以透過外部請求喚醒來解決無狀態節點叢集的平行喚醒問題。在指定時刻由外部應用發送一個請求給服務叢集觸發任務,該請求最終只會交給一個節點處理,因此實現了獨立喚醒。
無狀態節點叢集設計簡單,可以方便地進行擴充,較少遇到協作問題,但只適合無狀態應用,有很大的局限性。
很多應用是有狀態的,如某個節點接收到外部請求後修改了某物件的屬性,後面的請求再查詢物件屬性時便應該讀取到修改後的結果。如果後面的請求落到了其他節點上,則可能讀取到修改前的結果。這類應用無法擴充為無狀態的節點叢集。
▨ 單一服務的節點叢集
許多服務是有狀態的,使用者的歷史請求在應用中組成了上下文,應用必須結合上下文對使用者的請求進行回覆。舉例來說,在聊天應用中,使用者之前的對話(透過過去的請求實現)便是上下文;在遊戲應用中,使用者之前購買的裝備、晉升的等級(透過過去的請求實現)便是上下文。
有狀態的服務在處理使用者的每個請求時必須讀取和修改使用者的上下文資訊,這在單體應用中是容易實現的,但在節點叢集中,這一切就變得複雜起來。其中一個最簡單的辦法是在節點和使用者之間建立對應關係:
■任意使用者都有一個對應的節點,該節點上保存該使用者的上下文資訊。
■某個使用者的請求總落在與之對應的節點上。
使用者與指定節點的對應關係如圖2 所示。其典型特點就是各個節點是完全隔離的。這些節點執行同樣的程式,具有同樣的設定,然而卻保存了不同使用者的上下文資訊,各自服務自身對應的使用者。
雖然叢集包含多個節點,但是從使用者角度來看,服務某個使用者的始終是同一個節點,因此我們將這種叢集稱為單一服務的節點叢集。
實現單一服務的節點叢集要解決的問題是,如何建立和維護使用者與節點之間的對應關係。具體的實現有很多種,我們列舉常用的幾種。
■在使用者註冊帳號時由使用者自由選擇節點。很多遊戲服務就採用這種方式,讓使用者自由選擇帳號所在的區。
■在使用者註冊帳號時根據使用者所處的網路分配節點。一些郵件服務就採用這種方式。
■在使用者註冊帳號時根據使用者 ID 隨機分配節點。許多聊天應用就採用這種方式。
■在使用者登入帳號時隨機或使用規則分配節點,然後將分配結果寫入cookie,接下來根據請求中的cookie 將使用者請求分配到指定節點。
其中,最後一種方式與前幾種方式略有不同。前幾種方式能保證使用者對應的節點在整個使用者週期內不改變,而最後一種方式則只保證使用者對應的節點在一次階段週期內不改變。最後一種方式適合用在兩次階段之間無上下文關係的場景中,如一些登入應用、許可權應用等,它則只需要維護使用者這次階段內的上下文資訊。
無論採用了哪種方式,使用者的請求都會被路由傳輸到其對應的節點上。根據應用分流方案的不同,該路由操作可以由反向代理、閘道等元件完成。
單一服務的節點叢集能夠解決有狀態服務的問題,但因為各個節點之間是隔離的,無法互相備份。當某個服務節點崩潰時,會使得該節點對應的使用者失去服務。因此,這種設計方案的容錯性比較差。
看完了以上的教學,想必讀者對分散式系統會有更多的認識。
《最新世代平行運算 - 分散式系統主流框架實作指南》/ 易哥 著