圖解 | 你管這破玩意叫哨兵?

我是一個苦逼的運維,有一次老闆過來找我。

老闆:現在有四個 redis 節點擺在你面前,一主三從,你負責盯着點,主節點掛了你趕緊想辦法拿從節點頂上來,交給你了!

這還不簡單!

首先我先分別連上這四台 redis 節點。

redis-cli -h 10.232.0.0 -p 6379
redis-cli -h 10.232.0.1 -p 6379
redis-cli -h 10.232.0.2 -p 6379
redis-cli -h 10.232.0.3 -p 6379

然後每隔 1s 分別發送 redis 專屬的命令 PING

我就這樣一直不斷地發送着 PING 命令,日復一日。

終於有一天,發送給主節點的 PING 命令收到了無效回復!

我立刻打起了精神,開始操作了起來。

但我沒有慌亂了手腳,很快我就梳理好了即將要做的三件事。

 

選擇一個從節點,將其變為主節點。

 

選哪個節點好呢?先別管那麼多了,隨便選一個,就 10.232.0.3:6379 這個吧!

我對着這個節點,發送了一個命令。

10.232.0.3:6379> slaveof no one
OK

我想,這個節點應該就已經變成了主節點了,但我不太敢確定,於是又發送了一個命令進行確認。

10.232.0.3:6379> info

role:slave

誒,還沒有變成主節點呢,那再給他點時間。一秒鐘之後,我再次進行查看。

10.232.0.3:6379> info

role:master

嗯,這回已經成功變成主節點啦,進行下一步!

 

修改其他從節點的附屬主節點

 

很簡單,向另外兩台從節點發送命令。

10.232.0.1:6379> slaveof 10.232.0.3 6379
OK
10.232.0.2:6379> slaveof 10.232.0.3 6379
OK

 

將掛掉的主節點變為從節點

 

這一步充分體現了我多年的運維經驗,很多人都想不到。

原來的主節點我可不能不管,萬一他又復活了,就得乖乖成為新主節點的從節點。

10.232.0.0:6379> slaveof 10.232.0.3 6379

但是我不能直接發送這個命令給它,因為它還掛着呢,所以我將命令保存起來,只要它一復活我就發給它這個命令。

整個三步看起來是這個樣子。

經過多次這樣的操作,我終於熟悉了整個流程。

為了解放我自己的雙手,我把這個固定的流程,寫成了一個程序。

這個程序能實時監控這些 redis 節點的狀態,並能自動報告並處理突發情況,我給他命名為哨兵程序

而這個哨兵程序我單獨用一台服務器部署,這個服務器就稱為哨兵節點

哨兵一開始就連接這 4 個 redis 節點,並持續我剛剛的操作過程。

 

優化

 

我還發現了一個小的優化點,我無需知道這 4 個節點的全部信息,只需要知道主節點即可。

從節點的信息,我通過向主節點發送 info 命令即可獲取,而且可以不斷獲取來更新。

10.232.0.0:6379> info

role:master

slave0:ip=10.232.0.1,port=6379,state=online …
slave0:ip=10.232.0.2,port=6379,state=online …
slave0:ip=10.232.0.3,port=6379,state=online …

這樣,我在啟動哨兵時,只要知道主節點即可,而且這樣獲取的從節點信息更準確,也更實時,就不用一直問老闆啦。

雖然已經可以解放雙手,但興緻來了的我仍然沒有收手。

剛剛主節點掛了之後,我隨機從三個從節點中選擇了一個作為主節點,不妨讓這個隨機也智能一些吧,不然總覺得太 low。

首先,我把所有的從節點的主要信息列出來(這裡假設多一些節點方便分析)

節點

狀態

距離上次回復的時間

複製偏移量

uid

1

DISCONNECTED

8

50

12345

2

DOWN

8

50

12346

3

7

50

12347

4

1

50

12348

5

DOWN

8

50

12349

6

1

50

12350

先去掉所有斷線或下線的節點。

節點

狀態

距離上次

回復的時間

複製偏移量

uid

1

DISCONNECTED

8

50

12345

2

DOWN

8

50

12346

3

7

50

12347

4

1

50

12348

5

DOWN

8

50

12349

6

1

50

12350

再去掉最後一個 ping 請求過去後,未回應的時間大於 5s 的。

節點

狀態

距離上次

回復的時間

複製偏移量

uid

1

DISCONNECTED

8

50

12345

2

DOWN

8

50

12346

3

7

50

12347

4

1

50

12348

5

DOWN

8

50

12349

6

1

50

12350

剩下兩個,是至少狀態健康的節點,繼續擇優錄取。

我們比較其複製偏移量的值,這個代表其從主節點成功複製了多少數據,選擇一個複製偏移量最多的,也就是與主節點最接近同步的。

節點

狀態

距離上次

回復的時間

複製偏移量

uid

4

1

50

12348

6

1

50

12350

不過我們發現其偏移量一樣。

到現在,這兩個節點無論從健康狀態,還是同步狀態,都是完全一樣的,沒辦法分出誰好誰壞了,那怎麼辦呢?

沒關係,還有一個終極武器,就是唯一標識 uid,這兩個 uid 在啟動節點時就保證了必然不相同,我們選擇一個相對較小的。

節點

狀態

距離上次

回復的時間

複製偏移量

uid

4

1

50

12348

OK,最終可以唯一確定一個從節點,就把它變為主節點了!

我把這個複雜的過程,寫成了一個方法,sentinelSelectSlave(),放在了哨兵程序中,用來選擇一個從節點。

嗯,現在這個程序看起來,已經很完善了!

我放心地把這個哨兵程序啟動起來,之後的很長一段時間,我就靠着我的哨兵程序,成功自動應對了很多次突發情況,有一次甚至在半夜兩點多迅速將問題發現並解決。

老闆一直誇我堅守崗位,半夜了還這麼負責,我很快得到了晉陞。

直到有一次,我正在開開心心摸魚,老闆氣哄哄地走來。

老闆:redis 都掛了一個小時了!你怎麼還不處理!額?你這是看什麼?leetcode?是準備跳槽了么!

我一臉懵逼,趕緊看了一下我的哨兵進程,我擦,哨兵服務器掛掉了!

我被降了職,但仍然要負責看着這些 redis 節點,這回我可不敢怠慢了。

我繼續用哨兵程序監控着這些節點的生死,但我自己又多了一項任務,就是監控哨兵節點的狀態,彷彿一夜回到解放前。

怎麼樣再次解放我的雙手,讓程序幫我去監控和處理這個哨兵節點的健康狀態呢?

我靈機一動,部署多個哨兵節點,成為哨兵集群!只要有一個節點活着就行,這樣同時都掛掉的概率就非常小了。

當然,有三個哨兵時,每個哨兵就不能太自我了,得聽從組織統一安排。

 

主客觀問題

 

比如說,當哨兵 1 認為主節點已經掛掉時,不能認為主節點就真的掛掉了,這種判斷叫做主觀下線

哨兵 1 主觀認為主節點下線時,需要詢問其他節點,主節點是否已經下線。

如果其中哨兵 2 回復,主節點下線了,哨兵 3 回復,主節點沒下線。

那麼這個時候,哨兵集群中,一共有 2 個哨兵都主觀認為主節點下線。

當主觀下線的數量達到一定值時,比如說 >=2 時,我們就可以認為,主節點客觀下線

一旦主節點客觀下線了,那就又可以走之前的故障處理流程,即選擇一個從節點變成主節點。

 

領頭問題

 

接下來,將從節點變成主節點,也就是後續的這個故障處理流程,由哪個哨兵來完成呢?

總不能同時來操作吧。

那就必然需要選舉出一個領頭來完成這個事。

怎麼選舉出一個領頭呢?我總不能再用一個哨兵去做吧,那樣就無限套娃了,最好的方式就是讓他們仨自發地決定。

這部分有點複雜,在這裡展開不太合適,可以單獨水一篇文章來講解,感興趣的同學可以看一下 Raft 算法,哨兵集群正是通過這個算法來選舉領頭的。

OK,我終於再次解放了我的雙手!

我把這個破玩意,稱為哨兵系統,或者哨兵集群!

我再給哨兵起個英文名字,叫 Sentinel 吧!

後記

本次選取的 redis 代碼為 redis-3.0.0。

之所以能夠通過”我”這個視角來寫哨兵,正是因為哨兵這個程序,完全可以由人不斷輸入 redis 命令來輕鬆完成,並不需要什麼其他協議的支持。

比如判斷節點健康狀態的 ping,拿到節點信息的 info,設置主從節點的 slaveof,甚至詢問其他哨兵節點是否在線的命令 sentinel is-master-down-by addr 等等,都是 redis 支持的客戶端命令,對用戶端非常友好。

redis 的源代碼也是非常乾淨,而且設計得很精妙,建議有興趣的讀者可以深入源碼進行閱讀,不算難。

比如上面講的,如何從一堆從節點中,選取一個作為主節點。

這個知識點網上搜,你會搜到很多雲里霧裡的解釋,而如果你看源碼,你會發現這個過程非常清晰。

sentinelRedisInstance *sentinelSelectSlave() {
    ...
    // 去掉一些節點
    while((de = dictNext(di)) != NULL) {
        ...
        if (slave->flags & (DOWN||DISCONNECTED)) continue;
        if (mstime() - slave->last_avail_time > 5000) continue;
        if (slave->slave_priority == 0) continue;
        if (...) continue;
        ...
    }
    // 剩下的節點排個序
    qsort(..., compareSlavesForPromotion);
    // 取第一個
    return instance[0];
}


// 怎麼排序呢?就這麼排
int compareSlavesForPromotion(const void *a, const void *b) {
    // 先按優先級排
    if ((*sa)->slave_priority != (*sb)->slave_priority)
        return (*sa)->slave_priority - (*sb)->slave_priority;
    // 優先級一樣按偏移量排
    if ((*sa)->slave_repl_offset > (*sb)->slave_repl_offset) {
        return -1;
    } else if ((*sa)->slave_repl_offset < (*sb)->slave_repl_offset) {
        return 1;
    }
    ...
    // 偏移量一樣按唯一標識排
    return strcasecmp(sa_runid, sb_runid);
}

 

我想相信如果你停下來仔細看幾秒,哪怕你對 c 語言並不熟悉,也能看懂個大概了,再結合網上或者書上關於這塊的描述,你就有了很直觀的印象。

關於 redis 源碼的深入學習,我建議先閱讀黃健宏的《Redis 設計與實現》,這本書代碼量很少,但邏輯描述完全按照寫代碼的思維來講,你讀一下就知道了。

讀完這本書,直接上手 redis 源碼的閱讀,你可以選擇 redis-1.0.0 代碼,非常少,主要閱讀其整個網絡 IO 以及命令處理的流程。

接着,從 redis-3.0.0 開始,有針對性研究其主從、集群、哨兵等特性。

這樣,redis 在你這,就不再是模模糊糊了。