深入學習Redis(5):叢集

程式設計迷思發表於2018-10-30

前言

在前面的文章中,已經介紹了Redis的幾種高可用技術:持久化、主從複製和哨兵,但這些方案仍有不足,其中最主要的問題是儲存能力受單機限制,以及無法實現寫操作的負載均衡。

Redis叢集解決了上述問題,實現了較為完善的高可用方案。本文將詳細介紹叢集,主要內容包括:叢集的作用;叢集的搭建方法及設計方案;叢集的基本原理;客戶端訪問叢集的方法;以及其他實踐中需要的叢集知識(叢集擴容、故障轉移、引數優化等)

系列文章

深入學習Redis(1):Redis記憶體模型

深入學習Redis(2):持久化

深入學習Redis(3):主從複製

深入學習Redis(4):哨兵

深入學習Redis(5):叢集

目錄

一、叢集的作用

二、叢集的搭建

     1. 執行Redis命令搭建叢集

     2. 使用Ruby指令碼搭建叢集

     3. 叢集方案設計

三、叢集的基本原理

     1. 資料分割槽方案

     2. 節點通訊機制

     3. 資料結構

     4. 叢集命令的實現

四、客戶端訪問叢集

     1. redis-cli

     2. Smart客戶端

五、實踐須知

     1. 叢集伸縮

     2. 故障轉移

     3. 叢集的限制及應對方法

     4. Hash Tag

     5. 引數優化

     6. redis-trib.rb

參考文獻

一、叢集的作用

叢集,即Redis Cluster,是Redis 3.0開始引入的分散式儲存方案。

叢集由多個節點(Node)組成,Redis的資料分佈在這些節點中。叢集中的節點分為主節點和從節點:只有主節點負責讀寫請求和叢集資訊的維護;從節點只進行主節點資料和狀態資訊的複製。

叢集的作用,可以歸納為兩點:

1、資料分割槽:資料分割槽(或稱資料分片)是叢集最核心的功能。

叢集將資料分散到多個節點,一方面突破了Redis單機記憶體大小的限制,儲存容量大大增加;另一方面每個主節點都可以對外提供讀服務和寫服務,大大提高了叢集的響應能力。

Redis單機記憶體大小受限問題,在介紹持久化和主從複製時都有提及;例如,如果單機記憶體太大,bgsave和bgrewriteaof的fork操作可能導致主程式阻塞,主從環境下主機切換時可能導致從節點長時間無法提供服務,全量複製階段主節點的複製緩衝區可能溢位……。

2、高可用:叢集支援主從複製和主節點的自動故障轉移(與哨兵類似);當任一節點發生故障時,叢集仍然可以對外提供服務。

本文內容基於Redis 3.0.6。

二、叢集的搭建

這一部分我們將搭建一個簡單的叢集:共6個節點,3主3從。方便起見:所有節點在同一臺伺服器上,以埠號進行區分;配置從簡。3個主節點埠號:7000/7001/7002,對應的從節點埠號:8000/8001/8002。

叢集的搭建有兩種方式:(1)手動執行Redis命令,一步步完成搭建;(2)使用Ruby指令碼搭建。二者搭建的原理是一樣的,只是Ruby指令碼將Redis命令進行了打包封裝;在實際應用中推薦使用指令碼方式,簡單快捷不容易出錯。下面分別介紹這兩種方式。

1. 執行Redis命令搭建叢集

叢集的搭建可以分為四步:(1)啟動節點:將節點以叢集模式啟動,此時節點是獨立的,並沒有建立聯絡;(2)節點握手:讓獨立的節點連成一個網路;(3)分配槽:將16384個槽分配給主節點;(4)指定主從關係:為從節點指定主節點。

實際上,前三步完成後叢集便可以對外提供服務;但指定從節點後,叢集才能夠提供真正高可用的服務。

(1)啟動節點

叢集節點的啟動仍然是使用redis-server命令,但需要使用叢集模式啟動。下面是7000節點的配置檔案(只列出了節點正常工作關鍵配置,其他配置(如開啟AOF)可以參照單機節點進行):

#redis-7000.conf
port 7000
cluster-enabled yes
cluster-config-file "node-7000.conf"
logfile "log-7000.log"
dbfilename "dump-7000.rdb"
daemonize yes

其中的cluster-enabled和cluster-config-file是與叢集相關的配置。

cluster-enabled yesRedis例項可以分為單機模式(standalone)和叢集模式(cluster);cluster-enabled yes可以啟動叢集模式。在單機模式下啟動的Redis例項,如果執行info server命令,可以發現redis_mode一項為standalone,如下圖所示:

叢集模式下的節點,其redis_mode為cluster,如下圖所示:

cluster-config-file該引數指定了叢集配置檔案的位置。每個節點在執行過程中,會維護一份叢集配置檔案;每當叢集資訊發生變化時(如增減節點),叢集內所有節點會將最新資訊更新到該配置檔案;當節點重啟後,會重新讀取該配置檔案,獲取叢集資訊,可以方便的重新加入到叢集中。也就是說,當Redis節點以叢集模式啟動時,會首先尋找是否有叢集配置檔案,如果有則使用檔案中的配置啟動,如果沒有,則初始化配置並將配置儲存到檔案中。叢集配置檔案由Redis節點維護,不需要人工修改。

編輯好配置檔案後,使用redis-server命令啟動該節點:

redis-server redis-7000.conf

節點啟動以後,通過cluster nodes命令可以檢視節點的情況,如下圖所示。

其中返回值第一項表示節點id,由40個16進位制字串組成,節點id與 主從複製 一文中提到的runId不同:Redis每次啟動runId都會重新建立,但是節點id只在叢集初始化時建立一次,然後儲存到叢集配置檔案中,以後節點重新啟動時會直接在叢集配置檔案中讀取。

其他節點使用相同辦法啟動,不再贅述。需要特別注意,在啟動節點階段,節點是沒有主從關係的,因此從節點不需要加slaveof配置。

(2)節點握手

節點啟動以後是相互獨立的,並不知道其他節點存在;需要進行節點握手,將獨立的節點組成一個網路。

節點握手使用cluster meet {ip} {port}命令實現,例如在7000節點中執行cluster meet 192.168.72.128 7001,可以完成7000節點和7001節點的握手;注意ip使用的是區域網ip而不是localhost或127.0.0.1,是為了其他機器上的節點或客戶端也可以訪問。此時再使用cluster nodes檢視:

在7001節點下也可以類似檢視:

同理,在7000節點中使用cluster meet命令,可以將所有節點加入到叢集,完成節點握手:

cluster meet 192.168.72.128 7002
cluster meet 192.168.72.128 8000
cluster meet 192.168.72.128 8001
cluster meet 192.168.72.128 8002

執行完上述命令後,可以看到7000節點已經感知到了所有其他節點:

通過節點之間的通訊,每個節點都可以感知到所有其他節點,以8000節點為例:

(3)分配槽

在Redis叢集中,藉助槽實現資料分割槽,具體原理後文會介紹。叢集有16384個槽,槽是資料管理和遷移的基本單位。當資料庫中的16384個槽都分配了節點時,叢集處於上線狀態(ok);如果有任意一個槽沒有分配節點,則叢集處於下線狀態(fail)。

cluster info命令可以檢視叢集狀態,分配槽之前狀態為fail:

分配槽使用cluster addslots命令,執行下面的命令將槽(編號0-16383)全部分配完畢:

redis-cli -p 7000 cluster addslots {0..5461}
redis-cli -p 7001 cluster addslots {5462..10922}
redis-cli -p 7002 cluster addslots {10923..16383}

此時檢視叢集狀態,顯示所有槽分配完畢,叢集進入上線狀態:

(4)指定主從關係

叢集中指定主從關係不再使用slaveof命令,而是使用cluster replicate命令;引數使用節點id。

通過cluster nodes獲得幾個主節點的節點id後,執行下面的命令為每個從節點指定主節點:

redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1

此時執行cluster nodes檢視各個節點的狀態,可以看到主從關係已經建立。

至此,叢集搭建完畢。

2. 使用Ruby指令碼搭建叢集

在{REDIS_HOME}/src目錄下可以看到redis-trib.rb檔案,這是一個Ruby指令碼,可以實現自動化的叢集搭建。

(1)安裝Ruby環境

以Ubuntu為例,如下操作即可安裝Ruby環境:

apt-get install ruby #安裝ruby環境
gem install redis #gem是ruby的包管理工具,該命令可以安裝ruby-redis依賴

(2)啟動節點

與第一種方法中的“啟動節點”完全相同。

(3)搭建叢集

redis-trib.rb指令碼提供了眾多命令,其中create用於搭建叢集,使用方法如下:

./redis-trib.rb create --replicas 1 192.168.72.128:7000 192.168.72.128:7001 192.168.72.128:7002 192.168.72.128:8000 192.168.72.128:8001 192.168.72.128:8002

其中:--replicas=1表示每個主節點有1個從節點;後面的多個{ip:port}表示節點地址,前面的做主節點,後面的做從節點。使用redis-trib.rb搭建叢集時,要求節點不能包含任何槽和資料。

執行建立命令後,指令碼會給出建立叢集的計劃,如下圖所示;計劃包括哪些是主節點,哪些是從節點,以及如何分配槽。

輸入yes確認執行計劃,指令碼便開始按照計劃執行,如下圖所示。

至此,叢集搭建完畢。

3. 叢集方案設計

設計叢集方案時,至少要考慮以下因素:

(1)高可用要求:根據故障轉移的原理,至少需要3個主節點才能完成故障轉移,且3個主節點不應在同一臺物理機上;每個主節點至少需要1個從節點,且主從節點不應在一臺物理機上;因此高可用叢集至少包含6個節點。

(2)資料量和訪問量:估算應用需要的資料量和總訪問量(考慮業務發展,留有冗餘),結合每個主節點的容量和能承受的訪問量(可以通過benchmark得到較準確估計),計算需要的主節點數量。

(3)節點數量限制:Redis官方給出的節點數量限制為1000,主要是考慮節點間通訊帶來的消耗。在實際應用中應儘量避免大叢集;如果節點數量不足以滿足應用對Redis資料量和訪問量的要求,可以考慮:(1)業務分割,大叢集分為多個小叢集;(2)減少不必要的資料;(3)調整資料過期策略等。

(4)適度冗餘:Redis可以在不影響叢集服務的情況下增加節點,因此節點數量適當冗餘即可,不用太大。

三、叢集的基本原理

上一章介紹了叢集的搭建方法和設計方案,下面將進一步深入,介紹叢集的原理。叢集最核心的功能是資料分割槽,因此首先介紹資料的分割槽規則;然後介紹叢集實現的細節:通訊機制和資料結構;最後以cluster meet(節點握手)、cluster addslots(槽分配)為例,說明節點是如何利用上述資料結構和通訊機制實現叢集命令的。

1. 資料分割槽方案

資料分割槽有順序分割槽、雜湊分割槽等,其中雜湊分割槽由於其天然的隨機性,使用廣泛;叢集的分割槽方案便是雜湊分割槽的一種。

雜湊分割槽的基本思路是:對資料的特徵值(如key)進行雜湊,然後根據雜湊值決定資料落在哪個節點。常見的雜湊分割槽包括:雜湊取餘分割槽、一致性雜湊分割槽、帶虛擬節點的一致性雜湊分割槽等。

衡量資料分割槽方法好壞的標準有很多,其中比較重要的兩個因素是(1)資料分佈是否均勻(2)增加或刪減節點對資料分佈的影響。由於雜湊的隨機性,雜湊分割槽基本可以保證資料分佈均勻;因此在比較雜湊分割槽方案時,重點要看增減節點對資料分佈的影響。

(1)雜湊取餘分割槽

雜湊取餘分割槽思路非常簡單:計算key的hash值,然後對節點數量進行取餘,從而決定資料對映到哪個節點上。該方案最大的問題是,當新增或刪減節點時,節點數量發生變化,系統中所有的資料都需要重新計算對映關係,引發大規模資料遷移。

(2)一致性雜湊分割槽

一致性雜湊演算法將整個雜湊值空間組織成一個虛擬的圓環,如下圖所示,範圍為0-2^32-1;對於每個資料,根據key計算hash值,確定資料在環上的位置,然後從此位置沿環順時針行走,找到的第一臺伺服器就是其應該對映到的伺服器。

圖片來源:https://www.cnblogs.com/lpfuture/p/5796398.html

與雜湊取餘分割槽相比,一致性雜湊分割槽將增減節點的影響限制在相鄰節點。以上圖為例,如果在node1和node2之間增加node5,則只有node2中的一部分資料會遷移到node5;如果去掉node2,則原node2中的資料只會遷移到node4中,只有node4會受影響。

一致性雜湊分割槽的主要問題在於,當節點數量較少時,增加或刪減節點,對單個節點的影響可能很大,造成資料的嚴重不平衡。還是以上圖為例,如果去掉node2,node4中的資料由總資料的1/4左右變為1/2左右,與其他節點相比負載過高。

(3)帶虛擬節點的一致性雜湊分割槽

該方案在一致性雜湊分割槽的基礎上,引入了虛擬節點的概念。Redis叢集使用的便是該方案,其中的虛擬節點稱為槽(slot)。槽是介於資料和實際節點之間的虛擬概念;每個實際節點包含一定數量的槽,每個槽包含雜湊值在一定範圍內的資料。引入槽以後,資料的對映關係由資料hash->實際節點,變成了資料hash->槽->實際節點。

在使用了槽的一致性雜湊分割槽中,槽是資料管理和遷移的基本單位。槽解耦了資料和實際節點之間的關係,增加或刪除節點對系統的影響很小。仍以上圖為例,系統中有4個實際節點,假設為其分配16個槽(0-15); 槽0-3位於node1,4-7位於node2,以此類推。如果此時刪除node2,只需要將槽4-7重新分配即可,例如槽4-5分配給node1,槽6分配給node3,槽7分配給node4;可以看出刪除node2後,資料在其他節點的分佈仍然較為均衡。

槽的數量一般遠小於2^32,遠大於實際節點的數量;在Redis叢集中,槽的數量為16384。

下面這張圖很好的總結了Redis叢集將資料對映到實際節點的過程:

圖片來源:https://blog.csdn.net/yejingtao703/article/details/78484151

(1)Redis對資料的特徵值(一般是key)計算雜湊值,使用的演算法是CRC16。

(2)根據雜湊值,計算資料屬於哪個槽。

(3)根據槽與節點的對映關係,計算資料屬於哪個節點。

2. 節點通訊機制

叢集要作為一個整體工作,離不開節點之間的通訊。

兩個埠

在哨兵系統中,節點分為資料節點和哨兵節點:前者儲存資料,後者實現額外的控制功能。在叢集中,沒有資料節點與非資料節點之分:所有的節點都儲存資料,也都參與叢集狀態的維護。為此,叢集中的每個節點,都提供了兩個TCP埠:

  • 普通埠:即我們在前面指定的埠(7000等)。普通埠主要用於為客戶端提供服務(與單機節點類似);但在節點間資料遷移時也會使用。
  • 叢集埠:埠號是普通埠+10000(10000是固定值,無法改變),如7000節點的叢集埠為17000。叢集埠只用於節點之間的通訊,如搭建叢集、增減節點、故障轉移等操作時節點間的通訊;不要使用客戶端連線叢集介面。為了保證叢集可以正常工作,在配置防火牆時,要同時開啟普通埠和叢集埠。

Gossip協議

節點間通訊,按照通訊協議可以分為幾種型別:單對單、廣播、Gossip協議等。重點是廣播和Gossip的對比。

廣播是指向叢集內所有節點傳送訊息;優點是叢集的收斂速度快(叢集收斂是指叢集內所有節點獲得的叢集資訊是一致的),缺點是每條訊息都要傳送給所有節點,CPU、頻寬等消耗較大。

Gossip協議的特點是:在節點數量有限的網路中,每個節點都“隨機”的與部分節點通訊(並不是真正的隨機,而是根據特定的規則選擇通訊的節點),經過一番雜亂無章的通訊,每個節點的狀態很快會達到一致。Gossip協議的優點有負載(比廣播)低、去中心化、容錯性高(因為通訊有冗餘)等;缺點主要是叢集的收斂速度慢。

訊息型別

叢集中的節點採用固定頻率(每秒10次)的定時任務進行通訊相關的工作:判斷是否需要傳送訊息及訊息型別、確定接收節點、傳送訊息等。如果叢集狀態發生了變化,如增減節點、槽狀態變更,通過節點間的通訊,所有節點會很快得知整個叢集的狀態,使叢集收斂。

節點間傳送的訊息主要分為5種:meet訊息、ping訊息、pong訊息、fail訊息、publish訊息。不同的訊息型別,通訊協議、傳送的頻率和時機、接收節點的選擇等是不同的。

  • MEET訊息:在節點握手階段,當節點收到客戶端的CLUSTER MEET命令時,會向新加入的節點傳送MEET訊息,請求新節點加入到當前叢集;新節點收到MEET訊息後會回覆一個PONG訊息。
  • PING訊息:叢集裡每個節點每秒鐘會選擇部分節點傳送PING訊息,接收者收到訊息後會回覆一個PONG訊息。PING訊息的內容是自身節點和部分其他節點的狀態資訊;作用是彼此交換資訊,以及檢測節點是否線上。PING訊息使用Gossip協議傳送,接收節點的選擇兼顧了收斂速度和頻寬成本,具體規則如下:(1)隨機找5個節點,在其中選擇最久沒有通訊的1個節點(2)掃描節點列表,選擇最近一次收到PONG訊息時間大於cluster_node_timeout/2的所有節點,防止這些節點長時間未更新。
  • PONG訊息:PONG訊息封裝了自身狀態資料。可以分為兩種:第一種是在接到MEET/PING訊息後回覆的PONG訊息;第二種是指節點向叢集廣播PONG訊息,這樣其他節點可以獲知該節點的最新資訊,例如故障恢復後新的主節點會廣播PONG訊息。
  • FAIL訊息:當一個主節點判斷另一個主節點進入FAIL狀態時,會向叢集廣播這一FAIL訊息;接收節點會將這一FAIL訊息儲存起來,便於後續的判斷。
  • PUBLISH訊息:節點收到PUBLISH命令後,會先執行該命令,然後向叢集廣播這一訊息,接收節點也會執行該PUBLISH命令。

3. 資料結構

節點需要專門的資料結構來儲存叢集的狀態。所謂叢集的狀態,是一個比較大的概念,包括:叢集是否處於上線狀態、叢集中有哪些節點、節點是否可達、節點的主從狀態、槽的分佈……

節點為了儲存叢集狀態而提供的資料結構中,最關鍵的是clusterNode和clusterState結構:前者記錄了一個節點的狀態,後者記錄了叢集作為一個整體的狀態。

clusterNode

clusterNode結構儲存了一個節點的當前狀態,包括建立時間、節點id、ip和埠號等。每個節點都會用一個clusterNode結構記錄自己的狀態,併為叢集內所有其他節點都建立一個clusterNode結構來記錄節點狀態。

下面列舉了clusterNode的部分欄位,並說明了欄位的含義和作用:

typedef struct clusterNode {
    //節點建立時間
    mstime_t ctime;

    //節點id
    char name[REDIS_CLUSTER_NAMELEN];

    //節點的ip和埠號
    char ip[REDIS_IP_STR_LEN];
    int port;

    //節點標識:整型,每個bit都代表了不同狀態,如節點的主從狀態、是否線上、是否在握手等
    int flags;

    //配置紀元:故障轉移時起作用,類似於哨兵的配置紀元
    uint64_t configEpoch;

    //槽在該節點中的分佈:佔用16384/8個位元組,16384個位元;每個位元對應一個槽:位元值為1,則該位元對應的槽在節點中;位元值為0,則該位元對應的槽不在節點中
    unsigned char slots[16384/8];

    //節點中槽的數量
    int numslots;

    …………

} clusterNode;

除了上述欄位,clusterNode還包含節點連線、主從複製、故障發現和轉移需要的資訊等。

clusterState

clusterState結構儲存了在當前節點視角下,叢集所處的狀態。主要欄位包括:

typedef struct clusterState {

    //自身節點
    clusterNode *myself;

    //配置紀元
    uint64_t currentEpoch;

    //叢集狀態:線上還是下線
    int state;

    //叢集中至少包含一個槽的節點數量
    int size;

    //雜湊表,節點名稱->clusterNode節點指標
    dict *nodes;
 
    //槽分佈資訊:陣列的每個元素都是一個指向clusterNode結構的指標;如果槽還沒有分配給任何節點,則為NULL
    clusterNode *slots[16384];

    …………
    
} clusterState;

除此之外,clusterState還包括故障轉移、槽遷移等需要的資訊。

4. 叢集命令的實現

這一部分將以cluster meet(節點握手)、cluster addslots(槽分配)為例,說明節點是如何利用上述資料結構和通訊機制實現叢集命令的。

cluster meet

假設要向A節點傳送cluster meet命令,將B節點加入到A所在的叢集,則A節點收到命令後,執行的操作如下:

1)  A為B建立一個clusterNode結構,並將其新增到clusterState的nodes字典中

2)  A向B傳送MEET訊息

3)  B收到MEET訊息後,會為A建立一個clusterNode結構,並將其新增到clusterState的nodes字典中

4)  B回覆A一個PONG訊息

5)  A收到B的PONG訊息後,便知道B已經成功接收自己的MEET訊息

6)  然後,A向B返回一個PING訊息

7)  B收到A的PING訊息後,便知道A已經成功接收自己的PONG訊息,握手完成

8)  之後,A通過Gossip協議將B的資訊廣播給叢集內其他節點,其他節點也會與B握手;一段時間後,叢集收斂,B成為叢集內的一個普通節點

通過上述過程可以發現,叢集中兩個節點的握手過程與TCP類似,都是三次握手:A向B傳送MEET;B向A傳送PONG;A向B傳送PING。

cluster addslots

叢集中槽的分配資訊,儲存在clusterNode的slots陣列和clusterState的slots陣列中,兩個陣列的結構前面已做介紹;二者的區別在於:前者儲存的是該節點中分配了哪些槽,後者儲存的是叢集中所有槽分別分佈在哪個節點。

cluster addslots命令接收一個槽或多個槽作為引數,例如在A節點上執行cluster addslots {0..10}命令,是將編號為0-10的槽分配給A節點,具體執行過程如下:

1)  遍歷輸入槽,檢查它們是否都沒有分配,如果有一個槽已分配,命令執行失敗;方法是檢查輸入槽在clusterState.slots[]中對應的值是否為NULL。

2)  遍歷輸入槽,將其分配給節點A;方法是修改clusterNode.slots[]中對應的位元為1,以及clusterState.slots[]中對應的指標指向A節點

3)  A節點執行完成後,通過節點通訊機制通知其他節點,所有節點都會知道0-10的槽分配給了A節點

四、客戶端訪問叢集

在叢集中,資料分佈在不同的節點中,客戶端通過某節點訪問資料時,資料可能不在該節點中;下面介紹叢集是如何處理這個問題的。

1. redis-cli

當節點收到redis-cli發來的命令(如set/get)時,過程如下:

(1)計算key屬於哪個槽:CRC16(key) & 16383

叢集提供的cluster keyslot命令也是使用上述公式實現,如:

(2)判斷key所在的槽是否在當前節點:假設key位於第i個槽,clusterState.slots[i]則指向了槽所在的節點,如果clusterState.slots[i]==clusterState.myself,說明槽在當前節點,可以直接在當前節點執行命令;否則,說明槽不在當前節點,則查詢槽所在節點的地址(clusterState.slots[i].ip/port),並將其包裝到MOVED錯誤中返回給redis-cli。

(3)redis-cli收到MOVED錯誤後,根據返回的ip和port重新傳送請求。

下面的例子展示了redis-cli和叢集的互動過程:在7000節點中操作key1,但key1所在的槽9189在節點7001中,因此節點返回MOVED錯誤(包含7001節點的ip和port)給redis-cli,redis-cli重新向7001發起請求。

上例中,redis-cli通過-c指定了叢集模式,如果沒有指定,redis-cli無法處理MOVED錯誤:

2. Smart客戶端

redis-cli這一類客戶端稱為Dummy客戶端,因為它們在執行命令前不知道資料在哪個節點,需要藉助MOVED錯誤重新定向。與Dummy客戶端相對應的是Smart客戶端。

Smart客戶端(以Java的JedisCluster為例)的基本原理:

(1)JedisCluster初始化時,在內部維護slot->node的快取,方法是連線任一節點,執行cluster slots命令,該命令返回如下所示:

(2)此外,JedisCluster為每個節點建立連線池(即JedisPool)。

(3)當執行命令時,JedisCluster根據key->slot->node選擇需要連線的節點,傳送命令。如果成功,則命令執行完畢。如果執行失敗,則會隨機選擇其他節點進行重試,並在出現MOVED錯誤時,使用cluster slots重新同步slot->node的對映關係。

下面程式碼演示瞭如何使用JedisCluster訪問叢集(未考慮資源釋放、異常處理等):

public static void test() {
   Set<HostAndPort> nodes = new HashSet<>();
   nodes.add(new HostAndPort("192.168.72.128", 7000));
   nodes.add(new HostAndPort("192.168.72.128", 7001));
   nodes.add(new HostAndPort("192.168.72.128", 7002));
   nodes.add(new HostAndPort("192.168.72.128", 8000));
   nodes.add(new HostAndPort("192.168.72.128", 8001));
   nodes.add(new HostAndPort("192.168.72.128", 8002));
   JedisCluster cluster = new JedisCluster(nodes);
   System.out.println(cluster.get("key1"));
   cluster.close();
}

注意事項如下:

(1)JedisCluster中已經包含所有節點的連線池,因此JedisCluster要使用單例。

(2)客戶端維護了slot->node對映關係以及為每個節點建立了連線池,當節點數量較多時,應注意客戶端記憶體資源和連線資源的消耗。

(3)Jedis較新版本針對JedisCluster做了一些效能方面的優化,如cluster slots快取更新和鎖阻塞等方面的優化,應儘量使用2.8.2及以上版本的Jedis。

五、實踐須知

前面介紹了叢集正常執行和訪問的方法和原理,下面是一些重要的補充內容。

1. 叢集伸縮

實踐中常常需要對叢集進行伸縮,如訪問量增大時的擴容操作。Redis叢集可以在不影響對外服務的情況下實現伸縮;伸縮的核心是槽遷移:修改槽與節點的對應關係,實現槽(即資料)在節點之間的移動。例如,如果槽均勻分佈在叢集的3個節點中,此時增加一個節點,則需要從3個節點中分別拿出一部分槽給新節點,從而實現槽在4個節點中的均勻分佈。

增加節點

假設要增加7003和8003節點,其中8003是7003的從節點;步驟如下:

(1)啟動節點:方法參見叢集搭建

(2)節點握手:可以使用cluster meet命令,但在生產環境中建議使用redis-trib.rb的add-node工具,其原理也是cluster meet,但它會先檢查新節點是否已加入其它叢集或者存在資料,避免加入到叢集后帶來混亂。

redis-trib.rb add-node 192.168.72.128:7003 192.168.72.128 7000
redis-trib.rb add-node 192.168.72.128:8003 192.168.72.128 7000

(3)遷移槽:推薦使用redis-trib.rb的reshard工具實現。reshard自動化程度很高,只需要輸入redis-trib.rb reshard ip:port (ip和port可以是叢集中的任一節點),然後按照提示輸入以下資訊,槽遷移會自動完成:

    • 待遷移的槽數量:16384個槽均分給4個節點,每個節點4096個槽,因此待遷移槽數量為4096
    • 目標節點id:7003節點的id
    • 源節點的id:7000/7001/7002節點的id

(4)指定主從關係:方法參見叢集搭建

減少節點

假設要下線7000/8000節點,可以分為兩步:

(1)遷移槽:使用reshard將7000節點中的槽均勻遷移到7001/7002/7003節點

(2)下線節點:使用redis-trib.rb del-node工具;應先下線從節點再下線主節點,因為若主節點先下線,從節點會被指向其他主節點,造成不必要的全量複製。

redis-trib.rb del-node 192.168.72.128:7001 {節點8000的id}
redis-trib.rb del-node 192.168.72.128:7001 {節點7000的id}

ASK錯誤

叢集伸縮的核心是槽遷移。在槽遷移過程中,如果客戶端向源節點傳送命令,源節點執行流程如下:

圖片來源:《Redis設計與實現》

客戶端收到ASK錯誤後,從中讀取目標節點的地址資訊,並向目標節點重新傳送請求,就像收到MOVED錯誤時一樣。但是二者有很大區別:ASK錯誤說明資料正在遷移,不知道何時遷移完成,因此重定向是臨時的,SMART客戶端不會重新整理slots快取;MOVED錯誤重定向則是(相對)永久的,SMART客戶端會重新整理slots快取。

2. 故障轉移

在 哨兵 一文中,介紹了哨兵實現故障發現和故障轉移的原理。雖然細節上有很大不同,但叢集的實現與哨兵思路類似:通過定時任務傳送PING訊息檢測其他節點狀態;節點下線分為主觀下線和客觀下線;客觀下線後選取從節點進行故障轉移。

與哨兵一樣,叢集只實現了主節點的故障轉移;從節點故障時只會被下線,不會進行故障轉移。因此,使用叢集時,應謹慎使用讀寫分離技術,因為從節點故障會導致讀服務不可用,可用性變差。

這裡不再詳細介紹故障轉移的細節,只對重要事項進行說明:

節點數量:在故障轉移階段,需要由主節點投票選出哪個從節點成為新的主節點;從節點選舉勝出需要的票數為N/2+1;其中N為主節點數量(包括故障主節點),但故障主節點實際上不能投票。因此為了能夠在故障發生時順利選出從節點,叢集中至少需要3個主節點(且部署在不同的物理機上)。

故障轉移時間:從主節點故障發生到完成轉移,所需要的時間主要消耗在主觀下線識別、主觀下線傳播、選舉延遲等幾個環節;具體時間與引數cluster-node-timeout有關,一般來說:

故障轉移時間(毫秒) ≤ 1.5 * cluster-node-timeout + 1000

cluster-node-timeout的預設值為15000ms(15s),因此故障轉移時間會在20s量級。

3. 叢集的限制及應對方法

由於叢集中的資料分佈在不同節點中,導致一些功能受限,包括:

(1)key批量操作受限:例如mget、mset操作,只有當操作的key都位於一個槽時,才能進行。針對該問題,一種思路是在客戶端記錄槽與key的資訊,每次針對特定槽執行mget/mset;另外一種思路是使用Hash Tag,將在下一小節介紹。

(2)keys/flushall等操作:keys/flushall等操作可以在任一節點執行,但是結果只針對當前節點,例如keys操作只返回當前節點的所有鍵。針對該問題,可以在客戶端使用cluster nodes獲取所有節點資訊,並對其中的所有主節點執行keys/flushall等操作。

(3)事務/Lua指令碼:叢集支援事務及Lua指令碼,但前提條件是所涉及的key必須在同一個節點。Hash Tag可以解決該問題。

(4)資料庫:單機Redis節點可以支援16個資料庫,叢集模式下只支援一個,即db0。

(5)複製結構:只支援一層複製結構,不支援巢狀。

4. Hash Tag

Hash Tag原理是:當一個key包含 {} 的時候,不對整個key做hash,而僅對 {} 包括的字串做hash

Hash Tag可以讓不同的key擁有相同的hash值,從而分配在同一個槽裡;這樣針對不同key的批量操作(mget/mset等),以及事務、Lua指令碼等都可以支援。不過Hash Tag可能會帶來資料分配不均的問題,這時需要:(1)調整不同節點中槽的數量,使資料分佈儘量均勻;(2)避免對熱點資料使用Hash Tag,導致請求分佈不均。

下面是使用Hash Tag的一個例子;通過對product加Hash Tag,可以將所有產品資訊放到同一個槽中,便於操作。

5. 引數優化

cluster_node_timeout

cluster_node_timeout引數在前面已經初步介紹;它的預設值是15s,影響包括:

(1)影響PING訊息接收節點的選擇:值越大對延遲容忍度越高,選擇的接收節點越少,可以降低頻寬,但會降低收斂速度;應根據頻寬情況和應用要求進行調整。

(2)影響故障轉移的判定和時間:值越大,越不容易誤判,但完成轉移消耗時間越長;應根據網路狀況和應用要求進行調整。

cluster-require-full-coverage

前面提到,只有當16384個槽全部分配完畢時,叢集才能上線。這樣做是為了保證叢集的完整性,但同時也帶來了新的問題:當主節點發生故障而故障轉移尚未完成,原主節點中的槽不在任何節點中,此時會叢集處於下線狀態,無法響應客戶端的請求。

cluster-require-full-coverage引數可以改變這一設定:如果設定為no,則當槽沒有完全分配時,叢集仍可以上線。引數預設值為yes,如果應用對可用性要求較高,可以修改為no,但需要自己保證槽全部分配。

6. redis-trib.rb

redis-trib.rb提供了眾多實用工具:建立叢集、增減節點、槽遷移、檢查完整性、資料重新平衡等;通過help命令可以檢視詳細資訊。在實踐中如果能使用redis-trib.rb工具則儘量使用,不但方便快捷,還可以大大降低出錯概率。

參考文獻

《Redis開發與運維》

《Redis設計與實現》

https://redis.io/topics/cluster-tutorial

https://redis.io/topics/cluster-spec

https://mp.weixin.qq.com/s/d6hzmk31o7VBsMYaLdQ5mw

https://www.cnblogs.com/lpfuture/p/5796398.html

http://www.zsythink.net/archives/1182/

https://www.cnblogs.com/xxdfly/p/5641719.html

 

創作不易,如果文章對你有幫助,就點個贊、評個論唄~

創作不易,如果文章對你有幫助,就點個贊、評個論唄~

創作不易,如果文章對你有幫助,就點個贊、評個論唄~

 

相關文章