Redis Cluster 原理與管理

從事Redis Cluster相關(guān)工具開發(fā)半年多, 記錄一下對(duì)它的理解和集群管理的想法吧. 這里不復(fù)述Redis Cluster基礎(chǔ)的東西, 需先看官方文檔.
Redis Cluster 要求客戶端使用新的協(xié)議, 我們公司為此開發(fā)了 corvus 這個(gè)proxy來讓客戶端可以繼續(xù)使用單機(jī)Redis協(xié)議. 不像twemproxy和codis, corvus本身無狀態(tài), 不需要依賴zookeeper, 而只是從redis集群中拉取集群信息來做路由表.
Redis Cluster 使用gossip協(xié)議維護(hù)集群信息. 它的gossip協(xié)議并不能嚴(yán)格保證集群信息一致, 在誤用或極端情況下, 集群信息并不能自動(dòng)恢復(fù)一致, 而且不容易修復(fù). 使用Redis Cluster需要理解透其中原理, 不隨意亂做變更操作, 并且要有一套成熟的運(yùn)維系統(tǒng).
我們的業(yè)務(wù)對(duì)緩存可用性要求較高, 使用Redis Cluster的方針是首要保證能夠快速創(chuàng)建一個(gè)可用的集群, 其次要嚴(yán)格限制可對(duì)集群做的變更操作, 還有盡可能用小集群.

集群信息一致性問題

這里不是指數(shù)據(jù)的一致性, 而是集群信息的一致性. 最重要的兩個(gè)集群信息是主從角色和slot的歸屬. 個(gè)人感覺集群信息管理松散混亂, 但是在一般情況下能維持一致性. 如果真出現(xiàn)了不一致的問題, 建議不要浪費(fèi)時(shí)間, 直接重建集群吧. 有些坑不是一時(shí)半會(huì)能解決的.
為什么我不提節(jié)點(diǎn)列表的一致性問題? 固然集群里面有哪些節(jié)點(diǎn)這個(gè)信息可以說是所有其它信息的基礎(chǔ), 但是從實(shí)用的角度來說, 這可以由運(yùn)維系統(tǒng)來保證不出問題, 下面另述.
主從和slot的一致性是由epoch來管理的. epoch就像Raft中的term, 但僅僅是像. 每個(gè)節(jié)點(diǎn)有一個(gè)自己獨(dú)特的epoch和整個(gè)集群的epoch, 為簡化下面都稱為node epoch和cluster epoch. node epoch一直遞增, 其表示某節(jié)點(diǎn)最后一次變成主節(jié)點(diǎn)或獲取新slot所有權(quán)的邏輯時(shí)間. cluster epoch則是整個(gè)集群中最大的那個(gè)node epoch. 我們稱遞增node epoch為bump epoch, 它會(huì)用當(dāng)前的cluster epoch加一來更新自己的node epoch.
在使用gossip協(xié)議中, 如果多個(gè)節(jié)點(diǎn)聲稱不同的集群信息, 那對(duì)于某個(gè)節(jié)點(diǎn)來說究竟要相信誰呢? Redis Cluster規(guī)定了每個(gè)主節(jié)點(diǎn)的epoch都不可以相同. 而一個(gè)節(jié)點(diǎn)只會(huì)去相信擁有更大node epoch的節(jié)點(diǎn)聲稱的信息, 因?yàn)楦蟮膃poch代表更新的集群信息.
原則上:
(1)如果epoch不變, 集群就不應(yīng)該有變更(包括選舉和遷移槽位)
(2)每個(gè)節(jié)點(diǎn)的node epoch都是獨(dú)一無二的
(3)擁有越高epoch的節(jié)點(diǎn), 集群信息越新

Epoch Collision

實(shí)際上, 在遷移slot或者使用cluster failover的時(shí)候, 如果多個(gè)節(jié)點(diǎn)同時(shí)bump epoch, 就有可能出現(xiàn)多個(gè)節(jié)點(diǎn)擁有同一個(gè)epoch, 違反上述原則(2)和(3). 這個(gè)時(shí)候擁有較小node id的節(jié)點(diǎn)就會(huì)自動(dòng)再一次bump epoch, 以保證原則(3). 而原則(2)實(shí)際上因此也并不嚴(yán)格成立, 因?yàn)榻鉀Qepoch collision需要一小段時(shí)間.

選舉

從節(jié)點(diǎn)選舉的時(shí)候其實(shí)沒什么問題, 就是一個(gè)從節(jié)點(diǎn)搶選票的過程. 我們稱管理相同slot集合的所有主從節(jié)點(diǎn)為一個(gè)分片. 選舉的時(shí)候, 掛掉分片的所有從節(jié)點(diǎn)會(huì)向其它分片的所有主節(jié)點(diǎn)索取選票, 如果取到的選票超過分片數(shù)的半數(shù), 該從節(jié)點(diǎn)就選舉成功.

slot

最大的問題在于slot. 我們遇到過數(shù)次遷移slot失敗后出現(xiàn)slot不一致的情況. 如果還沒搞懂它怎么管slot, 請記住下面這句話:
不要用亂用cluster setslot node.
我相信大多數(shù)不一致問題都是我們作死用這個(gè)命令造成的. 除了它我暫時(shí)還沒找到有什么大概率的情況會(huì)導(dǎo)致不一致.

slot 管理

首先我們搞清楚slot究竟是怎么管的. 每個(gè)節(jié)點(diǎn)都有一份16384長的表對(duì)應(yīng)每個(gè)slot究竟歸哪個(gè)節(jié)點(diǎn), 并且會(huì)保存當(dāng)前節(jié)點(diǎn)所認(rèn)為的其它節(jié)點(diǎn)的node epoch. 這樣每個(gè)slot實(shí)際上綁定了一個(gè)節(jié)點(diǎn)及其node epoch. 然后由自認(rèn)為擁有某slot的節(jié)點(diǎn)來負(fù)責(zé)通知其它節(jié)點(diǎn)這個(gè)slot的歸屬. 其它節(jié)點(diǎn)收到這個(gè)消息后, 會(huì)對(duì)比該slot原先綁定節(jié)點(diǎn)的node epoch, 如果收到的是更大的node epoch則更新, 否則不予理睬. 除此之外, 除了使用slot相關(guān)命令做變更, 集群沒有其它途徑修改slot的歸屬.

     slot x 是我管的, 我的node epoch是 y
node A ------------------------------> node B
          (原來slot x歸node C管, 如果 y 比 node C 的node epoch大, 我就更新slot x的歸屬)

這實(shí)際上依賴上述的原則(3), 并且相信slot的舊主人還沒有更新epoch.

遷移slot的一致性

下面來看遷移slot如何保證slot歸屬的一致性.
從node A遷移一個(gè)槽位到node B的流程是:
(1) node A 設(shè)置migrating flag, node B 設(shè)置importing flag
(2) 遷移所有該slot的數(shù)據(jù)到node B
(3) 對(duì)兩個(gè)節(jié)點(diǎn)使用cluster setslot node來消除importing和migrating flag, 并且設(shè)置槽位
重點(diǎn)在于遷移最后一步消除importing flag使用的cluster setslot node, 如果對(duì)一個(gè)節(jié)點(diǎn)使用cluster setslot node的時(shí)候節(jié)點(diǎn)有importing flag, 節(jié)點(diǎn)會(huì)bump epoch, 這樣這個(gè)節(jié)點(diǎn)聲稱slot所有權(quán)時(shí)別的節(jié)點(diǎn)就會(huì)認(rèn)可.
但是這里并沒有跑一遍選舉中的投票流程. 如果另外一個(gè)節(jié)點(diǎn)也同時(shí)bump epoch, 就出現(xiàn)epoch collision. 這里是一個(gè)不完美但又略精妙的地方. 不管這個(gè)清importing flag的節(jié)點(diǎn)在解決collision后是否獲得更高的epoch, 其epoch肯定大于migrating那個(gè)節(jié)點(diǎn)之前的epoch.
但這里還是有漏洞, 萬一node B在廣播自己的新node epoch前, node A做了什么變更而獲取了一個(gè)更大的node epoch呢? 萬一發(fā)生collision的是node A和node B兩個(gè)節(jié)點(diǎn)呢? 這個(gè)時(shí)候假如node A的node id更小, node A會(huì)拿到更大的新epoch. 只要某個(gè)節(jié)點(diǎn)先收到node A的消息, 這個(gè)slot的遷移信息就永遠(yuǎn)寫不進(jìn)這個(gè)節(jié)點(diǎn)了, 因?yàn)閚ode A的node epoch比node B更大.
上面提到的cluster setslot node的問題在于, 如果節(jié)點(diǎn)沒有importing flag, 它會(huì)直接設(shè)置槽位, 但不會(huì)增加自己的node epoch. 這樣當(dāng)他告訴別的節(jié)點(diǎn)對(duì)這個(gè)槽位的所有權(quán)時(shí), 其他節(jié)點(diǎn)并不認(rèn)可. 這實(shí)際上違反了上述原則(1). 詳細(xì)見這里. 所以實(shí)在要在遷移slot以外的地方用這個(gè)命令, 必須要給它發(fā)一次cluster bumpepoch.

運(yùn)維系統(tǒng)

運(yùn)維成百上千大大小小的集群不是寫腳本能勝任的事情. 官方那個(gè)Ruby腳本絕對(duì)不能作為最終方案. 現(xiàn)在我們的方案是以一個(gè)可靠的運(yùn)維系統(tǒng)為基礎(chǔ)把Redis Cluster池化.

檢查, 容錯(cuò), 重試, 回滾

實(shí)際運(yùn)維的時(shí)候會(huì)有各種極端情況. 做任何變更操作, 都要先確保集群是一致并且穩(wěn)定的. 穩(wěn)定是指已經(jīng)沒有還沒同步的信息, 例如多個(gè)主節(jié)點(diǎn)有相同的epoch而未處理. 如果集群本身不穩(wěn)定, 有可能觸發(fā)上述遷移slot的時(shí)候發(fā)生epoch collision. 而且對(duì)于每一步操作, 一定要檢查前提條件是否成立, 例如遷slot最后用cluster setslot node時(shí)需先檢查有沒有importing flag. 還要確保操作是否完成. Redis回一個(gè)OK并不能表示操作沒有問題, 因?yàn)榇蟛糠謗edis變更命令都是異步的. 例如踢節(jié)點(diǎn)的時(shí)候, 假如過了60秒還有節(jié)點(diǎn)認(rèn)為被踢的節(jié)點(diǎn)還在, 就會(huì)因?yàn)間ossip的傳播把那個(gè)節(jié)點(diǎn)重新加進(jìn)集群.
還要有容錯(cuò). 例如在對(duì)集群操作的時(shí)候Redis給你返回Loading Error, 這個(gè)時(shí)候Redis是處于不能處理大部分命令的狀態(tài), 連cluster nodes都不能. 這個(gè)時(shí)候運(yùn)維系統(tǒng)要等待并不斷檢查節(jié)點(diǎn)可以接受命令沒有.
基本上每個(gè)變更操作都是大操作, 操作跑到一半可能只是部分掛了, 這時(shí)要重試, 實(shí)在不行要盡可能回滾.

用chunk管理節(jié)點(diǎn)

為了簡化管理, 我們規(guī)定了集群的規(guī)格. 具體做法是每個(gè)主節(jié)點(diǎn)有且只有一個(gè)從節(jié)點(diǎn). 并且以4個(gè)節(jié)點(diǎn)為最小的管理單位, 我們稱為chunk. 一個(gè)chunk有兩主兩從, 分布在兩臺(tái)機(jī)器上面, 每臺(tái)機(jī)器兩個(gè)節(jié)點(diǎn), 且4個(gè)節(jié)點(diǎn)內(nèi)互相組成主從關(guān)系, 要求負(fù)責(zé)一個(gè)分片的主從分布在不同的機(jī)器上面.

一個(gè)chunk:
machine A    machine B
 master 1 \/ master 2
  slave 2 /\ slave 1

所有的集群都由 n 個(gè)chunk組成而成.
首先為了方便管理部署了不同集群的機(jī)器, 要把節(jié)點(diǎn)分組管理才容易. 其次, 這么做保證了主從不可能在同一臺(tái)機(jī)器上面. 然后在擴(kuò)容跟縮容的時(shí)候, 只要增加或剔除chunk就好了, 可以盡可能平均每臺(tái)機(jī)器的節(jié)點(diǎn)數(shù), 但又不會(huì)破壞主從關(guān)系. 并且要求一個(gè)集群使用的機(jī)器數(shù)量最少為3臺(tái), 這樣一臺(tái)掛了也不會(huì)導(dǎo)致有slot沒人管. 我們曾想過用6個(gè)節(jié)點(diǎn)為一個(gè)chunk, 但是在分配chunk的時(shí)候找不出一種好的分配算法, 而4個(gè)卻找到了分配算法.
我們只使用1主對(duì)應(yīng)1從, 是因?yàn)槲覀冞€未發(fā)現(xiàn)多個(gè)從節(jié)點(diǎn)有什么好處, 而且從節(jié)點(diǎn)不能頂請求壓力還因?yàn)橹鲝耐较牟簧儋Y源. 如果把讀分一部分流量到從節(jié)點(diǎn)還會(huì)讀到舊數(shù)據(jù), 而且還提高選舉延遲發(fā)生的概率.
并且應(yīng)當(dāng)關(guān)掉replica migration, Redis Cluster自身管理松散, 但實(shí)踐中應(yīng)當(dāng)嚴(yán)格規(guī)定好節(jié)點(diǎn)的分布.

chunk分配算法

下面簡述如何分配chunk. 輸入是每臺(tái)機(jī)器的節(jié)點(diǎn)數(shù), 要求擁有最多節(jié)點(diǎn)數(shù)的機(jī)器上的節(jié)點(diǎn)數(shù), 不能超過總節(jié)點(diǎn)數(shù)的一半. 并且每臺(tái)機(jī)器的節(jié)點(diǎn)數(shù)是偶數(shù), 總節(jié)點(diǎn)數(shù)是4的倍數(shù)(一個(gè)chunk4個(gè)節(jié)點(diǎn)). 算法會(huì)把這些節(jié)點(diǎn)按照chunk的定義組成一個(gè)一個(gè)chunk, 并且一定能找到一種分配結(jié)果.
算法每次循環(huán):
(1)找出還沒組成chunk的節(jié)點(diǎn)數(shù)最多的那臺(tái)機(jī)器
(2)然后再找出這臺(tái)機(jī)器跟哪臺(tái)機(jī)器擁有最少的共同chunk數(shù)
(3)從這兩臺(tái)機(jī)器各取兩個(gè)節(jié)點(diǎn), 組成一個(gè)chunk
其中(1)保證了算法能終止. (2)使一臺(tái)機(jī)器掛掉后, 主從切換后, 壓力能夠盡可能平均分到多臺(tái)機(jī)器上.
我們證明了算法能終止, 關(guān)鍵點(diǎn)是每次循環(huán)擁有最多節(jié)點(diǎn)數(shù)的機(jī)器上的節(jié)點(diǎn)數(shù), 不超過總節(jié)點(diǎn)數(shù)的一半能一直成立, 證明這里就不寫了.

下面是各個(gè)運(yùn)維操作要怎么做.

創(chuàng)建集群

用上述的分配算法算好哪臺(tái)機(jī)器部署哪些節(jié)點(diǎn), 然后往上面部署. 我們沒有用官方那個(gè)冗長的流程來創(chuàng)建集群, 而是偽造nodes.conf這個(gè)用來存集群信息的文件, 然后把相應(yīng)的節(jié)點(diǎn)進(jìn)程都拉起來, 最后調(diào)整一下主從角色(因?yàn)槔鸺旱臅r(shí)候可能發(fā)生了主從切換), 這樣一個(gè)集群就好了. 用這種辦法還有個(gè)好處, 我們可以自己構(gòu)造node id, 把用于管理的元信息放在里面.

擴(kuò)容

首先用建集群的方法建一個(gè)沒有槽位的集群, 然后用cluster meet把兩個(gè)集群融合起來, 等待所有新節(jié)點(diǎn)都成功加進(jìn)去了, 再去均分槽位. 如果有節(jié)點(diǎn)硬是加不進(jìn)去(一直處于handshake), 踢掉所有新節(jié)點(diǎn), 重新來過. 因?yàn)榭偸强梢曰貪L干凈, 所以不用擔(dān)心擴(kuò)容失敗會(huì)導(dǎo)致集群不一致.

下面的操作還未實(shí)現(xiàn), 先給出方案.

并行遷移slot

有人在github給Redis提過這個(gè)需求, 希望腳本可以并行遷slot, 作者似乎不想實(shí)現(xiàn)這個(gè)功能. 遷移slot一直都是一個(gè)很慢的操作, redis已經(jīng)改了幾次方案了, 但明明并行遷移就可以大大加快遷移速度, 而且只要運(yùn)維腳本去做就好了, 為什么作者不這么做呢? 我猜測是怕一致性會(huì)有問題. 上面提到, 如果migrating和importing的兩個(gè)節(jié)點(diǎn)都bump epoch, 是有可能導(dǎo)致集群信息不一致的. 但實(shí)際上還是可以做的. 因?yàn)榛旧显谶w移槽位的時(shí)候, 一個(gè)節(jié)點(diǎn)要么是遷入方, 要么是遷出方, 遷出方除非發(fā)生什么特殊情況, 例如epoch collision, 不然是不會(huì)bump epoch的. 防止epoch collision的辦法是操作前先查一遍集群的epoch穩(wěn)定了沒有. 另外, 在cluster setslot node之后, 要查一遍是不是所有節(jié)點(diǎn)都認(rèn)可了自己的所有權(quán), 如果不是, 先cluster bumpepoch, 然后再靠gossip來廣播. 如果檢查一段時(shí)間后發(fā)現(xiàn)還是沒得到所有節(jié)點(diǎn)的認(rèn)可, 重復(fù)上述流程直到所有節(jié)點(diǎn)都認(rèn)同自己對(duì)slot的所有權(quán).

遷移機(jī)器

有時(shí)候機(jī)器掛了或者有問題, 想把集群某臺(tái)機(jī)器的節(jié)點(diǎn)遷移到另一臺(tái)機(jī)器上. 這個(gè)時(shí)候可以把nodes.conf文件拷貝到新機(jī)器上, 改掉nodes.conf中的ip, 把原節(jié)點(diǎn)關(guān)掉, 把新節(jié)點(diǎn)拉起來, 加進(jìn)去集群里面. 這利用了只要節(jié)點(diǎn)的node id一樣, Redis就會(huì)把新節(jié)點(diǎn)替換掉原節(jié)點(diǎn), 并且自動(dòng)更新ip和port.

備份集群

這個(gè)主要是為了繞過集群不一致的問題. 在做遷移slot前, 先copy一份rdb文件在本地, 如果集群出現(xiàn)不一致并且難以修復(fù), 在原來的機(jī)器上重新建立一個(gè)除了節(jié)點(diǎn)port, 其它跟遷移slot前一模一樣的集群, 并且用上之前備份的rdb文件. 最后把不一致的集群刪掉, 用新集群替換老集群.

吐槽

Redis Cluster一個(gè)進(jìn)程一個(gè)節(jié)點(diǎn)會(huì)導(dǎo)致難以管理集群. 從方便管理的角度來看, 一個(gè)集群在一臺(tái)機(jī)器應(yīng)當(dāng)只有一個(gè)集群實(shí)例, 用多線程或多進(jìn)程, 每個(gè)線程/進(jìn)程管理該實(shí)例的一部分槽位. 現(xiàn)在這種單進(jìn)程的做法導(dǎo)致大集群產(chǎn)生很大的ping包流量, 有一個(gè)幾百個(gè)節(jié)點(diǎn)的集群光放在那里沒有任何請求都有300MB的流量.
Redis Cluster的集群協(xié)議理論上只保證了正常流程中集群信息能一致. 只要有一套完善的運(yùn)維系統(tǒng), 它仍然是一個(gè)不完美但可用的方案.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 基本目標(biāo)與設(shè)計(jì)基本思想 Redis cluster 目標(biāo) 高性能,并且能線性擴(kuò)展到1000個(gè)節(jié)點(diǎn)。不需要代理,使用...
    tafeng閱讀 2,805評(píng)論 0 0
  • 轉(zhuǎn)發(fā):Redis Cluster探索與思考 Redis Cluster的基本原理和架構(gòu) Redis Cluster...
    meng_philip123閱讀 3,617評(píng)論 0 14
  • 本文檔翻譯自 http://redis.io/topics/cluster-tutorial 。 本文檔是 Red...
    會(huì)跳舞的機(jī)器人閱讀 66,985評(píng)論 2 21
  • 本文將從設(shè)計(jì)思路,功能實(shí)現(xiàn),源碼幾個(gè)方面介紹Redis Cluster。假設(shè)讀者已經(jīng)了解Redis Cluster...
    CatKang閱讀 1,485評(píng)論 0 2
  • Redis Cluster介紹 redis cluster是Redis的分布式解決方案,在3.0版本推出后有效地解...
    dayspring閱讀 11,044評(píng)論 0 3