本系列文章整理自《大型網站系統與Java中間件實踐》
分布式系統的定義
A distributed system is one in which components located at networked computers communicate and coordinate their actions only by passing messages.
組件分布在網絡計算機上,組件之間僅僅通過消息傳遞來通信并協調行動。
理解:分布式系統一定是由多個節點組成的系統,一般來說一個節點就是一臺計算機,節點之間互連;最后這些節點部署了我們的組件,并且相互之間的操作會有協同。
分布式系統的意義
- 升級單機處理能力的性價比越來越低;
- 單機處理能力存在瓶頸;
- 出于穩定性和可用性考慮。
分布式系統基本知識
1.組成計算機的5要素
輸入設備、輸出設備、運算器、控制器、存儲器(內存和外存)。計算機斷電時內存的數據會丟失,外存的數據依然存在。
2.線程與進程的執行模式
明確:這里指的是單進程下的多線程。多線程開發中,我們需要處理線程間的通信,需要對線程并發做控制,需要做好線程間的協調工作。
2.1阿姆達爾定律
P指的是程序中可并行部分的程序在單核上執行時間的占比,N表示處理器的個數(總核心數)。S(N)是指程序在N個處理器(總核心數)相對在單個處理器(單核)中速度的提升比。
這個公式告訴我們,程序中可并行的代碼比例決定你增加處理器(總核心數)所能帶來的速度提升上限。例如當P=0.5,速度上限是2。當P=0.2,速度上限是1.25??梢?,在多核的時代,并發程序的開發多么重要。
2.2互不通信的多線程模式
在多線程程序中,多個線程會在系統中并發執行。如果線程之間不需要處理共享的數據,也不需要動作協調,將會非常簡單,就是多個線程獨立完成自己線程的工作。如圖:
2.3基于共享容器協同的多線程模式
多個線程之間對共享數據進行處理。如經典的生產者和消費者例子,我們有一個隊列用于生產和消費,那么這個隊列就是多個線程會共享一個容器或者數據對象,多個線程并發地訪問這個隊列。
對于這種在多線程環境下對統一數據的訪問,我們需要有所保護和控制以保證訪問的正確性。對于存儲數據的容器或者對象,有線程安全和線程不安全之分,對于線程不安全的,一般通過加鎖或者通過Copy On Write的方式來控制并發訪問。使用加鎖的方式時,如果數據讀寫比例很高,一般采用讀寫鎖而非簡單的互斥鎖。對于線程安全的容器或對象,可以在多線程環境下直接使用。
注意:有時通過加鎖把使用線程不安全容器的代碼改為使用線程安全容器的代碼時,會遇到一個陷阱。
即在一個使用Map進行計數統計總數的例子中,map中value整型使用線程不安全容器HashMap是這樣的:
public class TestClass {
private HashMap<String, Integer> map = new HashMap<String, Integer>();
public void add(String key){
Integer value = map.get(key);
if (value == null) {
map.put(key, 1);
}else{
map.put(key, value + 1);
}
}
}
使用ConcurrentHashMap來替換HashMap,并且僅僅去掉Synchoronized關鍵字,問題出現了。(Java代碼如下)
public class TestClass {
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
public void add(String key) {
Integer value = map.get(key);
if (value == null) {
map.put(key, 1);
} else {
map.put(key, value + 1);
}
}
}
我的理解是對ConcurrentHashMap的get和put方法是線程安全的,但這個計數過程是要求對整個過程(get值判斷再put)線程安全的,先get值判斷再put得操作不是原子的,所以不線程安全,會造成計數小于實際。
2.4通過事件協同的多線程模式
除了并發控制訪問,線程間會存在協調的需求。例如A、B線程,B線程需要等到某個狀態或事件發生后才能繼續自己的工作,而這個狀態發生或者改變和A線程相關。這個場景下,需要完成線程間的協調。
如上圖,右側線程執行到某個步驟需要等待事件觸發,這個事件由左側線程觸發并通知。期間,右側線程一直阻塞直到事件通知后才繼續執行。
同時還需要避免死鎖情況,一般能夠原子性的獲取需要的多個鎖,或者注意調整對多個鎖的獲取順序,能有效避免死鎖。
下面來看一個死鎖的例子:加鎖有兩個鎖A和B,兩個線程T1和T2,兩個線程T1,T2某段代碼都需要獲取A鎖和B鎖,偽代碼如下
T1代碼
……
A.lock();
B.lock();
……
T2代碼
……
B.lock();
A.lock();
……
這個時候T1等不到B,T2等不到A。下面這種做法可以避免死鎖:
T1代碼
……
A.lock();
B.lock();
……
T2代碼
……
A.lock();
B.lock();
……
T2線程獲取鎖的順序發生變化,現在和T1一樣,都獲取A,再獲取B鎖。
還有另一種方式避免死鎖:
T1代碼
……
getLocks(A,B);
……
T2代碼
……
getLocks(A,B);
……
getLocks方法一次性獲取兩個鎖。
2.5 多進程模式
下面關注進程間的關系,多進程和多線程有許多相似之處。線程是屬于進程的,一個進程的多個線程共享內存空間,而多個進程之間內存空間是相互獨立的,因此多個進程間通過內存共享,交換數據的方式有所不同。進程間的通信,協調以及事件通知或者等待鎖釋放也與多線程不一樣。
區別:多進程相對于單進程多線程的方式來說,資源控制更容易實現。多進程的單個進程錯誤不會導致系統整體不可用。
分布式系統是多機組成的系統,可以把它看做單機多線程變成了多機多線程。多機系統帶來一個好處,即當單個機器出現問題時,如果處理得好,就不會影響整體的集群。
3.網絡通信的基礎知識
3.1 OSI與TCP/IP模型
ISO的OSI七層網絡模型
OSI模型與TCP/IP模型對比
3.2網絡IO實現方式
接觸比較多的網絡模型主要是TCP/IP協議棧,UDP也在一些場景下用到。
當我們使用socket通信時,有三種方式:BIO、NIO和AIO。
BIO方式
BIO即Blokcing IO,采用阻塞的方式實現。即一個Socket通信需要一個線程處理。發生建立連接、讀數據、寫數據的操作時,都可能會阻塞。這個模式簡單。但帶來主要問題是當作為Server端,一個線程只處理一個socket,在支持并發連接時,需要更多的線程完成這個工作。
BIO的工作方式:
NIO方式
NIO即NonBlocking IO,基于事件驅動思想,采用Reactor模式。Java實現的服務器系統較多采用。相對于BIO,NIO一個明顯的好處是不需要為每個socket套接字分配一個線程,而可以在一個線程處理多個Socket套接字相關的工作。
上圖,Rector會管理所有的hanler,并且把出現的事件交給相應的Handle處理。
通信中的應用:在NIO模式下不是用單個線程去應對單個Socket套接字,而是統一通過Reactor的所有客戶端的Socket套接字處理,然后派發到不同線程中。這就解決了BIO為支撐更多Socket套接字而需要打開更多線程的問題。
AIO方式
AIO即AsynchronousIO,即異步IO。通過Proactor模式。AIO與NIO不同之處在于,AIO在讀寫操作時,只需調用相應的read/write方法,并且傳入ComletionHandler(動作完成處理器);
在動作完成后,會調用動作完成處理器。
AIO與NIO最大區別:NIO在有通知時可以進行讀或寫,而AIO在有通知時表示相關操作已完成。
4.如何把應用從單機擴展到分布式
輸入、輸出、運算、存儲、控制這五個方面組成計算機,分布式有何變化?
4.1輸入設備的變化
分布式系統由通過網絡連接的多個節點組成,一種是互相連接的多個節點,在接收其他節點傳來的信息時,該節點可以看做是輸入設備;另外一種就是傳統意義上的人機交互輸入設備。
4.2輸出設備變化
一種是指系統中的節點;另一種是用戶終端屏幕。
4.3控制器變化
單機系統中指的是CPU中的控制器。
分布式系統中是由多個節點通過網絡連接在一起并通過消息的傳遞進行協調的系統。
控制器的主要作用就是協調和控制節點之間的動作和行為。
如上圖是一個遠程服務調用的場景。所有請求經過中間負載均衡設備來完成請求轉發的控制,這就是一種控制的方式。
使用LVS請求調用,這種方式代價低,可控性強。一般稱之為透明代理。
在集群中,這種方式對于請求發起方和處理請求的一方都是透明的。
這種方式有兩種不足:一方面增加網絡開銷(流量),延遲。數據量越大約明顯。由于中間的透明代理處于請求的必經路徑上,如果代理出現問題,所有請求都會影響。
第三種方式
請求發起和處理方直連,外部有一個名稱服務。作用:一個收集提供請求處理的服務器處理地址信息;另外提供地址信息給請求發起方。起到地址交換作用,在發起請求的機器上需要根據從名稱服務得到的服務器地址進行負載均衡的工作。原來透明代理上的工作被拆分到名稱服務上和請求發起方。
第四種-規則服務器請求直連
這種模式下,請求方和處理方也是直連的。那么請求方如何選擇處理方的機器呢?規則服務器的規則。
在請求方機器,會有對規則進行處理的代碼邏輯。規則服務器只負責把規則提供給請求方。
最后一種方式Master+Worker
Master管理任務,分配給不同的Worker來做。場景:任務分配和管理。
4.4運算器的變化
多臺服務器,由DNS服務器進行調度和控制,在用戶解析DNS時,就會給予一個服務器地址。
另一種方案:負載均衡
中間增加負載均衡設備(純硬件或LVS),DNS永遠返回負載均衡的地址,而用戶訪問是通過負載均衡到達后面的服務器。
總結來說,構成運算器的多個節點在控制器的配合下對外提供服務,構成分布式系統的運算器。
日志處理系統場景:
單日志系統
改進Master方式
4.5存儲器變化
分布式系統中需要把承擔存儲功能的多個節點組織在一起,使之看起來是一個存儲器。
單機的Key-Value場景
改進:使用代理。代理服務器作為控制器轉發來自應用服務器的請求。一般可以根據Key來劃分。
使用名稱服務的Key-Value服務
應用服務器與KV服務器直接相連。KV服務器的選擇邏輯放在應用服務器上完成。實際實施中通過規則服務器配合;另外則是對等看待,靈活適應KV服務器的增加減少。
規則服務器不僅寫明了如何對數據做劃分,還包含了具體的KV服務器地址。
不同的是Master會根據請求返回目標的KV服務器地址,然后由應用服務器直接請求KV服務器。KV存儲服務器選擇工作在MASTER完成。
應用服務器只需根據Master返回的結果去訪問相應的KV服務器即可。具體應用廣泛。
5.分布式系統難點
5.1缺乏全局時鐘
很多時候,我們使用時鐘可以區分2個動作的順序,而不是一定要知道準確時間。這個工作交給單獨集群來完成,通過集群區分多個動作的順序。
5.2面對故障獨立性
5.3處理單點故障
整個分布式系統如果某個功能只有某臺單機支撐,這個節點稱為單點。故障稱為單點故障。SPoF(Single Point of Failure).分布式系統中盡量避免單點。
無法避免:
1.給單點備份,盡量做到遇到問題,自動恢復。
2.降低單點故障影響范圍。如:
5.4事務的挑戰
兩階段提交(2PC)、最終一致、BASE、CAP、Paxos等。至此簡要介紹了分布式系統中比較基本的偏實踐的知識。