Redis Cluster 原理相關說明
背景
之前寫的 Redis Cluster部署、管理和測試 和 Redis 5.0 redis-cli --cluster help說明 已經比較詳細的介紹瞭如何安裝和維護Cluster。但關於Cluster各個節點的通訊和原理沒有說明,為了方便自己以後查閱,先做些記錄。順便對Redis 4.0和5.0的相關特性也做下說明。
Redis 4.0 新功能說明
Redis4.0版本增加了很多新的特性,如:
1 Redis Memeory Command:詳細分析記憶體使用情況,記憶體使用診斷,記憶體碎片回收; 2 PSYNC2:解決failover和從例項重啟不能部分同步;PSYNC3已經路上了; 3 LazyFree: 再也不用怕big key的刪除引起叢集故障切換; 4 LFU: 支援近似的LFU記憶體淘汰演算法; 5 Active Memory Defragmentation:記憶體碎片回收效果很好(實驗階段); 6 Modules: Redis成為更多的可能(覺得像mongo/mysql引入engine的階段);
一、Lazyfree
redis-4.0帶來的Lazyfree機制可以避免del,flushdb/flushall,rename等命令引起的redis-server阻塞,提高服務穩定性。
① unlink
在redis-4.0之前,redis執行del命令會在釋放掉key的所有記憶體以後才會返回OK,這在key比較大的時候(比如說一個hash裡頭有1000W條資料),其他連線可能要等待很久。為了相容已有的del語義,redis-4.0引入unlink命令,效果以及用法和del完全一樣,但記憶體釋放動作放到後臺執行緒中執行。
UNLINK key [key ...]
② flushdb/flushall
flushdb/flushall在redis-4.0中新引入了選項,可以指定是否使用Lazyfree的方式來清空整個記憶體。
FLUSHALL [ASYNC] FLUSHDB [ASYNC]
③ rename
執行 rename oldkey newkey 時,如果newkey已經存在,redis會先刪除,這也會引發上面提到的刪除大key問題。
lazyfree-lazy-server-del yes/no
④ 其他場景
某些使用者對資料設定過期時間,依賴redis的淘汰機制去刪除已經過期的資料,這同樣也存在上面提到的問題,淘汰某個大key會導致程序CPU出現抖動,redis-4.0提供了兩個配置,可以讓redis在淘汰或者逐出資料時也使用lazyfree的方式。
lazyfree-lazy-eviction yes/no lazyfree-lazy-expire yes/no
二、memory
redis-4.0之前只能通過info memory來了解redis內部有限的記憶體資訊,4.0提供了memory命令,幫助使用者全面瞭解redis的記憶體狀態。
127.0.0.1:6379> memory help 1) "MEMORY DOCTOR - Outputs memory problems report" 2) "MEMORY USAGE <key> [SAMPLES <count>] - Estimate memory usage of key" 3) "MEMORY STATS - Show memory usage details" 4) "MEMORY PURGE - Ask the allocator to release memory" 5) "MEMORY MALLOC-STATS - Show allocator internal stats"
① memory usage
usage子命令可以檢視某個key在redis內部實際佔用多少記憶體,這裡有兩點需要說明:
1. 不光key, value需要佔用記憶體,redis管理這些資料還需要一部分記憶體
2. 對於hash, list, set, sorted set這些型別,結果是取樣計算的,可以通過SAMPLES 來控制取樣數量
② memory stats
在redis 4.0之前,我們只能通過info memory檢視redis例項的記憶體大體使用狀況;而記憶體的使用細節,比如expire的消耗,client output buffer, query buffer等是很難直觀顯示的。 memory stats命令就是為展現redis內部記憶體使用細節。
③ memory doctor
主要用於給一些診斷建議,提前發現潛在問題。
④ memory purge
memory purge命令通過呼叫jemalloc內部命令,進行記憶體釋放,儘量把redis程序佔用但未有效使用記憶體,即常說的記憶體碎片釋放給作業系統。只適用於使用jemalloc作為allocator的例項。
⑤ memory malloc-stats
用於列印allocator內部的狀態,目前只支援jemalloc。
三、LFU
redis-4.0新增了 allkey-lfu 和 volatile-lfu 兩種資料逐出策略,同時還可以通過object命令來獲取某個key的訪問頻度。
object freq user_key
基於LFU機制,使用者可以使用 scan + object freq 來發現熱點key,當然redis也一起釋出了更好用的 :
redis-cli --hotkeys
四、psync2
Redis4.0新特性psync2(partial resynchronization version2)部分重新同步(partial resync)增加版本;主要解決Redis運維管理過程中,從例項重啟和主例項故障切換等場景帶來的全量重新同步(full resync)問題。
五、持久化
redis有兩種持久化的方式——RDB和AOF其中RDB是一份記憶體快照AOF則為可回放的命令日誌他們兩個各有特點也相互獨立。4.0開始允許使用RDB-AOF混合持久化的方式結合了兩者的優點通過aof-use-rdb-preamble配置項可以開啟混合開關。
Redis 5.0 新功能說明
Redis5.0版是Redis產品的重大版本釋出,它的最新特點:
1 新的流資料型別(Stream data type) https://redis.io/topics/streams-intro 2 新的 Redis 模組 API:定時器、叢集和字典 API(Timers, Cluster and Dictionary APIs) 3 RDB 增加 LFU 和 LRU 資訊 4 叢集管理器從 Ruby (redis-trib.rb) 移植到了redis-cli 中的 C 語言程式碼 5 新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞變體(blocking variants) 6 升級 Active defragmentation 至 v2 版本 7 增強 HyperLogLog 的實現 8 更好的記憶體統計報告 9 許多包含子命令的命令現在都有一個 HELP 子命令 10 客戶端頻繁連線和斷開連線時,效能表現更好 11 許多錯誤修復和其他方面的改進 12 升級 Jemalloc 至 5.1 版本 13 引入 CLIENT UNBLOCK 和 CLIENT ID 14 新增 LOLWUT 命令 http://antirez.com/news/123 15 在不存在需要保持向後相容性的地方,棄用 "slave" 術語 16 網路層中的差異優化 17 Lua 相關的改進 18 引入動態的 HZ(Dynamic HZ) 以平衡空閒 CPU 使用率和響應性 19 對 Redis 核心程式碼進行了重構並在許多方面進行了改進
Redis Cluster總覽
一、簡介
在官方文件Cluster Spec中,作者詳細介紹了Redis叢集為什麼要設計成現在的樣子。最核心的目標有三個:
1 效能:增加叢集功能後不能對效能產生太大影響,所以Redis採取了P2P而非Proxy方式、非同步複製、客戶端重定向等設計。 2 水平擴充套件:文件中稱可以線性擴充套件到1000結點。 3 可用性:在Cluster推出之前,可用性要靠Sentinel保證。有了叢集之後也自動具有了Sentinel的監控和自動Failover能力。
如果需要全面的瞭解,那一定要看官方文件Cluster Tutorial。
Redis Cluster是一個高效能高可用的分散式系統。由多個Redis例項組成的整體,資料按照Slot儲存分佈在多個Redis例項上,通過Gossip協議來進行節點之間通訊。功能特點如下:
1 所有的節點相互連線 2 叢集訊息通訊通過叢集匯流排通訊,叢集匯流排埠大小為客戶端服務埠+10000(固定值) 3 節點與節點之間通過二進位制協議進行通訊 4 客戶端和叢集節點之間通訊和通常一樣,通過文字協議進行 5 叢集節點不會代理查詢 6 資料按照Slot儲存分佈在多個Redis例項上 7 叢集節點掛掉會自動故障轉移 8 可以相對平滑擴/縮容節點
關於Cluster相關的原始碼可以見:src/cluster.c和src/cluster.h
二、通訊
2.1 CLUSTER MEET
需要組建一個真正的可工作的叢集,我們必須將各個獨立的節點連線起來,構成一個包含多個節點的叢集。連線各個節點的工作使用CLUSTER MEET命令來完成。
CLUSTER MEET <ip> <port>
CLUSTER MEET命令實現:
1 節點 A 會為節點 B 建立一個 clusterNode 結構,並將該結構新增到自己的 clusterState.nodes 字典裡面。 2 節點A根據CLUSTER MEET命令給定的IP地址和埠號,向節點B傳送一條MEET訊息。 3 節點B接收到節點A傳送的MEET訊息,節點B會為節點A建立一個clusterNode結構,並將該結構新增到自己的clusterState.nodes字典裡面。 4 節點B向節點A返回一條PONG訊息。 5 節點A將受到節點B返回的PONG訊息,通過這條PONG訊息節點A可以知道節點B已經成功的接收了自己傳送的MEET訊息。 6 節點A將向節點B返回一條PING訊息。 7 節點B將接收到的節點A返回的PING訊息,通過這條PING訊息節點B可以知道節點A已經成功的接收到了自己返回的PONG訊息,握手完成。 8 節點A會將節點B的資訊通過Gossip協議傳播給叢集中的其他節點,讓其他節點也與節點B進行握手,最終,經過一段時間後,節點B會被叢集中的所有節點認識。
2.2 訊息處理 clusterProcessPacket
1 更新接收訊息計數器 2 查詢傳送者節點並且不是handshake節點 3 更新自己的epoch和slave的offset資訊 4 處理MEET訊息,使加入叢集 5 從goosip中發現未知節點,發起handshake 6 對PING,MEET回覆PONG 7 根據收到的心跳資訊更新自己clusterState中的master-slave,slots資訊 8 對FAILOVER_AUTH_REQUEST訊息,檢查並投票 9 處理FAIL,FAILOVER_AUTH_ACK,UPDATE資訊
2.3 定時任務clusterCron
1 對handshake節點建立Link,傳送Ping或Meet 2 向隨機幾點傳送Ping 3 如果是從檢視是否需要做Failover 4 統計並決定是否進行slave的遷移,來平衡不同master的slave數 5 判斷所有pfail報告數是否過半數
2.4 心跳資料
- 傳送訊息頭資訊Header
- 所負責slots的資訊
- 主從資訊
- ip, port資訊
- 狀態資訊
- 傳送其他節點Gossip資訊
- ping_sent, pong_received
- ip, port資訊
- 狀態資訊,比如傳送者認為該節點已經不可達,會在狀態資訊中標記其為PFAIL或FAIL
clusterMsg結構的currentEpoch、sender、myslots等屬性記錄了傳送者自身的節點資訊,接收者會根據這些資訊,在自己的clusterState.nodes字典裡找到傳送者對應的clusterNode結構,並對結構進行更新。
Redis叢集中的各個節點通過Gossip協議來交換各自關於不同節點的狀態資訊,其中Gossip協議由MEET、PING、PONG三種訊息實現,這三種訊息的正文都由兩個clusterMsgDataGossip結構組成。
每次傳送MEET、PING、PONG訊息時,傳送者都從自己的已知節點列表中隨機選出兩個節點(可以是主節點或者從節點),並將這兩個被選中節點的資訊分別儲存到兩個結構中。當接收者收到訊息時,接收者會訪問訊息正文中的兩個結構,並根據自己是否認識clusterMsgDataGossip結構中記錄的被選中節點進行操作:
1 如果被選中節點不存在於接收者的已知節點列表,那麼說明接收者是第一次接觸到被選中節點,接收者將根據結構中記錄的IP地址和埠號等資訊,與被選擇節點進行握手。 2 如果被選中節點已經存在於接收者的已知節點列表,那麼說明接收者之前已經與被選中節點進行過接觸,接收者將根據clusterMsgDataGossip結構記錄的資訊,對被選中節點對應的clusterNode結構進行更新。
2.5 資料結構
clusterNode 結構儲存了一個節點的當前狀態, 如節點的建立時間、節點的名字、節點當前的配置紀元、節點的 IP 和埠等:
1 slots:點陣圖,由當前clusterNode負責的slot為1 2 salve, slaveof:主從關係資訊 3 ping_sent, pong_received:心跳包收發時間 4 clusterLink *link:節點間的連線 5 list *fail_reports:收到的節點不可達投票
clusterState 結構記錄了在當前節點的叢集目前所處的狀態:
1 myself:指標指向自己的clusterNode 2 currentEpoch:當前節點的最大epoch,可能在心跳包的處理中更新 3 nodes:當前節點記錄的所有節點,為clusterNode指標陣列 4 slots:slot與clusterNode指標對映關係 5 migrating_slots_to,importing_slots_from:記錄slots的遷移資訊 6 failover_auth_time,failover_auth_count,failover_auth_sent,failover_auth_rank,failover_auth_epoch:Failover相關資訊
clusterLink 結構儲存了連線節點所需的有關資訊, 比如套接字描述符, 輸入緩衝區和輸出緩衝區。
三、資料分佈及槽資訊
3.1 槽(slot)概念
Redis Cluster中有一個16384長度的槽的概念,他們的編號為0、1、2、3……16382、16383。這個槽是一個虛擬的槽,並不是真正存在的。正常工作的時候,Redis Cluster中的每個Master節點都會負責一部分的槽,當有某個key被對映到某個Master負責的槽,那麼這個Master負責為這個key提供服務,至於哪個Master節點負責哪個槽,這是可以由使用者指定的,也可以在初始化的時候自動生成。在Redis Cluster中,只有Master才擁有槽的所有權,如果是某個Master的slave,這個slave只負責槽的使用,但是沒有所有權。
3.2 資料分片
在Redis Cluster中,擁有16384個slot,這個數是固定的,儲存在Redis Cluster中的所有的鍵都會被對映到這些slot中。資料庫中的每個鍵都屬於這 16384 個雜湊槽的其中一個,叢集使用公式 CRC16(key) % 16384 來計算鍵 key 屬於哪個槽,其中 CRC16(key) 語句用於計算鍵 key 的 CRC16 校驗和。叢集中的每個節點負責處理一部分雜湊槽。
3.3 節點的槽指派資訊
clusterNode結構的slots屬性和numslot屬性記錄了節點負責處理那些槽:
struct clusterNode { //… unsignedchar slots[16384/8]; };
Slots屬性是一個二進位制位陣列(bitarray),這個陣列的長度為16384/8=2048個位元組,共包含16384個二進位制位。Master節點用bit來標識對於某個槽自己是否擁有。比如對於編號為1的槽,Master只要判斷序列的第二位(索引從0開始)是不是為1即可。時間複雜度為O(1)。
3.4 叢集所有槽的指派資訊
通過將所有槽的指派資訊儲存在clusterState.slots數組裡面,程式要檢查槽i是否已經被指派,又或者取得負責處理槽i的節點,只需要訪問clusterState.slots[i]的值即可,複雜度僅為O(1)。
3.5 請求重定向
由於每個節點只負責部分slot,以及slot可能從一個節點遷移到另一節點,造成客戶端有可能會向錯誤的節點發起請求。因此需要有一種機制來對其進行發現和修正,這就是請求重定向。有兩種不同的重定向場景:
a) MOVED錯誤
-
請求的key對應的槽不在該節點上,節點將檢視自身內部所儲存的雜湊槽到節點 ID 的對映記錄,節點回復一個 MOVED 錯誤。
-
需要客戶端進行再次重試。
b) ASK錯誤
-
請求的key對應的槽目前的狀態屬於MIGRATING狀態,並且當前節點找不到這個key了,節點回復ASK錯誤。ASK會把對應槽的IMPORTING節點返回給你,告訴你去IMPORTING的節點嘗試找找。
-
客戶端進行重試 首先發送ASKING命令,節點將為客戶端設定一個一次性的標誌(flag),使得客戶端可以執行一次針對 IMPORTING 狀態的槽的命令請求,然後再發送真正的命令請求。
-
不必更新客戶端所記錄的槽至節點的對映。
四、資料遷移
當槽x從Node A向Node B遷移時,Node A和Node B都會有這個槽x,Node A上槽x的狀態設定為MIGRATING,Node B上槽x的狀態被設定為IMPORTING。
MIGRATING狀態
-
如果key存在則成功處理
-
如果key不存在,則返回客戶端ASK,客戶端根據ASK首先發送ASKING命令到目標節點,然後傳送請求的命令到目標節點
-
當key包含多個命令,
-
如果都存在則成功處理
-
如果都不存在,則返回客戶端ASK
-
如果一部分存在,則返回客戶端TRYAGAIN,通知客戶端稍後重試,這樣當所有的key都遷移完畢的時候客戶端重試請求的時候回得到ASK,然後經過一次重定向就可以獲取這批鍵
-
此時不重新整理客戶端中node的對映關係
IMPORTING狀態
-
如果key不在該節點上,會被MOVED重定向,重新整理客戶端中node的對映關係
-
如果是ASKING命令則命令會被執行,key不在遷移的節點已經被遷移到目標的節點
-
Key不存在則新建
4.1 讀寫請求
槽裡面的key還未遷移,並且槽屬於遷移中。
假如槽x在Node A,需要遷移到Node B上,槽x的狀態為migrating,其中的key1還沒輪到遷移。此時訪問key1則先計算key1所在的Slot,存在key1則直接返回。
4.2 MOVED請求
槽裡面的key已經遷移過去,並且槽屬於遷移完。
假如槽x在Node A,需要遷移到Node B上,遷移完成。此時訪問key1則先計算key1所在的Slot,因為已經遷移至Node B上,Node A上不存在,則返回 moved slotid IP:PORT,再根據返回的資訊去Node B訪問key1。
4.3 ASK請求
槽裡面的key已經遷移完,並且槽屬於遷移中的狀態。
假如槽x在Node A,需要遷移到Node B上,遷移完成,但槽x的狀態為migrating。此時訪問key1則先計算key1所在的Slot,不存在key1則返回ask slotid IP:PORT,再根據返回的資訊傳送asking請求到Node B,最後沒問題後再去Node B上訪問key1。
五、通訊故障
5.1 故障檢測
叢集中的每個節點都會定期地向叢集中的其他節點發送PING訊息,以此交換各個節點狀態資訊,檢測各個節點狀態:線上狀態、疑似下線狀態PFAIL、已下線狀態FAIL。
當主節點A通過訊息得知主節點B認為主節點D進入了疑似下線(PFAIL)狀態時,主節點A會在自己的clusterState.nodes字典中找到主節點D所對應的clusterNode結構,並將主節點B的下線報告(failure report)新增到clusterNode結構的fail_reports連結串列中。
struct clusterNode { //... //記錄所有其他節點對該節點的下線報告 list*fail_reports; //... };
如果叢集裡面,半數以上的主節點都將主節點D報告為疑似下線,那麼主節點D將被標記為已下線(FAIL)狀態,將主節點D標記為已下線的節點會向叢集廣播主節點D的FAIL訊息,所有收到FAIL訊息的節點都會立即更新nodes裡面主節點D狀態標記為已下線。
將 node 標記為 FAIL 需要滿足以下兩個條件:
1 有半數以上的主節點將 node 標記為 PFAIL 狀態。 2 當前節點也將 node 標記為 PFAIL 狀態。
5.2 多個從節點選主
選新主的過程基於Raft協議選舉方式來實現的:
1 當從節點發現自己的主節點進行已下線狀態時,從節點會廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST訊息,要求所有收到這條訊息,並且具有投票權的主節點向這個從節點投票 2 如果一個主節點具有投票權,並且這個主節點尚未投票給其他從節點,那麼主節點將向要求投票的從節點返回一條,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,表示這個主節點支援從節點成為新的主節點 3 每個參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,並根據自己收到了多少條這種訊息來統計自己獲得了多少主節點的支援 4 如果叢集裡有N個具有投票權的主節點,那麼當一個從節點收集到大於等於叢集N/2+1張支援票時,這個從節點就成為新的主節點 5 如果在一個配置紀元沒有從能夠收集到足夠的支援票數,那麼叢集進入一個新的配置紀元,並再次進行選主,直到選出新的主節點為止
5.3 故障轉移
當從節點發現自己的主節點變為已下線(FAIL)狀態時,便嘗試進Failover,以期成為新的主。
以下是故障轉移的執行步驟:
1 從下線主節點的所有從節點中選中一個從節點 2 被選中的從節點執行SLAVEOF NO NOE命令,成為新的主節點 3 新的主節點會撤銷所有對已下線主節點的槽指派,並將這些槽全部指派給自己 4 新的主節點對叢集進行廣播PONG訊息,告知其他節點已經成為新的主節點 5 新的主節點開始接收和處理槽相關的請求
總結:
Redis Cluster採用無中心節點方式實現,無需proxy代理,客戶端直接與redis叢集的每個節點連線,根據同樣的hash演算法計算出key對應的slot,然後直接在slot對應的Redis上執行命令。從CAP定理來看,Cluster支援了AP(Availability&Partition-Tolerancy),這樣讓Redis從一個單純的NoSQL記憶體資料庫變成了分散式NoSQL資料庫。
參考文件:
深入淺出 Redis Cluster 原理