Redis源码解析:25集群(一)握手、心跳消息以及下线检测

本文深入解析Redis集群的初始化、节点握手过程、心跳消息和下线检测机制。通过数据结构和源码分析,阐述了如何通过CLUSTER MEET命令建立节点间的连接,以及Gossip协议在节点间传播信息的作用。同时,介绍了心跳包的发送策略和下线检测的判断逻辑,帮助理解Redis集群的稳定性和容错性。
摘要由CSDN通过智能技术生成

         Redis集群是Redis提供的分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能。

 

一:初始化

1:数据结构

         在源码中,通过server.cluster记录整个集群当前的状态,比如集群中的所有节点;集群目前的状态,比如是上线还是下线;集群当前的纪元等等。该属性是一个clusterState类型的结构体。该结构体的定义如下:

typedef struct clusterState {
    clusterNode *myself;  /* This node */
    ...
    int state;            /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */
    int size;             /* Num of master nodes with at least one slot */
    dict *nodes;          /* Hash table of name -> clusterNode structures */
    ...
    clusterNode *slots[REDIS_CLUSTER_SLOTS];
    zskiplist *slots_to_keys;
    ...
} clusterState;

         myself指向当前Redis实例所表示的节点;state表示集群状态;字典nodes中记录了,包括自己在内的所有集群节点,该字典以节点名为key,以结构体clusterNode为value。其他属性与具体的流程相关,后续在介绍集群各种流程时会介绍。

 

         集群中的节点是由clusterNode表示的,该结构体的定义如下:

typedef struct clusterNode {
    mstime_t ctime; /* Node object creation time. */
    char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    int flags;      /* REDIS_NODE_... */
    ...
    mstime_t ping_sent;      /* Unix time we sent latest ping */
    mstime_t pong_received;  /* Unix time we received the pong */
    ...
    char ip[REDIS_IP_STR_LEN];  /* Latest known IP address of this node */
    int port;                   /* Latest known port of this node */
    clusterLink *link;          /* TCP/IP link with this node */
    list *fail_reports;         /* List of nodes signaling this as failing */
} clusterNode;

         该结构体记录了节点的状态和属性。ctime表示节点的创建时间;name表示节点名,每个节点都有一个40字节长的随机字符串作为名字,该名字同时也作为该节点在字典server.cluster->nodes中的key;flags表示节点的类型和状态,比如节点是否下线,是主节点还是从节点等,都记录在标志位flags中;ip和port表示该节点的地址属性;link表示当前节点与该节点间的TCP连接,该结构中包含socket描述符、输入缓冲区和输出缓冲区等属性。在link所表示的TCP连接中,当前节点为客户端,clusterNode所表示的节点为服务端。其他属性与具体的流程相关,后续在介绍集群各种流程时会介绍。

 

2:初始化

         Redis实例启动时,根据配置文件中的"cluster-enabled"选项,决定该Redis实例是否处于集群模式。如果该选项值为”yes”,则Redis实例中的server.cluster_enabled被置为1,表示当前处于集群模式。

         在集群模式下,Redis实例启动时,首先会调用clusterInit函数,初始化集群需要使用的结构,并创建监听端口。该函数的代码如下:

void clusterInit(void) {
    int saveconf = 0;

    server.cluster = zmalloc(sizeof(clusterState));
    server.cluster->myself = NULL;
    server.cluster->currentEpoch = 0;
    server.cluster->state = REDIS_CLUSTER_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 = REDIS_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();

    /* Lock the cluster config file to make sure every node uses
     * its own nodes.conf. */
    if (clusterLockConfig(server.cluster_configfile) == REDIS_ERR)
        exit(1);

    /* Load or create a new nodes configuration. */
    if (clusterLoadConfig(server.cluster_configfile) == REDIS_ERR) {
        /* No configuration found. We will just use the random name provided
         * by the createClusterNode() function. */
        myself = server.cluster->myself =
            createClusterNode(NULL,REDIS_NODE_MYSELF|REDIS_NODE_MASTER);
        redisLog(REDIS_NOTICE,"No cluster configuration found, I'm %.40s",
            myself->name);
        clusterAddNode(myself);
        saveconf = 1;
    }
    if (saveconf) clusterSaveConfigOrDie(1);

    /* We need a listening TCP port for our cluster messaging needs. */
    server.cfd_count = 0;

    /* Port sanity check II
     * The other handshake port check is triggered too late to stop
     * us from trying to use a too-high cluster port number. */
    if (server.port > (65535-REDIS_CLUSTER_PORT_INCR)) {
        redisLog(REDIS_WARNING, "Redis port number too high. "
                   "Cluster communication port is 10,000 port "
                   "numbers higher than your Redis port. "
                   "Your Redis port number must be "
                   "lower than 55535.");
        exit(1);
    }

    if (listenToPort(server.port+REDIS_CLUSTER_PORT_INCR,
        server.cfd,&server.cfd_count) == REDIS_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)
                    redisPanic("Unrecoverable error creating Redis Cluster "
                                "file event.");
        }
    }

    /* The slots -> keys map is a sorted set. Init it. */
    server.cluster->slots_to_keys = zslCreate();

    /* Set myself->port to my listening port, we'll just need to discover
     * the IP address via MEET messages. */
    myself->port = server.port;

    server.cluster->mf_end = 0;
    resetManualFailover();
}

         在该函数中,首先初始化clusterState结构类型server.cluster中的各个属性;

         如果在Redis配置文件中指定了"cluster-config-file"选项的值,则用server.cluster_configfile属性记录该选项值,表示集群配置文件。接下来,就根据配置文件的内容,初始化server.cluster中的各个属性;

         如果加载集群配置文件失败(或者配置文件不存在),则以REDIS_NODE_MYSELF和REDIS_NODE_MASTER为标记,创建一个clusterNode结构表示自己本身,置为主节点,并设置自己的名字为一个40字节的随机串;然后将该节点添加到server.cluster->nodes中;

        

         接下来,调用listenToPort函数,在集群监端口上创建socket描述符进行监听。该集群监听端口是在Redis监听端口基础上加10000,比如如果Redis监听客户端的端口为6379,则集群监听端口就是16379,该监听端口用于接收其他集群节点的TCP建链,集群中的每个节点,都会与其他节点进行建链,因此整个集群就形成了一个强连通网状图;

         然后注册监听端口上的可读事件,事件回调函数为clusterAcceptHandler。

         当当前节点收到其他集群节点发来的TCP建链请求之后,就会调用clusterAcceptHandler函数accept连接。在clusterAcceptHandler函数中,对于每个已经accept的链接,都会创建一个clusterLink结构表示该链接,并注册socket描述符上的可读事件,事件回调函数为clusterReadHandler。

 

二:集群节点间的握手

1:CLUSTER  MEET命令

         Redis实例以集群模式启动之后,此时,在它的视角中,当前集群只有他自己一个节点。如何认识集群中的其他节点呢,这就需要客户端发送”CLUSTER  MEET”命令。

         客户端向集群节点A发送命令” CLUSTER  MEET nodeB_ip  nodeB_port”, 其中的nodeB_ip和nodeB_port,表示节点B的ip和port。节点A收到客户端发来的该命令后,调用clusterCommand函数处理。这部分的代码如下:

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

        if (getLongLongFromObject(c->argv[3], &port) != REDIS_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);
        }
    }

         以命令中的ip和port为参数,调用clusterStartHandshake函数,节点A开始向节点B进行握手。

         在clusterStartHandshake函数中,会以REDIS_NODE_HANDSHAKE|REDIS_NODE_MEET为标志,创建一个clusterNode结构表示节点B,该结构的ip和port属性分别置为节点B的ip和port,并将该节点插入到字典server.cluster->nodes中。      这部分的代码如下:

/* Add the node with a random address (NULL as first argument to
 * createClusterNode()). Everything will be fixed during the
 * handshake. */
n = createClusterNode(NULL,REDIS_NODE_HANDSHAKE|REDIS_NODE_MEET);
memcpy(n->ip,norm_ip,sizeof(n->ip));
n->port = port;
clusterAddNode(n);

         注意,因为此时A还不知道节点B的名字,因此以NULL为参数调用函数createClusterNode,该函数中,会暂时以一个随机串当做B的名字,后续交互过程中,节点B会在PONG包中发来自己的名字。

 

2:TCP建链

         在集群定时器函数clusterCron中,会轮训字典server.cluster->nodes中的每一个节点node,一旦发现node->link为NULL,就表示尚未向该节点建链(或是之前的连接已断开)。因此,开始向其集群端口发起TCP建链,这部分代码如下:

       if (node->link == NULL) {
            int fd;
            mstime_t old_ping_sent;
            clusterLink *link;

            fd = anetTcpNonBlockBindConnect(server.neterr, node->ip,
                node->port+REDIS_CLUSTER_PORT_INCR, REDIS_BIND_ADDR);
            if (fd == -1) {
                /* We got a synchronous erro
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值