1.Redis Cluster
1.1 Redis 集羣方案的演變
大規模數據存儲系統都會面臨的一個問題就是如何橫向拓展。當你的數據集越來越大,一主多從的模式已經無法支撐這麼大量的數據存儲,於是你首先考慮將多個主從模式結合在一起對外提供服務,但是這裏有兩個問題就是如何實現數據分片的邏輯和在哪裏實現這部分邏輯?業界常見的解決方案有兩種,一是引入 Proxy 層來嚮應用端屏蔽身後的集羣分佈,客戶端可以藉助 Proxy 層來進行請求轉發和 Key 值的散列從而進行進行數據分片,這種方案會損失部分性能但是遷移升級等運維操作都很方便,業界 Proxy 方案的代表有 Twitter 的 Twemproxy 和豌豆莢的 Codis;二是 smart client 方案,即將 Proxy 的邏輯放在客戶端做,客戶端根據維護的映射規則和路由表直接訪問特定的 Redis 實例,但是增減 Redis 實例都需要重新調整分片邏輯。
1.2 Redis Cluster 簡介
Redis 集羣是一個分佈式(distributed)、容錯(fault-tolerant)的 Redis 實現, 集羣可以使用的功能是普通單機 Redis 所能使用的功能的一個子集(subset)。Redis 3.0 版本開始官方正式支持集羣模式,Redis 集羣模式提供了一種能將數據在多個節點上進行分區存儲的方法,採取了和上述兩者不同的實現方案——去中心化的集羣模式,集羣通過分片進行數據共享,分片內採用一主多從的形式進行副本複製,並提供複製和故障恢復功能。
1.3 去中心化的集羣模式特點
- 所有的redis節點彼此互聯(PING-PONG機制),內部使用二進制協議優化傳輸速度和帶寬。
- 節點的fail是通過集羣中超過半數的節點檢測失效時才生效。
- 客戶端與redis節點直連,不需要中間proxy層.客戶端不需要連接集羣所有節點,連接集羣中任何一個可用節點即可。
- redis-cluster把所有的物理節點映射到[0-16383]slot上(不一定是平均分配),cluster 負責維護node<->slot<->value。
- Redis集羣預分好16384個桶,當需要在 Redis 集羣中放置一個 key-value 時,根據 CRC16(key) mod 16384的值,決定將一個key放到哪個桶中。
下圖是一個三主三從的 Redis Cluster,三機房部署(其中一主一從構成一個分片,之間通過異步複製同步數據,一旦某個機房掉線,則分片上位於另一個機房的 slave 會被提升爲 master 從而可以繼續提供服務) ;每個 master 負責一部分 slot,數目儘量均攤;客戶端對於某個 Key 操作先通過公式計算(計算方法見下文)出所映射到的 slot,然後直連某個分片,寫請求一律走 master,讀請求根據路由規則選擇連接的分片節點。
1.4 三種集羣方案的優缺點
集羣模式 |
優點 |
缺點 |
---|---|---|
客戶端分片 |
不使用第三方中間件,實現方法和代碼可以自己掌控並且可隨時調整。這種分片性能比代理式更好(因爲少了分發環節),分發壓力在客戶端,無服務端壓力增加 |
不能平滑地水平擴容,擴容/縮容時,必須手動調整分片程序,出現故障不能自動轉移,難以運維 |
代理層分片 |
運維成本低。業務方不用關心後端 Redis 實例,跟操作單點 Redis 實例一樣。Proxy 的邏輯和存儲的邏輯是隔離的 |
代理層多了一次轉發,性能有所損耗;進行擴容/縮容時候,部分數據可能會失效,需要手動進行遷移,對運維要求較高,而且難以做到平滑的擴縮容;出現故障,不能自動轉移,運維性很差。Codis 做了諸多改進,相比於 Twemproxy 可用性和性能都好得多 |
Redis Cluster |
無中心節點,數據按照 slot 存儲分佈在多個 Redis 實例上,平滑的進行擴容/縮容節點,自動故障轉移(節點之間通過 Gossip 協議交換狀態信息,進行投票機制完成 slave 到 master 角色的提升)降低運維成本,提高了系統的可擴展性和高可用性 |
開源版本缺乏監控管理,原生客戶端太過簡陋,failover 節點的檢測過慢,維護 Membership 的 Gossip 消息協議開銷大,無法根據統計區分冷熱數據 |
2. 哈希槽
2.1 什麼是哈希槽
Redis Cluster 中,數據分片藉助哈希槽 (下文均稱 slot) 來實現,集羣預先劃分 16384 個 slot,對於每個請求集羣的鍵值對,根據 Key 進行散列生成的值唯一匹配一個 slot。Redis Cluster 中每個分片的 master 負責 16384 個 slot 中的一部分,當且僅當每個 slot 都有對應負責的節點時,集羣才進入可用狀態。當動態添加或減少節點時,需要將 16384 個 slot 做個再分配,slot 中的鍵值也要遷移。
2.2 哈希槽的好處
使用哈希槽的好處就在於可以方便的添加或移除節點。
當需要增加節點時,只需要把其他節點的某些哈希槽挪到新節點就可以了;
當需要移除節點時,只需要把移除節點上的哈希槽挪到其他節點就行了;
在這一點上,我們以後新增或移除節點的時候不用先停掉所有的 redis 服務。
3. 故障檢測
跟大多數分佈式系統一樣,Redis Cluster 的節點間通過持續的 heart beat 來保持信息同步,不過 Redis Cluster 節點信息同步是內部實現的,並不依賴第三方組件如 Zookeeper。集羣中的節點持續交換 PING、PONG 數據,消息協議使用 Gossip,這兩種數據包的數據結構一樣,之間通過 type 字段進行區分。
Redis 集羣中的每個節點都會定期向集羣中的其他節點發送 PING 消息,以此來檢測對方是否存活,如果接收 PING 消息的節點在規定時間內(node_timeout)沒有回覆 PONG 消息,那麼之前向其發送 PING 消息的節點就會將其標記爲疑似下線狀態(PFAIL)。每次當節點對其他節點發送 PING 命令的時候,它都會隨機地廣播三個它所知道的節點的信息,這些信息裏面的其中一項就是說明節點是否已經被標記爲 PFAIL 或者 FAIL。當節點接收到其他節點發來的信息時,它會記下那些被集羣中其他節點標記爲 PFAIL 的節點,這稱爲失效報告(failure report)。如果節點已經將某個節點標記爲 PFAIL ,並且根據自身記錄的失效報告顯示,集羣中的大部分 master 也認爲該節點進入了 PFAIL 狀態,那麼它會進一步將那個失效的 master 的狀態標記爲 FAIL 。隨後它會向集羣廣播 “該節點進一步被標記爲 FAIL ” 的這條消息,所有收到這條消息的節點都會更新自身保存的關於該 master 節點的狀態信息爲 FAIL。
4. 故障轉移(Failover)
4.1 紀元(epoch)
Redis Cluster 使用了類似於 Raft 算法 term(任期)的概念稱爲 epoch(紀元),用來給事件增加版本號。Redis 集羣中的紀元主要是兩種:currentEpoch 和 configEpoch。
4.1.1 currentEpoch
這是一個集羣狀態相關的概念,可以當做記錄集羣狀態變更的遞增版本號。每個集羣節點,都會通過 server.cluster->currentEpoch 記錄當前的 currentEpoch。
集羣節點創建時,不管是 master 還是 slave,都置 currentEpoch 爲 0。當前節點接收到來自其他節點的包時,如果發送者的 currentEpoch(消息頭部會包含發送者的 currentEpoch)大於當前節點的currentEpoch,那麼當前節點會更新 currentEpoch 爲發送者的 currentEpoch。因此,集羣中所有節點的 currentEpoch 最終會達成一致,相當於對集羣狀態的認知達成了一致。
4.1.2 currentEpoch 作用
currentEpoch 作用在於,當集羣的狀態發生改變,某個節點爲了執行一些動作需要尋求其他節點的同意時,就會增加 currentEpoch 的值。目前 currentEpoch 只用於 slave 的故障轉移流程,這就跟哨兵中的sentinel.current_epoch 作用是一模一樣的。當 slave A 發現其所屬的 master 下線時,就會試圖發起故障轉移流程。首先就是增加 currentEpoch 的值,這個增加後的 currentEpoch 是所有集羣節點中最大的。然後slave A 向所有節點發起拉票請求,請求其他 master 投票給自己,使自己能成爲新的 master。其他節點收到包後,發現發送者的 currentEpoch 比自己的 currentEpoch 大,就會更新自己的 currentEpoch,並在尚未投票的情況下,投票給 slave A,表示同意使其成爲新的 master。
4.1.3 configEpoch
這是一個集羣節點配置相關的概念,每個集羣節點都有自己獨一無二的 configepoch。所謂的節點配置,實際上是指節點所負責的槽位信息。
每一個 master 在向其他節點發送包時,都會附帶其 configEpoch 信息,以及一份表示它所負責的 slots 信息。而 slave 向其他節點發送包時,其包中的 configEpoch 和負責槽位信息,是其 master 的 configEpoch 和負責的 slot 信息。節點收到包之後,就會根據包中的 configEpoch 和負責的 slots 信息,記錄到相應節點屬性中。
4.1.4 configEpoch 作用
configEpoch 主要用於解決不同的節點的配置發生衝突的情況。舉個例子就明白了:節點A 宣稱負責 slot 1,其向外發送的包中,包含了自己的 configEpoch 和負責的 slots 信息。節點 C 收到 A 發來的包後,發現自己當前沒有記錄 slot 1 的負責節點(也就是 server.cluster->slots[1] 爲 NULL),就會將 A 置爲 slot 1 的負責節點(server.cluster->slots[1] = A),並記錄節點 A 的 configEpoch。後來,節點 C 又收到了 B 發來的包,它也宣稱負責 slot 1,此時,如何判斷 slot 1 到底由誰負責呢?
這就是 configEpoch 起作用的時候了,C 在 B 發來的包中,發現它的 configEpoch,要比 A 的大,說明 B 是更新的配置。因此,就將 slot 1 的負責節點設置爲 B(server.cluster->slots[1] = B)。在 slave 發起選舉,獲得足夠多的選票之後,成功當選時,也就是 slave 試圖替代其已經下線的舊 master,成爲新的 master 時,會增加它自己的 configEpoch,使其成爲當前所有集羣節點的 configEpoch 中的最大值。這樣,該 slave 成爲 master 後,就會向所有節點發送廣播包,強制其他節點更新相關 slots 的負責節點爲自己。
5 集羣數據一致性
Redis 集羣儘可能保證數據的一致性,但在特定條件下會丟失數據,原因有兩點:異步複製機制以及可能出現的網絡分區造成腦裂問題。
5.1 異步複製
master 以及對應的 slaves 之間使用異步複製機制,考慮如下場景:
寫命令提交到 master,master 執行完畢後向客戶端返回 OK,但由於複製的延遲此時數據還沒傳播給 slave;如果此時 master 不可達的時間超過閥值,此時集羣將觸發 failover,將對應的 slave 選舉爲新的master,此時由於該 slave 沒有收到複製流,因此沒有同步到 slave 的數據將丟失。
5.2 腦裂(split-brain)
在發生網絡分區時,有可能出現新舊 master 同時存在的情況,考慮如下場景:
由於網絡分區,此時 master 不可達,且客戶端與 master 處於一個分區,並且由於網絡不可達,此時客戶端仍會向 master 寫入。由於 failover 機制,將其中一個 slave 提升爲新的 master,等待網絡分區消除後,老的 master 再次可達,但此時該節點會被降爲 slave 清空自身數據然後複製新的 master ,而在這段網絡分區期間,客戶端仍然將寫命令提交到老的 master,但由於被降爲 slave 角色這些數據將永遠丟失。