一、請求重定向
- 關於“CLUSTER KEYLOST命令、MOVED錯誤”的設計與實現可以參閱:https://blog.csdn.net/qq_41453285/article/details/103394429
MOVED重定向
- 概念:在集羣模式下,Redis接收任何鍵相關命令時首先計算鍵對應的槽,再根據槽找出所對應的節點,如果節點是自身,則處理鍵命令;否則回覆MOVED重定向錯誤,通知客戶端請求正確的節點。如下圖所示
演示案例
- 例如,在之前搭建的集羣上執行如下命令:
127.0.0.1:6379> set key:test:1 value-1 OK
- 執行set命令成功,因爲鍵key:test:1對應槽5191正好位於6379節點負責的槽範圍內,可以藉助cluster keyslot {key}命令返回key所對應的槽,如下所示:
127.0.0.1:6379> cluster keyslot key:test:1 (integer) 5191 127.0.0.1:6379> cluster nodes cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 10 connected 1366-4095 4097-5461 12288-13652 ...
- 再執行以下命令,由於鍵對應槽是9252,不屬於6379節點,則回覆MOVED {slot} {ip} {port}格式重定向信息:
127.0.0.1:6379> set key:test:2 value-2 (error) MOVED 9252 127.0.0.1:6380 127.0.0.1:6379> cluster keyslot key:test:2 (integer) 9252
- 重定向信息包含了鍵所對應的槽以及負責該槽的節點地址,根據這些信息客戶端就可以向正確的節點發起請求。在6380節點上成功執行之前的命令:
127.0.0.1:6380> set key:test:2 value-2 OK
- 使用redis-cli命令時,可以加入-c參數支持自動重定向,簡化手動發起重定向操作,如下所示:
#redis-cli -p 6379 -c 127.0.0.1:6379> set key:test:2 value-2 -> Redirected to slot [9252] located at 127.0.0.1:6380 OK
- redis-cli自動幫我們連接到正確的節點執行命令,這個過程是在redis-cli內部維護,實質上是client端接到MOVED信息之後再次發起請求,並不在Redis節點中完成請求轉發,如下圖所示
- 節點對於不屬於它的鍵命令只回復重定向響應,並不負責轉發。熟悉Cassandra的用戶希望在這裏做好區分,不要混淆。正因爲集羣模式下把解析發起重定向的過程放到客戶端完成,所以集羣客戶端協議相對於單機有了很大的變化
- 鍵命令執行步驟主要分兩步:
- 計算槽
- 查找槽所對應的節點
①計算槽
- Redis首先需要計算鍵所對應的槽。根據鍵的有效部分使用CRC16函數計算出散列值,再取對16383的餘數,使每個鍵都可以映射到0~16383槽範圍內。僞代碼如下:
def key_hash_slot(key): int keylen = key.length(); for (s = 0; s < keylen; s++): if (key[s] == '{'): break; if (s == keylen) return crc16(key,keylen) & 16383; for (e = s+1; e < keylen; e++): if (key[e] == '}') break; if (e == keylen || e == s+1) return crc16(key,keylen) & 16383; /* 使用{和}之間的有效部分計算槽 */ return crc16(key+s+1,e-s-1) & 16383;
- 根據僞代碼,如果鍵內容包含{和}大括號字符,則計算槽的有效部分是括號內的內容;否則採用鍵的全內容計算槽
- cluster keyslot命令就是採用key_hash_slot函數實現的,例如:
127.0.0.1:6379> cluster keyslot key:test:111 (integer) 10050 127.0.0.1:6379> cluster keyslot key:{hash_tag}:111 (integer) 2515 127.0.0.1:6379> cluster keyslot key:{hash_tag}:222 (integer) 2515
- 其中鍵內部使用大括號包含的內容又叫做hash_tag,它提供不同的鍵可以具備相同slot的功能,常用於Redis IO優化
- 例如在集羣模式下使用mget等命令優化批量調用時,鍵列表必須具有相同的slot,否則會報錯。這時可以利用hash_tag讓不同的鍵具有相同的slot達到優化的目的。命令如下:
127.0.0.1:6385> mget user:10086:frends user:10086:videos (error) CROSSSLOT Keys in request don't hash to the same slot 127.0.0.1:6385> mget user:{10086}:friends user:{10086}:videos 1) "friends" 2) "videos"
- 開發提示:Pipeline同樣可以受益於hash_tag,由於Pipeline只能向一個節點批量發送執行命令,而相同slot必然會對應到唯一的節點,降低了集羣使用Pipeline的門檻
②槽節點查找
- Redis計算得到鍵對應的槽後,需要查找槽所對應的節點。集羣內通過消息交換每個節點都會知道所有節點的槽信息,內部保存在clusterState結構中,結構所示:
typedef struct clusterState { clusterNode *myself; /* 自身節點,clusterNode代表節點結構體 */ clusterNode *slots[CLUSTER_SLOTS]; /* 16384個槽和節點映射數組,數組下標代表對應的槽 */ ... } clusterState;
- slots數組表示槽和節點對應關係,實現請求重定向僞代碼如下:
def execute_or_redirect(key): int slot = key_hash_slot(key); ClusterNode node = slots[slot]; if(node == clusterState.myself): return executeCommand(key); else: return '(error) MOVED {slot} {node.ip}:{node.port}';
- 根據僞代碼看出節點對於判定鍵命令是執行還是MOVED重定向,都是藉助slots [CLUSTER_SLOTS]數組實現。根據MOVED重定向機制,客戶端可以隨機連接集羣內任一Redis獲取鍵所在節點,這種客戶端又叫Dummy(傀儡)客戶端,它優點是代碼實現簡單,對客戶端協議影響較小,只需要根據 重定向信息再次發送請求即可。但是它的弊端很明顯,每次執行鍵命令前都要到Redis上進行重定向才能找到要執行命令的節點,額外增加了IO開銷, 這不是Redis集羣高效的使用方式。正因爲如此通常集羣客戶端都採用另一 種實現:Smart(智能)客戶端
二、Smart客戶端
待續
三、ASK重定向
- 關於“ASK錯誤、ASKING命令”的設計與實現還可以參閱:https://blog.csdn.net/qq_41453285/article/details/103395224
①客戶端ASK重定向流程
- Redis集羣支持在線遷移槽(slot)和數據來完成水平伸縮,當slot對應的數據從源節點到目標節點遷移過程中,客戶端需要做到智能識別,保證鍵命令可正常執行。例如當一個slot數據從源節點遷移到目標節點時,期間可能出現一部分數據在源節點,而另一部分在目標節點,如下圖所示
- 當出現上述情況時,客戶端鍵命令執行流程將發生變化,如下所示:
- 1)客戶端根據本地slots緩存發送命令到源節點,如果存在鍵對象則直 接執行並返回結果給客戶端
- 2)如果鍵對象不存在,則可能存在於目標節點,這時源節點會回覆 ASK重定向異常。格式如下:(error) ASK {slot} {targetIP}:{targetPort}
- 3)客戶端從ASK重定向異常提取出目標節點信息,發送asking命令到目標節點打開客戶端連接標識,再執行鍵命令。如果存在則執行,不存在則返 回不存在信息
- ASK重定向整體流程如下圖所示:
- ASK與MOVED雖然都是對客戶端的重定向控制,但是有着本質區別:
- ASK重定向說明集羣正在進行slot數據遷移,客戶端無法知道什麼時候遷移 完成,因此只能是臨時性的重定向,客戶端不會更新slots緩存
- 但是MOVED重定向說明鍵對應的槽已經明確指定到新的節點,因此需要更新slots緩存
②節點內部處理
- 爲了支持ASK重定向,源節點和目標節點在內部的clusterState結構中維 護當前正在遷移的槽信息,用於識別槽遷移情況,結構如下:
typedef struct clusterState { clusterNode *myself; /* 自身節點 / clusterNode *slots[CLUSTER_SLOTS]; /* 槽和節點映射數組 */ clusterNode *migrating_slots_to[CLUSTER_SLOTS];/* 正在遷出的槽節點數組 */ clusterNode *importing_slots_from[CLUSTER_SLOTS];/* 正在遷入的槽節點數組*/ ... } clusterState;
- 節點每次接收到鍵命令時,都會根據clusterState內的遷移屬性進行命令 處理,如下所示:
- 如果鍵所在的槽由當前節點負責,但鍵不存在則查找migrating_slots_to 數組查看槽是否正在遷出,如果是返回ASK重定向
- 如果客戶端發送asking命令打開了CLIENT_ASKING標識,則該客戶端 下次發送鍵命令時查找importing_slots_from數組獲取clusterNode,如果指向 自身則執行命令
- 需要注意的是,asking命令是一次性命令,每次執行完後客戶端標識都 會修改回原狀態,因此每次客戶端接收到ASK重定向後都需要發送asking命令
- 批量操作。ASK重定向對單鍵命令支持得很完善,但是,在開發中我 們經常使用批量操作,如mget或pipeline。當槽處於遷移狀態時,批量操作會受到影響
- 例如,手動使用遷移命令讓槽4096處於遷移狀態,並且數據各自分散在 目標節點和源節點,如下所示:
#6379節點準備導入槽4096數據 127.0.0.1:6379>cluster setslot 4096 importing 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 OK #6385節點準備導出槽4096數據 127.0.0.1:6379>cluster setslot 4096 migrating cfb28ef1deee4e0fa78da86abe5d24566744411e OK # 查看槽4096下的數據 127.0.0.1:6385> cluster getkeysinslot 4096 100 1) "key:test:5028" 2) "key:test:68253" 3) "key:test:79212" # 遷移鍵key:test:68253和key:test:79212到6379節點 127.0.0.1:6385>migrate 127.0.0.1 6379 "" 0 5000 keys key:test:68253 key:test:79212 OK
- 現在槽4096下3個鍵數據分別位於6379和6380兩個節點,使用Jedis客戶 端執行批量操作。mget代碼如下:
@Test public void mgetOnAskTest() { JedisCluster jedisCluster = new JedisCluster(new HostAndPort("127.0.0.1", 6379)); List<String> results = jedisCluster.mget("key:test:68253", "key:test:79212"); System.out.println(results); results = jedisCluster.mget("key:test:5028", "key:test:68253", "key:test:79212"); System.out.println(results); }
- 運行mget測試結果如下:
- 第1個mget運行成功,這是因爲鍵key:test:68253,key:test:79212 已經遷移到目標節點,當mget鍵列表都處於源節點/目標節點時,運行成功
- 第2個mget拋出異常,當鍵列表中任何鍵不存在於源節點時,拋出異常
- 綜上所處,當在集羣環境下使用mget、mset等批量操作時,slot遷移數據期間由於鍵列表無法保證在同一節點,會導致大量錯誤
- Pipeline代碼如下:
- Pipeline的代碼中,由於Jedis沒有開放slot到Jedis的查詢,使用了匿名內 部類暴露JedisSlotBasedConnectionHandler。通過Jedis獲取Pipeline對象組合3 條get命令一次發送。運行結果如下:
- 結果分析:返回結果並沒有直接拋出異常,而是把ASK異常 JedisAskDataException包含在結果集中。但是使用Pipeline的批量操作也無法 支持由於slot遷移導致的鍵列表跨節點問題
- 得益於Pipeline並沒有直接拋出異常,可以藉助於JedisAskDataException 內返回的目標節點信息,手動重定向請求給目標節點,修改後的程序如下:
- 修改後的Pipeline運行結果以下:
- 根據結果,我們成功獲取到了3個鍵的數據。以上測試能夠成功的前提 是:
- 1)Pipeline嚴格按照鍵發送的順序返回結果,即使出現異常也是如此 (更多細節見“Pipeline”:https://blog.csdn.net/qq_41453285/article/details/106060154)
- 2)理解ASK重定向之後,可以手動發起ASK流程保證Pipeline的結果正 確性
- 綜上所處,使用smart客戶端批量操作集羣時,需要評估mget/mset、 Pipeline等方式在slot遷移場景下的容錯性,防止集羣遷移造成大量錯誤和數 據丟失的情況
- 開發提示:集羣環境下對於使用批量操作的場景,建議優先使用Pipeline方式,在 客戶端實現對ASK重定向的正確處理,這樣既可以受益於批量操作的IO優 化,又可以兼容slot遷移場景