漫談Gossip協(xié)議與其在Redis Cluster中的實(shí)現(xiàn)

前言

之前給小伙伴們科普ClickHouse集群的時(shí)候雳旅,我曾經(jīng)提到ClickHouse集群幾乎是去中心化的(decentralized)箫老,亦即集群中各個(gè)CK實(shí)例是對(duì)等的怪蔑,沒有主從之分歉糜。集群上的復(fù)制表卵酪、分布式表機(jī)制只是靠外部ZooKeeper做分布式協(xié)調(diào)工作。想了想熙掺,又補(bǔ)了一句:

“其實(shí)單純靠P2P互相通信就能維護(hù)完整的集群狀態(tài)未斑,實(shí)現(xiàn)集群自治,比如Redis Cluster币绩±啵”

當(dāng)然限于時(shí)間沒有展開說。這個(gè)周末休息夠了缆镣,難得有空芽突,來隨便講兩句吧。

在官方Redis Cluster出現(xiàn)之前董瞻,要實(shí)現(xiàn)集群化Redis都是依靠Sharding+Proxy技術(shù)寞蚌,如Twemproxy和Codis(筆者之前也寫過Codis集群的事兒)。而官方Redis Cluster走了去中心化的路钠糊,其通信基礎(chǔ)就是Gossip協(xié)議挟秤,同時(shí)該協(xié)議還能保證一致性和可用性。本文先來介紹一下它眠蚂。

Gossip協(xié)議

簡(jiǎn)介

最近幾個(gè)月一直在看《Friends》下飯煞聪。認(rèn)為自己從不gossip的Rachel一語(yǔ)道破了gossip的本質(zhì)斗躏。

現(xiàn)實(shí)生活中的流言八卦傳播的機(jī)制就是“I hear something and I pass that information on”逝慧,并且其傳播速度非澄舾快。而Gossip協(xié)議就是借鑒了這個(gè)特點(diǎn)產(chǎn)生的笛臣,在P2P網(wǎng)絡(luò)和分布式系統(tǒng)中應(yīng)用廣泛云稚,它的方法論也特別簡(jiǎn)單:

在一個(gè)處于有界網(wǎng)絡(luò)的集群里,如果每個(gè)節(jié)點(diǎn)都隨機(jī)與其他節(jié)點(diǎn)交換特定信息沈堡,經(jīng)過足夠長(zhǎng)的時(shí)間后静陈,集群各個(gè)節(jié)點(diǎn)對(duì)該份信息的認(rèn)知終將收斂到一致。

這里的“特定信息”一般就是指集群狀態(tài)诞丽、各節(jié)點(diǎn)的狀態(tài)以及其他元數(shù)據(jù)等鲸拥。可見僧免,Gossip協(xié)議是完全符合BASE理論精神的刑赶,所以它基本可以用于任何只要求最終一致性的領(lǐng)域,典型的例子就是區(qū)塊鏈懂衩,以及部分分布式存儲(chǔ)撞叨。另外,它可以很方便地實(shí)現(xiàn)彈性集群(即節(jié)點(diǎn)可以隨時(shí)上下線)浊洞,如失敗檢測(cè)與動(dòng)態(tài)負(fù)載均衡等牵敷。

以下GIF圖示出Gossip協(xié)議下一種可能的消息傳播過程。藍(lán)色節(jié)點(diǎn)表示對(duì)消息無感知法希,紅色節(jié)點(diǎn)表示有感知枷餐。

Source: https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/

為了使Gossip協(xié)議更易于表達(dá)和分析,一般都會(huì)借用流行病學(xué)(epidemiology)中的SIR模型進(jìn)行描述铁材,因?yàn)榇罅餍胁尖淘。╬andemic,比如這次新冠肺炎)的傳播與流言八卦的傳播具有相似性著觉,并且已經(jīng)由前人總結(jié)出一套成熟的數(shù)學(xué)模型了村生。

流行病學(xué)SIR模型

SIR模型早在1927年就由Kermack與McKendrick提出。該模型將傳染病流行范圍內(nèi)的人群分為3類:

  • S(易感者/susceptible)饼丘,指未患病的人趁桃,但缺乏免疫能力,與感染者接觸之后容易受到感染肄鸽。
  • I(感染者/infective)卫病,指已患病的人,并且可以將病原體傳播給易感者人群典徘;
  • R(隔離者/removed)蟀苛,指被隔離在無傳染環(huán)境,或者因病愈獲得免疫力而不再易感的人逮诲。

如果不考慮人口的增長(zhǎng)和減少帜平,即s(t)+i(t)+r(t)始終為一常量的話幽告,那么SIR模型就可以用如下的微分方程組來表示。

其中裆甩,系數(shù)β是感染率冗锁,γ則是治愈率。為了阻止以至消滅傳染病的流行嗤栓,醫(yī)學(xué)界會(huì)努力降低感染率冻河,提高治愈率。但是在Gossip協(xié)議的語(yǔ)境下茉帅,計(jì)算機(jī)科學(xué)家要做的恰恰相反叨叙,即盡量高效地讓集群內(nèi)所有節(jié)點(diǎn)都“感染”(對(duì)信息有感知)。由SIR模型推演出的Gossip協(xié)議傳播模型主要有兩種堪澎,即反熵(Anti-entropy)和謠言傳播(Rumor-mongering)摔敛,下面分別介紹之。

反熵(Anti-entropy)

熵是物理學(xué)中體系混亂程度的度量全封,而反熵就是通過看似雜亂無章的通信達(dá)到最終一致马昙。反熵只用到SIR模型中的S和I狀態(tài),S狀態(tài)表示節(jié)點(diǎn)尚未感知到數(shù)據(jù)刹悴,I狀態(tài)表示節(jié)點(diǎn)已感知到數(shù)據(jù)行楞,并且正在傳播給其他節(jié)點(diǎn)。具體來講土匀,反熵Gossip協(xié)議有3種實(shí)現(xiàn)方式:

  • 推模式(push):處于I狀態(tài)的節(jié)點(diǎn)周期性地隨機(jī)選擇其他節(jié)點(diǎn)子房,并將自己持有的數(shù)據(jù)發(fā)送出去;
  • 拉模式(pull):處于S狀態(tài)的節(jié)點(diǎn)周期性地隨機(jī)選擇其他節(jié)點(diǎn)就轧,并請(qǐng)求接收其他節(jié)點(diǎn)持有的數(shù)據(jù)证杭;
  • 推-拉模式(push-pull):即以上兩者的綜合。

下圖示出在有界集群P中妒御,以周期Δ執(zhí)行反熵Gossip協(xié)議的偽代碼描述解愤。

如何分析其效率呢?為了簡(jiǎn)化問題乎莉,提出以下約束:

  • 每一輪周期每個(gè)節(jié)點(diǎn)都只隨機(jī)選擇一個(gè)其他節(jié)點(diǎn)進(jìn)行通信送讲;
  • 起始時(shí),只有一個(gè)節(jié)點(diǎn)處于I狀態(tài)惋啃,其他節(jié)點(diǎn)都處于S狀態(tài)哼鬓。

令s(t)表示在時(shí)刻t時(shí),S狀態(tài)的節(jié)點(diǎn)占總節(jié)點(diǎn)數(shù)n的比例(注意是比例)边灭,那么顯然有s(0) = 1 - 1/n异希,可以計(jì)算出s(t)的期望為:

  • 推模式
  • 拉模式

由下圖可見,拉模式的信息傳播效率比推模式高绒瘦,達(dá)到了真正的指數(shù)級(jí)收斂速度称簿。綜合了兩者的推-拉模式效率則比拉模式更高味榛。

但是,推模式每輪只需要1次信息交換予跌,拉模式需要2次,推-拉模式需要3次善茎。由于反熵Gossip協(xié)議每次都交換全量消息券册,數(shù)據(jù)量可能會(huì)比較大,因此具體選擇哪種模式垂涯,還是需要考慮網(wǎng)絡(luò)資源的開銷再?zèng)Q定烁焙。

謠言傳播(Rumor-mongering)

謠言傳播與反熵不同的一點(diǎn)是挪挤,它采用完整的SIR模型游盲。處于R狀態(tài)的結(jié)點(diǎn)表示已經(jīng)獲取到了信息兑巾,但是不會(huì)將這個(gè)信息分享給其他節(jié)點(diǎn)抡驼,就像“謠言止于智者”一樣能颁。另一個(gè)不同點(diǎn)是晓折,謠言傳播機(jī)制每次只會(huì)交換發(fā)生變化的信息钻弄,而不是全量信息,所以它對(duì)網(wǎng)絡(luò)資源的開銷會(huì)比反熵機(jī)制要小很多苔咪。

下圖示出在有界集群P中,以周期Δ執(zhí)行謠言傳播Gossip協(xié)議的偽代碼描述遇八。

圖中的blind/feedback和coin/counter是怎么一回事呢是掰?它們表示節(jié)點(diǎn)從I狀態(tài)轉(zhuǎn)移到R狀態(tài)的條件虑鼎。

  • coin:在每輪傳播中,節(jié)點(diǎn)以1/k的概率從I轉(zhuǎn)移到R狀態(tài)键痛。
  • counter:在參與k輪傳播之后(即發(fā)送k次信息)之后炫彩,節(jié)點(diǎn)從I狀態(tài)轉(zhuǎn)移到R狀態(tài)。
  • feedback:在發(fā)出信息后絮短,對(duì)位節(jié)點(diǎn)有反饋才可以進(jìn)入R狀態(tài)江兢。
  • blind:在發(fā)出信息后,不必等待對(duì)位節(jié)點(diǎn)有反饋丁频,隨時(shí)都可以進(jìn)入R狀態(tài)划址。

由上可見,謠言傳播模式的結(jié)束條件是所有節(jié)點(diǎn)都對(duì)謠言“免疫”限府,但是又有可能造成部分節(jié)點(diǎn)始終無法對(duì)消息有感知(即保持S狀態(tài))夺颤。以coin條件為例,可以寫出如下的微分方程組胁勺。其中s和i仍然表示S狀態(tài)和I狀態(tài)的節(jié)點(diǎn)占總節(jié)點(diǎn)數(shù)的比例世澜。

消去t,可得:

根據(jù)初始條件:i(1 - 1/n) = 1署穗,可以推導(dǎo)出:

如果我們要讓i(s*) = 0的話:

可見寥裂,s*會(huì)隨著k值的增高而指數(shù)級(jí)下降嵌洼。當(dāng)k = 1時(shí),s*約為20%封恰,而當(dāng)k = 5時(shí)麻养,s*就只有約0.24%了。也就是說诺舔,如果節(jié)點(diǎn)每輪以1/5的概率從I轉(zhuǎn)換為R狀態(tài)鳖昌,就已經(jīng)比較安全了。

在實(shí)際應(yīng)用中低飒,反熵和謠言傳播的各種方式往往結(jié)合在一起使用许昨,因此Gossip協(xié)議非常靈活,沒有完全統(tǒng)一的標(biāo)準(zhǔn)褥赊。以下就看一看Redis Cluster的實(shí)現(xiàn)糕档。

Redis Cluster的Gossip方案

Redis Cluster是在3.0版本加入的feature,故我們就選擇3.0版本的源碼來簡(jiǎn)單解說拌喉。下圖是主從架構(gòu)的Redis Cluster示意圖速那,其中虛線表示各個(gè)節(jié)點(diǎn)之間的Gossip通信。

消息類型

Gossip協(xié)議是個(gè)松散的協(xié)議尿背,沒有對(duì)數(shù)據(jù)交換的格式做特別的約束琅坡,各框架可以自由設(shè)定自己的implementation。Redis Cluster有以下9種消息類型的定義残家,詳情可見注釋(注釋非我所寫榆俺,而是來自redis-3.0-annotated項(xiàng)目,致敬)坞淮。

/* Note that the PING, PONG and MEET messages are actually the same exact
 * kind of packet. PONG is the reply to ping, in the exact format as a PING,
 * while MEET is a special PING that forces the receiver to add the sender
 * as a node (if it is not already in the list). */
// 注意茴晋,PING 、 PONG 和 MEET 實(shí)際上是同一種消息回窘。
// PONG 是對(duì) PING 的回復(fù)诺擅,它的實(shí)際格式也為 PING 消息,
// 而 MEET 則是一種特殊的 PING 消息啡直,用于強(qiáng)制消息的接收者將消息的發(fā)送者添加到集群中
// (如果節(jié)點(diǎn)尚未在節(jié)點(diǎn)列表中的話)
// PING
#define CLUSTERMSG_TYPE_PING 0          /* Ping */
// PONG (回復(fù) PING)
#define CLUSTERMSG_TYPE_PONG 1          /* Pong (reply to Ping) */
// 請(qǐng)求將某個(gè)節(jié)點(diǎn)添加到集群中
#define CLUSTERMSG_TYPE_MEET 2          /* Meet "let's join" message */
// 將某個(gè)節(jié)點(diǎn)標(biāo)記為 FAIL
#define CLUSTERMSG_TYPE_FAIL 3          /* Mark node xxx as failing */
// 通過發(fā)布與訂閱功能廣播消息
#define CLUSTERMSG_TYPE_PUBLISH 4       /* Pub/Sub Publish propagation */
// 請(qǐng)求進(jìn)行故障轉(zhuǎn)移操作烁涌,要求消息的接收者通過投票來支持消息的發(fā)送者
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 /* May I failover? */
// 消息的接收者同意向消息的發(fā)送者投票
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6     /* Yes, you have my vote */
// 槽布局已經(jīng)發(fā)生變化,消息發(fā)送者要求消息接收者進(jìn)行相應(yīng)的更新
#define CLUSTERMSG_TYPE_UPDATE 7        /* Another node slots configuration */
// 為了進(jìn)行手動(dòng)故障轉(zhuǎn)移酒觅,暫停各個(gè)客戶端
#define CLUSTERMSG_TYPE_MFSTART 8       /* Pause clients for manual failover */

可見撮执,Redis Gossip除了負(fù)責(zé)信息交換之外,還會(huì)負(fù)責(zé)節(jié)點(diǎn)的上下線及failover舷丹。

消息格式

Redis Gossip消息分為消息頭和消息體抒钱,消息體一共有4類,其中MEET、PING和PONG消息都用clusterMsgDataGossip結(jié)構(gòu)來表示谋币。

typedef struct {
    // 節(jié)點(diǎn)的名字
    // 在剛開始的時(shí)候仗扬,節(jié)點(diǎn)的名字會(huì)是隨機(jī)的
    // 當(dāng) MEET 信息發(fā)送并得到回復(fù)之后,集群就會(huì)為節(jié)點(diǎn)設(shè)置正式的名字
    char nodename[REDIS_CLUSTER_NAMELEN];
    // 最后一次向該節(jié)點(diǎn)發(fā)送 PING 消息的時(shí)間戳
    uint32_t ping_sent;
    // 最后一次從該節(jié)點(diǎn)接收到 PONG 消息的時(shí)間戳
    uint32_t pong_received;
    // 節(jié)點(diǎn)的 IP 地址
    char ip[REDIS_IP_STR_LEN];    /* IP address last time it was seen */
    // 節(jié)點(diǎn)的端口號(hào)
    uint16_t port;  /* port last time it was seen */
    // 節(jié)點(diǎn)的標(biāo)識(shí)值
    uint16_t flags;
    // 對(duì)齊字節(jié)蕾额,不使用
    uint32_t notused; /* for 64 bit alignment */
} clusterMsgDataGossip;

typedef struct {
    // 下線節(jié)點(diǎn)的名字
    char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;

typedef struct {
    // 頻道名長(zhǎng)度
    uint32_t channel_len;
    // 消息長(zhǎng)度
    uint32_t message_len;
    // 消息內(nèi)容早芭,格式為 頻道名+消息
    // bulk_data[0:channel_len-1] 為頻道名
    // bulk_data[channel_len:channel_len+message_len-1] 為消息
    unsigned char bulk_data[8]; /* defined as 8 just for alignment concerns. */
} clusterMsgDataPublish;

typedef struct {
    // 節(jié)點(diǎn)的配置紀(jì)元
    uint64_t configEpoch; /* Config epoch of the specified instance. */
    // 節(jié)點(diǎn)的名字
    char nodename[REDIS_CLUSTER_NAMELEN]; /* Name of the slots owner. */
    // 節(jié)點(diǎn)的槽布局
    unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* Slots bitmap. */
} clusterMsgDataUpdate;

union clusterMsgData {
    /* PING, MEET and PONG */
    struct {
        /* Array of N clusterMsgDataGossip structures */
        clusterMsgDataGossip gossip[1];
    } ping;
    /* FAIL */
    struct {
        clusterMsgDataFail about;
    } fail;
    /* PUBLISH */
    struct {
        clusterMsgDataPublish msg;
    } publish;
    /* UPDATE */
    struct {
        clusterMsgDataUpdate nodecfg;
    } update;
};

調(diào)度Gossip通信

在redis.c中,有一個(gè)負(fù)責(zé)調(diào)度執(zhí)行Redis server內(nèi)周期性任務(wù)的函數(shù)诅蝶,名為serverCron()退个。其中,與集群相關(guān)的代碼段如下秤涩。

/* Run the Redis Cluster cron. */
// 如果服務(wù)器運(yùn)行在集群模式下,那么執(zhí)行集群操作
run_with_period(100) {
    if (server.cluster_enabled)     clusterCron();
}

可見司抱,在啟用集群時(shí)筐眷,每個(gè)節(jié)點(diǎn)都會(huì)每隔100毫秒執(zhí)行關(guān)于集群的周期性任務(wù)clusterCron(),該函數(shù)中與Gossip有關(guān)的代碼有多處习柠,以下是部分節(jié)選匀谣。注釋寫得非常清楚,筆者就不再獻(xiàn)丑了资溃。

節(jié)點(diǎn)加入集群
// 為未創(chuàng)建連接的節(jié)點(diǎn)創(chuàng)建連接
if (node->link == NULL) {
    // .....
    /* Queue a PING in the new connection ASAP: this is crucial
     * to avoid false positives in failure detection.
     *
     * If the node is flagged as MEET, we send a MEET message instead
     * of a PING one, to force the receiver to add us in its node
     * table. */
    // 向新連接的節(jié)點(diǎn)發(fā)送 PING 命令武翎,防止節(jié)點(diǎn)被識(shí)進(jìn)入下線
    // 如果節(jié)點(diǎn)被標(biāo)記為 MEET ,那么發(fā)送 MEET 命令溶锭,否則發(fā)送 PING 命令
    old_ping_sent = node->ping_sent;
    clusterSendPing(link, node->flags & REDIS_NODE_MEET ?
            CLUSTERMSG_TYPE_MEET : CLUSTERMSG_TYPE_PING);
    // 這不是第一次發(fā)送 PING 信息宝恶,所以可以還原這個(gè)時(shí)間
    // 等 clusterSendPing() 函數(shù)來更新它
    if (old_ping_sent) {
        /* If there was an active ping before the link was
         * disconnected, we want to restore the ping time, otherwise
         * replaced by the clusterSendPing() call. */
        node->ping_sent = old_ping_sent;
    }
    /* We can clear the flag after the first packet is sent.
     *
     * 在發(fā)送 MEET 信息之后,清除節(jié)點(diǎn)的 MEET 標(biāo)識(shí)趴捅。
     *
     * If we'll never receive a PONG, we'll never send new packets
     * to this node. Instead after the PONG is received and we
     * are no longer in meet/handshake status, we want to send
     * normal PING packets. 
     *
     * 如果當(dāng)前節(jié)點(diǎn)(發(fā)送者)沒能收到 MEET 信息的回復(fù)垫毙,
     * 那么它將不再向目標(biāo)節(jié)點(diǎn)發(fā)送命令。
     *
     * 如果接收到回復(fù)的話拱绑,那么節(jié)點(diǎn)將不再處于 HANDSHAKE 狀態(tài)综芥,
     * 并繼續(xù)向目標(biāo)節(jié)點(diǎn)發(fā)送普通 PING 命令。
     */
    node->flags &= ~REDIS_NODE_MEET;
    redisLog(REDIS_DEBUG,"Connecting with Node %.40s at %s:%d",
            node->name, node->ip, node->port+REDIS_CLUSTER_PORT_INCR);
}
隨機(jī)周期性發(fā)送PING消息
/* Ping some random node 1 time every 10 iterations, so that we usually ping
 * one random node every second. */
// clusterCron() 每執(zhí)行 10 次(至少間隔一秒鐘)猎拨,就向一個(gè)隨機(jī)節(jié)點(diǎn)發(fā)送 gossip 信息
if (!(iteration % 10)) {
    int j;
    /* Check a few random nodes and ping the one with the oldest
     * pong_received time. */
    // 隨機(jī) 5 個(gè)節(jié)點(diǎn)膀藐,選出其中一個(gè)
    for (j = 0; j < 5; j++) {
        // 隨機(jī)在集群中挑選節(jié)點(diǎn)
        de = dictGetRandomKey(server.cluster->nodes);
        clusterNode *this = dictGetVal(de);
        /* Don't ping nodes disconnected or with a ping currently active. */
        // 不要 PING 連接斷開的節(jié)點(diǎn),也不要 PING 最近已經(jīng) PING 過的節(jié)點(diǎn)
        if (this->link == NULL || this->ping_sent != 0) continue;
        if (this->flags & (REDIS_NODE_MYSELF|REDIS_NODE_HANDSHAKE))
            continue;
        // 選出 5 個(gè)隨機(jī)節(jié)點(diǎn)中最近一次接收 PONG 回復(fù)距離現(xiàn)在最舊的節(jié)點(diǎn)
        if (min_pong_node == NULL || min_pong > this->pong_received) {
            min_pong_node = this;
            min_pong = this->pong_received;
        }
    }
    // 向最久沒有收到 PONG 回復(fù)的節(jié)點(diǎn)發(fā)送 PING 命令
    if (min_pong_node) {
        redisLog(REDIS_DEBUG,"Pinging node %.40s", min_pong_node->name);
        clusterSendPing(min_pong_node->link, CLUSTERMSG_TYPE_PING);
    }
}
防止節(jié)點(diǎn)假超時(shí)及狀態(tài)過期
/* If we are waiting for the PONG more than half the cluster
 * timeout, reconnect the link: maybe there is a connection
 * issue even if the node is alive. */
// 如果等到 PONG 到達(dá)的時(shí)間超過了 node timeout 一半的連接
// 因?yàn)楸M管節(jié)點(diǎn)依然正常红省,但連接可能已經(jīng)出問題了
if (node->link && /* is connected */
    now - node->link->ctime >
    server.cluster_node_timeout && /* was not already reconnected */
    node->ping_sent && /* we already sent a ping */
    node->pong_received < node->ping_sent && /* still waiting pong */
    /* and we are waiting for the pong more than timeout/2 */
    now - node->ping_sent > server.cluster_node_timeout/2)
{
    /* Disconnect the link, it will be reconnected automatically. */
    // 釋放連接额各,下次 clusterCron() 會(huì)自動(dòng)重連
    freeClusterLink(node->link);
}
/* If we have currently no active ping in this instance, and the
 * received PONG is older than half the cluster timeout, send
 * a new ping now, to ensure all the nodes are pinged without
 * a too big delay. */
// 如果目前沒有在 PING 節(jié)點(diǎn)
// 并且已經(jīng)有 node timeout 一半的時(shí)間沒有從節(jié)點(diǎn)那里收到 PONG 回復(fù)
// 那么向節(jié)點(diǎn)發(fā)送一個(gè) PING ,確保節(jié)點(diǎn)的信息不會(huì)太舊
// (因?yàn)橐徊糠止?jié)點(diǎn)可能一直沒有被隨機(jī)中)
if (node->link &&
    node->ping_sent == 0 &&
    (now - node->pong_received) > server.cluster_node_timeout/2)
{
    clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
    continue;
}
處理failover和標(biāo)記疑似下線
/* If we are a master and one of the slaves requested a manual
 * failover, ping it continuously. */
// 如果這是一個(gè)主節(jié)點(diǎn)吧恃,并且有一個(gè)從服務(wù)器請(qǐng)求進(jìn)行手動(dòng)故障轉(zhuǎn)移
// 那么向從服務(wù)器發(fā)送 PING 臊泰。
if (server.cluster->mf_end &&
    nodeIsMaster(myself) &&
    server.cluster->mf_slave == node &&
    node->link)
{
    clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
    continue;
}
/* Check only if we have an active ping for this instance. */
// 以下代碼只在節(jié)點(diǎn)發(fā)送了 PING 命令的情況下執(zhí)行
if (node->ping_sent == 0) continue;
/* Compute the delay of the PONG. Note that if we already received
 * the PONG, then node->ping_sent is zero, so can't reach this
 * code at all. */
// 計(jì)算等待 PONG 回復(fù)的時(shí)長(zhǎng)
delay = now - node->ping_sent;
// 等待 PONG 回復(fù)的時(shí)長(zhǎng)超過了限制值,將目標(biāo)節(jié)點(diǎn)標(biāo)記為 PFAIL (疑似下線)
if (delay > server.cluster_node_timeout) {
    /* Timeout reached. Set the node as possibly failing if it is
     * not already in this state. */
    if (!(node->flags & (REDIS_NODE_PFAIL|REDIS_NODE_FAIL))) {
        redisLog(REDIS_DEBUG,"*** NODE %.40s possibly failing",
            node->name);
        // 打開疑似下線標(biāo)記
        node->flags |= REDIS_NODE_PFAIL;
        update_state = 1;
    }
}

由上可知,server.cluster_node_timeout是判斷節(jié)點(diǎn)狀態(tài)過期及疑似下線的標(biāo)準(zhǔn)缸逃,所以對(duì)于不同網(wǎng)絡(luò)狀態(tài)和規(guī)模的集群针饥,要視實(shí)際情況設(shè)定。

實(shí)際發(fā)送Gossip消息

以下是前方多次調(diào)用過的clusterSendPing()方法的源碼需频,不難理解丁眼。

/* Send a PING or PONG packet to the specified node, making sure to add enough
 * gossip informations. */
// 向指定節(jié)點(diǎn)發(fā)送一條 MEET 、 PING 或者 PONG 消息
void clusterSendPing(clusterLink *link, int type) {
    unsigned char buf[sizeof(clusterMsg)];
    clusterMsg *hdr = (clusterMsg*) buf;
    int gossipcount = 0, totlen;
    /* freshnodes is the number of nodes we can still use to populate the
     * gossip section of the ping packet. Basically we start with the nodes
     * we have in memory minus two (ourself and the node we are sending the
     * message to). Every time we add a node we decrement the counter, so when
     * it will drop to <= zero we know there is no more gossip info we can
     * send. */
    // freshnodes 是用于發(fā)送 gossip 信息的計(jì)數(shù)器
    // 每次發(fā)送一條信息時(shí)昭殉,程序?qū)?freshnodes 的值減一
    // 當(dāng) freshnodes 的數(shù)值小于等于 0 時(shí)苞七,程序停止發(fā)送 gossip 信息
    // freshnodes 的數(shù)量是節(jié)點(diǎn)目前的 nodes 表中的節(jié)點(diǎn)數(shù)量減去 2 
    // 這里的 2 指兩個(gè)節(jié)點(diǎn),一個(gè)是 myself 節(jié)點(diǎn)(也即是發(fā)送信息的這個(gè)節(jié)點(diǎn))
    // 另一個(gè)是接受 gossip 信息的節(jié)點(diǎn)
    int freshnodes = dictSize(server.cluster->nodes)-2;

    // 如果發(fā)送的信息是 PING 挪丢,那么更新最后一次發(fā)送 PING 命令的時(shí)間戳
    if (link->node && type == CLUSTERMSG_TYPE_PING)
        link->node->ping_sent = mstime();

    // 將當(dāng)前節(jié)點(diǎn)的信息(比如名字蹂风、地址、端口號(hào)乾蓬、負(fù)責(zé)處理的槽)記錄到消息里面
    clusterBuildMessageHdr(hdr,type);

    /* Populate the gossip fields */
    // 從當(dāng)前節(jié)點(diǎn)已知的節(jié)點(diǎn)中隨機(jī)選出兩個(gè)節(jié)點(diǎn)
    // 并通過這條消息捎帶給目標(biāo)節(jié)點(diǎn)惠啄,從而實(shí)現(xiàn) gossip 協(xié)議

    // 每個(gè)節(jié)點(diǎn)有 freshnodes 次發(fā)送 gossip 信息的機(jī)會(huì)
    // 每次向目標(biāo)節(jié)點(diǎn)發(fā)送 2 個(gè)被選中節(jié)點(diǎn)的 gossip 信息(gossipcount 計(jì)數(shù))
    while(freshnodes > 0 && gossipcount < 3) {
        // 從 nodes 字典中隨機(jī)選出一個(gè)節(jié)點(diǎn)(被選中節(jié)點(diǎn))
        dictEntry *de = dictGetRandomKey(server.cluster->nodes);
        clusterNode *this = dictGetVal(de);

        clusterMsgDataGossip *gossip;
        int j;

        /* In the gossip section don't include:
         * 以下節(jié)點(diǎn)不能作為被選中節(jié)點(diǎn):
         * 1) Myself.
         *    節(jié)點(diǎn)本身。
         * 2) Nodes in HANDSHAKE state.
         *    處于 HANDSHAKE 狀態(tài)的節(jié)點(diǎn)任内。
         * 3) Nodes with the NOADDR flag set.
         *    帶有 NOADDR 標(biāo)識(shí)的節(jié)點(diǎn)
         * 4) Disconnected nodes if they don't have configured slots.
         *    因?yàn)椴惶幚砣魏尾鄱粩嚅_連接的節(jié)點(diǎn) 
         */
        if (this == myself ||
            this->flags & (REDIS_NODE_HANDSHAKE|REDIS_NODE_NOADDR) ||
            (this->link == NULL && this->numslots == 0))
        {
                freshnodes--; /* otherwise we may loop forever. */
                continue;
        }

        /* Check if we already added this node */
        // 檢查被選中節(jié)點(diǎn)是否已經(jīng)在 hdr->data.ping.gossip 數(shù)組里面
        // 如果是的話說明這個(gè)節(jié)點(diǎn)之前已經(jīng)被選中了
        // 不要再選中它(否則就會(huì)出現(xiàn)重復(fù))
        for (j = 0; j < gossipcount; j++) {
            if (memcmp(hdr->data.ping.gossip[j].nodename,this->name,
                    REDIS_CLUSTER_NAMELEN) == 0) break;
        }
        if (j != gossipcount) continue;

        /* Add it */

        // 這個(gè)被選中節(jié)點(diǎn)有效撵渡,計(jì)數(shù)器減一
        freshnodes--;

        // 指向 gossip 信息結(jié)構(gòu)
        gossip = &(hdr->data.ping.gossip[gossipcount]);

        // 將被選中節(jié)點(diǎn)的名字記錄到 gossip 信息
        memcpy(gossip->nodename,this->name,REDIS_CLUSTER_NAMELEN);
        // 將被選中節(jié)點(diǎn)的 PING 命令發(fā)送時(shí)間戳記錄到 gossip 信息
        gossip->ping_sent = htonl(this->ping_sent);
        // 將被選中節(jié)點(diǎn)的 PING 命令回復(fù)的時(shí)間戳記錄到 gossip 信息
        gossip->pong_received = htonl(this->pong_received);
        // 將被選中節(jié)點(diǎn)的 IP 記錄到 gossip 信息
        memcpy(gossip->ip,this->ip,sizeof(this->ip));
        // 將被選中節(jié)點(diǎn)的端口號(hào)記錄到 gossip 信息
        gossip->port = htons(this->port);
        // 將被選中節(jié)點(diǎn)的標(biāo)識(shí)值記錄到 gossip 信息
        gossip->flags = htons(this->flags);

        // 這個(gè)被選中節(jié)點(diǎn)有效,計(jì)數(shù)器增一
        gossipcount++;
    }

    // 計(jì)算信息長(zhǎng)度
    totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
    totlen += (sizeof(clusterMsgDataGossip)*gossipcount);
    // 將被選中節(jié)點(diǎn)的數(shù)量(gossip 信息中包含了多少個(gè)節(jié)點(diǎn)的信息)
    // 記錄在 count 屬性里面
    hdr->count = htons(gossipcount);
    // 將信息的長(zhǎng)度記錄到信息里面
    hdr->totlen = htonl(totlen);

    // 發(fā)送信息
    clusterSendMessage(link,buf,totlen);
}

The End

貼的源碼有點(diǎn)多了死嗦,自己看著都略頭疼趋距。

明天還要搬磚,晚安吧各位越除。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末节腐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子摘盆,更是在濱河造成了極大的恐慌铜跑,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骡澈,死亡現(xiàn)場(chǎng)離奇詭異锅纺,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)肋殴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門囤锉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人护锤,你說我怎么就攤上這事官地。” “怎么了烙懦?”我有些...
    開封第一講書人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵驱入,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng)亏较,這世上最難降的妖魔是什么莺褒? 我笑而不...
    開封第一講書人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮雪情,結(jié)果婚禮上遵岩,老公的妹妹穿的比我還像新娘。我一直安慰自己巡通,他們只是感情好尘执,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宴凉,像睡著了一般誊锭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弥锄,一...
    開封第一講書人閱讀 49,785評(píng)論 1 290
  • 那天丧靡,我揣著相機(jī)與錄音,去河邊找鬼叉讥。 笑死窘行,一個(gè)胖子當(dāng)著我的面吹牛饥追,可吹牛的內(nèi)容都是我干的图仓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼但绕,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼救崔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捏顺,我...
    開封第一講書人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤六孵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后幅骄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體劫窒,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年拆座,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了主巍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挪凑,死狀恐怖孕索,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情躏碳,我是刑警寧澤搞旭,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響肄渗,放射性物質(zhì)發(fā)生泄漏镇眷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一恳啥、第九天 我趴在偏房一處隱蔽的房頂上張望偏灿。 院中可真熱鬧,春花似錦钝的、人聲如沸翁垂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)沿猜。三九已至,卻和暖如春碗脊,著一層夾襖步出監(jiān)牢的瞬間啼肩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工衙伶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祈坠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓矢劲,卻偏偏與公主長(zhǎng)得像赦拘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芬沉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348