《Redis官方教程》Redis集群規範(一)
Redis集群規範
歡迎來到Redis集群規範。在這裏你可以找到有關Redis的算法和設計的基本原理。這篇文章是一項正在進行的工作,因為它是不斷地與Redis的實際實現同步。
主要屬性和設計原理
Redis的集群目標
Redis集群是一個分布式的實現,具有以下目標,按設計的重要性排序:
- 高性能,並且多達1000個節點的線性可擴展性。沒有代理,使用異步複製,並且在進行賦值時沒有合並操作。
- 可接受程度的寫安全:當客戶端與大多數master節點建立連接後,係統努力(使用最優的方式)保持來自客戶端的寫操作。通常有小窗口,其中確認的寫操作可能會丟失。當客戶端在一個小的分區中,窗口丟失寫操作會更大。
- 可用性:Redis集群支持網絡分區——其中大部分主節點都可訪問,並且不可訪問的各master節點對應的從至少一個可訪問。而且采用副本遷移,有多個從的主會提供一個從給沒有從的主。
本文檔中描述的在Redis >=3.0的版本中實現。
已實現的部分
Redis集群實現了所有Redis的非分布式版本中提供的單鍵命令。命令執行複雜的多鍵操作, 像set類型的合集或交集的命令,隻要鍵是屬於同一個節點就行。
Redis群集實現有一個散列標簽的概念,能強製讓特定的鍵存儲在相同的節點。但是在手動重新散列期間,多鍵操作的可能不可用,而單鍵操作總是可用。
Redis集群不支持這樣的Redis的單實例版本的多個數據庫。隻是有數據庫0並且SELECT命令是不允許的。
客戶端和服務器在Redis集群協議的角色
在Redis的群集節點負責保持數據,並持有群集的狀態,其中包括映射鍵到正確的節點。集群節點也能自動發現其他節點,檢測非工作節點,當發生失敗,必要時把slave切換成master,以便在繼續發生故障時,持續運作。
執行任務的所有群集節點使用的是TCP總線和二進製協議連接,稱為Redis集群總線。每個節點被連接到使用群集總線的所有其他節點。節點使用Gossip協議傳播有關群集的信息,以發現新節點,發送Ping報文,以確保所有其他節點工作正常,並發送必要信息觸發特定條件。群集總線也用於以傳播跨集群發布/訂閱消息,並當用戶請求來協調手工故障轉移(手動故障轉移是未由Redis集群故障檢測器發起的故障轉移,而是直接由係統管理員)。
因為集群節點不能代理請求,客戶端可以使用重定向錯誤-MOVED和-ASK重定向到其他節點。客戶端是在理論上自由將請求發送到集群中的所有節點,如果需要的話得到重定向,因此客戶端不需要保持群集的狀態。然而,客戶端緩存鍵和節點之間的映射可以明顯的改善性能。
Redis 集群協議中的客戶端和服務器端
在 Redis 集群中,節點負責存儲數據、記錄集群的狀態,包括鍵值到正確節點的映射。集群節點同樣能自動發現其他節點,檢測非正常工作節點, 並且在需要的時候升級slave成master來保證故障發生時持續運作。
為了執行這些任務,所有的集群節點都通過TCP總線和二進製協議連接,叫集群總線redis cluster bus。 每一個節點都通過集群總線與集群上的其餘每個節點連接。節點們使用一個 gossip 協議來傳播集群的信息,這樣可以:發現新的節點、 發送ping包用來確保所有節點都在正常工作、在特定情況發生時發送集群消息來觸發特定條件。集群總線也用於在集群中傳播 發布/訂閱 消息、用戶請求協調手動故障轉移(手動故障轉移都並非由Redis的集群故障檢測器啟動的故障轉移,而是直接由係統管理員)。
由於集群節點不能代理請求,所以客戶端在接收到重定向錯誤 -MOVED 和 -ASK 的時候, 將命令重定向到其他節點。理論上來說,客戶端是可以自由地向集群中的所有節點發送請求,在必要時候把請求重定向到其他節點,所以客戶端是不需要保存集群狀態。 不過客戶端可以緩存鍵值和節點之間的映射關係,這樣能明顯提高指令執行的效率。
寫安全
Redis 集群節點間使用異步副本備份,最後一個故障轉移取得隱式合並功能,這意味著最後選舉出的master數據最終替換其它副本.
通常存在一個時間窗口,可能在分片中丟失寫入數據。 但是一個連接到絕大部分master節點的客戶端的時間窗口,與一個連接到極小部分master節點的客戶端的時間窗口 有很大的區別。
Redis 集群會努力嚐試保存所有與大多數master節點連接的客戶端執行的寫操作,相比於與少數master節點連接的客戶端執行的寫操作,但以下兩種情況除外,會導致失敗期間在多數分片丟失寫操作:
1) A寫入操作能到達一個master節點,但當master節點要回複客戶端的時候,這個寫入有可能沒有通過master-slave異步備份傳播到slave節點那裏。 如果在某個寫入操作沒有到達slave節點的時候master節點已經宕機了,那麼該寫入會永遠地丟失掉(如果master長時間周期不可達而它的slave升級成master)。
這通常在所有情況中很難發現,master突然發生故障的情況下,由於master嚐試回複客戶端(寫入的應答)和slave(傳播寫操作)在大致相同時間。然而,它是一個現實世界的故障模式。
2) 另一個理論上可能會丟失寫入操作的模式是:
- A master因為分區不可達。
- 它故障轉移, 它的一個slave升級成了master。
- 過一段時間之後這個節點再次變得可達。
- 一個持有過期路由表的客戶端或許會在集群把這個master節點變成一個slave節點(新master節點的slave節點)之前對它進行寫入操作。
實際上這是極小概率事件,這是因為,那些由於長時間無法被大多數master節點訪問到的節點會被故障轉移掉,將不再接受任何寫入操作,當其分區修複好以後仍然會在一小段時間內拒絕寫入操作好讓其他節點有時間被告知配置信息的變更。這種失效模式也需要客戶端的路由表還沒有被更新。
通常所有節點都會嚐試通過非阻塞連接嚐試(non-blocking connection attempt)盡快去訪問一個再次加入到集群裏的節點,一旦跟該節點建立一個新的連接就會發送一個ping包過去(這足夠升級節點配置信息)。這就使得一個節點很難在恢複可寫入狀態之前沒被告知配置信息更改。
寫入操作到達少數分片會有更大的丟失窗口。比如:
Redis 集群在擁有少數master節點和至少一個客戶端的分片上容易丟失為數不少的寫入操作,這是因為,如果master節點被故障轉移到集群中多數節點那邊, 那麼所有發送到這些master節點的寫入操作可能會丟失。
特別是一個master節點要被故障轉移,必須是大多數master節點在至少 NODE_TIMEOUT 時長裏無法訪問到,所以如果分區在這段時間之前被修複,就沒有寫入操作會丟失。當分區故障持續超過 NODE_TIMEOUT,所有在少數節點一邊到該時間點執行的寫操作可能會丟失
,然而集群的少數節點這邊,和大多數節點失聯,會在 NODE_TIMEOUT 這個時間內開始拒絕往受損分區進行寫入,所以在少數節點這邊變得不再可用後,會有一個最大時間窗口. 因此在那時間之後將不會再有寫入操作被接收或丟失。
可用性
Redis 集群在分區的少數節點那邊不可用。在分區的多數節點這邊假設至少有大多數可達的master節點,並且對於每個不可達master節點都至少有一個slave節點可達,在經過了( NODE_TIMEOUT +n秒)時間後,有個slave節點選舉出來故障轉移成master節點,這時集群又再恢複可用(故障轉移通常在1-2秒內)。
這意味著 Redis 集群的設計是能容忍集群中少數節點的出錯,但對於要求大量網絡分片的可用性的應用來說,這並不是一個合適的解決方案。
在該示例,一個由 N 個master節點組成的集群,每個master節點都隻有一個slave節點。隻要有單個節點被分割出去,集群的多數節點這邊仍然是可訪問的。當有兩個節點被分割出去後集群仍可用的概率是 1-(1/(N*2-1)) (在第一個節點故障出錯後總共剩下 N*2-1 個節點,那麼失去slave節點隻剩master節點的出錯的概率是 1/(N*2-1))。
比如一個擁有5個節點的集群,每個節點都隻有一個slave節點,那麼在兩個節點從多數節點這邊分割出去後集群不再可用的概率是 1/(5*2-1) = 0.1111,即有大約 11% 的概率。
感謝redis集群特性 副本遷移,集群在真實環境可用性提升,因為副本升級為孤立的master節點(master節點不再有副本),所以每次成功的故障轉移,集群重新配置slave節點來更好地防止下次故障.
性能
在 Redis 集群中節點並不是把命令轉發到負責鍵的節點上,而是把客戶端重定向到服務一定範圍內的鍵的節點上。 最終客戶端獲得一份最新的集群路由表,裏麵有寫著哪些節點服務哪些鍵,所以在正常操作中客戶端是直接聯係到對應的節點來發送指令。
由於使用了異步複製,節點不會等待其他節點對寫入操作的回複。(除非顯式發送WAIT指令)
同樣,由於多鍵指令僅限於相鄰的鍵,如果不是重新分片,那麼數據是永遠不會在節點間移動的。
普通操作是可以被處理得跟在Redis單機版一樣的。這意味著,在一個擁有 N 個master節點的 Redis 集群中,由於線性擴展的設計,你可以認為同樣的操作在集群上的性能是Redis單機版的n倍。同時,請求通常在一次來回中被執行,客戶端會保持跟節點的長連接,所以延遲指標跟在Reids 單機版情況是一樣的。
為什麼要避免使用合並操作
Redis 集群的設計是避免在多個節點中存在同個鍵值對的衝突版本,在這點上 Redis 數據模型不總是滿足需要,Redis 中的值通常都是比較大的,經常可以看到列表或者有序集合中有數以百萬計的元素。數據類型也是語義複雜的。傳輸和合並這樣的值將會變成一個主要的性能瓶頸, 並且/或者可能需要應用端邏輯的引入,額外的內存來存儲元數據,諸如此類。
redis集群主要組件概覽
鍵分布模型
鍵空間被分割為 16384 槽(slot),事實上集群的最大master節點數量是 16384 個。(然而建議最大節點數量設置在1000)所有的master節點都負責 16384 個哈希槽中的一部分。當集群處於穩定狀態時,當集群中沒有在執行重配置操作(即:hash槽沒有從一處移到另一處)。當集群在穩定狀態,每個哈希槽都隻由一個節點進行支配(不過master節點可以有一個或多個slave節點,可以在網絡分區或節點失效時替換掉master節點,並且這樣可以用來水平擴展讀操作(這些讀操作不要求實時數據))。以下是用來把鍵映射到哈希槽的算法(下一段落,除了哈希標簽以外就是按照這個規則):
HASH_SLOT = CRC16(key) mod 16384
CRC16的定義如下:
- 名稱:XMODEM(也可以稱為 ZMODEM 或 CRC-16/ACORN)
- 輸出寬度:16 bit
- 多項數(poly):1021(即x16+ x12 + x5 + 1 )
- 初始化:0000
- 反射輸入字節(Reflect Input byte):False
- 反射輸出CRC(Reflect Output CRC):False
- 輸出CRC的異或常量(Xor constant to output CRC):0000
- 輸入”123456789″的輸出:31C3
CRC16的16位輸出中的14位會被使用(這也是為什麼上麵的式子中有一個對 16384 取餘的操作)。
在我們的測試中,CRC16能相當好地把不同的鍵均勻地分配到 16384 個槽中。
注意:在本文檔的附錄A中有CRC16算法的實現。
鍵哈希標簽
為了實現哈希標簽(hash tags),計算hash槽有一個特殊處理,哈希標簽是確保兩個鍵都在同一個哈希槽裏的一種方式。用來實現集群中多鍵多鍵操作。
為了實現哈希標簽,哈希槽在一定條件下是用另一種不同的方式計算的。基本來說,如果一個鍵包含一個 “{…}”模式,隻有 { 和 } 之間的部分字符串會用做哈希計算以獲取哈希槽。但是由於可能出現多個 { 或 },計算的算法詳細說明如下:
- 當鍵包含一個{ 字符。
- 並且當在{ 的右邊有一個 }。
- 並且當第一次出現{和第一次出現 } 之間有一個或多個字符。
然後不是直接計算鍵的哈希,隻有在第一個 { 和它右邊第一個 } 之間的內容會被用來計算哈希值。
例子:
- 比如這兩個鍵{user1000}.following 和 {user1000}.followers 會被哈希到同一個哈希槽裏,因為隻有 user1000 這個子串會被用來計算哈希槽。
- 對於foo{}{bar} 這個鍵,整個鍵跟普通鍵一樣被用來計算哈希值,因為第一個出現的 { 和右邊緊接著的 } 之間沒有任何字符。
- 對於foo{{bar}}zap 這個鍵,用來計算哈希值的是 {bar 這個子串,因為是它第一個 { 及其右邊第一個 } 之間的內容。
- 對於foo{bar}{zap} 這個鍵,用來計算哈希值的是 bar 這個子串,因為算法會在第一次有效或無效(中間沒有任何字節)的匹配到 { 和 } 的時候停止。
- 按照這個算法,如果一個鍵是以{} 開頭的話,整個鍵會被用來計算哈希值。當使用二進製數據做為鍵名的時候,這是非常有用的。
加上哈希標簽的特殊處理,下麵是用 Ruby 和 C 語言實現的 HASH_SLOT 函數。
Ruby example code:
def HASH_SLOT(key)
s = key.index "{"
if s
e = key.index "}",s+1
if e && e != s+1
key = key[s+1..e-1]
end
end
crc16(key) % 16384
end
C example code:
unsigned int HASH_SLOT(char *key, int keylen) {
int s, e; /* start-end indexes of { and } */
/* Search the first occurrence of '{'. */
for (s = 0; s < keylen; s++)
if (key[s] == '{') break;
/* No '{' ? Hash the whole key. This is the base case. */
if (s == keylen) return crc16(key,keylen) & 16383;
/* '{' found? Check if we have the corresponding '}'. */
for (e = s+1; e < keylen; e++)
if (key[e] == '}') break;
/* No '}' or nothing between {} ? Hash the whole key. */
if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;
/* If we are here there is both a { and a } on its right. Hash
* what is in the middle between { and }. */
return crc16(key+s+1,e-s-1) & 16383;
}
集群節點屬性
在集群中,每個節點都有一個唯一的名字。節點名字是一個十六進製表示的160 bit 隨機數,這個隨機數是節點第一次啟動時生成的(通常是用 /dev/urandom)。 節點會把它的ID保存在配置文件裏,以後永遠使用這個ID,或者至少隻要這個節點配置文件沒有被係統管理員刪除掉,或者通過指令CLUSTER RESET強製請求硬重置(hard reset)
節點ID是用於在整個集群中標識每個節點。節點改變IP地址,沒有任何必要改變節點ID。集群能檢測到 IP /端口的變化,然後使用在集群總線(cluster bus)上的 gossip 協議來重新配置。
節點ID不僅是關聯節點的信息,也是全局始終唯一的。 每個節點也有下麵的關聯信息。一些信息是具體集群節點的配置詳情,並且在集群中最終一致。一些其它信息,比如節點最後ping的時間,是每個節點和其它都不同的。
每個節維護了感知集群其它節點如下信息: 每個節點的節點ID,IP和端口,一係列標識,當標識為slave對應的master節點,最後節點ping包的時間和最後收到pong回複的時間,當前節點配額(後文會解釋),連接狀態和最後服務的一堆哈希槽。
詳細的描述在CLUSTER NODES文檔中:https://redis.io/commands/cluster-nodes
在任意節點執行 CLUSTER NODES 命令可以獲得上述信息。
下麵的例子是在一個隻有三個節點的小集群中發送 CLUSTER NODES 命令到一個master節點得到的輸出。
$ redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095
在上麵列出來的信息中,各個字段依次表示的是:節點ID,IP地址:端口號,標識,上一次發送 ping 指令的時間,上一次收到 pong 回複的時間,配額,連接狀態,節點使用的哈希槽,上述字段的詳情很快會在redis集群規範部分中全部講清楚
集群總線
每個Redis的集群節點都有一個額外的TCP端口,用於接收來自其他Redis的集群節點連接。此端口號(普通TCP端口號的的固定偏移值)用於接收來自客戶端的連接。為了Redis群集端口,加了10000到普通的命令端口。例如,如果一個Redis的節點監聽端口為6379,會打開集群總線端口16379(=10000+6379)。節點到節點的通信獨自使用群集總線和群集總線協議:不同類型和大小的幀組成一個二進製協議。群集總線二進製協議不公開發布,因為它的設計目的不是用這個協議和外部軟件的設備交互。然而,你可以通過閱讀cluster.h和cluster.c文件Redis的集群源代碼獲取有關群集總線協議的更多細節。
集群拓撲結構
Redis 集群是一個網狀結構,每個節點都通過 TCP 連接跟其他每個節點連接。
在一個有 N 個節點的集群中,每個節點都有 N-1 個對外的 TCP 連接,和 N-1 個對內的連接。
這些 TCP 連接會永久保持,並不是按需創建的。當一個節點在集群總線中預計回複一個ping,等待足夠長的時間,以標識節點不可達,它會嚐試從頭開始連接來刷新與節點的連接。
而Redis的群集節點形成全網狀,節點使用gossip協議和一個配置更新機製,以避免在正常條件下交換節點之間太多消息,因此,交換的消息的數量不是指數。
節點握手
節點總是通過集群總線端口接受連接,甚至會回複接收到的 ping 請求,即使發送 ping 請求的節點是不可信的。 然而如果某個節點不被認為是在集群中,那麼所有它發出的數據包都會被丟棄掉。
隻有在兩種方式下,一個節點才會認為另一個節點是集群中的一部分:
- 當一個節點使用MEET 消息介紹自己。一個 meet 消息跟一個 PING 消息完全一樣,但它會強製讓接收者接受自己為集群中的一部分。 隻有在係統管理員使用以下命令請求的時候,節點才會發送MEET 消息給其他節點:
CLUSTER MEET ip port
- 一個已被信任的節點能通過傳播gossip消息讓另一個節點被注冊為集群中的一部分。也就是說,如果 A 知道 B,B 知道 C,最終B會發知道C 的gossip消息給A。A 收到後就會把 C 當作是網絡中的一部分,並且嚐試連接 C。
這意味著,隻要我們往任何連接圖中加入節點,它們最終會自動形成一個完全連接圖。這表示集群能自動發現其他節點,但前提是有一個由係統管理員強製創建的信任關係。
這個機製能防止不同的 Redis 集群因為 IP 地址變更或者其他網絡事件而意外混合起來,從而使集群更健壯。
重定向和重分片
MOVED 重定向
一個 Redis 客戶端可以自由地向集群中的任意節點(包括slave節點)發送請求。接收的節點會分析請求,如果這個命令是集群可以執行的(就是查詢中隻涉及一個鍵,或者多鍵在同一個哈希槽),節點會找出這個鍵/這些鍵所屬的哈希槽對應的節點。
如果哈希槽在這個節點上,那麼這個請求就簡單的執行了。否則這個節點會查看它內部的 哈希槽-節點 映射,然後給客戶端返回一個 MOVED 錯誤,如下:
GET x
-MOVED 3999 127.0.0.1:6381
這個錯誤包括鍵的哈希槽和能處理這個查詢的節點的 IP:端口。客戶端需要重新發送請求到給定 ip 地址和端口號的節點。 注意,即使客戶端在重發請求之前等待了很長一段時間,與此同時集群的配置信息發生改變,如果哈希槽 3999 現在是歸屬其它節點,那麼目標節點會再向客戶端回複一個 MOVED 錯誤。如果連接的節點沒有信息變更,會重複這樣。
從集群的角度看,節點是以 ID來標識的。我們嚐試簡化接口,所以隻向客戶端暴露哈希槽和用 IP:端口 來標識的 Redis 節點之間的映射。
雖然並沒有要求,但是客戶端應該嚐試記住哈希槽 3999 歸屬於 127.0.0.1:6381。這樣的話一旦有一個新的命令需要發送,它能計算出目標鍵的哈希槽,找到正確節點的機率更高。
另一種方法是使用CLUSTER NODES 或CLUSTER SLOTS命令刷新整個客戶端集群布局。當遇到一個MOVED,當遇到重定向,很可能多個插槽進行重新配置,而不是隻有一個,所以盡快更新客戶端的配置往往是最好的策略。
注意,當集群是穩定的時候(配置沒有在變更),所有客戶端最終都會得到一份 哈希槽->節點 的映射表,這樣能使得集群效率非常高,客戶端直接定位目標節點,不用重定向、或代理或發生其他單點故障。
一個客戶端也應該能處理後文提到的 -ASK 重定向錯誤,否則不是一個完整的redis集群客戶端
集群在線重新配置
Redis 集群支持在集群運行過程中添加或移除節點。實際上,添加或移除節點都被抽象為同一個操作,那就是把哈希槽從一個節點移到另一個節點。這意味著相同的原理能用來重新平衡集群,增/刪節點,等等.
- 向集群添加一個新節點,就是把一個空節點加入到集群中並把某些哈希槽從已存在的節點移到新節點上。
- 從集群中移除一個節點,就是把該節點上的哈希槽移到其他已存在的節點上。
- 重新平衡,就是指向一堆哈希槽在節點間遷移
所以實現這個的核心是能把哈希槽移來移去。從實際角度看,哈希槽就隻是一堆鍵,所以 Redis 集群在重組分片時做的就是把鍵從一個節點移到另一個節點。移動一個哈希槽就是移動屬於這個槽的所有鍵。為了理解這是怎麼工作的,我們需要介紹 CLUSTER 的子命令,這些命令是用來操作 Redis 集群節點上的哈希槽轉換表。
有以下子命令(在這個案例中有的沒用到):
- CLUSTER ADDSLOTS slot1 [slot2] … [slotN]
- CLUSTER DELSLOTS slot1 [slot2] … [slotN]
- CLUSTER SETSLOT slot NODE node
- CLUSTER SETSLOT slot MIGRATING node
- CLUSTER SETSLOT slot IMPORTING node
頭兩個命令,ADDSLOTS 和 DELSLOTS,就是簡單地用來給Redis 節點指派或移除哈希槽。指派哈希槽就是告訴一個master節點它會負責存儲和服務指定的哈希槽。
在哈希槽被指派後會將這個消息通過 gossip 協議向整個集群傳播(協議在後文配置傳播章節說明)。
ADDSLOTS 命令通常是用於在一個集群剛建立的時候從新給所有master節點指派哈希槽,總共有16384個。
DELSLOTS主要用於群集配置的手工修改或用於調試任務:在實踐中很少使用。
SETSLOT用於將哈希槽指定給特定節點的ID ,如果SETSLOT <插槽>節點的形式被使用。否則,哈希槽可以在兩種特殊狀態MIGRATING 和 IMPORTING。這兩個特殊狀態用於哈希槽從一個節點遷移到另一個。
- 當一個槽被設置為 MIGRATING,持有該哈希槽的節點仍會接受所有跟這個哈希槽有關的請求,但隻有當查詢的鍵還存在原節點時,原節點會處理該請求,否則這個查詢會通過一個-ASK 重定向轉發到遷移的目標節點。
- 當一個槽被設置為 IMPORTING,隻有在接受到 ASKING 命令之後節點才會接受所有查詢這個哈希槽的請求。如果客戶端沒有發送 ASKING 命令,那麼查詢都會通過-MOVED 重定向錯誤轉發到真正的哈希槽歸屬節點那裏,這通常會發生。
這我們用實例讓它哈希槽遷移更清晰些。假設我們有兩個 Redis master節點,稱為 A 和 B。我們想要把哈希槽 8 從 節點A 移到 節點B,所以我們發送了這樣的命令:
- 我們向 節點B 發送:CLUSTER SETSLOT 8 IMPORTING A
- 我們向 節點A 發送:CLUSTER SETSLOT 8 MIGRATING B
其他所有節點在每次請求的一個鍵是屬於哈希槽 8 的時候,都會把客戶端引向節點”A”。具體如下:
- 所有關於已存在的鍵的查詢都由節點”A”處理。
- 所有關於不存在於節點 A 的鍵都由節點”B”處理,因為”A”將重定向客戶端請求到”B”。
這種方式讓我們可以不用在節點 A 中創建新的鍵。同時,一個叫做 redis-trib 的特殊腳本,用於重新分片和集群配置,把已存在的屬於哈希槽 8的鍵從節點 A 移到節點 B。這通過以下命令實現:
CLUSTER GETKEYSINSLOT slot count
上麵這個命令會返回指定的哈希槽中 count 個鍵。對於每個返回的鍵,redis-trib 向節點 A 發送一個MIGRATE 命令,這樣會以原子性的方式從A到B遷移指定的鍵(在移動鍵的過程中兩個節點都被鎖住,通常時間很短,所以不會出現競爭狀況)。以下是 MIGRATE 的工作原理:
MIGRATE target_host target_port key target_database id timeout
執行 MIGRATE 命令的節點會連接到目標節點,把序列化後的 key 發送過去,一旦收到 OK 回複就會從它自己的數據集中刪除老的 key。所以從一個外部客戶端看來,在某個時間點,一個 key 要不就存在於節點 A 中要不就存在於節點 B 中。
在 Redis 集群中,不需要指定一個除了 0 號之外的數據庫,但 MIGRATE 命令能用於其他跟 Redis 集群無關的的任務,所以它是一個通用的命令。MIGRATE 命令被優化了,使得即使在移動像長列表這樣的複雜鍵仍然能做到快速。 不過當在重配置一個擁有很多鍵且鍵的數據量都很大的集群的時候,如果使用它的應用程序來說就會有延時這個限製,這個過程就不是那麼好了。
ASK 重定向
在前麵的章節中,我們簡短地提到了 ASK 重定向(ASK redirection),為什麼我們不能單純地使用 MOVED 重定向呢?因為收到 MOVED,意味著我們認為哈希槽永久地歸屬到了另一個節點,並且接下來的所有請求都嚐試發到目標節點上去。而 ASK 意味著我們隻要下一個請求發送到目標節點上去。
這個命令是必要的,因為下一個關於哈希槽 8 的請求需要的鍵或許還在節點 A 中,所以我們希望客戶端嚐試在節點 A 中查找,如果需要的話然後在節點 B 中查找。 由於這是發生在 16384 個槽的其中一個槽,所以對於集群的性能影響是在可接受的範圍。
然而我們需要強製客戶端的行為,以確保客戶端會在嚐試 A 中查找後去嚐試在 B 中查找,如果客戶端在發送查詢前發送了 ASKING 命令,那麼節點 B 隻會接受被設為 IMPORTING 的槽的查詢。
基本上ASKING 命令在客戶端設置了一個一次性標識(one-time flag),強製一個節點可以執行一次關於帶有 IMPORTING 狀態的槽的查詢。
所以從客戶端看來,ASK 重定向的完整語義如下:
- 如果接受到 ASK 重定向,發送單次請求重定向到目標節點,接著發送後續的請求到老的節點。
- 先發送 ASKING 命令,再開始發送請求。
- 現在不要更新本地客戶端的映射表,把哈希槽 8 映射到B。
一旦完成了哈希槽 8 的轉移,節點 A 會發送一個 MOVED 消息,客戶端也許會永久地把哈希槽 8 映射到新的 ip:端口號 上。 注意,即使客戶端出現bug,過早地執行這個映射更新,也是沒有問題的,因為它不會在查詢前發送 ASKING 命令,節點 B 會用 MOVED 重定向錯誤把客戶端重定向到節點 A 上。
客戶端首次連接和處理重定向
雖然可以有一個Redis的群集客戶端不在內存中記住哈希槽配置(哈希槽與節點的映射),並隻能通過聯係隨機節點等待被重定向,這樣的客戶端將是效率非常低。
Redis的集群客戶應盡量足夠聰明,記憶哈希槽配置。然而這種配置不必是最新的。因為聯係錯誤的節點隻會導致一個重定向,應當觸發客戶視圖的更新。
客戶通常需要獲取哈希槽與節點映射的完整列表:
- 啟動時保存初始的哈希槽配置.
- 當收到 MOVED重定向.
請注意,客戶可能根據MOVED重定向更新變動的哈希槽,但是這通常不是有效的,因為通常配置中多個哈希槽一起修改(例如,如果一個slave升為master,所有歸屬老master的哈希槽會重新映射) 。更簡單對MOVED重定向做出回應是,重新獲取哈希槽節點映射表。
為了獲取哈希槽配置Redis的群集提供了另一種不需要的解析的命令CLUSTER NODES,並隻僅提供客戶端嚴格需要的信息。
新的命令被稱為CLUSTER SLOTS並槽提供了一組哈希槽範圍,關聯了主從節點服務於指定的哈希槽範圍。
下麵是CLUSTER插槽輸出的例子:
127.0.0.1:7000> cluster slots
1) 1) (integer) 5461
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 7001
4) 1) "127.0.0.1"
2) (integer) 7004
2) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 7000
4) 1) "127.0.0.1"
2) (integer) 7003
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7002
4) 1) "127.0.0.1"
2) (integer) 7005
返回的數組中的每個元素的前兩個子元素是該範圍的始未哈希槽。附加元素表示地址端口對。第一個地址端口對是服務於哈希槽的master,和附加的地址端口對服務於相同槽slave,它不存在錯誤條件(即故障標誌沒有被設置)。
例如,輸出的第一個元素表示,槽從5461至10922(開始和結束包括)由127.0.0.1:7001服務,並且可以通過127.0.0.1:7004水平擴展讀負載。
CLUSTER SLOTS是不能保證返回覆蓋整個16384插槽,如果群集配置不正確範圍,所以客戶初始化哈希槽配置時應當用NULL填充空節點,並報告一個錯誤,如果用戶試圖執行有關鍵的命令屬於未分配的插槽。
當一個哈希槽被發現是未分配之前,返回一個錯誤給調用者之前,客戶應該再次嚐試讀取哈希槽配置,檢查群集現在配置是否正確。
多鍵操作
使用哈希標簽,用戶可以自由地使用多鍵操作。例如下麵的操作是有效的:
MSET {user:1000}.name Angela {user:1000}.surname White
多鍵操作可能變得不可用,當鍵所屬的哈希槽在進行重新分片。
更具體地,即使重新分片期間,多鍵操作目標鍵都存在並且處於相同節點(源或目的地節點)仍然可用。
在重新分片時,操作的鍵不存在或鍵在源節點和目的節點之間,將產生 -TRYAGAIN 錯誤。客戶端可以一段時間後再嚐試操作,或報錯。
隻要指定的哈希槽的遷移已經終止,所有多鍵操作可再次用於該散列槽。
通過slave節點水平擴展讀
通常情況下從節點將客戶端重定向到給定的命令哈希槽對應的master,但是客戶端可以使用READONLY命令讀來水平擴展讀。
READONLY告訴客戶端是允許讀失效的數據並且不關心寫請求。
當連接處於隻讀模式,當操作涉及到不是slave的主節點提供服務的鍵,集群將發送一個重定向到客戶端。這可能發生的原因是:
1.客戶端發送一個命令對應的哈希槽不是由這個slave的master服務。
2.集群重新配置(例如重新分片 )並且slave不再能夠服務於給定的哈希槽命令。
當發生這種情況如前麵部分中說明的,客戶端應該更新其哈希槽映射表。
連接的隻讀狀態可以使用READWRITE命令清除。
容錯性(Fault Tolerance)
節點心跳和 gossip 消息
集群裏的節點不斷地交換 ping / pong 數據包。這兩種數據包有相同的數據結構,都傳輸重要的配置信息 。唯一不同是消息類型字段,我們將提到心跳ping/pong包的總數。
通常節點發送ping包,將觸發接收節點回複pong包。然而,這未必都是這樣的。可能節點隻是發送pong包的配置信息給其他節點,而不會觸發應答。這樣有用處,例如,為了盡快廣播新配置。
通常一個節點每秒會隨機 ping 幾個節點,這樣發送的 ping 包(和接收到的 pong 包)的總數會是一個跟集群節點數量無關的常數。
在過去的一半 NODE_TIMEOUT 時間裏都沒有發送 ping 包過去或接收從那節點發來的 pong 包的節點,會保證去 ping每一個其他節點:。 在 NODE_TIMEOUT 這麼長的時間過去之前,若當前的 TCP 連接有問題,節點會嚐試去重連接,以確保不會被當作不可達的節點。
如果 NODE_TIMEOUT 被設為一個很小的數而節點數量(N)非常大,那麼全局交互的消息會非常多,因為每個節點都會嚐試去 ping 每一個在過去一半 NODE_TIMEOUT 時間裏都沒更新信息的節點。
例如在一個擁有 100 個節點的集群裏,節點超時時限設為 60 秒,每個節點在每 30 秒中會嚐試發送 99 個 ping 包,也就是每秒發送的 ping 包數量是 3.3 個,乘以 100 個節點就是整個集群每秒有 330 個 ping 包。
有一些方法可以降低的消息的數量,但已經出現了與當前使用的Redis集群故障檢測的帶寬沒有報告的問題,所以現在明顯的和直接的設計被使用。注意,即使在上例中,每秒交換被均勻地劃分在100個不同的節點330的數據包,因此每個節點收到的通信量是可接受的。
最後更新:2017-05-22 10:03:02