Redis(開發與運維):50---集羣之(請求路由:請求重定向(MOVED)、ASK重定向)

一、請求重定向

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重定向流程

  • 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個鍵的數據。以上測試能夠成功的前提 是:
  • 綜上所處,使用smart客戶端批量操作集羣時,需要評估mget/mset、 Pipeline等方式在slot遷移場景下的容錯性,防止集羣遷移造成大量錯誤和數 據丟失的情況
  • 開發提示:集羣環境下對於使用批量操作的場景,建議優先使用Pipeline方式,在 客戶端實現對ASK重定向的正確處理,這樣既可以受益於批量操作的IO優 化,又可以兼容slot遷移場景
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章