我是一個苦逼的運維,有一次老闆過來找我。
老闆:現在有四個 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 在你這,就不再是模模糊糊了。