一文講清RedisCluster
1 集群的意義
從單機的一主多從復制架構到現在的分布式架構
主要有如下維度:
- 業務
- 追求更高QPS
- 數據量
- Scale Up已經無法滿足,超過了單機極限,考慮Scale Out分布式
- 網絡流量
- 業務流量超過服務器網卡上限,考慮分布式分流
- 離線計算
- 需要中間環節緩沖等需求
2 meet
節點之間完成相互通信的基礎,有一定的頻率和規則。
CLUSTER MEET命令被用來連接不同的開啟集群支持的 Redis 節點,以進入工作集群。
2.1 基本思想
每個節點默認都是相互不信任的,并且被認為是未知的節點,以便萬一因為系統管理錯誤或地址被修改,而不太可能將多個不同的集群節點混成一個集群。
因此,為了使給定節點能將另一個節點接收到組成 Redis Cluster 的節點列表中,這里只有兩種方法:
- 系統管理員發送一個CLUSTER MEET命令強制一個節點會見另一個節點
- 一個已知節點發送一個保存在 gossip 部分的節點列表,包含著未知節點。如果接收的節點已經將發送節點信任為已知節點,它會處理 gossip 部分并且發送一個握手消息給未知的節點。
Redis Cluster 需要形成一個完整的網絡(每個節點都連接著其他每個節點),但為創建一個集群,不需要發送形成網絡所需的所有CLUSTER MEET命令。發送CLUSTER MEET消息以便每個節點能夠到達其他每個節點只需通過一條已知的節點鏈就夠了。由于在心跳包中會交換 gossip 信息,將會創建節點間缺失的鏈接。
所以,如果我們通過CLUSTER MEET鏈接節點 A 和 B ,并且 B 和 C 有鏈接,那么節點 A 和 C 會發現他們握手和創建鏈接的方法。
2.2 案例
假設某一集群有A、B、C、D四個節點,可以只發送以下一組命令給 A :
- CLUSTER MEET B-ip B-port
- CLUSTER MEET C-ip C-port
- CLUSTER MEET D-ip D-port
由于 A 知道及被其他所有節點知道,它將會在發送的心跳包中包含gossip部分,這將允許其他每個節點彼此都創建一個鏈接,即使集群很大,也能在數秒內形成一個完整網絡。
CLUSTER MEET無需相互執行,即若發送命令給 A 以加入B ,那就不必也發送給 B 以加入 A。
2.3 實現細節:MEET 和 PING 包
當某一給定節點接收到一個MEET消息時,命令中指定的節點仍不知發送了該命令,所以為使節點強制將接收命令的節點將它作為信任的節點接受它,它會發送MEET包而非PING包。兩個消息包有相同的格式,但是MEET強制使接收消息包的節點確認發送消息包的節點為可信任的。
3 指派槽
- 把16384個槽平分給節點管理,每個節點只對自己負責的槽進行讀寫
- 每個節點間都相互通信,所以每個節點都知道其它節點所管理槽的范圍
客戶端與指派槽
4 集群伸縮
集群的伸縮包括新節點的加入和舊節點退出。
4.1 加入新節點
Redis集群加入新節點主要如下幾步:
- 準備新節點
啟動一個集群模式下的Redis節點
- 加入集群
通過與任意一集群中的節點握手加入新節點
- 遷移slot到新節點
再向新節點分配它負責的slot并向其遷移slot對應數據。
由于Redis采用Gossip協議,所以可讓新節點與任一現有集群節點握手,一段時間后整個集群都會知道加入了新節點。
4.1.1 案例
向如下集群中新加入一個節點6385。由于負載均衡的要求,加入后四個節點每個節點負責4096個slots,但集群中原來的每個節點都負責5462個slots,所以6379、6380、6381節點都需要向新的節點6385遷移1366個slots。
Redis集群并沒有一個自動實現負載均衡的工具,把多少slots從哪個節點遷移到哪個節點完全是由用戶指定。
遷移數據的流程圖
遷移key可以用pipeline進行批量的遷移。
對于擴容,原理已經很清晰了,至于具體操作,網上很多。
至于縮容,也是先手動完成數據遷移,再關閉redis。收縮時如果下線的節點有負責的槽需要遷移到其他節點,再通過cluster forget命令讓集群內所有節點忘記被下線節點
5 客戶端路由
5.1 moved重定向
每個節點通信共享Redis Cluster中槽和集群中對應節點的關系。
- 客戶端向Redis Cluster的任一節點發送命令
- 接收命令的節點再計算自己的槽和對應節點
1.如果保存數據的槽被分配給當前節點,則去槽中執行命令,并把命令執行結果返回給客戶端
2.如果保存數據的槽不在當前節點的管理范圍內,則向客戶端返回moved重定向異常
3.客戶端接收到節點返回的結果,如果是moved異常,則從moved異常中獲取目標節點的信息
4.客戶端向目標節點發送命令,獲取命令執行結果
客戶端不會自動找到目標節點執行命令,需要二次執行
5.2 ask重定向
由于集群伸縮時,需要數據遷移。
當客戶端訪問某key,節點告訴客戶端key在源節點,再去源節點訪問時,卻發現key已遷移到目標節點,就會返回ask。
- 客戶端向目標節點發送命令,目標節點中的槽已經遷移到其它節點
- 目標節點會返回ask轉向給客戶端
- 客戶端向新節點發送Asking命令
- 再向新節點發送命令
- 新節點執行命令,把命令執行結果返回給客戶端
為什么不能簡單使用MOVED重定向?
因為雖然MOVED意味著我們認為哈希槽由另一個節點永久提供,并且應該對指定節點嘗試下一個查詢,所以ASK意味著僅將下一個查詢發送到指定節點。
之所以需要這樣做,是因為下一個關于哈希槽的查詢可能是關于仍在A中的鍵的,因此我們始終希望客戶端嘗試A,然后在需要時嘗試B。由于只有16384個可用的哈希槽中有一個發生,因此群集上的性能下降是可以接受的。
5.3 moved V.S ask
都是客戶端重定向
- moved:槽已經確定轉移
- ask:槽還在遷移中
5.4 smart智能客戶端
目標
追求性能
設計思路
- 從集群中選一個可運行節點,使用Cluster slots初始化槽和節點映射
- 將Cluster slots的結果映射在本地,為每個節點創建JedisPool,然后就可以進行數據讀寫操作
注意事項
- 每個JedisPool中緩存了slot和節點node的關系
- key和slot的關系:對key進行CRC16規則進行hash后與16383取余得到的結果就是槽
- JedisCluster啟動時,已經知道key,slot和node之間的關系,可以找到目標節點
- JedisCluster對目標節點發送命令,目標節點直接響應給JedisCluster
- 如果JedisCluster與目標節點連接出錯,則JedisCluster會知道連接的節點是一個錯誤的節點
- 此時JedisCluster會隨機節點發送命令,隨機節點返回moved異常給JedisCluster
- JedisCluster會重新初始化slot與node節點的緩存關系,然后向新的目標節點發送命令,目標命令執行命令并向JedisCluster響應
- 如果命令發送次數超過5次,則拋出異常"Too many cluster redirection!"
- 基本圖示
全面圖示
6 批量操作
mget、mset須在同一槽。
Redis Cluster不同于Redis 單節點,甚至和一個 Sentinel 監控的主從模式也不一樣。主要是因為集群自動分片,將一個key 映射到16384槽之一,這些槽分布在多節。因此操作多 key 的命令必須保證所有的key都映射同一槽,避免跨槽執行錯誤。
一個單獨的集群節點,只服務一組專用的keys,請求一個命令到一個Server,只能得到該Server上擁有keys的對應結果。
一個非常簡單的例子是執行KEYS命令,當發布該命令到集群中某節點時,只能得到該節點上擁有key,并非集群中所有key。要得到集群中所有key,必須從集群的所有主節點上獲取所有key。
對于分散在redis集群中不同節點的數據,我們如何比較高效地批量獲取數據呢?
6.1 串行mget
定義for循環,遍歷所有key,分別去所有的Redis節點中獲取值并進行匯總,簡單,但效率不高,需n次網絡時間。
6.2 串行I/O
優化串行的mget,在客戶端本地做內聚,對每個key hash,然后取余,知道key對應槽
本地已緩存了槽與節點的對應關系,然后對key按節點進行分組,成立子集,然后使用pipeline把命令發送到對應的node,需要nodes次網絡時間,大大減少了網絡時間開銷。
6.3 并行I/O
優化串行IO,分組key后,根據節點數量啟動對應的線程數,根據多線程模式并行向node節點請求數據,只需1次網絡時間
6.4 hash_tag
不做任何改變,hash后就比較均勻地散在每個節點上
是否能像單機,一次IO將所有key取出呢?hash-tag提供了這樣功能:若將上述key改為如下,即大括號括起來相同的內容,保證所有的key只向一個node請求數據,這樣執行類似mget命令只需要去一個節點獲取數據即可,效率更高。
6.5 選型對比
第一種方式,使用多線程解決批量問題,減少帶寬時延,提高效率,這種做法就如上面所說簡單便捷(我們目前批量操作類型比較多),有效。但問題比較明顯。批量操作數量不大即可滿足。搜狐的cachecloud采用第二點,先將key獲取槽點,然后分node pipeline操作。這種做法相對比第一種做法較優。
7 故障轉移
7.1 故障發現
Redis Cluster通過ping/pong消息實現故障發現:不需要sentinel。ping/pong不僅能傳遞節點與槽的對應消息,也能傳遞其他狀態,比如:節點主從狀態,節點故障等
故障發現就是通過這種模式來實現,分為主觀下線和客觀下線:
7.1.1 主觀下線
定義
某節點認為另一節點不可用,這僅代表一個節點對另一節點的判斷,不代表所有節點的認知。
流程
- 節點-1定時發ping消息給節點-2
- 若發送成功,代表節點-2正常運行,節點-2會響應PONG消息給節點1,節點1更新與2的最后通信時間
- 若發送失敗,則節點-1與節點-2間通信異常判斷連接,在下一定時任務周期時,仍然會與節點2發送ping消息
- 若節點-1發現與節點-2最后通信時間超過node-timeout,則把節點2標識為pfail狀態
7.1.2 客觀下線
定義
當半數以上持有槽的主節點都標記某節點主觀下線。可以保證判斷的公平性。
集群模式下,只有主節點(master)才有讀寫權限和集群槽的維護權限,從節點(slave)只有復制的權限。
流程
1.某個節點接收到其他節點發送的ping消息,如果接收到的ping消息中包含了其他pfail節點,這個節點會將主觀下線的消息內容添加到自身的故障列表中,故障列表中包含了當前節點接收到的每一個節點對其他節點的狀態信息
2.當前節點把主觀下線的消息內容添加到自身的故障列表之后,會嘗試對故障節點進行客觀下線操作
7.2 故障恢復
從節點接收到它的主節點客觀下線的通知,則進行故障恢復。
資格檢查
- 對從節點的資格進行檢查,只有難過檢查的從節點才可以開始進行故障恢復
- 每個從節點檢查與故障主節點的斷線時間
- 超過cluster-node-timeout * cluster-slave-validity-factor數字,則取消資格
- cluster-node-timeout默認為15秒,cluster-slave-validity-factor默認值為10
- 如果這兩個參數都使用默認值,則每個節點都檢查與故障主節點的斷線時間,如果超過150秒,則這個節點就沒有成為替換主節點的可能性
準備選舉時間
使偏移量最大的從節點具備優先級成為主節點的條件。
選舉投票
對選舉出來的多個從節點進行投票,選出新的主節點。
替換主節點
- 當前從節點取消復制變成離節點。(slaveof no one)
- 執行cluster del slot撤銷故障主節點負責的槽,并執行cluster add slot把這些槽分配給自己
- 向集群廣播自己的pong消息,表明已經替換了故障從節點
8 開發運維常見問題
8.1 集群完整性
- cluster-require-full-coverage默認為yes,即集群中所有節點都在服務且16384個槽都可用,集群才會提供服務,以保證集群完整性。
- 當某節點故障或者正在故障轉移時獲取數據會提示:(error)CLUSTERDOWN The cluster is down
但是大多數業務都無法容忍,建議把cluster-require-full-coverage設為no
8.2 帶寬消耗
- Redis Cluster節點之間會定期交換Gossip消息,以及做一些心跳檢測
- 官方建議Redis Cluster節點數量不要超過1000個,當集群中節點數量過多時,會產生不容忽視的帶寬消耗
- 消息發送頻率:節點發現與其他節點最后通信時間超過cluster-node-timeout /2時,會直接發送PING消息
- 消息數據量:slots槽數組(2kb空間)和整個集群1/10的狀態數據(10個節點狀態數據約為1kb)
- 節點部署的機器規模:集群分布的機器越多且每臺機器劃分的節點數越均勻,則集群內整體的可用帶寬越高
帶寬優化
- 避免使用’大’集群:避免多業務使用一個集群,大業務可以多集群
- cluster-node-timeout:帶寬和故障轉移速度的均衡
- 盡量均勻分配到多機器上:保證高可用和帶寬
8.3 Pub/Sub廣播
- 在任意一個cluster節點執行publish,則發布的消息會在集群中傳播,集群中的其他節點都會訂閱到消息,這樣節點的帶寬的開銷會很大
- publish在集群每個節點廣播,加重帶寬
解決方案
單獨“走”一套redis sentinel。就是針對目標的幾個節點構建redis sentinel,在這個里面實現廣播。
8.4 集群傾斜
分布式數據庫存在傾斜問題是比較常見的。集群傾斜也就是各個節點使用的內存不一致
數據傾斜原因
1.節點和槽分配不均,如果使用redis-trib.rb工具構建集群,則出現這種情況的機會不多
- redis-trib.rb info ip:port查看節點,槽,鍵值分布
- redis-trib.rb rebalance ip:port進行均衡(謹慎使用)
2.不同槽對應鍵值數量差異比較大
- CRC16算法正常情況下比較均勻
- 可能存在hash_tag
- cluster countkeysinslot {slot}獲取槽對應鍵值個數
3.包含bigkey:例如大字符串,幾百萬的元素的hash,set等
- 在從節點:redis-cli --bigkeys
- 優化:優化數據結構
4.內存相關配置不一致
- hash-max-ziplist-value:滿足一定條件情況下,hash可以使用ziplist
- set-max-intset-entries:滿足一定條件情況下,set可以使用intset
- 在一個集群內有若干個節點,當其中一些節點配置上面兩項優化,另外一部分節點沒有配置上面兩項優化
- 當集群中保存hash或者set時,就會造成節點數據不均勻
- 優化:定期檢查配置一致性
5.請求傾斜:熱點key
重要的key或者bigkey
Redis Cluster某個節點有一個非常重要的key,就會存在熱點問題
優化
- 避免bigkey
- 熱鍵不要用hash_tag
- 當一致性不高時,可以用本地緩存+ MQ(消息隊列)
9 讀寫分離
- 只讀連接
- 集群模式下,從節點不接受任何讀寫請求。
- 當向從節點執行讀請求時,重定向到負責槽的主節點
- readonly命令可以讀:連接級別命令,當連接斷開之后,需要再次執行
- readonlyredis cluster 默認slave 也是不能讀的,如果要讀取,需要執行 readonly,就可以了。
讀寫分離:更加復雜(成本很高,盡量不要使用)
- 同樣的問題:復制延遲,讀取過期數據,從節點故障
- 修改客戶端:cluster slaves {nodeId}
10 集群優劣
集群的限制
- key批量操作支持有限:例如mget,mset必須在一個slot
- key事務和Lua支持有限:操作的key必須在一個節點
- key是數據分區的最小粒度:不支持bigkey分區
- 不支持多個數據庫:集群模式下只有一個db0
- 復制只支持一層:不支持樹形復制結構
集群不一定好
- Redis Cluster滿足容量和性能的擴展性,很多業務’不需要’
- 大多數時客戶端性能會’降低’
- 命令無法跨節點使用:mget,keys,scan,flush,sinter等
- Lua和事務無法跨節點使用
- 客戶端維護更復雜:SDK和應用本身消耗(例如更多的連接池)
- 很多場景Redis Sentinel已經夠用了
參考
https://www.slideshare.net/iammutex/redis-cluster
https://zhuanlan.zhihu.com/p/105569485
https://sunweiguo.github.io/2019/02/01/redis/Redis-Cluster%E7%90%86%E8%AE%BA%E8%AF%A6%E8%A7%A3/
https://redis.io/topics/cluster-spec
http://trumandu.github.io/2016/05/09/RedisCluster%E6%9E%84%E5%BB%BA%E6%89%B9%E9%87%8F%E6%93%8D%E4%BD%9C%E6%8E%A2%E8%AE%A8/