Redis Cluster 原理與管理
從事Redis Cluster相關工具開發半年多, 記錄一下對它的理解和集群管理的想法吧. 這裡不複述Redis Cluster基礎的東西, 需先看官方文檔.
Redis Cluster 要求客戶端使用新的協議, 我們公司為此開發了 corvus 這個proxy來讓客戶端可以繼續使用單機Redis協議. 不像twemproxy和codis, corvus本身無狀態, 不需要依賴zookeeper, 而只是從redis集群中拉取集群信息來做路由表.Redis Cluster 使用gossip協議維護集群信息. 它的gossip協議並不能嚴格保證集群信息一致, 在誤用或極端情況下, 集群信息並不能自動恢復一致, 而且不容易修復. 使用Redis Cluster需要理解透其中原理, 不隨意亂做變更操作, 並且要有一套成熟的運維繫統.我們的業務對緩存可用性要求較高, 使用Redis Cluster的方針是首要保證能夠快速創建一個可用的集群, 其次要嚴格限制可對集群做的變更操作, 還有儘可能用小集群.集群信息一致性問題
這裡不是指數據的一致性, 而是集群信息的一致性. 最重要的兩個集群信息是主從角色和slot的歸屬. 個人感覺集群信息管理鬆散混亂, 但是在一般情況下能維持一致性. 如果真出現了不一致的問題, 建議不要浪費時間, 直接重建集群吧. 有些坑不是一時半會能解決的.
為什麼我不提節點列表的一致性問題? 固然集群裡面有哪些節點這個信息可以說是所有其它信息的基礎, 但是從實用的角度來說, 這可以由運維繫統來保證不出問題, 下面另述.
主從和slot的一致性是由epoch來管理的. epoch就像Raft中的term, 但僅僅是像. 每個節點有一個自己獨特的epoch和整個集群的epoch, 為簡化下面都稱為node epoch和cluster epoch. node epoch一直遞增, 其表示某節點最後一次變成主節點或獲取新slot所有權的邏輯時間. cluster epoch則是整個集群中最大的那個node epoch. 我們稱遞增node epoch為bump epoch, 它會用當前的cluster epoch加一來更新自己的node epoch.在使用gossip協議中, 如果多個節點聲稱不同的集群信息, 那對於某個節點來說究竟要相信誰呢? Redis Cluster規定了每個主節點的epoch都不可以相同. 而一個節點只會去相信擁有更大node epoch的節點聲稱的信息, 因為更大的epoch代表更新的集群信息.原則上:(1)如果epoch不變, 集群就不應該有變更(包括選舉和遷移槽位)(2)每個節點的node epoch都是獨一無二的(3)擁有越高epoch的節點, 集群信息越新Epoch Collision
實際上, 在遷移slot或者使用cluster failover的時候, 如果多個節點同時bump epoch, 就有可能出現多個節點擁有同一個epoch, 違反上述原則(2)和(3). 這個時候擁有較小node id的節點就會自動再一次bump epoch, 以保證原則(3). 而原則(2)實際上因此也並不嚴格成立, 因為解決epoch collision需要一小段時間.
選舉
從節點選舉的時候其實沒什麼問題, 就是一個從節點搶選票的過程. 我們稱管理相同slot集合的所有主從節點為一個分片. 選舉的時候, 掛掉分片的所有從節點會向其它分片的所有主節點索取選票, 如果取到的選票超過分片數的半數, 該從節點就選舉成功.
slot
最大的問題在於slot. 我們遇到過數次遷移slot失敗後出現slot不一致的情況. 如果還沒搞懂它怎麼管slot, 請記住下面這句話:
不要用亂用cluster setslot node.
我相信大多數不一致問題都是我們作死用這個命令造成的. 除了它我暫時還沒找到有什麼大概率的情況會導致不一致.slot 管理
首先我們搞清楚slot究竟是怎麼管的. 每個節點都有一份16384長的表對應每個slot究竟歸哪個節點, 並且會保存當前節點所認為的其它節點的node epoch. 這樣每個slot實際上綁定了一個節點及其node epoch. 然後由自認為擁有某slot的節點來負責通知其它節點這個slot的歸屬. 其它節點收到這個消息後, 會對比該slot原先綁定節點的node epoch, 如果收到的是更大的node epoch則更新, 否則不予理睬. 除此之外, 除了使用slot相關命令做變更, 集群沒有其它途徑修改slot的歸屬.
slot x 是我管的, 我的node epoch是 ynode A ------------------------------> node B (原來slot x歸node C管, 如果 y 比 node C 的node epoch大, 我就更新slot x的歸屬)
這實際上依賴上述的原則(3), 並且相信slot的舊主人還沒有更新epoch.
遷移slot的一致性
下面來看遷移slot如何保證slot歸屬的一致性.
從node A遷移一個槽位到node B的流程是:(1) node A 設置migrating flag, node B 設置importing flag(2) 遷移所有該slot的數據到node B(3) 對兩個節點使用cluster setslot node來消除importing和migrating flag, 並且設置槽位重點在於遷移最後一步消除importing flag使用的cluster setslot node, 如果對一個節點使用cluster setslot node的時候節點有importing flag, 節點會bump epoch, 這樣這個節點聲稱slot所有權時別的節點就會認可.但是這裡並沒有跑一遍選舉中的投票流程. 如果另外一個節點也同時bump epoch, 就出現epoch collision. 這裡是一個不完美但又略精妙的地方. 不管這個清importing flag的節點在解決collision後是否獲得更高的epoch, 其epoch肯定大於migrating那個節點之前的epoch.
但這裡還是有漏洞, 萬一node B在廣播自己的新node epoch前, node A做了什麼變更而獲取了一個更大的node epoch呢? 萬一發生collision的是node A和node B兩個節點呢? 這個時候假如node A的node id更小, node A會拿到更大的新epoch. 只要某個節點先收到node A的消息, 這個slot的遷移信息就永遠寫不進這個節點了, 因為node A的node epoch比node B更大.上面提到的cluster setslot node的問題在於, 如果節點沒有importing flag, 它會直接設置槽位, 但不會增加自己的node epoch. 這樣當他告訴別的節點對這個槽位的所有權時, 其他節點並不認可. 這實際上違反了上述原則(1). 詳細見這裡. 所以實在要在遷移slot以外的地方用這個命令, 必須要給它發一次cluster bumpepoch.運維繫統
運維成百上千大大小小的集群不是寫腳本能勝任的事情. 官方那個Ruby腳本絕對不能作為最終方案. 現在我們的方案是以一個可靠的運維繫統為基礎把Redis Cluster池化.
檢查, 容錯, 重試, 回滾
實際運維的時候會有各種極端情況. 做任何變更操作, 都要先確保集群是一致並且穩定的. 穩定是指已經沒有還沒同步的信息, 例如多個主節點有相同的epoch而未處理. 如果集群本身不穩定, 有可能觸發上述遷移slot的時候發生epoch collision. 而且對於每一步操作, 一定要檢查前提條件是否成立, 例如遷slot最後用cluster setslot node時需先檢查有沒有importing flag. 還要確保操作是否完成. Redis回一個OK並不能表示操作沒有問題, 因為大部分redis變更命令都是非同步的. 例如踢節點的時候, 假如過了60秒還有節點認為被踢的節點還在, 就會因為gossip的傳播把那個節點重新加進集群.
還要有容錯. 例如在對集群操作的時候Redis給你返回Loading Error, 這個時候Redis是處於不能處理大部分命令的狀態, 連cluster nodes都不能. 這個時候運維繫統要等待並不斷檢查節點可以接受命令沒有.基本上每個變更操作都是大操作, 操作跑到一半可能只是部分掛了, 這時要重試, 實在不行要儘可能回滾.用chunk管理節點
為了簡化管理, 我們規定了集群的規格. 具體做法是每個主節點有且只有一個從節點. 並且以4個節點為最小的管理單位, 我們稱為chunk. 一個chunk有兩主兩從, 分布在兩台機器上面, 每台機器兩個節點, 且4個節點內互相組成主從關係, 要求負責一個分片的主從分布在不同的機器上面.
一個chunk:machine A machine B master 1 / master 2 slave 2 / slave 1
所有的集群都由 n 個chunk組成而成.
首先為了方便管理部署了不同集群的機器, 要把節點分組管理才容易. 其次, 這麼做保證了主從不可能在同一台機器上面. 然後在擴容跟縮容的時候, 只要增加或剔除chunk就好了, 可以儘可能平均每台機器的節點數, 但又不會破壞主從關係. 並且要求一個集群使用的機器數量最少為3台, 這樣一台掛了也不會導致有slot沒人管. 我們曾想過用6個節點為一個chunk, 但是在分配chunk的時候找不出一種好的分配演算法, 而4個卻找到了分配演算法.我們只使用1主對應1從, 是因為我們還未發現多個從節點有什麼好處, 而且從節點不能頂請求壓力還因為主從同步消耗不少資源. 如果把讀分一部分流量到從節點還會讀到舊數據, 而且還提高選舉延遲發生的概率.
並且應當關掉replica migration, Redis Cluster自身管理鬆散, 但實踐中應當嚴格規定好節點的分布.chunk分配演算法
下面簡述如何分配chunk. 輸入是每台機器的節點數, 要求擁有最多節點數的機器上的節點數, 不能超過總節點數的一半. 並且每台機器的節點數是偶數, 總節點數是4的倍數(一個chunk4個節點). 演算法會把這些節點按照chunk的定義組成一個一個chunk, 並且一定能找到一種分配結果.
演算法每次循環:(1)找出還沒組成chunk的節點數最多的那台機器(2)然後再找出這台機器跟哪台機器擁有最少的共同chunk數(3)從這兩台機器各取兩個節點, 組成一個chunk其中(1)保證了演算法能終止. (2)使一台機器掛掉後, 主從切換後, 壓力能夠儘可能平均分到多台機器上.我們證明了演算法能終止, 關鍵點是每次循環擁有最多節點數的機器上的節點數, 不超過總節點數的一半能一直成立, 證明這裡就不寫了.下面是各個運維操作要怎麼做.
創建集群
用上述的分配演算法算好哪台機器部署哪些節點, 然後往上面部署. 我們沒有用官方那個冗長的流程來創建集群, 而是偽造nodes.conf這個用來存集群信息的文件, 然後把相應的節點進程都拉起來, 最後調整一下主從角色(因為拉起集群的時候可能發生了主從切換), 這樣一個集群就好了. 用這種辦法還有個好處, 我們可以自己構造node id, 把用於管理的元信息放在裡面.
擴容
首先用建集群的方法建一個沒有槽位的集群, 然後用cluster meet把兩個集群融合起來, 等待所有新節點都成功加進去了, 再去均分槽位. 如果有節點硬是加不進去(一直處於handshake), 踢掉所有新節點, 重新來過. 因為總是可以回滾乾淨, 所以不用擔心擴容失敗會導致集群不一致.
下面的操作還未實現, 先給出方案.
並行遷移slot
有人在github給Redis提過這個需求, 希望腳本可以並行遷slot, 作者似乎不想實現這個功能. 遷移slot一直都是一個很慢的操作, redis已經改了幾次方案了, 但明明並行遷移就可以大大加快遷移速度, 而且只要運維腳本去做就好了, 為什麼作者不這麼做呢? 我猜測是怕一致性會有問題. 上面提到, 如果migrating和importing的兩個節點都bump epoch, 是有可能導致集群信息不一致的. 但實際上還是可以做的. 因為基本上在遷移槽位的時候, 一個節點要麼是遷入方, 要麼是遷出方, 遷出方除非發生什麼特殊情況, 例如epoch collision, 不然是不會bump epoch的. 防止epoch collision的辦法是操作前先查一遍集群的epoch穩定了沒有. 另外, 在cluster setslot node之後, 要查一遍是不是所有節點都認可了自己的所有權, 如果不是, 先cluster bumpepoch, 然後再靠gossip來廣播. 如果檢查一段時間後發現還是沒得到所有節點的認可, 重複上述流程直到所有節點都認同自己對slot的所有權.
遷移機器
有時候機器掛了或者有問題, 想把集群某台機器的節點遷移到另一台機器上. 這個時候可以把nodes.conf文件拷貝到新機器上, 改掉nodes.conf中的ip, 把原節點關掉, 把新節點拉起來, 加進去集群裡面. 這利用了只要節點的node id一樣, Redis就會把新節點替換掉原節點, 並且自動更新ip和port.
備份集群
這個主要是為了繞過集群不一致的問題. 在做遷移slot前, 先copy一份rdb文件在本地, 如果集群出現不一致並且難以修復, 在原來的機器上重新建立一個除了節點port, 其它跟遷移slot前一模一樣的集群, 並且用上之前備份的rdb文件. 最後把不一致的集群刪掉, 用新集群替換老集群.
吐槽
Redis Cluster一個進程一個節點會導致難以管理集群. 從方便管理的角度來看, 一個集群在一台機器應當只有一個集群實例, 用多線程或多進程, 每個線程/進程管理該實例的一部分槽位. 現在這種單進程的做法導致大集群產生很大的ping包流量, 有一個幾百個節點的集群光放在那裡沒有任何請求都有300MB的流量.
Redis Cluster的集群協議理論上只保證了正常流程中集群信息能一致. 只要有一套完善的運維繫統, 它仍然是一個不完美但可用的方案.推薦閱讀:
※Redis服務支持5000萬的QPS,有什麼好的思路?
※redis需要讀寫分離嗎?
※Linux安裝redis,並設置訪問許可權,及使用可視化工具
※scrapy-redis 和 scrapy 有什麼區別?
