Redis(開發與運維):48---集羣之(集羣通信:通信流程、Gossip消息、節點選擇)

一、通信流程

  • 在分佈式存儲中需要提供維護節點元數據信息的機制,所謂元數據是指:節點負責哪些數據,是否出現故障等狀態信息
  • 常見的元數據維護方式分爲:集中式和P2P方式
  • Redis集羣採用P2P的Gossip(流言)協議, Gossip協議工作原理就是節點彼此不斷通信交換信息,一段時間後所有的節 點都會知道集羣完整的信息,這種方式類似流言傳播,如下圖所示:

  • 通信過程說明:
    • 1)集羣中的每個節點都會單獨開闢一個TCP通道,用於節點之間彼此 通信,通信端口號在基礎端口上加10000
    • 2)每個節點在固定週期內通過特定規則選擇幾個節點發送ping消息
    • 3)接收到ping消息的節點用pong消息作爲響應
  • 集羣中每個節點通過一定規則挑選要通信的節點,每個節點可能知道全部節點,也可能僅知道部分節點,只要這些節點彼此可以正常通信,最終它們會達到一致的狀態。當節點出故障、新節點加入、主從角色變化、槽信息 變更等事件發生時,通過不斷的ping/pong消息通信,經過一段時間後所有的 節點都會知道整個集羣全部節點的最新狀態,從而達到集羣狀態同步的目的

二、Gossip消息

  • Gossip協議的主要職責就是信息交換。信息交換的載體就是節點彼此發送的Gossip消息,瞭解這些消息有助於我們理解集羣如何完成信息交換
  • 常用的Gossip消息可分爲:ping消息、pong消息、meet消息、fail消息等
    • meet消息:用於通知新節點加入。消息發送者通知接收者加入到當前 集羣,meet消息通信正常完成後,接收節點會加入到集羣中並進行週期性的 ping、pong消息交換
    • ping消息:集羣內交換最頻繁的消息,集羣內每個節點每秒向多個其 他節點發送ping消息,用於檢測節點是否在線和交換彼此狀態信息。ping消 息發送封裝了自身節點和部分其他節點的狀態數據。
    • pong消息:當接收到ping、meet消息時,作爲響應消息回覆給發送方確 認消息正常通信。pong消息內部封裝了自身狀態數據。節點也可以向集羣內 廣播自身的pong消息來通知整個集羣對自身狀態進行更新
    • fail消息:當節點判定集羣內另一個節點下線時,會向集羣內廣播一個 fail消息,其他節點接收到fail消息之後把對應節點更新爲下線狀態。具體細節將在後面的“故障轉移”文章中說明
  • 它們的通信模式如下圖所示:

  • 所有的消息格式劃分爲:消息頭和消息體
  • 消息頭包含發送節點自身狀態數據,接收節點根據消息頭就可以獲取到發送節點的相關數據,結構如下,集羣內所有的消息都採用相同的消息頭結構clusterMsg,它包含了發送節點關鍵信息,如節點id、槽映射、節點標識(主從角色,是否下線)等:
typedef struct {
    char sig[4]; /* 信號標示 */
    uint32_t totlen; /* 消息總長度 */
    uint16_t ver; /* 協議版本*/
    uint16_t type; /* 消息類型,用於區分meet,ping,pong等消息 */
    uint16_t count; /* 消息體包含的節點數量,僅用於meet,ping,ping消息類型*/
    uint64_t currentEpoch; /* 當前發送節點的配置紀元 */
    uint64_t configEpoch; /* 主節點/從節點的主節點配置紀元 */
    uint64_t offset; /* 複製偏移量 */
    char sender[CLUSTER_NAMELEN]; /* 發送節點的nodeId */
    unsigned char myslots[CLUSTER_SLOTS/8]; /* 發送節點負責的槽信息 */
    char slaveof[CLUSTER_NAMELEN]; /* 如果發送節點是從節點,記錄對應主節點的nodeId */
    uint16_t port; /* 端口號 */
    uint16_t flags; /* 發送節點標識,區分主從角色,是否下線等 */
    unsigned char state; /* 發送節點所處的集羣狀態 */
    unsigned char mflags[3]; /* 消息標識 */
    union clusterMsgData data /* 消息正文 */;
} clusterMsg;
  • 消息體在Redis內部採用clusterMsgData結構聲明,結構如下:
union clusterMsgData {
    /* ping,meet,pong消息體*/
    struct {
        /* gossip消息結構數組 */
        clusterMsgDataGossip gossip[1];
    } ping;
    /* FAIL 消息體 */
    struct {
        clusterMsgDataFail about;
    } fail;
    // ...
};
  • 消息體clusterMsgData定義發送消息的數據,其中ping、meet、pong都採用cluster MsgDataGossip數組作爲消息體數據,實際消息類型使用消息頭的type屬性區分。每個消息體包含該節點的多個clusterMsgDataGossip結構數據,用於信息交換,結構如下:
typedef struct {
    char nodename[CLUSTER_NAMELEN]; /* 節點的nodeId */
    uint32_t ping_sent; /* 最後一次向該節點發送ping消息時間 */
    uint32_t pong_received; /* 最後一次接收該節點pong消息時間 */
    char ip[NET_IP_STR_LEN]; /* IP */
    uint16_t port; /* port*/
    uint16_t flags; /* 該節點標識, */
} clusterMsgDataGossip;
  • 當接收到ping、meet消息時,接收節點會解析消息內容並根據自身的識別情況做出相應處理,對應流程如下圖所示

  • 接收節點收到ping/meet消息時,執行解析消息頭和消息體流程:
    • 解析消息頭過程:消息頭包含了發送節點的信息,如果發送節點是新 節點且消息是meet類型,則加入到本地節點列表;如果是已知節點,則嘗試 更新發送節點的狀態,如槽映射關係、主從角色等狀態
    • 解析消息體過程:如果消息體的clusterMsgDataGossip數組包含的節點 是新節點,則嘗試發起與新節點的meet握手流程;如果是已知節點,則根據 cluster MsgDataGossip中的flags字段判斷該節點是否下線,用於故障轉移
  • 消息處理完後回覆pong消息,內容同樣包含消息頭和消息體,發送節點接收到回覆的pong消息後,採用類似的流程解析處理消息並更新與接收節點 最後通信時間,完成一次消息通信

三、節點選擇

  • 雖然Gossip協議的信息交換機制具有天然的分佈式特性,但它是有成本 的。由於內部需要頻繁地進行節點信息交換,而ping/pong消息會攜帶當前節點和部分其他節點的狀態數據,勢必會加重帶寬和計算的負擔。Redis集羣內節點通信採用固定頻率(定時任務每秒執行10次)。因此節點每次選擇需要通信的節點列表變得非常重要。通信節點選擇過多雖然可以做到信息及時交換但成本過高。節點選擇過少會降低集羣內所有節點彼此信息交換頻率, 從而影響故障判定、新節點發現等需求的速度
  • 因此Redis集羣的Gossip協議需要兼顧信息交換實時性和成本開銷,通信節點選擇的規則如下圖所示:

  • 根據通信節點選擇的流程可以看出消息交換的成本主要體現在單位時間 選擇發送消息的節點數量和每個消息攜帶的數據量

①選擇發送消息的節點數量

  • 集羣內每個節點維護定時任務默認每秒執行10次,每秒會隨機選取5個節點找出最久沒有通信的節點發送ping消息,用於保證Gossip信息交換的隨機性
  • 每100毫秒都會掃描本地節點列表,如果發現節點最近一次接受pong 消息的時間大於cluster_node_timeout/2,則立刻發送ping消息,防止該節點信 息太長時間未更新。根據以上規則得出每個節點每秒需要發送ping消息的數量=1+10*num(node.pong_received>cluster_node_timeout/2),因此cluster_node_timeout參數對消息發送的節點數量影響非常大。當我們的帶寬 資源緊張時,可以適當調大這個參數,如從默認15秒改爲30秒來降低帶寬佔 用率。過度調大cluster_node_timeout會影響消息交換的頻率從而影響故障轉 移、槽信息更新、新節點發現的速度。因此需要根據業務容忍度和資源消耗進行平衡。同時整個集羣消息總交換量也跟節點數成正比

②消息數據量

  • 每個ping消息的數據量體現在消息頭和消息體中,其中消息頭主要佔用 空間的字段是myslots[CLUSTER_SLOTS/8],佔用2KB,這塊空間佔用相對 固定。消息體會攜帶一定數量的其他節點信息用於信息交換。具體數量見以下僞代碼:
def get_wanted():
    int total_size = size(cluster.nodes)
    # 默認包含節點總量的1/10
    int wanted = floor(total_size/10);
    if wanted < 3:
        # 至少攜帶3個其他節點信息
        wanted = 3;
    if wanted > total_size -2 :
        # 最多包含total_size - 2個
    wanted = total_size - 2;
    return wanted;
  • 根據僞代碼可以看出消息體攜帶數據量跟集羣的節點數息息相關,更大 的集羣每次消息通信的成本也就更高,因此對於Redis集羣來說並不是大而 全的集羣更好,對於集羣規模控制的建議見後面的“集羣運維”文章
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章