Redis 源碼分析之 cluster meet

Redis cluster 是 redis 官方提出的分佈式集羣解決方案,在此之前,有一些第三方的可選方案,如 codis、Twemproxy等。cluster 內部使用了 gossip 協議進行通信,以達到數據的最終一致性。詳細介紹可參考官網 Redis cluster tutorial
本文試圖藉着cluster meet 命令的實現來對其中的一些通信細節一探究竟。
我們都知道,當 redis server 以 cluster mode 啓動時,節點 A 想加入節點 B 所在的集羣,只需要執行 CLUSTER MEET ip port 這個命令即可,通過 gossip 通信,最終 B 所在集羣的其他節點也都會認識到 A。大概流程圖如下:

cluster 初始化

當 redis server 以 cluster mode 啓動時,即配置文件中的 cluster-enabled 選項設置爲 true,此時在服務啓動時,會有一個 cluster 初始化的流程,這個在之前的文章 《Redis 啓動流程》中有提到過,即執行函數 clusterInit。在 cluster 中有三個數據結構很重要, clusterStateclusterNodeclusterLink
每個節點都保存着一個 clusterState 結構,這個結構記錄了在當前節點的視角下,集羣目前所處的狀態,即“我看到的世界是什麼樣子”。
每個節點都會使用一個 clusterNode 結構來記錄自己的狀態, 併爲集羣中的所有其他節點(包括主節點和從節點)都創建一個相應的 clusterNode 結構, 以此來記錄其他節點的狀態。
clusterNode 結構的 link 屬性是一個 clusterLink 結構, 該結構保存了連接節點所需的有關信息, 比如套接字描述符, 輸入緩衝區和輸出緩衝區。
更多的細節可以通過網頁 《redis 設計與實現 - 節點》進行了解。
該初始化很簡單,首先是創建一個 clusterState 結構,並初始化一些成員,如下:

server.cluster = zmalloc(sizeof(clusterState));
server.cluster->myself = NULL;
server.cluster->currentEpoch = 0;     // 新節點的 currentEpoch = 0
server.cluster->state = CLUSTER_FAIL; // 初始狀態置爲 FAIL
server.cluster->size = 1;
server.cluster->todo_before_sleep = 0;
server.cluster->nodes = dictCreate(&clusterNodesDictType,NULL);
server.cluster->nodes_black_list = dictCreate(&clusterNodesBlackListDictType,NULL);
server.cluster->failover_auth_time = 0;
server.cluster->failover_auth_count = 0;
server.cluster->failover_auth_rank = 0;
server.cluster->failover_auth_epoch = 0;
server.cluster->cant_failover_reason = CLUSTER_CANT_FAILOVER_NONE;
server.cluster->lastVoteEpoch = 0;
server.cluster->stats_bus_messages_sent = 0;
server.cluster->stats_bus_messages_received = 0;
memset(server.cluster->slots,0, sizeof(server.cluster->slots));
clusterCloseAllSlots(); // Clear the migrating/importing state for all the slots

然後給 node.conf 文件加鎖,確保每個節點使用自己的 cluster 配置文件。

if (clusterLockConfig(server.cluster_configfile) == C_ERR)
    exit(1);

藉着這個機會學習下 redis 如何使用的文件鎖。

int fd = open(filename,O_WRONLY|O_CREAT,0644);
if (fd == -1) {
    serverLog(LL_WARNING,
              "Can't open %s in order to acquire a lock: %s",
              filename, strerror(errno));
    return C_ERR;
}

if (flock(fd,LOCK_EX|LOCK_NB) == -1) {
    if (errno == EWOULDBLOCK) {
        serverLog(LL_WARNING,
                  "Sorry, the cluster configuration file %s is already used "
                  "by a different Redis Cluster node. Please make sure that "
                  "different nodes use different cluster configuration "
                  "files.", filename);
    } else {
        serverLog(LL_WARNING,
                  "Impossible to lock %s: %s", filename, strerror(errno));
    }
    close(fd);
    return C_ERR;
}

然後加載 node.conf 文件,這個過程還會檢查這個文件是否合理。

如果加載失敗(或者配置文件不存在),則以 REDIS_NODE_MYSELF|REDIS_NODE_MASTER 爲標記,創建一個clusterNode 結構表示自己本身,置爲主節點,並設置自己的名字爲一個40字節的隨機串;然後將該節點添加到server.cluster->nodes中,這說明這是個新啓動的節點,生成的配置文件進行刷盤。

if (clusterLoadConfig(server.cluster_configfile) == C_ERR) {
    myself = server.cluster->myself =
        createClusterNode(NULL,CLUSTER_NODE_MYSELF|CLUSTER_NODE_MASTER);
    serverLog(LL_NOTICE,"No cluster configuration found, I'm %.40s",
              myself->name);
    clusterAddNode(myself);
    saveconf = 1;
}
if (saveconf) clusterSaveConfigOrDie(1); // 新節點,將配置刷到配置文件中,fsync

接下來,調用 listenToPort 函數,在集羣 gossip 通信端口上創建 socket fd 進行監聽。集羣內 gossip 通信端口是在 Redis 監聽端口基礎上加 10000,比如如果Redis監聽客戶端的端口爲 6379,則集羣監聽端口就是16379,該監聽端口用於接收其他集羣節點發送過來的 gossip 消息。

然後註冊監聽端口上的可讀事件,事件回調函數爲 clusterAcceptHandler

#define CLUSTER_PORT_INCR 10000

if (listenToPort(server.port+CLUSTER_PORT_INCR,
                 server.cfd,&server.cfd_count) == C_ERR)
{
    exit(1);
} else {
    int j;
    for (j = 0; j < server.cfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.cfd[j], AE_READABLE, 
                              clusterAcceptHandler, NULL) == AE_ERR)
            serverPanic("Unrecoverable error creating Redis Cluster "
                        "file event.");
    }
}

當前節點收到其他集羣節點發來的TCP建鏈請求之後,就會調用 clusterAcceptHandler 函數 accept 連接。在 clusterAcceptHandler函數中,對於每個已經 accept 的鏈接,都會創建一個clusterLink 結構表示該鏈接,並註冊 socket fd上的可讀事件,事件回調函數爲 clusterReadHandler

#define MAX_CLUSTER_ACCEPTS_PER_CALL 1000
void clusterAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd;
    int max = MAX_CLUSTER_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    clusterLink *link;
    ... ...
    // 如果服務器正在啓動,不要接受其他節點的連接, 因爲 UPDATE 消息可能會干擾數據庫內容
    if (server.masterhost == NULL && server.loading) return;
    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_VERBOSE,
                    "Error accepting cluster node: %s", server.neterr);
            return;
        }
        anetNonBlock(NULL,cfd);
        anetEnableTcpNoDelay(NULL,cfd);
        ... ...
        // 創建一個 link 結構來處理連接
        // 剛開始的時候, link->node 被設置成 null,因爲現在我們不知道是哪個節點
        link = createClusterLink(NULL);
        link->fd = cfd;
        aeCreateFileEvent(server.el,cfd,AE_READABLE,clusterReadHandler,link);
    }
}

最後是 reset mf 相關的參數。

CLUSTER MEET

A 節點接收 CLUSTER MEET 命令

A 節點在cluster.c -> clusterCommand 函數中,接收到 CLUSTER MEET 命令,即

if (!strcasecmp(c->argv[1]->ptr,"meet") && c->argc == 4) {
    long long port;

    // CLUSTER MEET <ip> <port>
    if (getLongLongFromObject(c->argv[3], &port) != C_OK) {
        addReplyErrorFormat(c,"Invalid TCP port specified: %s", (char*)c->argv[3]->ptr);
        return;
    }
    if (clusterStartHandshake(c->argv[2]->ptr,port) == 0 && errno == EINVAL)
    {
        addReplyErrorFormat(c,"Invalid node address specified: %s:%s",
                            (char*)c->argv[2]->ptr, (char*)c->argv[3]->ptr);
    } else {
        addReply(c,shared.ok);
    }
}

可以看到重點在 clusterStartHandshake 這個函數。

int clusterStartHandshake(char *ip, int port) {
    clusterNode *n;
    char norm_ip[NET_IP_STR_LEN];
    struct sockaddr_storage sa;
    /* IP and Port sanity check */
    ... ...
        
    // 檢查節點(flag) norm_ip:port 是否正在握手
    if (clusterHandshakeInProgress(norm_ip,port)) { 
        errno = EAGAIN;
        return 0;
    }
    // 創建一個含隨機名字的 node,type 爲 CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_MEET
    // 相關信息會在 handshake 過程中被修復
    n = createClusterNode(NULL,CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_MEET);
    memcpy(n->ip,norm_ip,sizeof(n->ip));
    n->port = port;
    clusterAddNode(n);
    return 1;
}
clusterNode *createClusterNode(char *nodename, int flags) {
    clusterNode *node = zmalloc(sizeof(*node));
    if (nodename)
        memcpy(node->name, nodename, CLUSTER_NAMELEN);
    else
        // 在本地新建一個 nodename 節點,節點名字隨機,跟它通信時它會告訴我真實名字
        getRandomHexChars(node->name, CLUSTER_NAMELEN);
    node->ctime = mstime(); // mstime
    node->configEpoch = 0;
    node->flags = flags;
    memset(node->slots,0,sizeof(node->slots));
    node->slaveof = NULL;
    ... ...
    node->link = NULL; // link 爲空, 在 clusterCron 中能檢查的到
    memset(node->ip,0,sizeof(node->ip));
    node->port = 0;
    node->fail_reports = listCreate();
    ... ...
    listSetFreeMethod(node->fail_reports,zfree);
    return node;
}

這個函數會首先進行一些 ip 和 port 的合理性檢查,然後去遍歷所看到的 nodes,這個 ip:port 對應的 node 是不是正處於 CLUSTER_NODE_HANDSHAKE 狀態,是的話,就說明這是重複 meet,沒必要往下走。之後,通過 createClusterNode 函數創建一個帶有 CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_MEET 標記的節點,名字爲一個隨機的 40 字節字符串(因爲此時對 A 來說,B 是一個陌生的節點,信息除了 ip 和 port,其他都不知道),通過 clusterAddNode 函數加到自己的 nodes 中。
這個過程成功後,就返回給客戶端 OK 了,其他事情需要通過 gossip 通信去做。

A 節點發送 MEET gossip 消息給 B 節點

A 節點在定時任務 clusterCron 中,會做一些事情。

handshake_timeout = server.cluster_node_timeout;
if (handshake_timeout < 1000) handshake_timeout = 1000;

// 檢查是否有 disconnected nodes 並且重新建立連接
di = dictGetSafeIterator(server.cluster->nodes); // 遍歷所有節點
while((de = dictNext(di)) != NULL) {
    clusterNode *node = dictGetVal(de);
    
     // 忽略掉 myself 和 noaddr 狀態的節點
    if (node->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR)) continue; 
    
    // 節點處於 handshake 狀態,且狀態維持時間超過 handshake_timeout,那麼從 nodes中刪掉它
    if (nodeInHandshake(node) && now - node->ctime > handshake_timeout) {
        clusterDelNode(node);
        continue;
    }

    // 剛剛收到 cluster meet 命令創建的新 node ,或是 server 剛啓動,或是由於某種原因斷開了
    if (node->link == NULL) { 
        int fd;
        mstime_t old_ping_sent;
        clusterLink *link;

        // 對端 gossip 通信端口爲 node 端口 + 10000,創建 tcp 連接, 本節點相當於 client
        fd = anetTcpNonBlockBindConnect(server.neterr, node->ip, node->port+CLUSTER_PORT_INCR, NET_FIRST_BIND_ADDR);
        ... ...
        link = createClusterLink(node);
        link->fd = fd;
        node->link = link;

        // 註冊 link->fd 上的可讀事件,事件回調函數爲 clusterReadHandler
        aeCreateFileEvent(server.el,link->fd,AE_READABLE, clusterReadHandler,link);
        ... ...

        // 如果 node 帶有 MEET flag,我們發送一個 MEET 包而不是 PING,
        // 這是爲了強制讓接收者把我們加到它的 nodes 中
        clusterSendPing(link, node->flags & CLUSTER_NODE_MEET ? CLUSTERMSG_TYPE_MEET : CLUSTERMSG_TYPE_PING);
        ... ...
        node->flags &= ~CLUSTER_NODE_MEET;
        ... ...
    }
}
dictReleaseIterator(di);

可以看到,遍歷自己看到的 nodes,當遍歷到 B 節點時,由於 node->link == NULL,因此會監聽 B 的啓動端口號+10000,即 gossip 通信端口,然後註冊可讀事件,處理函數爲 clusterReadHandler。接着會發送 CLUSTER_NODE_MEET 消息給 B 節點,消除掉 B 節點的 meet 狀態。

B 節點處理 A 發來的 MEET gossip 消息

當 B 節點接收到 A 節點發送 gossip 時,回調函數 clusterAcceptHandler 進行處理,然後會 accept 對端的 connect(B 作爲 server,對端作爲 client),註冊可讀事件,回調函數爲 clusterReadHandler,基本邏輯如下,

void clusterAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd;
    int max = MAX_CLUSTER_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    clusterLink *link;
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);

    // 如果服務器正在啓動,不要接受其他節點的鏈接,因爲 UPDATE 消息可能會干擾數據庫內容
    if (server.masterhost == NULL && server.loading) return;
    while(max--) { // 1000 個請求
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_VERBOSE,
                    "Error accepting cluster node: %s", server.neterr);
            return;
        }
        anetNonBlock(NULL,cfd);
        anetEnableTcpNoDelay(NULL,cfd);
        serverLog(LL_VERBOSE,"Accepted cluster node %s:%d", cip, cport);
   
        // 創建一個 link 結構來處理連接
        // 剛開始的時候, link->node 被設置成 null,因爲現在我們不知道是哪個節點
        link = createClusterLink(NULL);
        link->fd = cfd;
        aeCreateFileEvent(server.el,cfd,AE_READABLE,clusterReadHandler,link);
    }
}

可以看到每次 accept 對端connect時,都會創建一個 clusterLink 結構用來接收數據,

typedef struct clusterLink {
    mstime_t ctime;             /* Link creation time */
    int fd;                     /* TCP socket file descriptor */
    sds sndbuf;                 /* Packet send buffer */
    sds rcvbuf;                 /* Packet reception buffer */
    struct clusterNode *node;   /* Node related to this link if any, or NULL */
} clusterLink;

clusterLink 有一個指針是指向 node 自身的。
B 節點接收到 A 節點發送過來的信息,放到 clusterLinkrcvbuf 字段,然後使用 clusterProcessPacket 函數來處理(接收數據過程很簡單,不做分析)。
所以 clusterProcessPacket 函數的作用是處理別人發過來的 gossip 包。

if (!sender && type == CLUSTERMSG_TYPE_MEET) {
    clusterNode *node;

    // 創建一個帶有 CLUSTER_NODE_HANDSHAKE 標記的 cluster node,名字隨機
    node = createClusterNode(NULL,CLUSTER_NODE_HANDSHAKE);
    nodeIp2String(node->ip,link); // ip 和 port 信息均從 link 中獲得
    node->port = ntohs(hdr->port);

    clusterAddNode(node);
    clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG);
}
.....
clusterSendPing(link,CLUSTERMSG_TYPE_PONG);

由於這時 B 節點還不認識 A 節點,因此 B 節點從自己的 nodes 中找 A 節點是找不到的,所以 sender 是空,因此會走進如上的這段邏輯。同樣以隨機的名字,CLUSTER_NODE_HANDSHAKE 爲 flag 創建一個 node,加入自己的 nodes 中。
在這個邏輯末尾會給 A 節點回復一個 PONG 消息。

A 節點處理 B 節點回復的 PONG gossip 消息

同樣是在 clusterProcessPacket 中處理 gossip 消息。

if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG || type == CLUSTERMSG_TYPE_MEET) {
    ... ...
    if (link->node) {
        if (nodeInHandshake(link->node)) { // node 處於握手狀態
            ... ...
            clusterRenameNode(link->node, hdr->sender); // 修正節點名
            link->node->flags &= ~CLUSTER_NODE_HANDSHAKE; // 消除 handshake 狀態
            link->node->flags |= flags&(CLUSTER_NODE_MASTER|CLUSTER_NODE_SLAVE);
            clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG);
        }
}

這個時候 A 節點會根據 B 節點發來的消息,更正 A 節點 nodes 中關於 B 節點的名字,以及消除 handshake 狀態。

B 節點發送 PING gossip 消息給 A 節點

當 B 節點在做 clusterCron 時,發現自己看到的 A 節點中的 link 爲空,即 node->link == NULL,這與上面講的 A 節點給 B 節點發 MEET 消息類似,不過在 B 節點看了 A 節點沒有 meet flag,因此發送的是 PING 消息。

A 節點處理 B 節點發來的 PING 消息

做一些邏輯,不過跟這次要討論的事情無關,後面會詳寫。

對於 PING 和 MEET 消息,無論如何都是會回覆一個 PONG 消息的

B 節點處理 A 節點回復的 PONG 消息

邏輯同上,將 B 節點的 nodes 中 A 節點的名字進行更正,然後去掉 A 節點的 handshake flag。

小結

至此,一個 cluster meet 命令執行的完整過程就解釋清楚了,畫了一個流程圖可以幫助更好的理解這個流程。

cluster meet

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章