1. 程式人生 > 實用技巧 >分散式相關理論

分散式相關理論

1. 分散式相關理論

1.1CAP定理

CAP 理論含義是,一個分散式系統不可能同時滿足一致性(C:Consistency),可用性(A: Availability)和分割槽容錯性(P:Partition tolerance)這三個基本需求,最多隻能同時滿足其中的2個。

選項描述

C 一致性分散式系統當中的一致性指的是所有節點的資料一致,或者說是所有副本的資料一致 。如何實現:寫入主資料庫後,在向從資料庫同步期間要將從資料庫鎖定, 等待同步完成後在釋放鎖。

A 可用性Reads and writes always succeed. 也就是說系統一直可用,而且服務一直保持正常。如何實現:寫入主資料庫後,要向從資料庫同步,資料未同步成功時,也要返回查詢資料,不能返回錯誤和超時。

P 分割槽容錯性系統在遇到一些節點或者網路分割槽故障的時候,仍然能夠提供滿足一致性和可用性的服務。實現:使用非同步資料從主資料同步到從資料庫,新增資料庫節點。一個節點掛掉,從其它節點同步。

1.2BASE 理論

BASE:全稱:Basically Available(基本可用),Soft state(軟狀態),和 Eventually consistent(最終一致性)。BASE是對CAP中一致性和可用性權衡的結果,BASE理論的核心思想是:即使無法做到強一致性,但每個應用都可以根據自身業務特點,採用適當的方式來使系統達到最終一致性。

①Basically Available(基本可用):系統出現故障時,損失部分可用性。如12306查詢有票,下單時提示已經無票。

②Soft state(軟狀態):允許系統中的資料存在中間狀態,該狀態不影響系統的整體可用性,即允許副本在不同的多個節點同步時存在延遲。

③Eventually consistent(最終一致性):經過一段時間的同步後,最終能夠達到一個一致的狀態。

1.3 分散式事務

資料庫事務回顧:

Atomicity(原子性): 事務是一個不可分割的整體,要麼全做,要麼不做。

Consistency(一致性):事務執行前後,資料從一個狀態到另一個狀態必須是一致的。如轉賬:不能發生一個扣錢,一個沒扣錢的情況。

Isolation(隔離性):多個併發事務之間相互隔離,不能互相干擾。

Durablity(永續性):事務完成後,對資料庫的更改是永久儲存的。

分散式事務:

一致性協議 2PC:是將整個事務流程分為兩個階段,準備階段(Preparephase)、提交階段(commit phase),2是指兩個階段,P是指準備階段,C是指提交階段。

優點:原理簡單,實現方便。 缺點:同步阻塞,單點問題,資料不一致,過於保守。

一致性協議 3PC:CanCommit、PreCommit和doCommit三個階段組成的事務處理協議

2PC對比3PC:首先對於協調者和參與者都設定了超時機制(在2PC中,只有協調者擁有超時機制,即如果在一定時間內沒有收到參與者的訊息則預設失敗),主要是避免了參與者在長時間無法與協調者節點通訊(協調者掛掉了)的情況下,無法釋放資源的問題,因為參與者自身擁有超時機制會在超時後,自動進行本地commit從而進行釋放資源。而這種機制也側面降低了整個事務的阻塞時間和範圍。 2.通過CanCommit、PreCommit、DoCommit三個階段的設計,相較於2PC而言,多設定了一個緩衝階段保證了在最後提交階段之前各參與節點的狀態是一致的 。3.PreCommit是一個緩衝,保證了在最後提交階段之前各參與節點的狀態是一致的。

1.4一致性演算法 Paxos

Prxos解決了分散式系統一致性問題。

相關概念:提案 (Proposal):Proposal資訊包括提案編號 (Proposal ID) 和提議的值 (Value)。Client:客戶端,客戶端向分散式系統發出請求,並等待響應。Proposer:提案發起者,提案者提倡客戶請求,試圖說服Acceptor對此達成一致,並在發生衝突時充當協調者以推動協議向前發展。Acceptor:決策者,可以批准提案。Learners:最終決策的學習者,學習者充當該協議的複製因素。

Paxos演算法實現過程:

階段一:

(a) Proposer選擇一個提案編號N,然後向半數以上的Acceptor傳送編號為N的Prepare請求。

(b) 如果一個Acceptor收到一個編號為N的Prepare請求,且N大於該Acceptor已經響應過的所有Prepare請求的編號,那麼它就會將它已經接受過的編號最大的提案(如果有的話)作為響應反饋給Proposer,同時該Acceptor承諾不再接受任何編號小於N的提案。

階段二:

(a) 如果Proposer收到半數以上Acceptor對其發出的編號為N的Prepare請求的響應,那麼它就會發送一個針對[N,V]提案的Accept請求給半數以上的Acceptor。注意:V就是收到的響應中編號最大的提案的value,如果

響應中不包含任何提案,那麼V就由Proposer自己決定。(b) 如果Acceptor收到一個針對編號為N的提案的Accept請求,只要該Acceptor沒有對編號大於N的Prepare請求做出過響應,它就接受該提案。

當然,實際執行過程中,每一個Proposer都有可能產生多個提案,但只要每個Proposer都遵循如上所述的演算法運

行,就一定能夠保證演算法執行的正確性。

Learner學習被選定的value方案:Acceptor將批准的提案發送給一個特定的Learner集合,該集合中每個Learner都可以在一個提案被選定後通知其他的Learner。這個Learner集合中的Learner個數越多,可靠性就越好,但同時網路通訊的複雜度也就越高。

如何保證Paxos演算法的活性:極端情況下提交提案會陷入死迴圈,可以通過選取主Proposer,並規定只有主Proposer才能提出議案。這樣一來只要主Proposer和過半的Acceptor能夠正常進行網路通訊,那麼但凡主Proposer提出一個編號更高的提案,該提案終將會被批准,這樣通過選擇一個主Proposer,整套Paxos演算法就能夠保持活性。

1.5一致性演算法 Raft

Raft 是一種為了管理複製日誌的一致性演算法。Raft提供了和Paxos演算法相同的功能和效能,但是它的演算法結構和Paxos不同。Raft演算法更加容易理解並且更容易構建實際的系統。Raft演算法分為兩個階段,首先是選舉過程,然後在選舉出來的領導人帶領進行正常操作,比如日誌複製。

日誌複製(保證資料一致性)過程。

1. 客戶端的每一個請求都包含被複制狀態機執行的指令。

2. leader把這個指令作為一條新的日誌條目新增到日誌中,然後並行發起 RPC 給其他的伺服器,讓他們複製這條資訊。

3. 跟隨者響應ACK,如果 follower 宕機或者執行緩慢或者丟包,leader會不斷的重試,直到所有的 follower 最終都複製了所有的日誌條目。

4. 通知所有的Follower提交日誌,同時領導人提交這條日誌到自己的狀態機中,並返回給客戶端。可以看到,直到第四步驟,整個事務才會達成。中間任何一個步驟發生故障,都不會影響日誌一致

2. 分散式系統設計策略

2.1 心跳檢測

分散式存在非常多的節點(Node),其實質是這些節點分擔任務的執行、計算或者程式邏輯處理。那麼就有一個非常重要的問題,如何檢測一個節點出現了故障乃至無法工作了?通常解決這一問題是採用心跳檢測的手段,如同通過儀器對病人進行一些檢測診斷一樣。心跳顧名思義,就是以固定的頻率向其他節點彙報當前節點狀態的方式。收到心跳,一般可以認為一個節點和現在的網路拓撲是良好的。當然,心跳彙報時,一般也會攜帶一些附加的狀態、元資料資訊,以便管理。

週期檢測心跳機制:Server端每間隔 t 秒向Node叢集發起監測請求,設定超時時間,如果超過超時時間,則判斷“死亡”。

累計失效檢測機制:在週期檢測心跳機制的基礎上,統計一定週期內節點的返回情況(包括超時及正確返回),以此計算節點的“死亡”概率。另外,對於宣告“瀕臨死亡”的節點可以發起有限次數的重試,以作進一步判斷。通過週期檢測心跳機制、累計失效檢測機制可以幫助判斷節點是否“死亡”,如果判斷“死亡”,可以把該節點踢出叢集。

2.2 高可用設計

系統高可用性的常用設計模式包括三種:主備(Master-SLave)、互備(Active-Active)和叢集(Cluster)模式。

主備:MySQL之間資料複製的基礎是二進位制日誌檔案(binary log file)。一臺MySQL資料庫一旦啟用二進位制日誌後,作為master,它的資料庫中所有操作都會以“事件”的方式記錄在二進位制日誌中,其他資料庫作為slave通過一個I/O執行緒與主伺服器保持通訊,並監控master的二進位制日誌檔案的變化,如果發現master二進位制日誌檔案發生變化,則會把變化複製到自己的中繼日誌中,然後slave的一個SQL執行緒會把相關的“事件”執行到自己的資料庫中,以此實現從資料庫和主資料庫的一致性,也就實現了主從複製。

互備模式: 互備模式指兩臺主機同時執行各自的服務工作且相互監測情況。在資料庫高可用部分,常見的互備是MM模式。MM模式即Multi-Master模式,指一個系統存在多個master,每個master都具有read-write能力,會根據時間戳或業務邏輯合併版本。

叢集模式:是指有多個節點在執行,同時可以通過主控節點分擔服務請求。如Zookeeper。叢集模式需要解決主控節點本身的高可用問題,一般採用主備模式。

2.3 容錯性

舉例:我們在專案中使用快取通常都是先檢查快取中是否存在,如果存在直接返回快取內容,如果不存在就直接查詢資料庫然後再快取查詢結果返回。這個時候如果我們查詢的某一個數據在快取中一直不存在,就會造成每一次請求都查詢DB,這樣快取就失去了意義,在流量大時,或者有人惡意攻擊。

解決辦法:將這個不存在的key預先設定一個值。比如,key=“null”。在返回這個null值的時候,我們的應用就可以認為這是不存在的key,那我們的應用就可以決定是否繼續等待訪問,還是放棄掉這次操作。如果繼續等待訪問,過一個時間輪詢點後,再次請求這個key,如果取到的值不再是null,則可以認為這時候key有值了,從而避免了透傳到資料庫,把大量的類似請求擋在了快取之中。

2.4 負載均衡

負載均衡器有硬體解決方案,也有軟體解決方案。硬體解決方案有著名的F5,軟體有LVS、HAProxy、Nginx等。以Nginx為例,負載均衡有以下幾種策略:

·輪詢:即Round Robin,根據Nginx配置檔案中的順序,依次把客戶端的Web請求分發到不同的後端伺服器。

·最少連線:當前誰連線最少,分發給誰。

·IP地址雜湊:確定相同IP請求可以轉發給同一個後端節點處理,以方便session保持。

·基於權重的負載均衡:配置Nginx把請求更多地分發到高配置的後端伺服器上,把相對較少的請求分發到低配伺服器。

3分散式架構網路通訊

在分散式服務框架中,一個最基礎的問題就是遠端服務是怎麼通訊的,在Java領域中有很多可實現遠端通訊的技術,例如:RMI、Hessian、SOAP、ESB和JMS等。

3.1基本原理

要實現網路機器間的通訊,首先得來看看計算機系統網路通訊的基本原理,在底層層面去看,網路通訊需要做的就是將流從一臺計算機傳輸到另外一臺計算機,基於傳輸協議和網路IO來實現,其中傳輸協議比較出名的有tcp、udp等等,tcp、udp都是在基於Socket概念上為某類應用場景而擴展出的傳輸協議,網路IO,主要有bio、nio、aio三種方式,所有的分散式應用通訊都基於這個原理而實現,只是為了應用的易用。

3.2 RPC

RPC全稱為remote procedure call,即遠端過程呼叫。藉助RPC可以做到像本地呼叫一樣呼叫遠端服務,是一種程序間的通訊方式。比如兩臺伺服器A和B,A伺服器上部署一個應用,B伺服器上部署一個應用,A伺服器上的應用想呼叫B伺服器上的應用提供的方法,由於兩個應用不在一個記憶體空間,不能直接呼叫,所以需要通過網路來表達呼叫的語義和傳達呼叫的資料。需要注意的是RPC並不是一個具體的技術,而是指整個網路遠端呼叫過程。

RPC架構

客戶端(Client):服務的呼叫方。客戶端存根(Client Stub):存放服務端的地址訊息,再將客戶端的請求引數打包成網路訊息,然後通過網路遠端傳送給服務方。

服務端(Server):真正的服務提供者。服務端存根(Server Stub):接收客戶端傳送過來的訊息,將訊息解包,並呼叫本地的方法。

3.3 RMI

Java RMI 指的是遠端方法呼叫 (Remote Method Invocation),是java原生支援的遠端呼叫 ,採用JRMP(JavaRemote Messageing protocol)作為通訊協議,可以認為是純java版本的分散式遠端呼叫解決方案, RMI主要用於不同虛擬機器之間的通訊,這些虛擬機器可以在不同的主機上、也可以在同一個主機上,這裡的通訊可以理解為一個虛擬機器上的物件呼叫另一個虛擬機器上物件的方法。

程式碼實現:

1. 建立遠端介面。

import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* 遠端服務物件介面必須繼承Remote介面;同時方法必須丟擲RemoteExceptino異常
*/
public interface Hello extends Remote {
  public String sayHello(User user) throws RemoteException;
}

其中有一個引用物件作為引數。

import java.io.Serializable;
  /**
    * 引用物件應該是可序列化物件,這樣才能在遠端呼叫的時候:1. 序列化物件 2. 拷貝 3. 在網路中傳輸* 4. 服務端反序列化 5. 獲取引數進行方法呼叫; 這種方式其實是將遠端物件引用傳遞的方式轉化為值傳遞的方式
  */
public class User implements Serializable {
  private String name;
  private int age;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }
}

2. 實現遠端服務物件

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
* 遠端服務物件實現類寫在服務端;必須繼承UnicastRemoteObject或其子類
**/
public class HelloImpl extends UnicastRemoteObject implements Hello {
/**
* 因為UnicastRemoteObject的構造方法丟擲了RemoteException異常,因此這裡預設的構造方法必須寫,必須
宣告丟擲RemoteException異常
*
* @throws RemoteException
*/
  private static final long serialVersionUID = 3638546195897885959L;
  protected HelloImpl() throws RemoteException {
    super();
  // TODO Auto-generated constructor stub
  }
  @Override
  public String sayHello(User user) throws RemoteException {
    System.out.println("this is server, hello:" + user.getName());
    return "success";
  }
}

3. 服務端程式

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
/**
* 服務端程式
**/
public class Server {
  public static void main(String[] args) {
    try {
      Hello hello = new HelloImpl(); // 建立一個遠端物件,同時也會建立stub物件、skeleton物件
      //本地主機上的遠端物件登錄檔Registry的例項,並指定埠為8888,這一步必不可少(Java預設埠是1099),必不可缺的一步,缺少登錄檔建立,則無法繫結物件到遠端登錄檔上
      LocateRegistry.createRegistry(8080); //啟動註冊服務
      try {
      //繫結的URL標準格式為:rmi://host:port/name(其中協議名可以省略,下面兩種寫法都是正確的)
        Naming.bind("//127.0.0.1:8080/zm", hello); //將stub引用繫結到服務地址上
      } catch (MalformedURLException e) {
      // TODO Auto-generated catch block
        e.printStackTrace();
      }
      System.out.println("service bind already!!");
    } catch (RemoteException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

4. 客戶端程式

/**
* 客戶端程式
* @author zm
*
*/
public class Client {
  public static void main(String[] args) {
    try {
       //在RMI服務登錄檔中查詢名稱為RHello的物件,並呼叫其上的方法
       Hello hello = (Hello) Naming.lookup("//127.0.0.1:8080/zm");//獲取遠端物件
       User user = new User();
       user.setName("james");
       System.out.println(hello.sayHello(user));
    } catch (MalformedURLException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
    } catch (RemoteException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
    } catch (NotBoundException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
    }
}

3.4 BIO、NIO、AIO

同步和非同步:同步和非同步關注的是訊息通訊機制。所謂同步,就是在發出一個*呼叫*時,在沒有得到結果之前,該*呼叫*就不返回。但是一旦呼叫返回,就得到返回值了。換句話說,就是由*呼叫者*主動等待這個*呼叫*的結果。

而非同步則是相反,*呼叫*在發出之後,這個呼叫就直接返回了,所以沒有返回結果。換句話說,當一個非同步過程呼叫發出後,呼叫者不會立刻得到結果。而是在*呼叫*發出後,*被呼叫者*通過狀態、通知來通知呼叫者,或通過回撥函式處理這個呼叫。

阻塞與非阻塞:阻塞和非阻塞關注的是程式在等待呼叫結果(訊息,返回值)時的狀態。阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起。呼叫執行緒只有在得到結果之後才會返回。

非阻塞呼叫指在不能立刻得到結果之前,該呼叫不會阻塞當前執行緒。

BIO: 同步阻塞IO。伺服器實現模式為一個連線一個執行緒,即客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,當然可以通過執行緒池機制改。(不推薦)

NIO:同步非阻塞IO。當一個連線建立後,不會需要對應一個執行緒,這個連線會被註冊到多路複用器,所以一個連線只需要一個執行緒即可,所有的連線需要一個執行緒就可以操作,該執行緒的多路複用器會輪訓,發現連線有請求時,才開啟一個執行緒處理。

AIO:非同步非阻塞IO。當有流可以讀時,作業系統會將可以讀的流傳入read方法的緩衝區,並通知應用程式,對於寫操作,OS將write方法的流寫入完畢是作業系統會主動通知應用程式。因此read和write都是非同步 的,完成後會呼叫回撥函式。使用場景:連線數目多且連線比較長(重操作)的架構,比如相簿伺服器。重點呼叫了OS參與併發操作,程式設計比較複雜。

3.5 Netty

Netty 是由 JBOSS 提供一個非同步的、 基於事件驅動的網路程式設計框架。Netty 可以幫助你快速、 簡單的開發出一 個網路應用, 相當於簡化和流程化了 NIO 的開發過程。 作為當前最流行的 NIO 框架, Netty 在網際網路領域、 大資料分散式計算領域、 遊戲行業、 通訊行業等獲得了廣泛的應用, 知名的 Elasticsearch 、 Dubbo 框架內部都採用了 Netty。

NIO缺點: NIO 的類庫和 API 繁雜,使用麻煩。你需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。可靠性不強,開發工作量和難度都非常大NIO 的 Bug。例如 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。

Netty優點:對各種傳輸協議提供統一的 API,高度可定製的執行緒模型——單執行緒、一個或多個執行緒池更好的吞吐量,更低的等待延遲,更少的資源消耗,最小化不必要的記憶體拷貝。