redis 源码阅读之cluster(一)基本结构及Gossip

cluster基本结构

clusterNode

clusternode定义如下

typedef struct clusterNode {
    mstime_t ctime;  // 该node创建时间
    char name[CLUSTER_NAMELEN]; // 40位的node名字
     // node 状态标识通过投CLUSTER_NODE 定义。
     // 包括  maser slave self pfail fail handshake noaddr meet migrate_to null_name 这些状态
    int flags;     
    // 本节点最新epoch
    uint64_t configEpoch; 
    // 当前node负责的slot 通过bit表示
    unsigned char slots[CLUSTER_SLOTS/8]; 
    int numslots;   
    int numslaves; 
    struct clusterNode **slaves; 
    // 如果该node为从 则指向master节点
    struct clusterNode *slaveof; 
    mstime_t ping_sent;      /* Unix time we sent latest ping */
    mstime_t pong_received;  /* Unix time we received the pong */
    mstime_t fail_time;      /* Unix time when FAIL flag was set */
    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;      /* Last known repl offset for this node. */
    char ip[NET_IP_STR_LEN];  /* Latest known IP address of this node */
    int port;                   /* Latest known port of this node */
    
    clusterLink *link;          
    // 将该节点标记为失败的node list
    // 节点收到gossip消息后,如果gossip里标记该节点为pfail则加入改list
    // 比如:节点a向b发送gossip,消息包含了 c 节点且出于pfail,则a将被加入c的link。
    list *fail_reports;         
} clusterNode;

clusterMsgData 节点间消息协议

clusterMsgData定义了节点间通讯的数据结构
包含了 ping fail publish update四种类型
ping消息定义为

 struct {
       /* Array of N clusterMsgDataGossip structures */
       clusterMsgDataGossip gossip[1];
   } ping;

节点间通过ping保持心跳以及进行gossip集群状态同步,每次心跳时,节点会带上多个clusterMsgDataGossip消息体,经过多次心跳,该节点包含的其他节点信息将
同步到其他节点。

clusterState集群状态

定义了完整的集群信息

struct clusterState{
   // 集群最新的epoch,为64位的自增序列 
   uint64_t currentEpoch;
   // 包含的所有节点信息
   dict *nodes; 
   // 每个slot所属于的节点,包括处于migrating和importinng状态的slot
   clusterNode *migrating_slots_to[CLUSTER_SLOTS];
   clusterNode *importing_slots_from[CLUSTER_SLOTS];
   clusterNode *slots[CLUSTER_SLOTS]; 
   // 当前节点所包含的key 用于在getkeysinslot的时候返回key信息
   zskiplist *slots_to_keys;
   ...   
}

redis启动,判断是否允许cluster模式,如果允许,则调用clusterInit进行cluster信息的初始化。clusterState被初始化为初始值。
在后续节点meet及ping过程逐步更新clusterState信息。

Gossip实现

Send Ping

节点创建成功后,节点会向已知的其他节点发送ping消息保持心跳,ping消息体同时会携带已知节点的信息,
并通过gossip同步到集群的其他节点。

Ping节点选择

  • ping周期

node的ping由clusterCron负责调用,服务启动时,在serverCron内部会注册
clusterCron,该函数没秒执行10次,在clusterCron内部,维护着static变量iteration记录该函数被执行的次数
通过if (!(iteration % 10)){}的判断,使得node每秒发送一次心跳。

  • ping节点选择
    ping节点选择的代码逻辑如下
void clusterCron(void)
{
    // ...
    // 如果没有设置handshake超时,则默认超时未1s
    handshake_timeout = server.cluster_node_timeout;
    if (handshake_timeout < 1000)
        handshake_timeout = 1000;

    // 遍历nodes列表
    while ((de = dictNext(di)) != NULL)
    {
        //  删除handshake超时的节点
        if (nodeInHandshake(node) && now - node->ctime > handshake_timeout)
        {
            clusterDelNode(node);
            continue;
        }
        // 如果该节点的link为空,则为该节点新建连接,并且初始化ping初始时间
        if (node->link == NULL)
        {
        // 如果该节点处于meet状态,则直接发送meet让节点加入集群
        // 否则发送向该节点发送ping
        clusterSendPing(link, node->flags & CLUSTER_NODE_MEET ? CLUSTERMSG_TYPE_MEET : CLUSTERMSG_TYPE_PING);
        }
        // 函数每被调动10次,则发送一次ping,因此ping间隔为1s
        if (!(iteration % 10))
        {
        int j;
        for (j = 0; j < 5; j++)
        {
            // 随机选取节点并过滤link为空的以及self
            de = dictGetRandomKey(server.cluster->nodes);
            clusterNode *this = dictGetVal(de);
            if (this->link == NULL || this->ping_sent != 0)
                continue;
            if (this->flags & (CLUSTER_NODE_MYSELF | CLUSTER_NODE_HANDSHAKE))
                continue;
            
            // 挑选距离上次pong间隔最久的节点
            // redis会尽量选择距离上次ping间隔最久的节点,
            // 以此防止随机不均匀导致某些节点一直收不到ping
            if (min_pong_node == NULL || min_pong > this->pong_received)
            {
                min_pong_node = this;
                min_pong = this->pong_received;
            }
        }
    }
}

redis挑选node发送ping的时候,会优先给新加入的节点发送ping,其实再选择最久没被更新的节点,
通过对旧节点选择的加权,尽可能地保证了集群最新状态的一致。

Gossip携带节点选择

每次ping请求,node会从已知的nodes里面随机选取n个节点(n=1/10*len(nodes)&& n>=3),一段时间后,该节点已知的nodes将被同步到集群的其他节点,
集群状态信息达成最终一致。具体实现代码如下(只列出部分代码,完整代码见cluster.c/clusterSendPing)

void clusterSendPing(clusterLink *link, int type)
{
   // 选取1/10 的节点数并且要求大于3.
   // 1/10是个魔数,为啥是1/10在源码里有解释
   int freshnodes = dictSize(server.cluster->nodes) - 2;   
    wanted = floor(dictSize(server.cluster->nodes) / 10);
   if (wanted < 3)
       wanted = 3;
   if (wanted > freshnodes)
       wanted = freshnodes;
   while (freshnodes > 0 && gossipcount < wanted && maxiterations--)
   {
   // 通过随机函数随机选择一个节点,保证所有节点尽可能被同步到整个集群
   dictEntry *de = dictGetRandomKey(server.cluster->nodes);
   clusterNode *this = dictGetVal(de);
   // 为了保证失败的节点尽可能快地同步到集群其他节点,
   // 优先选取处于pfail以及fail状态的节点
   if (maxiterations > wanted * 2 &&
   !(this->flags & (CLUSTER_NODE_PFAIL | CLUSTER_NODE_FAIL)))
   continue;
   }
   // 如果被选中的节点处于
   // 1.handshake 并且noaddr状态
   // 2.其他节点没有包含该节点的信息,并且该节点没有拥有slot
   // 则跳过该节点并且将可用的节点数减1,以较少gossip数据同步的开销
   if (this->flags & (CLUSTER_NODE_HANDSHAKE | CLUSTER_NODE_NOADDR) ||
   (this->link == NULL && this->numslots == 0))
   {
   freshnodes--; /* Tecnically not correct, but saves CPU. */
   continue;
   }
}

通过随机选取合适数量的节点,以及对节点状态的过滤,保证了尽可能快的达成最终一致性的
同时,减少gossip的网络开销。

Receive Ping

cluster监听cluster 端口,并通过clusterAcceptHandler接受集群节点发起的连接请求,
通过aeCreateFileEvent将clusterReadHandler注册进事件回调里面,读取node发送的数据包。
clusterReadHandler读取到完整的数据包后,调用clusterProcessPacket处理包请求。
clusterProcessPacket包含收到数据包后完整的处理逻辑。

int clusterProcessPacket(clusterLink *link)
{
   // 判断是否为ping请求并校验数据包长度
   if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG ||
       type == CLUSTERMSG_TYPE_MEET)
   {
       uint16_t count = ntohs(hdr->count);
       uint32_t explen; /* expected length of this packet */

       explen = sizeof(clusterMsg) - sizeof(union clusterMsgData);
       explen += (sizeof(clusterMsgDataGossip) * count);
       if (totlen != explen)
           return 1;
   }   
   // ...
   
   // 是否为已知节点
   sender = clusterLookupNode(hdr->sender);
   if (sender && !nodeInHandshake(sender))
   {
       //  比较epoch并更新为最大的epoch
       if (senderCurrentEpoch > server.cluster->currentEpoch)
           server.cluster->currentEpoch = senderCurrentEpoch;
       /* Update the sender configEpoch if it is publishing a newer one. */
       if (senderConfigEpoch > sender->configEpoch)
       {
           sender->configEpoch = senderConfigEpoch;
           clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG |
                                CLUSTER_TODO_FSYNC_CONFIG);
       }
   }
   // 回复pong数据包
   clusterSendPing(link, CLUSTERMSG_TYPE_PONG);

   // 获取gossip消息并处理gossip请求
   if (sender)
   clusterProcessGossipSection(hdr, link);
}

clusterProcessGossipSection 读取携带的gossip node内容,并判断这些node是否failover

void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link)
{
    // ...


    if (flags & (CLUSTER_NODE_FAIL | CLUSTER_NODE_PFAIL))
{
    if (clusterNodeAddFailureReport(node, sender))
    {
        serverLog(LL_VERBOSE,
                  "Node %.40s reported node %.40s as not reachable.",
                  sender->name, node->name);
    }
    markNodeAsFailingIfNeeded(node);
}
else
{
    // 如果该node并非出于fail状态,则从fail link里删除该node
    if (clusterNodeDelFailureReport(node, sender))
    {
        serverLog(LL_VERBOSE,
                  "Node %.40s reported node %.40s is back online.",
                  sender->name, node->name);
    }
}
}
int clusterNodeDelFailureReport(clusterNode *node, clusterNode *sender)
{
    while ((ln = listNext(&li)) != NULL)
    {
        fr = ln->value;
        if (fr->node == sender)
            break;
    }
    if (!ln)
    return 0;
    // 如果之前被标记为失败,则从失败list里删除
    listDelNode(l, ln);
}

cluster会根据收到的gossip包里的msgdata来更新集群的状态信息,包括epoch,以及其余节点的状态。
如果node被标记为pfail或fail,则被加入fail_reports,当fail_reports长度超过半数节点数量时,该节点及被标记为failover。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容