天天看點

Redis之叢集

  Redis Cluster是 Redis的分布式解決方案,在3.0版本正式推出,有效地解決了Redis分布式方面的需求。當遇到單機記憶體、并發、流量等瓶頸時,可以采用Cluster架構方案達到負載均衡的目的。之前,Redis分布式方案一般有兩種:

□ 用戶端分區方案,優點是分區邏輯可控,缺點是需要自己處理資料路由、高可用、故障轉移等問題。

□ 代理方案,優點是簡化用戶端分布式邏輯和更新維護便利,缺點是加重架構部署複雜度和性能損耗。

  現在官方為我們提供了專有的叢集方案:Redis Cluster, 它非常優雅地解決了 Redis叢集方面的問題,是以了解應用好Redis Cluster将極大地解放我們使用分布式Redis的工作量,同時它也是學習分布式存儲的絕佳案例。

  本章将從資料分布、搭建叢集、節點通信、叢集伸縮、請求路由、故障轉移、叢集運維幾個方面介紹Redis Cluster。

1.資料分布

1.1 資料分布理論

  分布式資料庫首先要解決把整個資料集按照分區規則映射到多個節點的問題,即把資料集劃分到多個節點上,每個節點負責整體資料的一個子集。如圖10-1所示。需要重點關注的是資料分區規則。常見的分區規則有哈希分區和順序分區兩種,表10-1對這兩種分區規則進行了對比。

Redis之叢集
表10-1哈希分區和順序分區對比
分區方式 特點 代表産品
哈希分區

離散度好

資料分布業務無關

無法順序通路

Redis Cluster

Cassandra

Dynamo

順序分區

離散度易傾斜

資料分布業務相關

可順序通路

Bigtable

HBase

Hypertable

由于Redis Cluster采用哈希分區規則,這裡我們重點讨論哈希分區,常見的哈希分區規則有幾種,下面分别介紹。

1.節點取餘分區

  使用特定的資料,如 Redis的鍵或使用者ID,再根據節點數量N使用公式:hash (key) %N計算出哈希值,用來決定資料映射到哪一個節點上。這種方案存在一個問題:當節點數量變化時,如擴容或收縮節點,資料節點映射關系需要重新計算,會導緻資料的重新遷移。這種方式的突出優點是簡單性,常用于資料庫的分庫分表規則,一般采用預分區的方式,提前根據資料量規劃好分區數,比如劃分為512或 1024張表,保證可支撐未來一段時間的資料量,再根據負載情況将表遷移到其他資料庫中。擴容時通常采用翻倍擴容,避免資料映射全部被打亂導緻全量遷移的情況,如圖10-2所示。

Redis之叢集

2.一緻性哈希分區

一緻性哈希分區(Distributed Hash Table) 實作思路是為系統中每個節點配置設定一個token,範圍一般在0~232,這些token構成一個哈希環。資料讀寫執行節點查找操作時,先根據 key計算hash值,然後順時針找到第一個大于等于該哈希值的token節點,如圖 10-3所示。

Redis之叢集

  這種方式相比節點取餘最大的好處在于加入和删除節點隻影響哈希環中相鄰的節點,對其他節點無影響。但一緻性哈希分區存在幾個問題:

□ 加減節點會造成哈希環中部分資料無法命中,需要手動處理或者忽略這部分資料,是以一緻性哈希常用于緩存場景。

□ 當使用少量節點時,節點變化将大範圍影響哈希環中資料映射,是以這種方式不适合少量資料節點的分布式方案。

□ 普通的一緻性哈希分區在增減節點時需要增加一倍或減去一半節點才能保證資料和負載的均衡。

正因為一緻性哈希分區的這些缺點 ,一些分布式系統采用虛拟槽對一緻性哈希進行改進,比如Dynamo系統。

3.虛拟槽分區

  虛拟槽分區巧妙地使用了哈希空間,使用分散度良好的哈希函數把所有資料映射到一個固定範圍的整數集合中,整數定義為槽(slot)。這個範圍一般遠遠大于節點數,比如RedisCluster槽範圍是0 ~ 16383。槽是叢集内資料管理和遷移的基本機關。采用大範圍槽的主要目的是為了友善資料拆分和叢集擴充。每個節點會負責一定數量的槽,如圖10-4所示。

  目前叢集有5個節點,每個節點平均大約負責3276個槽。由于采用高品質的雜湊演算法,每個槽所映射的資料通常比較均勻,将資料平均劃分到5個節點進行資料分區。Redis Cluster就是采用虛拟槽分區,下面就介紹Redis資料分區方法。

Redis之叢集

1.2 Redis 資料分區

Redis Cluser采用虛拟槽分區,所有的鍵根據哈希函數映射到0~16383整數槽内,計算公式:slot=CRC16(key)&16383。每一個節點負責維護一部分槽以及槽所映射的鍵值資料,如圖10-5所示。

Redis之叢集

Redis虛拟槽分區的特點:

□ 解耦資料和節點之間的關系,簡化了節點擴容和收縮難度。

□ 節點自身維護槽的映射關系,不需要用戶端或者代理服務維護槽分區中繼資料。

□ 支援節點、槽、鍵之間的映射查詢,用于資料路由、線上伸縮等場景。

資料分區是分布式存儲的核心,了解和靈活運用資料分區規則對于掌握Redis Cluster非常有幫助。

1.3 叢集功能限制

Redis 叢集相對單機在功能上存在一些限制,需要開發人員提前了解,在使用時做好規避。限制如下:

1) key批量操作支援有限。如mset、mget,目前隻支援具有相同slot值的key執行批量操作。對于映射為不同slot值的key由于執行mget、mget等操作可能存在于多個節點上是以不被支援。

2) key事務操作支援有限。同理隻支援多key在同一節點上的事務操作,當多個key分布在不同的節點上時無法使用事務功能。

3) key作為資料分區的最小粒度,是以不能将一個大的鍵值對象如hash、list等映射到不同的節點。

4) 不支援多資料庫空間。單機下的 Redis 可以支援16個資料庫,叢集模式下隻能使用一個資料庫空間,即db0。

5) 複制結構隻支援一層,從節點隻能複制主節點,不支援嵌套樹狀複制結構。

2.搭建叢集

介紹完Redis叢集分區規則之後,下面我們開始搭建 Redis 叢集。搭建叢集工作需要以下三個步驟:

1) 準備節點。

2) 節點握手。

3) 配置設定槽。

2.1 準備節點

  Redis叢集一般由多個節點組成,節點數量至少為6個才能保證組成完整高可用的叢集。每個節點需要開啟配置cluster-enabled yes,讓Redis運作在叢集模式下。建議為叢集内所有節點統一目錄,一般劃分三個目錄:conf、data、log ,分别存放配置、資料和日志相關檔案。把6個節點配置統一放在conf目錄下,叢集相關配置如下:

# 節點端口
port 6379
# 開啟叢集模式
cluster-enabled yes
# 節點逾時時間,機關毫秒
cluster-node-timeout 15000
# 叢集内部配置檔案
cluster-config-file "nodes-6379.conf"      

  其他配置和單機模式一緻即可,配置檔案命名規則:redis-{port}.conf,準備好配置後啟動所有節點,指令如下:

redis-server conf/redis-6379.conf
redis-server conf/redis-6380.conf
redis-server conf/redis-6381.conf
redis-server conf/redis-6382.conf
redis-server conf/redis-6383.conf
redis-server conf/redis-6384.conf      

  檢查節點日志是否正确,日志内容如下:

cat log/redis-6379.log
* No cluster configuration found, I'm cfb28ef1deee4e0fa78da86abe5d24566744411e
* Server started, Redis version 3.0.7
* The server is now ready to accept connections on port 6379      

  6379節點啟動成功,第一次啟動時如果沒有叢集配置檔案,它會自動建立一份,檔案名稱采用cluster-config-file 參數項控制,建議采用node-{port}.conf格式定義,通過使用端口号區分不同節點,防止同一機器下多個節點彼此覆寫,造成叢集資訊異常。如果啟動時存在叢集配置檔案,節點會使用配置檔案内容初始化叢集資訊。啟動過程如圖10-6所示。

Redis之叢集

  叢集模式的Redis除了原有的配置檔案之外又加了一份叢集配置檔案。當叢集内節點資訊發生變化,如添加節點、節點下線、故障轉移等。節點會自動儲存叢集狀态到配置檔案中。需要注意的是,Redis自動維護叢集配置檔案,不要手動修改,防止節點重新開機時産生叢集資訊錯亂。如節點6379首次啟動後生成叢集配置如下:

#cat data/nodes-6379.conf
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0      

  檔案内容記錄了叢集初始狀态,這裡最重要的是節點ID, 它是一個40位16進制字元串,用于唯一辨別叢集内一個節點,之後很多叢集操作都要借助于節點ID來完成。需要注意是,節點ID不同于運作ID。節點ID在叢集初始化時隻建立一次,節點重新開機時會加載叢集配置檔案進行重用,而 Redis的運作ID每次重新開機都會變化。在節點6380執行cluster nodes指令擷取叢集節點狀态:

127.0.0.1:6380>cluster nodes
8e41673d59c9568aa9d29fbl74ce733345b3e8fl 127.0.0.1:6380 myself,master - 0 0 0 connected      

每個節點目前隻能識别出自己的節點資訊。我們啟動6個節點,但每個節點彼此并不知道對方的存在,下面通過節點握手讓6個節點彼此建立聯系進而組成一個叢集。

2.2 節點握手

節點握手是指一批運作在叢集模式下的節點通過Gossip協定彼此通信,達到感覺對方的過程。節點握手是叢集彼此通信的第一步,由用戶端發起指令:cluster meet {ip}{port},如圖10-7所示。

Redis之叢集

  圖中執行的指令是:cluster meet 127.0.0.1 6380讓節點6379和6380節點進行握手通信。cluster meet指令是一個異步指令,執行之後立刻返同。内部發起與目标節點進行握手通信,如圖10-8所示。

1) 節點6379本地建立6380節點資訊對象,并發送meet消息。

2) 節點6380接受到meet消息後,儲存6379節點資訊并回複pong消息。

3) 之後節點6379和 6380彼此定期通過ping/pong消息進行正常的節點通信。

  這裡的meet、ping、pong消息是Gossip協定通信的載體,之後的節點通信部分做進一步介紹,它的主要作用是節點彼此交換狀态資料資訊。6379和 6380節點通過meet指令彼此建立通信之後,叢集結構如圖10-9所示。

  對節點6379和 6380分别執行cluster nodes指令,可以看到它們彼此已經感覺到對方的存在。

Redis之叢集
127.0.0.1:6379> cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself, master - 0 0 0 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 master - 0 1468073534265 1 connected

127.0.0.1:6380 > cluster nodes
cfb28efldeee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 master - 0 1468073571641 0 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 myself, master - 0 0 1 connected      

  下面分别執行meet指令讓其他節點加入到叢集中:

127.0.0.1:6379>cluster meet 127.0.0.1 6381
127.0.0.1:6379>cluster meet 127.0.0.1 6382
127.0.0.1:6379>cluster meet 127.0.0.1 6383
127.0.0.1:6379>cluster meet 127.0.0.1 6384      

  我們隻需要在叢集内任意節點上執行cluster meet指令加入新節點,握手狀态會通過消息在叢集内傳播,這樣其他節點會自動發現新節點并發起握手流程。最後執行cluster nodes指令确認6個節點都彼此感覺并組成叢集:

127.0.0.1:6379> cluster nodes
4fa7eac4080f0b667ffeab9b87841da49b84a6e4 127.0.0.1:6384 master - 0 1468073975551 5 connected
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 0 connected
be9485a6a729fc98c5151374bc30277e89a461d8 127.0.0.1:6383 master - 0 1468073978579 4 connected
40622f9e7adc8ebd77fca0de9edfe691cb8a74fb 127.0.0.1:6382 master - 0 1468073980598 3 connected
8e41673d59c9568aa9d29fbl74ce733345b3e8f1 127.0.0.1:6380 master - 0 1468073974541 1 connected
40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 master - 0 1468073979589 2 connected      

  節點建立握手之後叢集還不能正常工作,這時叢集處于下線狀态,所有的資料讀寫都被禁止。通過如下指令可以看到:

127.0.0.1:6379> set hello redis
(error) CLUSTERDOWN The cluster is down      

通過cluster info指令可以擷取叢集目前狀态:

127.0.0.1:6379> cluster info
cluster_state: fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:0      

  從輸出内容可以看到,被配置設定的槽(cluster slots_assigned)是0,由于目前所有的槽沒有配置設定到節點,是以叢集無法完成槽到節點的映射。隻有當16384個槽全部配置設定給節點後,叢集才進入線上狀态。

2.3 配置設定槽

Redis叢集把所有的資料映射到16384個槽中。每個key會映射為一個固定的槽,隻有當節點配置設定了槽,才能響應和這些槽關聯的鍵指令。通過cluster addslots指令為節點配置設定槽。這裡利用bash特性批量設定槽(slots),指令如下:

redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0 .. 5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5 4 6 2... 10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923...16383}      

  把16384個slot平均配置設定給6379、6380、6381三個節點。執行cluster info檢視叢集狀态,如下所示:

127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch :5
cluster_my_epoch:0
cluster_stats_messages_sent: 4874
cluster_stats_messages_received:4726      

  目前叢集狀态是OK, 叢集進入線上狀态。所有的槽都已經配置設定給節點,執行cluster nodes指令可以看到節點和槽的配置設定關系:

127.0.0.1:6379> cluster nodes
4fa7eac4080f0b667ffeab9b87841da49b84a6e4    127.0.0.1:6384    master - 0 1468076240123 5 connected
cfb28ef1deee4e0fa78da86abe5d24566744411e    127.0.0.1:6379    myselfpaster - 0 0 0 connected 0-5461
be9485a6a729fc98c5151374bc30277e89a461d8    127.0.0.1:6383    master - 0 1468076239622 4 connected
40622f9e7adc8ebd77fca0de9edfe691cb8a74fb    127.0.0.1:6382    master - 0 1468076240628 3 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1    127.0.0.1:6380    master - 0 1468076237606  1 connected  5462-10922
40b8d09d44294d2e23c7c768efc8fcd153446746    127.0.0.1:6381    master - 0 1468076238612 2 connected  10923-16383      

  目前還有三個節點沒有使用,作為一個完整的叢集,每個負責處理槽的節點應該具有從節點,保證當它出現故障時可以自動進行故障轉移。叢集模式下,Reids節點角色分為主節點和從節點。首次啟動的節點和被配置設定槽的節點都是主節點,從節點負責複制主節點槽資訊和相關的資料。使用cluster replicate {nodeId}指令讓一個節點成為從節點。其中指令執行必須在對應的從節點上執行,nodeld是要複制主節點的節點ID,指令如下:

127.0.0.1:6382>cluster replicate Cfb28ef1deee4e0fa78da86abe5d24566744411e
OK
127.0.0.1:6383>cluster replicate 8e41673d59c9568aa9d29fbl74ce733345b3e8f1
OK
127.0.0.1:6384>cluster replicate 40b8d09d44294d2e23c7c768efc8fcd153446746
OK      

  Redis叢集模式下的主從複制使用了之前介紹的Redis複制流程,依然支援全量和部分複制。複制(replication) 完成後,整個叢集的結構如圖10-11所示。

Redis之叢集

  通過cluster nodes指令檢視叢集狀态和複制關系,如下所示:

127.0.0.1:6379> cluster nodes
4fa7eac4080f0b667ffeab9b87841da49b84a6e4 127.0.0.1:6384 slave 40b8d09d44294d2e23c7c768efc8fcd153446746 0 1468076865939 5 connected
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 0 connected 0-5461
be9485a6a729fc98c5151374bc30277e89a461d8 127.0.0.1:6383 slave 8e41673d59c9568aa9d29fb174ce733345b3e8f1 0 1468076868966 4 connected
40622f9e7adc8ebd77fca0de9edfe691cb8a74fb 127.0.0.1:6382 slave cfb28ef1deee4e0fa78da86abe5d24566744411e 0 1468076869976 3 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 master - 0 1468076870987 1 connected 5462-10922
40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 master - 0 1468076867957 2 connected 10923-16383      

  目前為止,我們依照Redis協定手動建立一個叢集。它由6個節點構成,3 個主節點負責處理槽和相關資料,3個從節點負責故障轉移。手動搭建叢集便于了解叢集建立的流程和細節,不過讀者也從中發現叢集搭建需要很多步驟,當叢集節點衆多時,必然會加大搭建叢集的複雜度和運維成本。是以Redis官方提供了 redis-trib.rb工具友善我們快速搭建叢集。

2.4 用redis-trib.rb搭建叢集

  redis-trib.rb是采用Ruby實作的Redis叢集管理工具。内部通過Cluster相關指令幫我們簡化叢集建立、檢查、槽遷移和均衡等常見運維操作,使用之前需要安裝Ruby依賴環境。下面介紹搭建叢集的詳細步驟。

1.Ruby環境準備

安裝Ruby:

--下載下傳 ruby
wget https://cache.ruby-lang.Org/pub/ruby/2.3/ruby-2.3.1.tar.gz
--安裝 ruby
tar xvf ruby-2.3.1.tar.gz
./ configure -prefix=/usr/local/ruby
make
make install
cd /usr/local/ruby
sudo cp bin/ruby /usr/local/bin
sudo cp bin/gem /usr/local/bin      

安裝 rubygem redis依賴:

wget http://rubygems.org/downloads/redis-3.3.0.gem
gem install -l redis-3.3.0.gem
gem list --check redis gem      

安裝 redis-trib.rb:

sudo cp /{redis_home}/src/redis-trib.rb /usr/local/bin      

安裝完Ruby環境後,執行redis-trib.rb指令确認環境是否正确,輸出如下:

# redis-trib.rb
Usage: redis-trib <command> <options> <arguments ...>
create  hostl:portl ... hostN:portN      

從 redis-trib.rb的提示資訊可以看出,它提供了叢集建立、檢查、修複、均衡等指令行工具。這裡我們關注叢集建立指令,使用redis-trib.rb create指令可快速搭建叢集。

  2.準備節點

  首先我們跟之前内容一樣準備好節點配置并啟動:

redis-server conf/redis-6481.conf
redis-server conf/redis-6482.conf
redis-server conf/redis-6483.conf
redis-server conf/redis-6484.conf
redis-server conf/redis-6485.conf
redis-server conf/redis-6486.conf      

  3.建立叢集

  啟動好6個節點之後,使用redis-trib.rb create指令完成節點握手和槽配置設定過程,指令如下:

redis-trib.rb create --replicas 1 127.0.0.1:6481 127.0.0.1:6482 127.0.0.1:6483
127.0.0.1:6484 127.0.0.1:6485 127.0.0.1:6486      

  --replicas參數指定叢集中每個主節點配備幾個從節點,這裡設定為1。我們出于測試目的使用本地IP位址127.0.0.1,如果部署節點使用不同的IP位址,redis-trib.rb會盡可能保證主從節點不配置設定在同一機器下,是以會重新排序節點清單順序。節點清單順序用于确定主從角色,先主節點之後是從節點。建立過程中首先會給出主從節點角色配置設定的計劃,如下所示。

>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
127.0.0.1:6481
127.0.0.1:6482
127.0.0.1:6483
Adding replica 127.0.0.1:6484 to 127.0.0.1:6481
Adding replica 127.0.0.1:6485 to 127.0.0.1:6482
Adding replica 127.0.0.1:6486 to 127.0.0.1:6483
M: 869de192169c4607bb886944588bc358d6045afa 127.0.0.1:6481
slots:0-5460 (5461 slots) master
M: 6f9f24923eb37f1e4dcelc88430f6fc23ad4a47b 127.0.0.1:6482
slots:5461-10922 (5462 slots) master
M: 6228a1adb6c26139b0adbe81828f43a4ec196271 127.0.0.1:6483
slots:10923-16383 (5461 slots) master
S: 22451ea81fac73fe7a91cf051cd50b2bf308c3f3 127.0.0.1:6484
replicates 869de192169c4607bb886944588bc358d6045afa
S: 89158df8e62958848134d632e75d1a8d2518f07b 127.0.0.1:6485
replicates 6f9f24923eb37f1e4dcelc88430f6fc23ad4a47b
S: bcb394c48d50941f235cd6988a40e469530137af 127.0.0.1:6486
replicates 6228a1adb6c26139b0adbe81828f43a4ec196271
Can I set the above configuration? (type 'yes' to accept):      

  當我們同意這份計劃之後輸入yes, redis-trib.rb開始執行節點握手和槽配置設定操作,輸出如下:

>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join..
>>> Performing Cluster Check (using node 127.0.0.1:6481)
... 忽 略 ...
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.      

  最後的輸出報告說明:16384個槽全部被配置設定,叢集建立成功。這裡需要注意給redis-trib.rb 的節點位址必須是不包含任何槽/資料的節點,否則會拒絕建立叢集。

  4.叢集完整性檢查

  叢集完整性指所有的槽都配置設定到存活的主節點上,隻要16384個槽中有一個沒有配置設定給節點則表示叢集不完整。可以使用redis-trib.rb check指令檢測之前建立的兩個叢集是否成功,check指令隻需要給出叢集中任意一個節點位址就可以完成整個叢集的檢査工作,指令如下:

redis-trib.rb check 127.0.0.1:6379
redis-trib.rb check 127.0.0.1:6481      

  當最後輸出如下資訊,提示叢集所有的槽都已配置設定到節點:

[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.      

3.節點通信

3.1 通信流程

  在分布式存儲中需要提供維護節點中繼資料資訊的機制,所謂中繼資料是指:節點負責哪些資料,是否出現故障等狀态資訊。常見的中繼資料維護方式分為:集中式和P2P方式。Redis叢集采用P2P的Gossip (流言)協定,Gossip協定工作原理就是節點彼此不斷通信交換資訊,一段時間後所有的節點都會知道叢集完整的資訊,這種方式類似流言傳播,如圖10-12所示。

Redis之叢集

  通信過程說明:

1) 叢集中的每個節點都會單獨開辟一個TCP通道,用于節點之間彼此通信,通信端口号在基礎端口上加10000。

2) 每個節點在固定周期内通過特定規則選擇幾個節點發送Ping消息。

3) 接收到Ping消息的節點用pong消息作為響應。

  叢集中每個節點通過一定規則挑選要通信的節點,每個節點可能知道全部節點,也可能僅知道部分節點,隻要這些節點彼此可以正常通信,最終它們會達到一緻的狀态。當節點出故障、新節點加入、主從角色變化、槽資訊變更等事件發生時,通過不斷的ping/pong消息通信,經過一段時間後所有的節點都會知道整個叢集全部節點的最新狀态,進而達到叢集狀态同步的目的。

3.2 Gossip 消息

  Gossip協定的主要職責就是資訊交換。資訊交換的載體就是節點彼此發送的Gossip消息,了解這些消息有助于我們了解叢集如何完成資訊交換。

  常用的Gossip消息可分為:ping消息、pong消息、meet消息、fail消息等,它們的通信模式如圖10-13所示。

Redis之叢集

□ meet消息:用于通知新節點加入。消息發送者通知接收者加入到目前叢集,meet消息通信正常完成後,接收節點會加人到叢集中并進行周期性的Ping、pong消息交換。

□ ping消息:叢集内交換最頻繁的消息,叢集内每個節點每秒向多個其他節點發送ping消息,用于檢測節點是否線上和交換彼此狀态資訊。ping消息發送封裝了自身節點和部分其他節點的狀态資料。

□ pong消息:當接收到ping、meet消息時,作為響應消息回複給發送方确認消息正常通信。pong消息内部封裝了自身狀态資料。節點也可以向叢集内廣播自身的pong消息來通知整個叢集對自身狀态進行更新。

□ fail消息:當節點判定叢集内另一個節點下線時,會向叢集内廣播一個fail消息,其他節點接收到fail消息之後把對應節點更新為下線狀态。具體細節将在後面10.6節 “故障轉移”中說明。

  所有的消息格式劃分為:消息頭和消息體。消息頭包含發送節點自身狀态資料,接收節點根據消息頭就可以擷取到發送節點的相關資料,結構如下:

typedef struct {
        char sig [4] ; /*  信号标示 */
        uint32_t totlen ; /* 消息總長度 */
        uint16_t ver; /*  協定版本 */
        uint16_t type; /*消息類型,用于區分meet,ping,pong等消息 */
        uint16_t count; / * 消息體包含的節點數量,僅用于 meet,ping,ping 消息類型 */
        uint64_t currentEpoch;  / * 目前發送節點的配置紀元 */
        uint64_ t configEpoch;  / * 主節點 / 從節點的主節點配置紀元 */
        uint64_t offset ;  /*複制偏移量 */
        char sender [CLUSTER_NAMELEN] ; /*  發送節點的 nodeId */
        unsigned char myslots [CLUSTER_SL0TS/8] ; /*  發送節點負責的槽資訊 */
        char slaveof[CLUSTER_NAMELEN]; / * 如果發送節點是從節點,記錄對應主節點的 nodeld */
        uint16_t port; /*  端口号 * /
        uintl6_t flags; / * 發送節點辨別,區分主從角色,是否下線等 */
        unsigned char state ; / * 發送節點所處的叢集狀态 */
        unsigned char mflags [3] ; / * 消 息 标 識 */
        union clusterMsgData data /*  消息正文 */;
} clusterMsg;      

  叢集内所有的消息都采用相同的消息頭結構clusterMsg,它包含了發送節點關鍵資訊,如節點id、槽映射、節點辨別(主從角色,是否下線)等。消息體在Redis内部采用clusterMsgData結構聲明,結構如下:

union clusterMsgData {
        /* ping,meet,pong  消息體 */
        struct {
                /* gossip 消息結構數組 */
                clusterMsgDataGossip gossip [1];
         } ping
         /* FAIL消息體 */
         struct {
         clusterMsgDataFail about;
         } fail ;
         // …
};      

  消息體clusterMsgData定義發送消息的資料,其中ping、meet、pong都采用 clusterMsgDataGossip 數組作為消息體資料,實際消息類型使用消息頭的type屬性區分。每個消息體包含該節點的多個clusterMsgDataGossip 結構資料,用于資訊交換,結構如下:

typedef struct {
        char nodename [CLUSTER_NAMELEN] ; / * 節點的  nodeId */
        uint32_t ping_sent;  / * 最後一次向該節點發送 ping 消息時間 */
        uint32_t pong_received; /* 最後一次接收該節點 pong 消息時間 */
        char ip[NET_IP_STR_LEN];  /* IP */
        uint16_t port; /* port */
        uintl6_t flags; /*  該節點辨別, */
} clusterMsgDataGossip;      

  當接收到Ping、meet消息時,接收節點會解析消息内容并根據自身的識别情況做出相應處理,對應流程如圖10-14 所示。

Redis之叢集

  接收節點收到ping/meet消息時,執行解析消息頭和消息體流程:

□ 解析消息頭過程:消息頭包含了發送節點的資訊,如果發送節點是新節點且消息是meet類型,則加入到本地節點清單;如果是已知節點,則嘗試更新發送節點的狀态,如槽映射關系、主從角色等狀态。

□ 解析消息體過程:如果消息體的clusterMsgDataGossip 數組包含的節點是新節 點,則嘗試發起與新節點的meet握手流程;如果是已知節點,則根據cluster MsgDataGossip 中的flags字段判斷該節點是否下線,用于故障轉移。

  消息處理完後回複pong消息,内容同樣包含消息頭和消息體,發送節點接收到回複的pong消息後,采用類似的流程解析處理消息并更新與接收節點最後通信時間,完成一次消息通信。

3.3 節點選擇

  雖然Gossip協定的資訊交換機制具有天然的分布式特性,但它是有成本的。由于内部需要頻繁地進行節點資訊交換,而ping/pong消息會攜帶目前節點和部分其他節點的狀态資料,勢必會加重帶寬和計算的負擔。Redis叢集内節點通信采用固定頻率(定時任務每秒執行10次)。是以節點每次選擇需要通信的節點清單變得非常重要。通信節點選擇過多雖然可以做到資訊及時交換但成本過高。節點選擇過少會降低叢集内所有節點彼此資訊交換頻率,進而影響故障判定、新節點發現等需求的速度。是以Redis叢集的Gossip 協定需要兼顧資訊交換實時性和成本開銷,通信節點選擇的規則如圖10-15所示。

Redis之叢集

  根據通信節點選擇的流程可以看出消息交換的成本主要展現在機關時間選擇發送消息的節點數量和每個消息攜帶的資料量。

1.選擇發送消息的節點數量

  叢集内每個節點維護定時任務預設每秒執行10次,每秒會随機選取5個節點找出最久沒有通信的節點發送Ping消息,用于保證Gossip資訊交換的随機性。每100毫秒都會掃描本地節點清單,如果發現節點最近一次接受Pong消息的時間大于cluster_node_timeout/ 2,則立刻發送ping消息,防止該節點資訊太長時間未更新。根據以上規則得出每個節點每秒需要發送ping消息的數量=1 + 10 * num(node.pong_received > cluster_node_tim eout/2),是以 cluster_node_timeout 參數對消息發送的節點數量影響非常大。當我們的帶寬資源緊張時,可以适當調大這個參數,如從預設15秒改為30秒來降低帶寬占用率。過度調大cluster_node_timeout會影響消息交換的頻率進而影響故障轉移、槽資訊更新、新節點發現的速度。是以需要根據業務容忍度和資源消耗進行平衡。同時整個叢集消息總交換量也跟節點數成正比。

2.消息資料量

  每個ping消息的資料量展現在消息頭和消息體中,其中消息頭主要占用空間的字段是myslots[CLUSTER_SLOTS/8],占用2KB,這塊空間占用相對固定。消息體會攜帶一定數量的其他節點資訊用于資訊交換。具體數量見以下僞代碼:

def get_wanted():
       int total_size = size (cluster.nodes)
       # 預設包含節點總量的1/10
       int wanted = floor (total_size/10) ;
       if wanted < 3:
               # 至少攜帶3個其他節點資訊
               wanted = 3 ;
       if wanted > total_size -2 :
             # 最多包含 total_ size - 2 個 
             wanted = total_ size - 2;
return wanted;      

  根據僞代碼可以看出消息體攜帶資料量跟叢集的節點數息息相關,更大的叢集每次消息通信的成本也就更高,是以對于Redis叢集來說并不是大而全的叢集更好,對于叢集規模控制的建議見之後10.7節“叢集運維”。

4.叢集伸縮

4.1 伸縮原理

  Redis叢集提供了靈活的節點擴容和收縮方案。在不影響叢集對外服務的情況下,可以為叢集添加節點進行擴容也可以下線部分節點進行縮容,如圖10-16所示。

Redis之叢集

  從圖10-16看出,Redis叢集可以實作對節點的靈活上下線控制。其中原理可抽象為槽和對應資料在不同節點之間靈活移動。首先來看我們之前搭建的叢集槽和資料與節點的對應關系,如圖10-17所示。

Redis之叢集

  三個主節點分别維護自己負責的槽和對應的資料,如果希望加入1個節點實作叢集擴容時,需要通過相關指令把一部分槽和資料遷移給新節點,如圖10-18 所示。

Redis之叢集

  圖中每個節點把一部分槽和資料遷移到新的節點6385,每個節點負責的槽和資料相比之前變少了進而達到了叢集擴容的目的。這裡我們故意忽略了槽和資料在節點之間遷移的細節,目的是想讓讀者重點關注在上層槽和節點配置設定上來,了解叢集的水準伸縮的上層原理:叢集伸縮=槽和資料在節點之間的移動,下面将介紹叢集擴容和收縮的細節。

4.2 擴容叢集

  擴容是分布式存儲最常見的需求,Redis叢集擴容操作可分為如下步驟:

1) 準備新節點。

2) 加入叢集。

3) 遷移槽和資料。

  1.準備新節點

  需要提前準備好新節點并運作在叢集模式下,新節點建議跟叢集内的節點配置保持一緻,便于管理統一。準備好配置後啟動兩個節點指令如下:

redis-server conf/redis-6385.conf
redis-server conf/redis-6386.conf      

啟動後的新節點作為孤兒節點運作,并沒有其他節點與之通信,叢集結構如圖10-19所示。

Redis之叢集

  2.加入叢集

  新節點依然采用cluster meet指令加入到現有叢集中。在叢集内任意節點執行clu ster meet指令讓6385和 6386節點加人進來,指令如下:

127.0.0.1:6379>  cluster meet 127.0.0.1 6385
127.0.0.1:6379>  cluster meet 127.0.0.1 6386      

  新節點加入後叢集結構如圖10-20所示。

Redis之叢集

  叢集内新舊節點經過一段時間的ping/pong消息通信之後,所有節點會發現新節點并将它們的狀态儲存到本地。例如我們在6380節點上執行cluster nodes指令可以看到新節點資訊,如下所示:

127.0.0.1:6380 > cluster ndoes
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385  master - 0 1469347800759 7  connected
475528b1bcf8e74d227104a6cf1bf70f00c24aae 127.0.0.1:6386  master - 0 14693477987438  connected      

  新節點剛開始都是主節點狀态,但是由于沒有負責的槽,是以不能接受任何讀寫操作。對于新節點的後續操作我們一般有兩種選擇:

□ 為它遷移槽和資料實作擴容。

口 作為其他主節點的從節點負責故障轉移。

  redis-trib.rb工具也實作了為現有叢集添加新節點的指令,還實作了直接添加為從節點的支援,指令如下:

redis-trib.rb add-node new_host:new_port existing_host:existing_port --slave --master-id <arg>      

  内部同樣采用cluster meet指令實作加人叢集功能。對于之前的加入叢集操作,我們可以采用如下指令實作新節點加入:

redis-trib.rb add-node 127.0.0.1:6385 127.0.0.1:6379
redis-trib.rb add-node 127.0.0.1:6386 127.0.0.1:6379      

  提示: 正式環境建議使用redis-trib.rb add-node指令加入新節點,該指令内部會執行新節點狀态檢查,如果新節點已經加入其他叢集或者包含資料,則放棄叢集加入操作并列印如下資訊:\

[ERR] Node 127.0.0.1:6385 is not empty. Either the node already knows other
nodes (check with CLUSTER NODES) or contains some key in database 0.      

  如果我們手動執行cluster meet指令加入已經存在于其他叢集的節點,會造成被加入節點的叢集合并到現有叢集的情況,進而造成資料丢失和錯亂,後果非常嚴重,線上謹慎操作。

3.遷移槽和資料

  加入叢集後需要為新節點遷移槽和相關資料,槽在遷移過程中叢集可以正常提供讀寫服務,遷移過程是叢集擴容最核心的環節,下面詳細講解。

(1) 槽遷移計劃

槽是Redis叢集管理資料的基本機關,首先需要為新節點制定槽的遷移計劃,确定原有節點的哪些槽需要遷移到新節點。遷移計劃需要確定每個節點負責相似數量的槽,進而保證各節點的資料均勻。例如,在叢集中加入6385節點,如圖10-21所示。加入6385節點後,原有節點負責的槽數量從6380變為4096個。

Redis之叢集

槽遷移計劃确定後開始逐個把槽内資料從源節點遷移到目标節點,如圖10-22所示。

(2) 遷移資料

資料遷移過程是逐個槽進行的,每個槽資料遷移的流程如圖10-23所示。

Redis之叢集

流程說明:

1) 對目标節點發送 cluster set slot {slot} importing {sourceNodeId}指令,讓目标節點準備導入槽的資料。

2) 對源節點發送 cluster setslot {slot} migrating {targetNodeId}指令,讓源節點準備遷出槽的資料。

3) 源節點循環執行 cluster getkeysinslot {slot} {count}指令,擷取 count個屬于槽{slot}的鍵。

4) 在源節點上執行migrate {targetlp} {targetPort} "" 0 {timeout} keys {keys…}  指令,把擷取的鍵通過流水線(pipeline) 機制批量遷移到目标節點,批量遷移版本的migrate指令在Redis 3.0.6以上版本提供,之前的migrate指令隻能單個鍵遷移。對于大量key的場景,批量鍵遷移将極大降低節點之間網絡IO次數。

5) 重複執行步驟3)和步驟4)直到槽下所有的鍵值資料遷移到目标節點。

6) 向叢集内所有主節點發送 cluster setslot {slot} node {targetNodeld}指令,通知槽配置設定給目标節點。為了保證槽節點映射變更及時傳播,需要周遊發送給所有主節點更新被遷移的槽指向新節點。

根據以上流程,我們手動使用指令把源節點6379負責的槽4096遷移到目标節點6385中,流程如下:

1) 目标節點準備導入槽4096資料:

127.0. 0.1:6385>cluster setslot 4096 importing cfb28ef1deee4e0fa78da86abe5d24566744411e
OK      

确認槽4096導入狀态開啟:

127.0.0.1:6385>cluster nodes
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385 myself,master - 0 0 7 connected
[4096-<-cfb28ef1deee4e0fa78da86abe5d24566744411e]      

2) 源節點準備導出槽4096資料:

127.0.0.1:6379>cluster setslot 4096 migrating 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756
OK      

确認槽4096導出狀态開啟:

127.0.0.1:6379>cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master  -  0 0 0  connected
0-5461 [4096->-1a205dd8b2819a00dd1e8b6be40a8e2abe77b756]      

3) 批量擷取槽4096對應的鍵,這裡我們擷取到3個處于該槽的鍵:

127.0.0.1:6379>cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master  -  0 0 0  connected
0-5461 [4096->-1a205dd8b2819a00dd1e8b6be40a8e2abe77b756]      

确認這三個鍵是否存在于源節點:

127.0.0.1:6379>mget key:test:5028 key:test:68253 key:test:79212
1) "value:5028"
2) "value:68253"
3) "value:79212"      

批量遷移這3個鍵,migrate指令保證了每個鍵遷移過程的原子性:

127.0.0.1:6379>migrate 127.0.0.1 6385 "" 0 5000 keys key:test:5028 key:test:68253 key:test:79212      

出于示範目的,我們繼續查詢這三個鍵,發現已經不在源節點中, Redis傳回ASK轉向錯誤, ASK轉向負責引導用戶端找到資料所在的節點,細節将在後面 10.5節“請求路由”中說明。

127.0.0.1:6379> mget key:test:5028 key:test:68253 key:test:79212
(error) ASK 4096 127.0.0.1:6385      

通知所有主節點槽4096指派給目标節點6385:

127.0.0.1:6379>cluster setslot 4096 node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756
127.0.0.1:6380>cluster setslot 4096 node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756
127.0.0.1:6381>cluster setslot 4096 node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756
127.0.0.1:6385>cluster setslot 4096 node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756      

确認源節點6379不再負責槽4096改為目标節點6385負責:

127.0.0.1:6379 > cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself ,master -  0 0 0 connected 0-4095 4097-5461
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385 master - 0 1469718011079 7 connected 4096      

手動執行指令示範槽遷移過程,是為了讓讀者更好地了解遷移流程,實際操作時肯定涉及大量槽并且每個槽對應非常多的鍵。是以redis-trib提供了槽重分片功能,指令如下:

redis-trib.rb reshard host:port --from <arg> --to <arg> --slots <arg> --yes --timeout <arg> --pipeline <arg>      

參數說明:

□ host:port:必傳參數,叢集内任意節點位址,用來擷取整個叢集資訊。

□ --from:制定源節點的id,如果有多個源節點,使用逗号分隔,如果是all源節點變為叢集内所有主節點,在遷移過程中提示使用者輸入。

□ --to: 需要遷移的目标節點的id, 目标節點隻能填寫一個,在遷移過程中提示使用者輸入。

□ --slots:需要遷移槽的總數量,在遷移過程中提示使用者輸入。

□ --yes:當列印出reshard執行計劃時,是否需要使用者輸入yes确認後再執行reshard。

□ --timeout:控制每次migrate操作的逾時時間,預設為60 000毫秒。

□ --pipeline:控制每次批量遷移鍵的數量,預設為10。

reshard指令簡化了資料遷移的工作量,其内部針對每個槽的資料遷移同樣使用之前的流程。我們已經為新節點6395遷移了一個槽4096,剩下的槽資料遷移使用redis-trib.rb 完成,指令如下:

#redis-trib.rb reshard 127.0.0.1:6379
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M:  Cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379
slots: 0-4095,4097-5461 (5461  slots) master
1 additional replica(s)
M: 40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381
slots: 10923-16383 (5461 slots) master
1 additionalreplica(s )
M: 8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380
slots : 5462-10922 (5461 slots ) master
1 additional replica(s)
M: 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385
slots:4096 (1 slots) master
0 additional replica(s)
// …
[OK] All nodes agree about slots configuration.
>>> Check for open slots ...
>>> Check slots coverage. . .
[OK] All 16384 slots covered.      

列印出叢集每個節點資訊後,reshard指令需要确認遷移的槽數量,這裡我們輸入4096個:

How many slots do you want to move (from 1 to 16384)?4096      

輸入6385的節點ID作為目标節點,目标節點隻能指定一個:

What is the receiving node ID? 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756      

之後輸入源節點的ID, 這裡分别輸入節點6379、6380、6381三個節點ID最後用done表示結束:

Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots .
Type 'done' once you entered all the source nodes IDs.
Source node #1:cfb28ef1deee4e0fa78da86abe5d24566744411e
Source node #2:8e41673d59c9568aa9d29fb174ce733345b3e8f1
Source node #3:40b8d09d44294d2e23c7c768efcBfcd153446746
Source node #4:done      

資料遷移之前會列印出所有的槽從源節點到目标節點的計劃,确認計劃無誤後輸入ye s執行遷移工作:

Moving slot 0 from cfb28ef1deee4e0fa78da86abe5d24566744411e
Moving slot 1365 from cfb28ef1deee4e0fa78da86abe5d24566744411e
Moving slot 5462 from 8e41673d59c9568aa9d29fb174ce733345b3e8f1
Moving slot 6826 from-8e41673d59c9568aa9d29fb174ce733345b3e8f1
Moving slot 10923 from 40b8d09d44294d2e23c7c768efc8fcd153446746
Moving slot 12287 from 40b8d09d44294d2e23c7c768efc8fcd153446746
Do you want to proceed with the proposed reshard plan (yes/no)? yes      

redis-trib工具會列印出每個槽遷移的進度,如下:

Moving slot 0 from 127.0.0.1:6379 to 127.0.0.1:6385 . . . .
Moving slot 1365 from 127.0.0.1:6379 to 127.0.0.1:6385 ..
Moving slot 5462 from 127.0.0.1:6380 to 127.0.0.1:6385:  . . . .
Moving slot 6826 from 127.0.0.1:6380 to 127.0.0.1:6385 ..
Moving slot 10923 from 127.0.0.1:6381 to 127.0.0.1:6385 ..
Moving slot 10923 from 127.0.0.1:6381 to 127.0.0.1:6385 ..      

當所有的槽遷移完成後,reshard指令自動退出,執行cluster nodes指令檢查節點和槽映射的變化,如下所示:

127.0.0.1:6379>cluster nodes
40622f9e7adc8ebd77fca0de9edfe691cb8a74fb 127.0.0.1:6382 slave cfb28efIdeee4e0fa
         78da86abe5d24566744411e 0 1469779084518 3 connected
40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 master  - 0
         1469779085528 2 connected 12288-16383
4fa7eac4080f0b667ffeab9b87841da49b84a6e4 127.0.0.1:6384 slave 40b8d09d44294d2e2
         3c7c768efc8fcd153446746 0 1469779087544 5 connected
be9485a6a729fc98c5151374bc30277e89a461d8 127.0.0.1:6383 slave 8e41673d59c9568aa
         9d29fb174ce733345b3e8f1 0 1469779088552 4 connected
Cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 0
          connected 1366-4095 4097-5461
475528b1bcf8e74d227104a6cf1bf70f00c24aae  127.0.0.1:6386  master  - 0
           1469779086536 8 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1  127.0.0.1:6380 master  - 0
            1469779085528 1 connected 6827-10922
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756  127.0.0.1: 6385 master  - 0
            1469779083513 9 connected 0-1365 4096 5462-6826 10923-12287      

節點6385負責的槽變為: 0-1365 4096 5462-6826 10923-l2287。由于槽用于hash運算本身順序沒有意義,是以無須強制要求節點負責槽的順序性。遷移之後建議使用redis-trib.rb rebalance指令檢查節點之間槽的均衡性。指令如下:

# redis-trib.rb rebalance 127.0.0.1:6380
>>> Performing Cluster Check (using node 127.0.0.1:6380)
[OK] All nodes agree about slots configuration.
>>> Check for open slots ...
>>> Check slots coverage ...
[OK] All 16384 slots covered.
*** No rebalancing needed! All nodes are within the 2.0% threshold.      

可以看出遷移之後所有主節點負責的槽數量差異在2% 以内,是以叢集節點資料相對均勻,無需調整。

(3) 添加從節點

擴容之初我們把6385、6386節點加人到叢集,節點6385遷移了部分槽和資料作為主節點,但相比其他主節點目前還沒有從節點,是以該節點不具備故障轉移的能力。這時需要把 節點6386作為6385的從節點,進而保證整個叢集的高可用。使用cluster replicate {masterNodeId}指令為主節點添加對應從節點,注意在叢集模式下slaveof添加從節點操作不再支援。如下所示:

# redis-trib.rb rebalance 127.0.0.1:6380
>>> Performing Cluster Check (using node 127.0.0.1:6380)
[OK] All nodes agree about slots configuration.
>>> Check for open slots ...
>>> Check slots coverage ...
[OK] All 16384 slots covered.
*** No rebalancing needed! All nodes are within the 2.0% threshold.      

從節點内部除了對主節點發起全量複制之外,還需要更新本地節點的叢集相關狀态,檢視節點6386狀态确認已經變成6385節點的從節點:

127.0. 0.1:6386>cluster nodes
475528b1bcf8e74d227104a6cf1bf70f00c24aae 127.0.0.1:6386 myself,slave 1a205dd8b2
819a00dd1e8b6be40a8e2abe77b756 0 0 8 connected
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385 master - 0 1469779083513 9
connected 0-1365 4096 5462-6826 10923-12287      

到此整個叢集擴容完成,叢集關系結構如圖 10-24 所示。

Redis之叢集

4.3 收縮叢集

  收縮叢集意味着縮減規模,需要從現有叢集中安全下線部分節點。安全下線節點流程如圖10-25所示。

Redis之叢集

  流程說明:

1) 首先需要确定下線節點是否有負責的槽,如果是,需要把槽遷移到其他節點,保證節點下線後整個叢集槽節點映射的完整性。

2) 當下線節點不再負責槽或者本身是從節點時,就可以通知叢集内其他節點忘記下線節點,當所有的節點忘記該節點後可以正常關閉。

1.下線遷移槽

  下線節點需要把自己負責的槽遷移到其他節點,原理與之前節點擴容的遷移槽過程一緻。例如我們把6381和6384節點下線,節點資訊如下:

127.0.0.1:6381> cluster nodes
40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 myself,master - 0 0 2 connected
12288-16383
4fa7eac4080f0b667ffeab9b87841da49b84a6e4 127.0.0.1:6384 slave 40b8d09d44294d2e2
3c7c768efc8fcd153446746 0 1469894180780 5 connected      

  6381是主節點,負責槽(12288-16383),6384是它的從節點,如圖10-26所示。下線6381之前需要把負責的槽遷移到其他節點。

  收縮正好和擴容遷移方向相反,6381變為源節點,其他主節點變為目标節點,源節點需要把自身負責的4096個槽均勻地遷移到其他主節點上。這裡直接使用redis-trib.rb reshard指令完成槽遷移。由于每次執行reshard指令隻能有一個目标節點,是以需要執行3次 reshard指令,分别遷移1365、1365、1366個槽,如下所示:

Redis之叢集
# redis-trib.rb reshard 127.0.0.1:6381
>>> Performing Cluster Check (using node 127.0.0.1:6381)
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)?1365
What is the receiving node  ID?  cfb28ef1deee4e0fa78da86abe5d24566744411e /*輸入6379節點id作為目标節點*/
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1:40b8d09d44294d2e23c7c768efc8fcd153446746 /*  源節點6381 id*/
Source node #2:done /*  輸入 done  确認 * /
Do you want to proceed with the proposed reshard plan (yes/no)? yes      

  槽遷移完成後,6379 節點接管了1365個槽12288 ~ 13652, 如下所示:

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
40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 master - 0 1469895725227 2
connected 13653-16383      

  繼續把1365個槽遷移到節點6380:

# redis-trib.rb reshard 127.0.0.1:6381
>>> Performing Cluster Check (using node 127.0.0.1:6381)
How many slots do you want to move (from 1 to 16384)? 1365
What is the receiving node ID? 8e41673d59c9568aa9d29fb174ce733345b3e8f1 /*6380 節點id作為目标節點 -*/
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type ' done' once you entered all the source nodes  IDs . .
Source node #1 :40b8d09d44294d2e23c7c768efc8fcd153446746
Source node #2:done
Do you want to proceed with the proposed reshard plan (yes/no)?yes      

  完成後,6380節點接管了1365個槽13653 ~ 15017,如下所示:

127.0.0.1:6379> cluster nodes
40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 master - 0 1469896123295 2
connected 15018-16383
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 master - 0 1469896125311 11
connected 6827-10922 13653-15017      

  把最後的1366個槽遷移到節點6385中,如下所示:

# redis-trib.rb reshard 127.0.0.1:6381
How many slots do you want to move (from 1 to 16384)? 1366
What is the receiving node ID? 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 /*6385節點id作為目标節點.*/
Please enter all the source node IDs.
Type all ' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1:40b8d09d44294d2e23c7c768efc8fcd153446746
Source node #2:done
Do you want to proceed with the proposed reshard plan (yes/no)? yes      

  到目前為止,節點6381所有的槽全部遷出完成,6381不再負責任何槽。狀态如下所示:

127.0.0.1:6379> cluster nodes
40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 master - 0 1469896444768 2
connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 master - 0 1469896443760 11
connected 6827-10922 13653-15017
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385 master - 0 1469896445777 12
connected 0-1365 4096 5462-6826 10923-12287 15018-16383
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 10 connected
1366-4095 4097-5461 12288-13652
be9485a6a729fc98c5151374bc30277e89a46148 127.0.0.1:6383 slave 8e41673d59c9568aa9d29fb174ce733345b3e8f1 0 1469896444264 11 connected      

  下線節點槽遷出完成後,剩下的步驟需要讓叢集忘記該節點。

2.忘記節點

  由于叢集内的節點不停地通過Gossip消息彼此交換節點狀态,是以需要通過一種健壯的機制讓叢集内所有節點忘記下線的節點。也就是說讓其他節點不再與要下線節點進行Gossip消息交換。Redis提供了 cluster forget {downNodeId}指令實作該功能,如圖10-27所示。

Redis之叢集

  當節點接收到cluster forget {downNodeld}指令後,會把nodeld指定的節點加入到禁用清單中,在禁用清單内的節點不再發送Gossip消息。禁用清單有效期是60秒,超過60秒節點會再次參與消息交換。也就是說當第一次forget指令發出後,我們有60秒的時間讓叢集内的所有節點忘記下線節點。

  線上操作不建議直接使用cluster forget指令下線節點,需要跟大量節點指令互動,實際操作起來過于繁瑣并且容易遺漏forget節點。建議使用redis-trib.rb del-node {host:port} {downNodeid}指令,内部實作的僞代碼如下:

def delnode_cluster_cmd(downNode):
          # 下線節點不允許包含 slots
          if downNode.slots.length!= 0
                exit 1
          end
# 向叢集内節點發送 cluster forget
for n in nodes:
      if n .id == downNode.id:
           # 不能對自己做 forget操作
           continue;
       # 如果下線節點有從節點則把從節點指向其他主節點
        if n. replicate && n.replicate.nodeld == downNode.id:
              # 指向擁有最少從節點的主節點
              master = get_master_with_least _replicas{} ;
               n.cluster( "replicate" , master.nodeid);
         # 發送忘記節點指令
         n.cluster('forget' , downNode.id)
# 節點關閉
downNode.shutdown() ;      

  從僞代碼看出del-node指令幫我們實作了安全下線的後續操作。當下線主節點具有從節點時需要把該從節點指向到其他主節點,是以對于主從節點都下線的情況,建議先下線從節點再下線主節點,防止不必要的全量複制。對于6381和6384節點下線操作,指令如下:

redis-trib.rb del-node 127.0.0.1:6379 4fa7eac4080f0b667ffeab9b87841da49b84a6e4 #從節點6384 id
redis-trib.rb del-node 127.0.0.1:6379 40b8d09d44294d2e23c7c768efc8fcdl53446746 #主節點6381 id      

  節點下線後确認節點狀态:

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
be9485a6a729fc98c5151374bc30277e89a461d8 127.0.0.1:6383 slave 8e41673d59c9568aa9d29fb174ce733345b3e8f1 0 1470048035624 11 connected
475528b1bcf8e74d227104a6cf1bf70f00c24aae 127.0.0.1:6386 slave 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 0 1470048032604 12 connected
40622f9e7adc8ebd77fca0de9edfe691cb8a74fb 127.0.0.1:6382 slave cfb28ef1deee4e0fa78da86abe5d24566744411e 0 1470048035120 10 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 master - 0 1470048034617 11 connected 6827-10922 13653-15017
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385 master - 0 1470048033614 12 connected 0-1365 4096 5462-6826 10923-12287 15018-16383      

  叢集節點狀态中已經不包含6384和 6381節點,到目前為止,我們完成了節點的安全下線,新的叢集結構如圖10-28所示。

  本節介紹了 Redis叢集伸縮的原理和操作方式,它是Redis叢集化之後最重要的功能,熟練掌握叢集伸縮技巧後,可以針對線上的資料規模和并發量做到從容應對。

Redis之叢集

5.請求路由

  目前我們已經搭建好Redis叢集并且了解了通信和伸縮細節,但還沒有使用用戶端去操作叢集。Redis叢集對用戶端通信協定做了比較大的修改,為了追求性能最大化,并沒有采用代理的方式而是采用用戶端直連節點的方式。是以對于希望從單機切換到叢集環境的應用需要修改用戶端代碼。本節我們關注叢集請求路由的細節,以及用戶端如何高效地操作叢集。

5.1 請求重定向

  在叢集模式下,Redis接收任何鍵相關指令時首先計算鍵對應的槽,再根據槽找出所對應的節點,如果節點是自身,則處理鍵指令;否則回複MOVED重定向錯誤,通知用戶端請求正确的節點。這個過程稱為 moved 重定向,如圖10-29所示。例如,在之前搭建的叢集上執行如下指令:

127.0.0.1:6379> set key:test:1 value-1
OK      
Redis之叢集

  執行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 節點中完成請求轉發,如圖10-30所示。

  節點對于不屬于它的鍵指令隻回複重定向響應,并不負責轉發。熟悉 Cassandra 的使用者希望在這裡做好區分,不要混淆。正因為叢集模式下把解析發起重定向的過程放到用戶端完成,是以叢集用戶端協定相對于單機有了很大的變化。

  鍵指令執行步驟主要分兩步:計算槽,查找槽所對應的節點。下面分别介紹。

Redis之叢集

1.計算槽

  Redis首先需要計算鍵所對應的槽。根據鍵的有效部分使用CRC16函數計算出散列值,再取對16383的餘數,使每個鍵都可以映射到0 ~ 16383槽範圍内。

2.槽節點查找

  Redis計算得到鍵對應的槽後,需要查找槽所對應的節點。叢集内通過消息交換每個節點都會知道所有節點的槽資訊,内部儲存在clusterState結構中。

  具體僞代碼這裡不做展示,具體實作見如下:

  節點對于判定鍵指令是執行還是moved重定向,都是借助slots[ CLUSTER_SLOTS ]數組實作。根據 MOVED重定向機制,用戶端可以随機連接配接叢集内任一Redis擷取鍵所在節點,這種用戶端又叫Dummy(傀儡)用戶端,它優點是代碼實作簡單,對用戶端協定影響較小,隻需要根據重定向資訊再次發送請求即可。但是它的弊端很明顯,每次執行鍵指令前都要到Redis上進行重定向才能找到要執行指令的節點,額外增加了IO開銷,這不是Redis叢集高效的使用方式。正因為如此通常叢集用戶端都采用另一種實作:Smart (智能> 用戶端。

5.2 Smart用戶端

1.smart用戶端原理

  大多數開發語言的Redis用戶端都采用Smart用戶端支援叢集協定,用戶端如何選擇見:http://redis.io/clients, 從中找出符合自己要求的用戶端類庫。Smart用戶端通過在内部維護slot→node的映射關系,本地就可實作鍵到節點的查找,進而保證IO效率的最大化,而MOVED重定向負責協助Smart用戶端更新slot→node映射。我們以Java的Jedis為例,說明Smart用戶端操作叢集的流程。

1) 首先在JedisCluster初始化時會選擇一個運作節點,初始化槽和節點映射關系,使用cluster slots 指令完成,如下所示:

127.0.0.1:6379 > cluster slots
1)  1)  (integer) 0 // 開始槽範圍
    2)  (integer) 1365 // 結束槽範圍
    3)  1) "127.0.0.1" //主節點 ip
        2)  (integer) 6385 // 主節點位址
    4)  1) "127.0.0.1” // 從節點 ip
        2)  (integer) 6386 / / 從節點端口
2)  1) (integer) 5462
    2) (integer) 6826
    3)  1) "127.0.0.1"
        2) (integer) 6385
    4)  1) "127.0.0.1 "
        2) (integer) 6386      

2) JedisCluster 解析cluster slots結果緩存在本地,并為每個節點建立唯一的JedisPool 連接配接池。映射關系在JedisCluster InfoCache類中。

3) JedisCluster執行鍵指令的過程有些複雜,但是了解這個過程對于開發人員分析定位問題非常有幫助,具體代碼就不做展示。

鍵指令執行流程:

1) 計算slot并根據slots緩存擷取目标節點連接配接,發送指令。

2) 如果出現連接配接錯誤,使用随機連接配接重新執行鍵指令,每次指令重試對redi­rections 參數減1。

3) 捕獲到MOVED重定向錯誤,使用cluster slots指令更新slots緩存(renewSlotCache 方法)。

4) 重複執行1)~ 3)步,直到指令執行成功,或者當redirections<=0時抛出JedisClusterMaxRedirectionsException 異常。

整個流程如圖10-31所示。

Redis之叢集

從指令執行流程中發現,用戶端需要結合異常和重試機制時刻保證跟Redis叢集的slots同步,是以Smart用戶端相比單機用戶端有了很大的變化和實作難度。了解指令執行流程後,下面我們對Smart用戶端成本和可能存在的問題進行分析:

1) 用戶端内部維護slots緩存表,并且針對每個節點維護連接配接池,當叢集規模非常大時,用戶端會維護非常多的連接配接并消耗更多的記憶體。

2) 使用Jedis操作叢集時最常見的錯誤是:

throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");      

這經常會引起開發人員的疑惑,它隐藏了内部錯誤細節,原因是節點當機或請求逾時都會抛出JedisConnectionException,導緻觸發了随機重試,當重試次數耗盡抛出這個錯誤。

3) 當出現 JedisConnectionException 時,Jedis認為可能是叢集節點故障需要随機重試來更新slots 緩存,是以了解哪些異常将抛出JedisConnectionException 變得非常重要,有如下幾種情況會抛出 JedisConnectionException:

□ Jedis連接配接節點發生 socket 錯誤時抛出。

□ 所有指令/Lua腳本讀寫逾時抛出。

□ JedisPool連接配接池擷取可用Jedis對象逾時抛出。

前兩點都可能是節點故障需要通過JedisConnectionException 來更新 slots緩存,但是第三點沒有必要,因Jedis2.8.1版本之後對于連接配接池的逾時抛出 JedisException, 進而避免觸發随機重試機制。

4) Redis叢集支援自動故障轉移,但是從故障發現到完成轉移需要一定的時間,節點當機期間所有指向這個節點的指令都會觸發随機重試,每次收到 MOVED 重定向後會調用JedisClusterlnfoCache類的renewSlotCache方法。

思路為: 獲得寫鎖後再執行cluster slots 指令初始化緩存,由于叢集所有的鍵指令都會執getSlotPool方法計算槽對應節點,它内部要求讀鎖。 ReentrantReadWriteLock是讀鎖共享且讀寫鎖互斥,進而導緻所有的請求都會造成阻塞。對于并發量高的場景将極大地影響叢集吞吐。這個現象稱為cluster slots 風暴,有如下現象:

□ 重試機制導緻IO通信放大問題。比如預設重試5次的情況,當抛出JedisC lusterMaxRedirectionsException異常時,内部最少需要9次IO通信:5次發送指令+2次ping指令保證随機節點正常+2次cluster slots指令初始化slots緩存。導緻異常判定時間變長。

□ 個别節點操作異常導緻頻繁的更新slots緩存,多次調用cluster slots 指令,高并發時将過度消耗Redis節點資源,如果叢集slot<->node映射龐大則cluster slots傳回資訊越多,問題越嚴重。

□ 頻繁觸發更新本地slots 緩存操作,内部使用了寫鎖,阻塞對叢集所有的鍵指令調用。

針對以上問題在 Jedis 2.8.2 版本做了改進:

□ 當接收到JedisConnectionException時不再輕易初始化slots緩存,大幅降低内部IO次數。隻有當重試次數到最後1次或者出現MovedDataException時才更新 slots操作,降低了 cluster slots指令調用次數。

□ 當更新slots緩存時,不再使用ping指令檢測節點活躍度,并且使用redis covering變量保證同一時刻隻有一個線程更新slots緩存,其他線程忽略,優化了寫鎖阻塞和cluster slots調用次數。

綜上所述,Jedis2.8.2之後的版本,當出現JedisConnectionException時,指令發送次數變為5次:4次重試指令+1次cluster slots指令,同時避免了 clusterslots不必要的并發調用。

這裡我們用大量篇幅介紹了Smart用戶端Jedis與叢集互動的細節,主要原因是針對于高并發的場景,這裡是絕對的熱點。叢集協定通過Smart用戶端全面高效的支援需要一個過程,是以使用者在選擇Smart用戶端時要重點稽核叢集互動代碼,防止線上踩坑。必要時可以自行優化修改用戶端源碼。

具體每個語言的用戶端可以去對應官網及redis官網查找。

5.3 ASK重定向

1.用戶端ASK重定向流程

  Redis叢集支援線上遷移槽(slot)和資料來完成水準伸縮,當slot對應的資料從源節點到目标節點遷移過程中,用戶端需要做到智能識别,保證鍵指令可正常執行。例如當一個slot 資料從源節點遷移到目标節點時,期間可能出現一部分資料在源節點,而另一部分在目标節點,如圖10-32所示。

  當出現上述情況時,用戶端鍵指令執行流程将發生變化,如下所示:

1) 用戶端根據本地slots緩存發送指令到源節點,如果存在鍵對象則直接執行并傳回結果給用戶端。

2) 如果鍵對象不存在,則可能存在于目标節點,這時源節點會回複ASK重定向異常。格式如下:(error) ASK {slot} {targetIP} : {targetPort}。

3) 用戶端從ASK重定向異常提取出目标節點資訊,發送asking指令到目标節點打開用戶端連接配接辨別,再執行鍵指令。如果存在則執行,不存在則傳回不存在資訊。

ASK重定向整體流程如圖10-33 所示。

Redis之叢集

  ASK與MOVED雖然都是對用戶端的重定向控制,但是有着本質差別。ASK重定向說明叢集正在進行slot 資料遷移,用戶端無法知道什麼時候遷移完成,是以隻能是臨時性的重定向,用戶端不會更新slots緩存。但是MOVED重定向說明鍵對應的槽已經明确指定到新的節點,是以需要更新slots緩存。

2.節點内部處理

  為了支援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。當槽處于遷移狀态時,批量操作會受到影響。

  綜上所處,使用 smart 用戶端批量操作叢集時,需要評估mget/mset、Pipeline等方式在slot遷移場景下的容錯性,防止叢集遷移造成大量錯誤和資料丢失的情況。

  叢集環境下對于使用批量操作的場景,建議優先使用Pipeline方式,在用戶端實作對ASK重定向的正确處理,這樣既可以受益于批量操作的IO優化,又可以相容slot遷移場景。

6.故障轉移

  Redis叢集自身實作了高可用。高可用首先需要解決叢集部分失敗的場景:當叢集内少量節點出現故障時通過自動故障轉移保證叢集可以正常對外提供服務。本節介紹故障轉移的細節,分析故障發現和替換故障節點的過程。

6.1 故障發現

  當叢集内某個節點出現問題時,需要通過一種健壯的方式保證識别出節點是否發生了故障。Redis叢集内節點通過ping/pong消息實作節點通信,消息不但可以傳播節點槽資訊,還可以傳播其他狀态如:主從狀态、節點故障等。是以故障發現也是通過消息傳播機制實作的,主要環節包括:主觀下線(pfail)和客觀下線(fail)。

□ 主觀下線:指某個節點認為另一個節點不可用,即下線狀态,這個狀态并不是最終的故障判定,隻能代表一個節點的意見,可能存在誤判情況。

□ 客觀下線:名額記一個節點真正的下線,叢集内多個節點都認為該節點不可用,進而達成共識的結果。如果是持有槽的主節點故障,需要為該節點進行故障轉移。

1.主觀下線

  叢集中每個節點都會定期向其他節點發送ping消息,接收節點回複pong消息作為響應。如果在cluster-node-timeout時間内通信一直失敗,則發送節點會認為接收節點存在故障,把接收節點标記為主觀下線(pfail) 狀态。流程如圖10-34所示。

Redis之叢集

1) 節點a 發送ping消息給節點b, 如果通信正常将接收到pong消息,節點a更新最近一次與節點b 的通信時間。

2) 如果節點a與節點b通信出現問題則斷開連接配接,下次會進行重連。如果一直通信失敗,則節點a 記錄的與節點b 最後通信時間将無法更新。

3) 節點a内的定時任務檢測到與節點b最後通信時間超高cluster-node-timeout時,更新本地對節點b的狀态為主觀下線(pfail)。

  主觀下線簡單來講就是,當cluster-note-timeout時間内某節點無法與另一個節點順利完成Ping消息通信時,則将該節點标記為主觀下線狀态。每個節點内的cluster State結構都需要儲存其他節點資訊,用于從自身視角判斷其他節點的狀态。結構關鍵屬性如下:

typedef struct clusterState {
       clusterNode *myself;  /* 自身節點 /
       dict *nodes;/* 目前叢集内所有節點的字典集合,key為節點 ID, value為對應節點ClusterNode 結構 */
} clusterState;
字典nodes屬性中的clusterNode 結構儲存了節點的狀态,關鍵屬性如下:
typedef struct clusterNode {
        int flags; /* 目前節點狀态,如:主從角色,是否下線等 */
        mstime_t ping_sent; / *最後一次與該節點發送 ping消息的時間*/
        mstime_t pong_received; /* 最後一次接收到該節點 pong消息的時間 */
} clusterNode;      

  其中最重要的屬性是flags, 用于标示該節點對應狀态,取值範圍如下:

CLUSTER_NODE_MASTER 1 /* 目前為主節點 */
CLUSTER_NODE_SLAVE 2 /* 目前為從節點 */
CLUSTER_N0DE_PFAIL 4 /* 主觀下線狀态 */
CLUSTER_NODE_FAIL 8 /* 客觀下線狀态 */
CLUSTER_NODE_MYSELF 16 /* 表示自身節點 */
CLUSTER_N0DE_HANDSHAKE 32 / * 握手狀态,未與其他節點進行消息通信 */
CLUSTER_N0DE_N0ADDR 64 /* 無位址節點,用于第一次meet 通信未完成或者通信失敗 */
CLUSTER_NODE_MEET 128  /* 需要接受 m eet 消息的節點狀态 */
CLUSTER_NODE_MIGRATE_TO 256 / * 該節點被選中為新的主節點狀态 */      

  使用以上結構,主觀下線判斷僞代碼如下:

// 定時任務,預設每秒執行10次
def clusterCron( ) :
      //  . . . 忽略其他代碼
      for(node in server.cluster.nodes):
           //忽略自身節點比較
          if (node.flags == CLUSTER_NODE_MYSELF):
               continue;
          //系統目前時間
          long now = mstime();
          // 自身節點最後一次與該節點PING通信的時間差
          long delay = now - node.ping_sent;
          // 如果通信時間差超過 cluster_node_timeout,将該節點标記為 PFAIL ( 主觀下線)
          if (delay > server.cluster_node_timeout):
             node, flags = CLUSTER_NODE_PFAIL;      

  Redis叢集對于節點最終是否故障判斷非常嚴謹,隻有一個節點認為主觀下線并不能準确判斷是否故障。例如圖10-35的場景。節點6379與 6385通信中斷,導緻6379判斷6385為主觀下線狀态,但是6380與 6385節點之間通信正常,這種情況不能判定節點6385發生故障。是以對于一個健壯的故障發現機制,需要叢集内大多數節點都判斷6385故障時,才能認為6385确實發生故障,然後為6385節點進行故障轉移。而這種多個節點協作完成故障發現的過程叫做客觀下線。

Redis之叢集

2.客觀下線

  當某個節點判斷另一個節點主觀下線後,相應的節點狀态會跟随消息在叢集内傳播。ping/pong消息的消息體會攜帶叢集1/10的其他節點狀态資料,當接受節點發現消息體中含有主觀下線的節點狀态時,會在本地找到故障節點的ClusterNode結構,儲存到下線報告連結清單中。結構如下:

struct ClusterNode {/*  認為是主觀下線的  ClusterNode  結構 */
       list *fail_reports;  / * 記錄了所有其他節點對該節點的下線報告*/
       ...
};      

  通過Gossip消息傳播,叢集内節點不斷收集到故障節點的下線報告。當半數以上持有槽的主節點都标記某個節點是主觀下線時。觸發客觀下線流程。這裡有兩個問題:

1) 為什麼必須是負責槽的主節點參與故障發現決策?因為叢集模式下隻有處理槽的主節點才負責讀寫請求和叢集槽等關鍵資訊維護,而從節點隻進行主節點資料和狀态資訊的複制。

2) 為什麼半數以上處理槽的主節點?必須半數以上是為了應對網絡分區等原因造成的叢集分割情況,被分割的小叢集因為無法完成從主觀下線到客觀下線這一關鍵過程,進而防止小叢集完成故障轉移之後繼續對外提供服務。假設節點a标記節點b 為主觀下線 ,一 段時間後節點a通過消息把節點b的狀态發送到其他節點,當節點c接受到消息并解析出消息體含有節點b的pfail狀态時,會觸發客觀下線流程,如圖10-36所示。

Redis之叢集

流程說明:

1) 當消息體内含有其他節點的Pfail狀态會判斷發送節點的狀态,如果發送節點是主節點則對報告的pfail狀态處理,從節點則忽略。

2) 找到pfail對應的節點結構,更新ClusterNode内部下線報告連結清單。

3) 根據更新後的下線報告連結清單告嘗試進行客觀下線。

  這裡針對維護下線報告和嘗試客觀下線邏輯進行詳細說明。

(1) 維護下線報告連結清單

每個節點ClusterNode結構中都會存在一個下線連結清單結構,儲存了其他主節點針對目前節點的下線報告,結構如下:

typedef struct clusterNodeFailReport {
       struct ClusterNode *node; / * 報告該節點為主觀下線的節點*/
       mstime_t time; / *最近收到下線報告的時間 */
} clusterNodeFailReport;      

下線報告中儲存了報告故障的節點結構和最近收到下線報告的時間,當接收到fail狀态時,會維護對應節點的下線上報連結清單,僞代碼如下:

def clusterNodeAddFailureReport(clusterNode failNode, clusterNode senderNode):
      //擷取故障節點的下線報告連結清單
       list report_list=failNode_fail_reports;
      //查找發送節點的下線報告是否存在
       for(clusterNodeFailReport report : report_list):
      //存在發送節點的下線報告上報
               if(senderNode == report.node):
                      //更新下線報告時間
                      report.time = now();
                      return 0;
                //如果下線報告不存在,插入新的下線報告
                report_list.add(new clusterNodeFailReport(senderNode,now ( } ) ) ;
        return 1;      

每個下線報告都存在有效期,每次在嘗試觸發客觀下線時,都會檢測下線報告是否過期,對于過期的下線報告将被删除。如果在cluster-node-time* 2 的時間内該下線報告沒有得到更新則過期并删除,僞代碼如下:

def clusterNodeCleanupFailureReports(clusterNode node) :
       list report_list = node.fail_reports;
       long maxtime = server.cluster_node_timeout * 2;
       long now = now();
       for (clusterNodeFailReport report:report_list) :
       // 如果最後上報過期時間大于 cluster_node_timeout * 2 則删除
                  if(now - report.time > maxtime):
                      report_list.del(report) ;      

下線報告的有效期限是server.cluster_node_timeout * 2, 主要是針對故障誤報的情況。例如節點A 在上一小時報告節點B 主觀下線,但是之後又恢複正常。現在又有其他節點上報節點B 主觀下線,根據實際情況之前的屬于誤報不能被使用。

提示: 如果在cluster-node-time * 2 時間内無法收集到一半以上槽節點的下線報告,那麼之前的下線報告将會過期,也就是說主觀下線上報的速度追趕不上下線報告過期的速度,那麼故障節點将永遠無法被标記為客觀下線進而導緻故障轉移失敗。是以不建議将cluster-node-time設定得過小。

(2) 嘗試客觀下線

叢集中的節點每次接收到其他節點的pfail狀态,都會嘗試觸發客觀下線,流程如圖 10-37所示。

Redis之叢集

1) 首先統計有效的下線報告數量,如果小于叢集内持有槽的主節點總數的一半則退出。

2) 當下線報告大于槽主節點數量一半時,标記對應故障節點為客觀下線狀态。

3) 向叢集廣播一條fail消息,通知所有的節點将故障節點标記為客觀下線,fail消息的消息體隻包含故障節點的ID。

使用僞代碼分析客觀下線的流程,如下所示:

def markNodeAsFailinglfNeeded(clusterNode failNode)  {
      //擷取叢集持有槽的節點數量
      int slotNodeSize = getSlotNodeSize( ) ;
     //主觀下線節點數必須超過槽節點數量的一半
     int needed_quorum = (slotNodeSize / 2) + 1;
     // 統計 failNode 節點有效的下線報告數量(不包括目前節點)
     int failures = clusterNodeFailureReportsCount(failNode);
      // 如果目前節點是主節點,将目前節點計累加到 failures
      if (nodelsMaster(myself) ) :
         failures++ ;
      //下線報告數量不足槽節點的一半退出
      if (failures < needed_quorum):
          return;
       // 将改節點标記為客觀下線狀态(fail)
       failNode.flags = REDIS_N0DE_FAIL;
       //更新客觀下線的時間
        failNode.fail_time = mstime();
        // 如果目前節點為主節點,向叢集廣播對應節點的 fail 消息
        if (nodelsMaster(myself))
            clusterSendFail(failNode);      

廣播fail消息是客觀下線的最後一步,它承擔着非常重要的職責:

□ 通知叢集内所有的節點标記故障節點為客觀下線狀态并立刻生效。

□ 通知故障節點的從節點觸發故障轉移流程。

需要了解的是,盡管存在廣播fail消息機制,但是叢集所有節點知道故障節點進入客觀下線狀态是不确定的。比如當出現網絡分區時有可能叢集被分割為一大一小兩個獨立叢集中。大的叢集持有半數槽節點可以完成客觀下線并廣播fail消息,但是小叢集無法接收到fail消息,如圖10-38所示。但是當網絡恢複後,隻要故障節點變為客觀下線,最終總會通過Gossip消息傳播至叢集的所有節點。

Redis之叢集

提示: 分區會導緻分割後的小叢集無法收到大叢集的fail消息,是以如果故障節點所有的從節點都在小叢集内将導緻無法完成後續故障轉移,是以部署主從結構時需要根據自身機房/機架拓撲結構,降低主從被分區的可能性。

6.2 故障恢複

  故障節點變為客觀下線後,如果下線節點是持有槽的主節點則需要在它的從節點中選出一個替換它,進而保證叢集的高可用。下線主節點的所有從節點承擔故障恢複的義務,當從節點通過内部定時任務發現自身複制的主節點進入客觀下線時,将會觸發故障恢複流程,如圖10-39所示。

Redis之叢集

1.資格檢查

  每個從節點都要檢查最後與主節點斷線時間,判斷是否有資格替換故障的主節點。如果從節點與主節點斷線時間超過 cluster-node-time * cluster-slave-validity-factor,則目前從節點不具備故障轉移資格。參數cluster-slave-validity-factor用于從節點的有效因子,預設為10。

  2.準備選舉時間

當從節點符合故障轉移資格後,更新觸發故障選舉的時間,隻有到達該時間後才能執行後續流程。故障選舉時間相關字段如下:

struct clusterState {
      mstime_t failover_auth_time; / * 記錄之前或者下次将要執行故障選舉時間 */
      int failover_auth_rank ; /* 記錄目前從節點排名 */
}      

  這裡之是以采用延遲觸發機制,主要是通過對多個從節點使用不同的延遲選舉時間來支援優先級問題。複制偏移量越大說明從節點延遲越低,那麼它應該具有更高的優先級來替換故障主節點。優先級計算僞代碼如下:

def clusterGetSlaveRank():
       int rank = 0;
       //擷取從節點的主節點
      ClusteRNode master = myself.slaveof;
      //擷取目前從節點複制偏移量
      long myoffset = replicationGetSlaveOffset( ) ;
      //跟其他從節點複制偏移量對比
      for (int j = 0 ; j < m aster.slaves.length; j++ ) :
              //rank 表示目前從節點在所有從節點的複制偏移量排名,為 0 表示偏移量最大 .
              if (master.slaves [j]  != myself && master.slaves[j].repl_offset > myoffset):
                  rank++;
        return rank;
}      

使用之上的優先級排名,更新選舉觸發時間,僞代碼如下:

def updateFailoverTime( ) :
      // 預設觸發選舉時間:發現客觀下線後一秒内執行。
      server.cluster.failover_auth_time = now() + 500 + random() % 500;
     //擷取目前從節點排名
      int rank = clusterGetSlaveRank( ) ;
      long added_delay = rank * 1000;
      // 使用 added_delay 時間累加到 failover_auth_time中
      server.cluster.failover_auth_time += added_delay;
      //更新目前從節點排名
      server.cluster.failover_auth_rank = rank;      

  所有的從節點中複制偏移量最大的将提前觸發故障選舉流程,如圖10-40所示。

  主節點b進入客觀下線後,它的三個從節點根據自身複制偏移量設定延遲選舉時間,如複制偏移量最大的節點slaveb-1延遲1秒執行,保證複制延遲低的從節點優先發起選舉。

Redis之叢集

3.發起選舉

當從節點定時任務檢測到達故障選舉時間(fail over_auth_time) 到達後,發起選舉流程如下:

(1) 更新配置紀元

配置紀元是一個隻增不減的整數,每個主節點自身維護一個配置紀元(clusterNode.configEpoch) 标示目前主節點的版本,所有主節點的配置紀元都不相等,從節點會複制主節點的配置紀元。整個叢集又維護一個全局的配置紀元(clusterState. currentEpoch),用于記錄叢集内所有主節點配置紀元的最大版本。執行cluster info指令可以檢視配置紀元資訊:

127.0.0.1:6379> cluster info
cluster_current_epoch: 15  //整個叢集最大配置紀元
cluster_my_epoch: 13  //目前主節點配置紀元      

配置紀元會跟随ping/pong消息在叢集内傳播,當發送方與接收方都是主節點且配置紀元相等時代表出現了沖突,nodeId更大的一方會遞增全局配置紀元并指派給目前節點來區分沖突,僞代碼如下:

def clusterHandleConfigEpochCollision (clusterNode sender ) :
       if (sender.configEpoch != myself .configEpoch ||  !nodelsMaster(sender)  || !nodeIsMaster (myself) ) :
       return;
        // 發送節點的 nodeld 小于自身節點 nodeld 時忽略
       if (sender.nodeld <= myself.nodeld ):
           return
       //更新全局和自身配置紀元
       server.cluster.currentEpoch++;
       myself.configEpoch = server.cluster.currentEpoch;      

配置紀元的主要作用:

□ 标示叢集内每個主節點的不同版本和目前叢集最大的版本。

□ 每次叢集發生重要事件時,這裡的重要事件指出現新的主節點(新加入的或者由從節點轉換而來),從節點競争選舉。都會遞增叢集全局的配置紀元并指派給相關主節點,用于記錄這一關鍵事件。

□ 主節點具有更大的配置紀元代表了更新的叢集狀态,是以當節點間進行ping/pong消息交換時,如出現slots等關鍵資訊不一緻時,以配置紀元更大的一方為準,防止過時的消息狀态污染叢集。

配置紀元的應用場景有:

□ 新節點加入。

□ 槽節點映射沖突檢測。

□ 從節點投票選舉沖突檢測

之前在通過cluster setslot 指令修改槽節點映射時,需要確定執行請求的主節點本地配置紀元(configEpoch) 是最大值,否則修改後的槽資訊在消息傳播中不會被擁有更高的配置紀元的節點采納。由于Gossip通信機制無法準确知道目前最大的配置紀元在哪個節點,是以在槽遷移任務最後的cluster setslot {slot} node{nodeId} 指令需要在全部主節點中執行一遍。

從節點每次發起投票時都會自增叢集的全局配置紀元,并單獨儲存在clusterState.failover_auth_epoch變量中用于辨別本次從節點發起選舉的版本。

(2) 廣播選舉消息

在叢集内廣播選舉消息( failover_auth_request),并記錄已發送過消息的狀态,保證該從節點在一個配置紀元内隻能發起一次選舉。消息内容如同ping消息隻是将type類型變為 FAILOVER_AUTH_REQUEST。

4.選舉投票

  隻有持有槽的主節點才會處理故障選舉消息(FAILOVER_AUTH__REQUEST),因為每個持有槽的節點在一個配置紀元内都有唯一的一張選票,當接到第一個請求投票的從節點消息時回複FAILOVER_AUTH_ACK消息作為投票,之後相同配置紀元内其他從節點的選舉消息将忽略。

  投票過程其實是一個上司者選舉的過程,如叢集内有N個持有槽的主節點代表有N張選票。由于在每個配置紀元内持有槽的主節點隻能投票給一個從節點,是以隻能有一個從節點獲得N2+1的選票,保證能夠找出唯一的從節點。

  Redis叢集沒有直接使用從節點進行上司者選舉,主要因為從節點數必須大于等于3 個才能保證湊夠N/2 + 1個節點,将導緻從節點資源浪費。使用叢集内所有持有槽的主節點進行上司者選舉,即使隻有一個從節點也可以完成選舉過程。

  當從節點收集到N/2+1個持有槽的主節點投票時,從節點可以執行替換主節點操作,例如叢集内有5 個持有槽的主節點,主節點b 故障後還有4 個,當其中一個從節點收集到3 張投票時代表獲得了足夠的選票可以進行替換主節點操作,如圖10-41所示。

Redis之叢集

  提示: 主節點也算在投票數内,假設叢集内節點規模是3主3從,其中有2個主節點部署在一台機器上,當這台機器當機時,由于從節點無法收集到3/2+1個主節點選票将導緻故障轉移失敗。這個問題也适用于故障發現環節。是以部署叢集時所有主節點最少需要部署在3 台實體機上才能避免單點問題。

  投票廢棄:每個配置紀元代表了一次選舉周期,如果在開始投票之後的cluster-node-timeout*2 時間内從節點沒有擷取足夠數量的投票,則本次選舉廢棄。從節點對配置紀元自增并發起下一輪投票,直到選舉成功為止。

5.替換主節點

  當從節點收集到足夠的選票之後,觸發替換主節點操作:

1) 目前從節點取消複制變為主節點。

2) 執行clusterDelSlot操作撤銷故障主節點負責的槽,并執行clusterAddSlot把這些槽委派給自己。

3) 向叢集廣播自己的pong消息,通知叢集内所有的節點目前從節點變為主節點并接管了故障主節點的槽資訊。

6.3 故障轉移時間

  在介紹完故障發現和恢複的流程後,這時我們可以估算出故障轉移時間:

1) 主觀下線(pfail)識别時間=cluster-node-timeout。

2) 主觀下線狀态消息傳播時間<=cluster-node-timeout/2。消息通信機制對超過cluster-node-timeout/2 未通信節點會發起ping消息,消息體在選擇包含哪些節點時會優先選取下線狀态節點,是以通常這段時間内能夠收集到半數以上主節點的pfail報告進而完成故障發現。

3) 從節點轉移時間<= 1000毫秒。由于存在延遲發起選舉機制,偏移量最大的從節點會最多延遲1秒發起選舉。通常第一次選舉就會成功,是以從節點執行轉移時間在1秒以内。

  根據以上分析可以預估出故障轉移時間,如下:

failover-time ( 毫秒)≤ cluster-node-timeout + cluster-node-timeout/2 + 1000      

  是以,故障轉移時間跟cluster-node-timeout參數息息相關,預設15秒。配置時可以根據業務容忍度做出适當調整,但不是越小越好,下一節的帶寬消耗部分會進一步說明。

6.4 故障轉移演練

  到目前為止介紹了故障轉移的主要細節,下面通過之前搭建的叢集模拟主節點故障場景,對故障轉移行為進行分析。使用kill -9強制關閉主節點6385程序,如圖10-42所示。

Redis之叢集

  确認叢集狀态:

127.0.0.1:6379> cluster nodes
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385 master - 0 1471877563600 16
     connected 0-1365 5462-6826 10923-12287 15018-16383
40622f9e7adc8ebd77fca0de9edfe691cb8a74fb 127.0.0.1:6382 slave cfb28ef1deee4e0fa78da
     86abe5d24566744411e 0 1471877564608 13 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 master - 0 1471877567129 11
     connected 6827-10922 13653-15017
475528b1bcf8e74d227104a6cf1bf70f00c24aae 127.0.0.1:6386 slave 1a205dd8b2819a00dd1e8
     b6be40a8e2abe77b756 0 1471877569145 16 connected
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 my self, master - 0 0 13
     connected 1366-5461 12288-13652
be9485a6a729fc98c5151374bc30277e89a461d8 127.0.0.1:6383 slave 8e41673d59c9568aa9
     d29fb174ce733345b3e8f1 0 1471877568136 11 connected      

強制關閉6385程序:

# ps -ef | grep redis-server | grep 6385
501  1362  1  0 10:50 0:11.65 redis-server *:6385 [cluster]
# kill -9 1362      

日志分析如下:

□ 從節點6386與主節點6385複制中斷,日志如下:

==> redis-6386.log <==
# Connection with master lost .
# Caching the disconnected master state.
# Connecting to MASTER 127.0.0.1:6385
# MASTER <-> SLAVE sync started
# Error condition on socket for SYNC: Connection refused      

□ 6379和 6380兩個主節點都标記6385為主觀下線,超過半數是以标記為客觀下線狀态,列印如下日志:

==> redis-6380.log <==
# Marking node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 as failing (quorum reached).
==> redis-6379.log <==
# Marking node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 as failing (quorum reached).      

□ 從節點識别正在複制的主節點進人客觀下線後準備選舉時間,日志列印了選舉延遲964毫秒之後執行,并列印目前從節點複制偏移量。

==> redis-6386.log <==
# Start of election delayed for 964 milliseconds (rank #0, offset 1822).      

□ 延遲選舉時間到達後,從節點更新配置紀元并發起故障選舉。

==> redis-6386.log <==
1364:S 22 Aug 23:12:25.064 # Starting a failover election for epoch 17.      

□ 6379和6380主節點為從節點6386投票,日志如下:

==> redis-6380.log <==
# Failover auth granted to 475528b1bcf8e74d227104a6cf1bf70f00c24aae for epoch 17
==> redis-6379.log <==
# Failover auth granted to 475528b1bcf8e74d227104a6cf1bf70f00c24aae for epoch 17      

□ 從節點擷取2 個主節點投票之後,超過半數執行替換主節點操作,進而完成故障轉移:

==> redis-6386.log <==
# Failover election won: I'm the new master.
# configEpoch set to 17 after successful failover      

成功完成故障轉移之後,我們對已經出現故障節點6385進行恢複,觀察節點狀态是否正确:

1) 重新啟動故障節點6385。

#redis-server conf/redis-6385.conf      

2) 6385節點啟動後發現自己負責的槽指派給另一個節點,則以現有叢集配置為準,變為新主節點6386的從節點,關鍵日志如下:

# I have keys for slot 4096, but the slot is assigned to another node. Setting it to importing state.
# Configuration change detected.Reconfiguring myself as a replica of 475528b1bcf8e74d227104a6cf1bf70f00c24aae      

3) 叢集内其他節點接收到6385發來的ping消息,清空客觀下線狀态:

==> redis-6379.log <==
* Clear FAIL state for node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756:master without slots is reachable again.
==> redis-6380.log <==
* Clear FAIL state for node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756:master without slots is reachable again.
==> redis-6382.log <==
* Clear FAIL state for node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756:master without slots is reachable again.
==> redis-6383.log <==
* Clear FAIL state for node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756:master without slots is reachable again.
==> redis-6386.log <==
* Clear FAIL state for node 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756: master without slots is reachable again.      

4) 6385節點變為從節點,對主節點6386發起複制流程:

==> redis-6385.log <==
* MASTER <-> SLAVE sync: Flushing old data
* MASTER <-> SLAVE sync: Loading DB in memory
* MASTER <-> SLAVE sync: Finished with success      

5) 最終叢集狀态如圖10-43所示。

Redis之叢集

7.叢集運維

  Redis叢集由于自身的分布式特性,相比單機場景在開發和運維方面存在一些差異。本節我們關注于常見的問題進行分析定位。

7.1 叢集完整性

  為了保證叢集完整性,預設情況下當叢集16384個槽任何一個沒有指派到節點時整個叢集不可用。執行任何鍵指令傳回(error) CLUSTERDOWN Hash slot not served錯誤。這是對叢集完整性的一種保護措施,保證所有的槽都指派給線上的節點。但是當持有槽的主節點下線時,從故障發現到自動完成轉移期間整個叢集是不可用狀态,對于大多數業務無法容忍這種情況,是以建議将參數cluster-require-full-coverage配置為no, 當主節點故障時隻影響它負責槽的相關指令執行,不會影響其他主節點的可用性。

7.2 帶寬消耗

  叢集内Gossip消息通信本身會消耗帶寬,官方建議叢集最大規模在1000以内,也是出于對消息通信成本的考慮,是以單叢集不适合部署超大規模的節點。在之前節點通信小節介紹到,叢集内所有節點通過ping/pong消息彼此交換資訊,節點間消息通信對帶寬的消耗展現在以下幾個方面:

□ 消息發送頻率:跟cluster-node-timeout密切相關,當節點發現與其他節點最後通信時間超過cluster-node-timeout/2 時會直接發送ping消息。

□ 消息資料量:每個消息主要的資料占用包含:slots槽數組(2KB空間)和整個叢集1/10的狀态資料(10個節點狀态資料約1KB)。

□ 節點部署的機器規模:機器帶寬的上線是固定的,是以相同規模的叢集分布的機器越多每台機器劃分的節點越均勻,則叢集内整體的可用帶寬越高。

  例如,一個總節點數為200的Redis叢集,部署在20台實體機上每台劃分10個節點,cluster-node-timeout采用預設15秒,這時ping/pong消息占用帶寬達到25Mb。如果把 cluster-node-timeout設為20,對帶寬的消耗降低到15Mb以下。

  叢集帶寬消耗主要分為:讀寫指令消耗+ Gossip消息消耗。是以搭建Redis叢集時需要根據業務資料規模和消息通信成本做出合理規劃:

1) 在滿足業務需要的情況下盡量避免大叢集。同一個系統可以針對不同業務場景拆分使用多套叢集。這樣每個叢集既滿足伸縮性和故障轉移要求,還可以規避大規模叢集的弊端。如本書作者維護的一個推薦系統,根據資料特征使用了 5 個 Redis叢集,每個叢集節點規模控制在100以内。

2) 适度提高cluster-node-timeout降低消息發送頻率,同時cluster-node-timeout還影響故障轉移的速度 ,是以需要根據自身業務場景兼顧二者的平衡。

3) 如果條件允許叢集盡量均勻部署在更多機器上。避免集中部署,如叢集有60個節點,集中部署在3 台機器上每台部署20個節點,這時機器帶寬消耗将非常嚴重。

7.3 Pub/Sub廣播問題

  Redis在 2.0版本提供了 Pub/Sub (釋出/訂閱)功能,用于針對頻道實作消息的釋出和訂閱。但是在叢集模式下内部實作對所有的publish指令都會向所有的節點進行廣播,造成每條publish資料都會在叢集内所有節點傳播一次,加重帶寬負擔,如圖10-44所示:通過指令示範Pub/Sub廣播問題,如下所示:

Redis之叢集

1) 對叢集所有主從節點執行subscribe指令訂閱 cluster_pub_spread 頻道,用于驗證叢集是否廣播消息:

127.0.0.1:6379> subscribe cluster_pub_spread
127.0.0.1:6380> subscribe cluster_pub_spread
127.0.0.1:6382> subscribe cluster_pub_spread
127.0.0.1:6383> subscribe cluster_pub_spread
127.0.0.1:6385> subscribe cluster_pub_spread
127.0.0.1:6386> subscribe cluster_pub_spread      

2) 在6379節點上釋出頻道為cluster_pub_spread的消息:

127.0.0.1:6379> publish cluster_pub_spread message_body_1      

3) 叢集内所有的節點訂閱用戶端全部收到了消息:

127.0.0.1:6380> subscribe cluster_pub_spread
1) "message"
2) "cluster_pub_spread"
3) "message_body_1
127.0.0.1:6382> subscribe cluster_pub_spread
1) "message"
2) "cluster_pub_spread"
3) "message_body_1      

針對叢集模式下publish廣播問題,需要引起開發人員注意,當頻繁應用Pub/Sub功能時應該避免在大量節點的叢集内使用,否則會嚴重消耗叢集内網絡帶寬。針對這種情況建議使用sentinel結構專門用于Pub/Sub功能,進而規避這一問題。

7.4 叢集傾斜

  叢集傾斜指不同節點之間資料量和請求量出現明顯差異,這種情況将加大負載均衡和開發運維的難度。是以需要了解哪些原因會造成叢集傾斜,進而避免這一問題。

1.資料傾斜

  資料傾斜主要分為以下幾種:  .

□ 節點和槽配置設定嚴重不均。

□ 不同槽對應鍵數量差異過大。

口 集合對象包含大量元素。

□  記憶體相關配置不一緻。

1) 節點和槽配置設定嚴重不均。針對每個節點配置設定的槽不均的情況,可以使用 redis-trib.rb info {host:ip} 進行定位,指令如下:

#redis-trib.rb info 127.0.0.1:6379
127.0.0.1:6379 (cfb28ef1 ...)  -> 33348 keys | 5461 slots | 1 slaves.
127.0.0.1:6380 (8e41673d...) -> 33391 keys | 5461 slots | 1 slaves.
127.0.0.1:6386 (475528b1...) -> 33263 keys | 5462 slots | 1 slaves.
[OK] 100002 keys in 3 masters.
6.10 keys per slot on average.      

以上資訊列舉出每個節點負責的槽和鍵總量以及每個槽平均鍵數量。當節點對應槽數量不均勻時,可以使用redis-trib.rb rebalance指令進行平衡:

#redis-trib.rb rebalance 127.0.0.1:6379
[OK] All 16384 slots covered.
*** No rebalancing needed! All nodes are within the 2.0% threshold.      

2) 不同槽對應鍵數量差異過大。鍵通過CRC16哈希函數映射到槽上,正常情況下槽内鍵數量會相對均勻。但當大量使用hash_tag時,會産生不同的鍵映射到同一個槽的情況。特别是選擇作為hash_tag的資料離散度較差時,将加速槽内鍵數量傾斜情況。通過指令:cluster countkeysinslot {slot}可以擷取槽對應的鍵數量,識别出哪些槽映射了過多的鍵。再通過指令cluster getkey sin slot {slot} {count}循環疊代出槽下所有的鍵。進而發現過度使用hash_tag的鍵。

3) 集合對象包含大量元素。對于大集合對象的識别可以使用redis-cli --bigkeys指令識别。找出大集合之後可以根據業務場景進行拆分。同時叢集槽資料遷移是對鍵執行 migrate 操作完成,過大的鍵集合如幾百兆,容易造成migrate指令逾時導緻資料遷移失敗。

4) 記憶體相關配置不一緻。記憶體相關配置指hash-max-ziplist-value、set-max-intset-entries等壓縮資料結構配置。當叢集大量使用 hash、set等資料結構時,如果記憶體壓縮資料結構配置不一緻,極端情況下會相差數倍的記憶體,進而造成節點記憶體量傾斜。

2.請求傾斜

  叢集内特定節點請求量/流量過大将導緻節點之間負載不均,影響叢集均衡和運維成本。常出現在熱點鍵場景,當鍵指令消耗較低時如小對象的get、set、incr等,即使請求量差異較大一般也不會産生負載嚴重不均。但是當熱點鍵對應高算法複雜度的指令或者是大對象操作如hgetall、smembers等,會導緻對應節點負載過高的情況。避免方式如下:

1) 合理設計鍵,熱點大集合對象做拆分或使用hmget替代hgetall避免整體讀取。

2) 不要使用熱鍵作為hash_tag,避免映射到同一槽。

3) 對于一緻性要求不高的場景,用戶端可使用本地緩存減少熱鍵調用。

7.5 叢集讀寫分離

1.隻讀連接配接

  叢集模式下從節點不接受任何讀寫請求,發送過來的鍵指令會重定向到負責槽的主節點上 (其中包括它的主節點)。當需要使用從節點分擔主節點讀壓力時,可以使用readonly指令打開用戶端連接配接隻讀狀态。之前的複制配置 slave-read-only 在叢集模式下無效。當開啟隻讀狀态時,從節點接收讀指令處理流程變為:如果對應的槽屬于自己正在複制的主節點則直接執行讀指令,否則傳回重定向資訊。指令如下:

//預設連接配接狀态為普通用戶端:flags=N
127.0.0.1:6382> client list
id=3 addr=127.0.0.1:56499 fd=6 name= age=130 idle=0 flags=N db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=32768 obl=0  oll=0 omem=0 events=r cmd=client
//指令重定向到主節點
127.0. 0.1:6382> get key:test:3130
(error) MOVED 12944 127.0.0.1:6379
//打開目前連接配接隻讀狀态
127.0. 0.1:6382> readonly
OK
//用戶端狀态變為隻讀:flags=r
127.0. 0.1:6382> client list
id=3 addr=127.0.0.1:56499 fd=6 name= age=154 idle=0 flags=r db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=32768 obl=0  oll=0 omem=0 events=r cmd=client
//從節點響應讀指令
127.0. 0.1:6382> get key:test:3130
"value:3130"      

readonly 指令是連接配接級别生效,是以每次建立連接配接時都需要執行readonly開啟隻讀狀态。執行readwrite指令可以關閉連接配接隻讀狀态。

2.讀寫分離

  叢集模式下的讀寫分離,同樣會遇到:複制延遲,讀取過期資料,從節點故障等問題,具體細節見複制運維小節。針對從節點故障問題,用戶端需要維護可用節點清單,叢集提供了cluster slaves {nodeld} 指令,傳回 nodeld 對應主節點下所有從節點資訊,資料格式同 cluster nodes, 指令如下:

// 傳回6379節點下所有從節點
127.0.0.1:6382> cluster slaves cfb28ef1deee4e0fa78da86abe5d24566744411e
1) "40622f9e7adc8ebd77fca0de9edfe691cb8a74fb 127.0.0.1:6382 myself,slave cfb28ef1deee4e0fa78da86abe5d24566744411e 0 0 3 connected"
2) "2e7cf7539d076a1217a408bb897727e5349bcfcf 127.0.0.1:6384 slave,fail cfb28ef1deee4e0fa78da86abe5d24566744411e 1473047627396 1473047622557 13 disconnected"      

  解析以上從節點清單資訊,排除fail狀态節點,這樣用戶端對從節點的故障判定可以委托給叢集處理,簡化維護可用從節點清單難度。

  提示:叢集模式下讀寫分離涉及對用戶端修改如下:

1) 維護每個主節點可用從節點清單。

2) 針對讀指令維護請求節點路由。

3) 從節點建立連接配接開啟readonly狀态。

  叢集模式下讀寫分離成本比較高,可以直接擴充主節點數量提髙叢集性能,一般不建議叢集模式下做讀寫分離。

  叢集讀寫分離有時用于特殊業務場景如:

1) 利用複制的最終一緻性使用多個從節點做跨機房部署降低讀指令網絡延遲。

2) 主節點故障轉移時間過長,業務端把讀請求路由給從節點保證讀操作可用。

  以上場景也可以在不同機房獨立部署Redis叢集解決,通過用戶端多寫來維護,讀指令直接請求到最近機房的Redis叢集,或者當一個叢集節點故障時用戶端轉向另一個叢集。

7.6 手動故障轉移

  Redis叢集提供了手動故障轉移功能:指定從節點發起轉移流程,主從節點角色進行切換,從節點變為新的主節點對外提供服務,舊的主節點變為它的從節點,如圖10-45所示。

Redis之叢集

  在從節點上執行 cluster failover 指令發起轉移流程,預設情況下轉移期間用戶端請求會有短暫的阻塞,但不會丢失資料,流程如下:

1) 從節點通知主節點停止處理所有用戶端請求。

2) 主節點發送對應從節點延遲複制的資料。

3) 從節點接收處理複制延遲的資料,直到主從複制偏移量一緻為止,保證複制資料不丢失。

4) 從節點立刻發起投票選舉(這裡不需要延遲觸發選舉)。選舉成功後斷開複制變為新的主節點,之後向叢集廣播主節點pong消息。

5) 舊主節點接受到消息後更新自身配置變為從節點,解除所有用戶端請求阻塞,這些請求會被重定向到新主節點上執行。

6) 舊主節點變為從節點後,向新的主節點發起全量複制流程

  主從節點轉移後,新的從節點由于之前沒有緩存主節點資訊無法使用部分複制功能,是以會發起全量複制,當節點包含大量資料時會嚴重消耗CPU和網絡資源,線上不要頻繁操作。Redis4.0的Rsync2将有效改善這一問題。

  手動故障轉移的應用場景主要如下:

1) 主節點遷移: 運維Redis叢集過程中經常遇到調整節點部署的問題,如節點所在的老機器替換到新機器等。由于從節點預設不響應請求可以安全下線關閉,但直接下線主節點會導緻故障自動轉移期間主節點無法對外提供服務,影響線上業務的穩定性。這時可以使用手動故障轉移,把要下線的主節點安全的替換為從節點後,再做下線操作操作,如圖10-46所示。

Redis之叢集

2) 強制故障轉移。當自動故障轉移失敗時,隻要故障的主節點有存活的從節點就可以通過手動轉移故障強制讓從節點替換故障的主節點,保證叢集的可用性。自動故障轉移失敗的場景有:

□ 主節點和它的所有從節點同時故障。這個問題需要通過調整節點機器部署拓撲做規避,保證主從節點不在同一機器/機架上。除非機房内大面積故障,否則兩台機器/機架同時故障機率很低。

□ 所有從節點與主節點複制斷線時間超過cluster-slave-validity-factor * cluster-node-timeout + repl-ping-slave-period, 導緻從節點被判定為沒有故障轉移資格,手動故障轉移從節點不做中斷逾時檢査。

□ 由于網絡不穩定等問題,故障發現或故障選舉時間無法在cluster-node-timeout* 2 内完成,流程會不斷重試,最終從節點複制中斷時間逾時,失去故障轉移資格無法完成轉移。

□ 叢集内超過一半以上的主節點同時故障。

  根據以上情況,cluster failover指令提供了兩個參數force/takeover提供支援:

□ cluster failover force---用于當主節點當機且無法自動完成故障轉移情況。從節點接到cluster failover force請求時,從節點直接發起選舉,不再跟主節點确認複制偏移量(從節點複制延遲的資料會丢失),當從節點選舉成功後替換為新的主節點并廣播叢集配置。

□ cluster failover takeover---用于叢集内超過一半以上主節點故障的場景,因為從節點無法收到半數以上主節點投票,是以無法完成選舉過程。可以執行 cluster failover takeover 強制轉移,接到指令的從節點不再進行選舉流程而是直接更新本地配置紀元并替換主節點。takeover故障轉移由于沒有通過上司者選舉發起故障轉移,會導緻配置紀元存在沖突的可能。當沖突發生時,叢集會以 nodeid 字典序更大的一方配置為準。是以要小心叢集分區後,手動執行takeover導緻的叢集沖突問題。如圖10-47所示。

Redis之叢集

  圖中Redis 叢集分别部署在2個同城機房,機房A部署節點:master-1、master-2、master-3、slave-4。機房B部署節點:slave-1、slave-2、slave-3、master-4。

□ 當機房之間出現網絡中斷時,機房A内的節點持有半數以上主節點可以完成故障轉移,會将slave-4轉換為master-4。

□ 如果用戶端應用都部署在機房B,運維人員為了快速恢複對機房B的 Redis 通路,對slave-1, slave-2,slave-3 分别執行cluster failover takeover 強制故障轉移,讓機房B的節點可以快速恢複服務。

□ 當機房專線恢複後,Redis 叢集會擁有兩套持有相同槽資訊的主節點。這時叢集會使用配置紀元更大的主節點槽資訊,配置紀元相等時使用 nodeId 更大的一方,是以最終會以哪個主節點為準是不确定的。如果叢集以機房 A 的主節點槽資訊為準,則這段時間内對機房 B 的寫入資料将會丢失。

  綜上所述,在叢集可以自動完成故障轉移的情況下,不要使用cluster failover takeover 強制幹擾叢集選舉機制,該操作主要用于半數以上主節點故障時采取的強制措施,請慎用。

  手動故障轉移時,在滿足目前需求的情況下建議優先級:cluster failver > cluster failover force > cluster failover takeover。

7.7 資料遷移

  應用Redis叢集時,常需要把單機Redis資料遷移到叢集環境。redis-trib.rb工具提供了導入功能,用于資料從單機向叢集環境遷移的場景,指令如下:

redis-trib.rb import host:port --from <arg> --copy --replace      

  redis-trib.rb import指令内部采用批量scan和 migrate的方式遷移資料。這種遷移方式存在以下缺點:

1) 遷移隻能從單機節點向叢集環境導人資料。

2) 不支援線上遷移資料,遷移資料時應用方必須停寫,無法平滑遷移資料。

3) 遷移過程中途如果出現逾時等錯誤,不支援斷點續傳隻能重新全量導人。

4) 使用單線程進行資料遷移,大資料量遷移速度過慢。

  正因為這些問題,社群開源了很多遷移工具,這裡推薦一款唯品會開發的redis-m igrate-tool,該工具可滿足大多數Redis遷移需求,特點如下:

□ 支援單機、Twemproxy、Redis Cluster、RDB/AOF等多種類型的資料遷移。

□ 工具模拟成從節點基于複制流遷移資料,進而支援線上遷移資料,業務方不需要停寫。

口 采用多線程加速資料遷移過程且提供資料校驗和査看遷移狀态等功能。

  更多細節見  GitHub:https://github.com/vipshop/redis-migrate-tool。

8. 總結

  1. redis叢集資料分區規則采用虛拟槽方式,所有的鍵映射到16384個槽中,每個節點負責一部分槽和相關資料,實作資料和請求的負載均衡。

  2. 叢集劃分三個步驟:準備節點,節點握手,配置設定槽。可以使用redis-trib.rb create指令快速搭建叢集。

  3. 叢集内部節點通信采用Gossip協定彼此發送消息,消息類型分為: ping消息、pong消息、meet消息、fail消息等。節點定期不斷發送和接受ping/pong消息來維護更新叢集的狀态。消息内容包括節點自身資料和部分其他節點的狀态資料。

  4. 叢集伸縮通過在節點之間移動槽和相關資料實作。擴容時根據槽遷移計劃把槽從源節點遷移到目标節點,源節點負責的槽相比之前變少進而達到叢集擴容的目的,收縮時如果下線的節點有負責的槽需要遷移到其他節點,再通過cluster forget指令讓叢集内其他節點忘記被下線節點。

  5. 使用Smart用戶端操作叢集達到通信效率最大化,用戶端内部負責計算維護鍵—槽—節點的映射,用于快速定位鍵指令到目标節點。叢集協定通過Smart用戶端全面高效的,支援需要一個過程,使用者在選擇Smart用戶端時建議review下叢集互動代碼如:異常判定和重試邏輯,更新槽的并發控制等。節點接收到鍵指令時會判斷相關的槽是否由自身節點負責,如果不是則傳回重定向資訊。重定向分為 MOVED 和ASK,ASK 說明叢集正在進行槽資料遷移,用戶端隻在本次請求中做臨時重定向,不會更新本地槽緩存。 MOVED 重定向說明槽已經明确分派到另一個節點,用戶端需要更新槽節點緩存。

  6. 叢集自動故障轉移過程分為故障發現和故障恢複。節點下線分為主觀下線和客觀下線,當超過半數主節點認為故障節點為主觀下線時标記它為客觀下線狀态。從節點負責對客觀下線的主節點觸發故障恢複流程,保證叢集的可用性。

  7. 開發和運維叢集過程中常見問題包括:超大規模叢集帶寬消耗,pub/sub廣播問題,叢集節點傾斜問題,手動故障轉移,線上遷移資料等

作者:小家電維修

出處:https://www.cnblogs.com/lizexiong/

轉世燕還故榻,為你銜來二月的花。