1. 程式人生 > 其它 >Sqlserver 之 CROSS APPLY 與 OUTER APPLY <--> INNER/LEFT JOIN

Sqlserver 之 CROSS APPLY 與 OUTER APPLY <--> INNER/LEFT JOIN

此文轉載自:https://blog.csdn.net/u012422440/article/details/110571640

大家好,我是歷小冰,今天來講一下 Reids Cluster 的 Gossip 協議和叢集操作,文章的思維導圖如下所示。

xmind

對於資料儲存領域,當資料量或者請求流量大到一定程度後,就必然會引入分散式。比如 Redis,雖然其單機效能十分優秀,但是因為下列原因時,也不得不引入叢集。

  • 單機無法保證高可用,需要引入多例項來提供高可用性
  • 單機能夠提供高達 8W 左右的QPS,再高的QPS則需要引入多例項
  • 單機能夠支援的資料量有限,處理更多的資料需要引入多例項;
  • 單機所處理的網路流量已經超過伺服器的網絡卡的上限值,需要引入多例項來分流。

有叢集,叢集往往需要維護一定的元資料,比如例項的ip地址,快取分片的 slots 資訊等,所以需要一套分散式機制來維護元資料的一致性。這類機制一般有兩個模式:分散式和集中式

分散式機制將元資料儲存在部分或者所有節點上,不同節點之間進行不斷的通訊來維護元資料的變更和一致性。Redis Cluster,Consul 等都是該模式。

Gossip_model

而集中式是將叢集元資料集中儲存在外部節點或者中介軟體上,比如 zookeeper。舊版本的 kafka 和 storm 等都是使用該模式。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-oJuubLBv-1607000950020)(http://cdn.remcarpediem.net/2020-12-02-150006.png)]

兩種模式各有優劣,具體如下表所示:

模式優點缺點
集中式資料更新及時,時效好,元資料的更新和讀取,時效性非常好,一旦元資料出現了變更,立即就更新到集中式的外部節點中,其他節點讀取的時候立即就可以感知到;較大資料更新壓力,更新壓力全部集中在外部節點,作為單點影響整個系統
分散式資料更新壓力分散,元資料的更新比較分散,不是集中某一個節點,更新請求比較分散,而且有不同節點處理,有一定的延時,降低了併發壓力資料更新延遲,可能導致叢集的感知有一定的滯後

分散式的元資料模式有多種可選的演算法進行元資料的同步,比如說 Paxos、Raft 和 Gossip。Paxos 和 Raft 等都需要全部節點或者大多數節點(超過一半)正常執行,整個叢集才能穩定執行,而 Gossip 則不需要半數以上的節點執行。

Gossip 協議,顧名思義,就像流言蜚語一樣,利用一種隨機、帶有傳染性的方式,將資訊傳播到整個網路中,並在一定時間內,使得系統內的所有節點資料一致。對你來說,掌握這個協議不僅能很好地理解這種最常用的,實現最終一致性的演算法,也能在後續工作中得心應手地實現資料的最終一致性。

Gossip 協議又稱 epidemic 協議(epidemic protocol),是基於流行病傳播方式的節點或者程序之間資訊交換的協議,在P2P網路和分散式系統中應用廣泛,它的方法論也特別簡單:

在一個處於有界網路的叢集裡,如果每個節點都隨機與其他節點交換特定資訊,經過足夠長的時間後,叢集各個節點對該份資訊的認知終將收斂到一致。

這裡的“特定資訊”一般就是指叢集狀態、各節點的狀態以及其他元資料等。Gossip協議是完全符合 BASE 原則,可以用在任何要求最終一致性的領域,比如分散式儲存和註冊中心。另外,它可以很方便地實現彈性叢集,允許節點隨時上下線,提供快捷的失敗檢測和動態負載均衡等。

此外,Gossip 協議的最大的好處是,即使叢集節點的數量增加,每個節點的負載也不會增加很多,幾乎是恆定的。這就允許 Redis Cluster 或者 Consul 叢集管理的節點規模能橫向擴充套件到數千個。

Redis Cluster 是在 3.0 版本引入叢集功能。為了讓讓叢集中的每個例項都知道其他所有例項的狀態資訊,Redis 叢集規定各個例項之間按照 Gossip 協議來通訊傳遞資訊。

在這裡插入圖片描述

上圖展示了主從架構的 Redis Cluster 示意圖,其中實線表示節點間的主從複製關係,而虛線表示各個節點之間的 Gossip 通訊。

Redis Cluster 中的每個節點都維護一份自己視角下的當前整個叢集的狀態,主要包括:

  1. 當前叢集狀態
  2. 叢集中各節點所負責的 slots資訊,及其migrate狀態
  3. 叢集中各節點的master-slave狀態
  4. 叢集中各節點的存活狀態及懷疑Fail狀態

也就是說上面的資訊,就是叢集中Node相互八卦傳播流言蜚語的內容主題,而且比較全面,既有自己的更有別人的,這麼一來大家都相互傳,最終資訊就全面而且一致了。

Redis Cluster 的節點之間會相互發送多種訊息,較為重要的如下所示:

  • MEET:通過「cluster meet ip port」命令,已有叢集的節點會向新的節點發送邀請,加入現有叢集,然後新節點就會開始與其他節點進行通訊;
  • PING:節點按照配置的時間間隔向叢集中其他節點發送 ping 訊息,訊息中帶有自己的狀態,還有自己維護的叢集元資料,和部分其他節點的元資料;
  • PONG: 節點用於迴應 PING 和 MEET 的訊息,結構和 PING 訊息類似,也包含自己的狀態和其他資訊,也可以用於資訊廣播和更新;
  • FAIL: 節點 PING 不通某節點後,會向叢集所有節點廣播該節點掛掉的訊息。其他節點收到訊息後標記已下線。

Redis 的原始碼中 cluster.h 檔案定義了全部的訊息型別,程式碼為 redis 4.0版本。

// 注意,PING 、 PONG 和 MEET 實際上是同一種訊息。
// PONG 是對 PING 的回覆,它的實際格式也為 PING 訊息,
// 而 MEET 則是一種特殊的 PING 訊息,用於強制訊息的接收者將訊息的傳送者新增到叢集中(如果節點尚未在節點列表中的話)
#define CLUSTERMSG_TYPE_PING 0          /* Ping 訊息 */
#define CLUSTERMSG_TYPE_PONG 1          /* Pong 用於回覆Ping */
#define CLUSTERMSG_TYPE_MEET 2          /* Meet 請求將某個節點新增到叢集中 */
#define CLUSTERMSG_TYPE_FAIL 3          /* Fail 將某個節點標記為 FAIL */
#define CLUSTERMSG_TYPE_PUBLISH 4       /* 通過釋出與訂閱功能廣播訊息 */
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 /* 請求進行故障轉移操作,要求訊息的接收者通過投票來支援訊息的傳送者 */
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6     /* 訊息的接收者同意向訊息的傳送者投票 */
#define CLUSTERMSG_TYPE_UPDATE 7        /* slots 已經發生變化,訊息傳送者要求訊息接收者進行相應的更新 */
#define CLUSTERMSG_TYPE_MFSTART 8       /* 為了進行手動故障轉移,暫停各個客戶端 */
#define CLUSTERMSG_TYPE_COUNT 9         /* 訊息總數 */

通過上述這些訊息,叢集中的每一個例項都能獲得其它所有例項的狀態資訊。這樣一來,即使有新節點加入、節點故障、Slot 變更等事件發生,例項間也可以通過 PING、PONG 訊息的傳遞,完成叢集狀態在每個例項上的同步。下面,我們依次來看看幾種常見的場景。

Redis Cluster 中的節點都會定時地向其他節點發送 PING 訊息,來交換各個節點狀態資訊,檢查各個節點狀態,包括線上狀態、疑似下線狀態 PFAIL 和已下線狀態 FAIL。

Redis 叢集的定時 PING/PONG 的工作原理可以概括成兩點:

  • 一是,每個例項之間會按照一定的頻率,從叢集中隨機挑選一些例項,把 PING 訊息傳送給挑選出來的例項,用來檢測這些例項是否線上,並交換彼此的狀態資訊。PING 訊息中封裝了傳送訊息的例項自身的狀態資訊、部分其它例項的狀態資訊,以及 Slot 對映表。
  • 二是,一個例項在接收到 PING 訊息後,會給傳送 PING 訊息的例項,傳送一個 PONG 訊息。PONG 訊息包含的內容和 PING 訊息一樣。

下圖顯示了兩個例項間進行 PING、PONG 訊息傳遞的情況,其中例項一為傳送節點,例項二是接收節點

在這裡插入圖片描述

Redis Cluster 加入新節點時,客戶端需要執行 CLUSTER MEET 命令,如下圖所示。

meet

節點一在執行 CLUSTER MEET 命令時會首先為新節點建立一個 clusterNode 資料,並將其新增到自己維護的 clusterState 的 nodes 字典中。有關 clusterState 和 clusterNode 關係,我們在最後一節會有詳盡的示意圖和原始碼來講解。

然後節點一會根據據 CLUSTER MEET 命令中的 IP 地址和埠號,向新節點發送一條 MEET 訊息。新節點接收到節點一發送的MEET訊息後,新節點也會為節點一建立一個 clusterNode 結構,並將該結構新增到自己維護的 clusterState 的 nodes 字典中。

接著,新節點向節點一返回一條PONG訊息。節點一接收到節點B返回的PONG訊息後,得知新節點已經成功的接收了自己傳送的MEET訊息。

最後,節點一還會向新節點發送一條 PING 訊息。新節點接收到該條 PING 訊息後,可以知道節點A已經成功的接收到了自己返回的P ONG訊息,從而完成了新節點接入的握手操作。

MEET 操作成功之後,節點一會通過稍早時講的定時 PING 機制將新節點的資訊傳送給叢集中的其他節點,讓其他節點也與新節點進行握手,最終,經過一段時間後,新節點會被叢集中的所有節點認識。

Redis Cluster 中的節點會定期檢查已經發送 PING 訊息的接收方節點是否在規定時間 ( cluster-node-timeout ) 內返回了 PONG 訊息,如果沒有則會將其標記為疑似下線狀態,也就是 PFAIL 狀態,如下圖所示。

在這裡插入圖片描述

然後,節點一會通過 PING 訊息,將節點二處於疑似下線狀態的資訊傳遞給其他節點,例如節點三。節點三接收到節點一的 PING 訊息得知節點二進入 PFAIL 狀態後,會在自己維護的 clusterState 的 nodes 字典中找到節點二所對應的 clusterNode 結構,並將主節點一的下線報告新增到 clusterNode 結構的 fail_reports 連結串列中。

PING_FAIL

隨著時間的推移,如果節點十 (舉個例子) 也因為 PONG 超時而認為節點二疑似下線了,並且發現自己維護的節點二的 clusterNode 的 fail_reports 中有半數以上的主節點數量的未過時的將節點二標記為 PFAIL 狀態報告日誌,那麼節點十將會把節點二將被標記為已下線 FAIL 狀態,並且節點十會立刻向叢集其他節點廣播主節點二已經下線的 FAIL 訊息,所有收到 FAIL 訊息的節點都會立即將節點二狀態標記為已下線。如下圖所示。

fail

需要注意的是,報告疑似下線記錄是由時效性的,如果超過 cluster-node-timeout *2 的時間,這個報告就會被忽略掉,讓節點二又恢復成正常狀態。

首先,我們先來講解一下其中涉及的資料結構,也就是上文提到的 ClusterNode 等結構。

每個節點都會維護一個 clusterState 結構,表示當前叢集的整體狀態,它的定義如下所示。

typedef struct clusterState {
   clusterNode *myself;  /* 當前節點的clusterNode資訊 */
   ....
   dict *nodes;          /* name到clusterNode的字典 */
   ....
   clusterNode *slots[CLUSTER_SLOTS]; /* slot 和節點的對應關係*/
   ....
} clusterState;

它有三個比較關鍵的欄位,具體示意圖如下所示:

  • myself 欄位,是一個 clusterNode 結構,用來記錄自己的狀態;
  • nodes 字典,記錄一個 name 到 clusterNode 結構的對映,以此來記錄其他節點的狀態;
  • slot 陣列,記錄slot 對應的節點 clusterNode結構。

在這裡插入圖片描述

clusterNode 結構儲存了一個節點的當前狀態,比如節點的建立時間、節點的名字、節點 當前的配置紀元、節點的IP地址和埠號等等。除此之外,clusterNode結構的 link 屬性是一個clusterLink結構,該結構儲存了連線節點所需的有關資訊**,比如**套接字描述符,輸入緩衝區和輸出緩衝區。clusterNode 還有一個 fail_report 的列表,用來記錄疑似下線報告。具體定義如下所示。

typedef struct clusterNode {
    mstime_t ctime; /* 建立節點的時間 */
    char name[CLUSTER_NAMELEN]; /* 節點的名字 */
    int flags;      /* 節點標識,標記節點角色或者狀態,比如主節點從節點或者線上和下線 */
    uint64_t configEpoch; /* 當前節點已知的叢集統一epoch */
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    int numslots;   /* Number of slots handled by this node */
    int numslaves;  /* Number of slave nodes, if this is a master */
    struct clusterNode **slaves; /* pointers to slave nodes */
    struct clusterNode *slaveof; /* pointer to the master node. Note that it
                                    may be NULL even if the node is a slave
                                    if we don't have the master node in our
                                    tables. */
    mstime_t ping_sent;      /* 當前節點最後一次向該節點發送 PING 訊息的時間 */
    mstime_t pong_received;  /* 當前節點最後一次收到該節點 PONG 訊息的時間 */
    mstime_t fail_time;      /* FAIL 標誌位被設定的時間 */
    mstime_t voted_time;     /* Last time we voted for a slave of this master */
    mstime_t repl_offset_time;  /* Unix time we received offset for this node */
    mstime_t orphaned_time;     /* Starting time of orphaned master condition */
    long long repl_offset;      /* 當前節點的repl便宜 */
    char ip[NET_IP_STR_LEN];  /* 節點的IP 地址 */
    int port;                   /* 埠 */
    int cport;                  /* 通訊埠,一般是埠+1000 */
    clusterLink *link;          /* 和該節點的 tcp 連線 */
    list *fail_reports;         /* 下線記錄列表 */
} clusterNode;

clusterNodeFailReport 是記錄節點下線報告的結構體, node 是報告節點的資訊,而 time 則代表著報告時間。

typedef struct clusterNodeFailReport {
    struct clusterNode *node;  /* 報告當前節點已經下線的節點 */
    mstime_t time;             /* 報告時間 */
} clusterNodeFailReport;