redis cluster 集羣 HA 原理和實操(史上最全、面試必備)

文章很長,建議收藏起來慢慢讀!瘋狂創客圈總目錄 語雀版 | 總目錄 碼雲版| 總目錄 博客園版 爲您奉上珍貴的學習資源 :


推薦:入大廠 、做架構、大力提升Java 內功 的 精彩博文

入大廠 、做架構、大力提升Java 內功 必備的精彩博文 2021 秋招漲薪1W + 必備的精彩博文
1:Redis 分佈式鎖 (圖解-秒懂-史上最全) 2:Zookeeper 分佈式鎖 (圖解-秒懂-史上最全)
3: Redis與MySQL雙寫一致性如何保證? (面試必備) 4: 面試必備:秒殺超賣 解決方案 (史上最全)
5:面試必備之:Reactor模式 6: 10分鐘看懂, Java NIO 底層原理
7:TCP/IP(圖解+秒懂+史上最全) 8:Feign原理 (圖解)
9:DNS圖解(秒懂 + 史上最全 + 高薪必備) 10:CDN圖解(秒懂 + 史上最全 + 高薪必備)
11: 分佈式事務( 圖解 + 史上最全 + 吐血推薦 ) 12:限流:計數器、漏桶、令牌桶
三大算法的原理與實戰(圖解+史上最全)
13:架構必看:12306搶票系統億級流量架構
(圖解+秒懂+史上最全)
14:seata AT模式實戰(圖解+秒懂+史上最全)
15:seata 源碼解讀(圖解+秒懂+史上最全) 16:seata TCC模式實戰(圖解+秒懂+史上最全)

Java 面試題 30個專題 , 史上最全 , 面試必刷 阿里、京東、美團... 隨意挑、橫着走!!!
1: JVM面試題(史上最強、持續更新、吐血推薦) 2:Java基礎面試題(史上最全、持續更新、吐血推薦
3:架構設計面試題 (史上最全、持續更新、吐血推薦) 4:設計模式面試題 (史上最全、持續更新、吐血推薦)
17、分佈式事務面試題 (史上最全、持續更新、吐血推薦) 一致性協議 (史上最全)
29、多線程面試題(史上最全) 30、HR面經,過五關斬六將後,小心陰溝翻船!
9.網絡協議面試題(史上最全、持續更新、吐血推薦) 更多專題, 請參見【 瘋狂創客圈 高併發 總目錄

SpringCloud 微服務 精彩博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
SpringCloud gateway (史上最全) 分庫分表sharding-jdbc底層原理與實操(史上最全,5W字長文,吐血推薦)

說明

redis cluster是 生存環境常用的組件,很多小夥伴沒有玩過,很可惜

本文從原理到實操,都給大家做了一個介紹,後面會 持續完善

實際上,尼恩給大家準備了一鍵搭建一套redis cluster集羣的 編排文件,可以在虛擬機上 體驗一下,比單機 安裝還簡單

如果有需要,可以來瘋狂創客圈 社羣來獲取

Redis集羣高可用常見的三種方式:

Redis高可用常見的有兩種方式:

  • Replication-Sentinel模式
  • Redis-Cluster模式
  • 中心化代理模式(proxy模式)

Replication-Sentinel模式

Redis sentinel 是一個分佈式系統中監控 redis 主從服務器,並在主服務器下線時自動進行故障轉移。

img

Redis sentinel 其中三個特性:

  • 監控(Monitoring):

Sentinel 會不斷地檢查你的主服務器和從服務器是否運作正常。

  • 提醒(Notification):

當被監控的某個 Redis 服務器出現問題時, Sentinel 可以通過 API 向管理員或者其他應用程序發送通知。

  • 自動故障遷移(Automatic failover):

當一個主服務器不能正常工作時, Sentinel 會開始一次自動故障遷移操作。

哨兵本身也有單點故障的問題,可以使用多個哨兵進行監控,哨兵不僅會監控redis集羣,哨兵之間也會相互監控。

每一個哨兵都是一個獨立的進程,作爲進程,它會獨立運行。

img

特點:

  • 1、保證高可用

  • 2、監控各個節點

  • 3、自動故障遷移

缺點:

主從模式,切換需要時間丟數據

沒有解決 master 寫的壓力

Redis-Cluster模式

redis在3.0上加入了 Cluster 集羣模式,實現了 Redis 的分佈式存儲,也就是說每臺 Redis 節點上存儲不同的數據。

cluster模式爲了解決單機Redis容量有限的問題,將數據按一定的規則分配到多臺機器,內存/QPS不受限於單機,可受益於分佈式集羣高擴展性。

RedisCluster 是 Redis 的親兒子,它是 Redis 作者自己提供的 Redis 集羣化方案。

相對於 Codis 的不同,它是去中心化的,如圖所示,該集羣有三個 Redis 節點組成, 每個節點負責整個集羣的一部分數據,每個節點負責的數據多少可能不一樣。這三個節點相 互連接組成一個對等的集羣,它們之間通過一種特殊的二進制協議相互交互集羣信息。

img

如上圖,官方推薦,集羣部署至少要 3 臺以上的master節點,最好使用 3 主 3 從六個節點的模式。

Redis Cluster 將所有數據劃分爲 16384 的 slots,它比 Codis 的 1024 個槽劃分得更爲精細,每個節點負責其中一部分槽位。槽位的信息存儲於每個節點中,它不像 Codis,它不 需要另外的分佈式存儲來存儲節點槽位信息。

Redis Cluster是一種服務器Sharding技術(分片和路由都是在服務端實現),採用多主多從,每一個分區都是由一個Redis主機和多個從機組成,片區和片區之間是相互平行的。

Redis Cluster集羣採用了P2P的模式,完全去中心化。

3 主 3 從六個節點的Redis集羣(Redis-Cluster)

Redis 集羣是一個提供在多個Redis節點間共享數據的程序集。

下圖以三個master節點和三個slave節點作爲示例。

在這裏插入圖片描述

Redis 集羣有16384個哈希槽,每個key通過CRC16校驗後對16384取模來決定放置哪個槽。

集羣的每個節點負責一部分hash槽,如圖中slots所示。

爲了使在部分節點失敗或者大部分節點無法通信的情況下集羣仍然可用,所以集羣使用了主從複製模型,每個節點都會有1-n個從節點。

例如master-A節點不可用了,集羣便會選舉slave-A節點作爲新的主節點繼續服務。

中心化代理模式(proxy模式)

這種方案,將分片工作交給專門的代理程序來做。代

理程序接收到來自業務程序的數據請求,根據路由規則,將這些請求分發給正確的 Redis 實例並返回給業務程序。

其基本原理是:通過中間件的形式,Redis客戶端把請求發送到代理 proxy,代理 proxy 根據路由規則發送到正確的Redis實例,最後 代理 proxy 把結果彙集返回給客戶端。

redis代理分片用得最多的就是Twemproxy,由Twitter開源的Redis代理,其基本原理是:通過中間件的形式,Redis客戶端把請求發送到Twemproxy,Twemproxy根據路由規則發送到正確的Redis實例,最後Twemproxy把結果彙集返回給客戶端。

img

這種機制下,一般會選用第三方代理程序(而不是自己研發),因爲後端有多個 Redis 實例,所以這類程序又稱爲分佈式中間件。

這樣的好處是,業務程序不用關心後端 Redis 實例,運維起來也方便。雖然會因此帶來些性能損耗,但對於 Redis 這種內存讀寫型應用,相對而言是能容忍的。

Twemproxy 代理分片

Twemproxy 是一個 Twitter 開源的一個 redis 和 memcache 快速/輕量級代理服務器; Twemproxy 是一個快速的單線程代理程序,支持 Memcached ASCII 協議和 redis 協議。

Twemproxy是由Twitter開源的集羣化方案,它既可以做Redis Proxy,還可以做Memcached Proxy。

它的功能比較單一,只實現了請求路由轉發,沒有像Codis那麼全面有在線擴容的功能,它解決的重點就是把客戶端分片的邏輯統一放到了Proxy層而已,其他功能沒有做任何處理。

img

Tweproxy推出的時間最久,在早期沒有好的服務端分片集羣方案時,應用範圍很廣,而且性能也極其穩定。

但它的痛點就是無法在線擴容、縮容,這就導致運維非常不方便,而且也沒有友好的運維UI可以使用。

Codis代理分片

Codis 是一個分佈式 Redis 解決方案, 對於上層的應用來說, 連接到 Codis Proxy 和連接原生的 Redis Server 沒有明顯的區別 (有一些命令不支持), 上層應用可以像使用單機的 Redis 一樣使用, Codis 底層會處理請求的轉發, 不停機的數據遷移等工作, 所有後邊的一切事情, 對於前面的客戶端來說是透明的, 可以簡單的認爲後邊連接的是一個內存無限大的 Redis 服務,

現在美團、阿里等大廠已經開始用codis的集羣功能了,

什麼是Codis?

Twemproxy不能平滑增加Redis實例的問題帶來了很大的不便,於是豌豆莢自主研發了Codis,一個支持平滑增加Redis實例的Redis代理軟件,其基於Go和C語言開發,並於2014年11月在GitHub上開源 codis開源地址

Codis的架構圖:

img

在Codis的架構圖中,Codis引入了Redis Server Group,其通過指定一個主CodisRedis和一個或多個從CodisRedis,實現了Redis集羣的高可用。

當一個主CodisRedis掛掉時,Codis不會自動把一個從CodisRedis提升爲主CodisRedis,這涉及數據的一致性問題(Redis本身的數據同步是採用主從異步複製,當數據在主CodisRedis寫入成功時,從CodisRedis是否已讀入這個數據是沒法保證的),需要管理員在管理界面上手動把從CodisRedis提升爲主CodisRedis。

如果手動處理覺得麻煩,豌豆莢也提供了一個工具Codis-ha,這個工具會在檢測到主CodisRedis掛掉的時候將其下線並提升一個從CodisRedis爲主CodisRedis。

Codis的預分片

Codis中採用預分片的形式,啓動的時候就創建了1024個slot,1個slot相當於1個箱子,每個箱子有固定的編號,範圍是1~1024。

Codis的分片算法

Codis proxy 代理通過一種算法把要操作的key經過計算後分配到各個組中,這個過程叫做分片。
在這裏插入圖片描述

在Codis裏面,它把所有的key分爲1024個槽,每一個槽位都對應了一個分組,具體槽位的分配,可以進行自定義,現在如果有一個key進來,首先要根據CRC32算法,針對key算出32位的哈希值,然後除以1024取餘,然後就能算出這個KEY屬於哪個槽,然後根據槽與分組的映射關係,就能去對應的分組當中處理數據了。

在這裏插入圖片描述

CRC全稱是循環冗餘校驗,主要在數據存儲和通信領域保證數據正確性的校驗手段,CRC校驗(循環冗餘校驗)是數據通訊中最常採用的校驗方式。

slot這個箱子用作存放Key,至於Key存放到哪個箱子,可以通過算法“crc32(key)%1024”獲得一個數字,這個數字的範圍一定是1~1024之間,Key就放到這個數字對應的slot。

例如,如果某個Key通過算法“crc32(key)%1024”得到的數字是5,就放到編碼爲5的slot(箱子)。

slot和Server Group的關係

1個slot只能放1個Redis Server Group,不能把1個slot放到多個Redis Server Group中。1個Redis Server Group最少可以存放1個slot,最大可以存放1024個slot。

因此,Codis中最多可以指定1024個Redis Server Group。

槽位和分組的映射關係就保存在codis proxy當中

redis主從複製

主從複製還是哨兵和集羣能夠實施的基礎,因此說主從複製是Redis高可用的基礎。

what is ?

主從複製,是指將一臺Redis服務器的數據,複製到其他的Redis服務器。

  • 前者稱爲主節點(master),後者稱爲從節點(slave);

  • 數據的複製是單向的,只能由主節點到從節點。

  • 默認情況下,每臺Redis服務器都是主節點;

  • 且一個主節點可以有多個從節點(或沒有從節點),但一個從節點只能有一個主節點。

主從複製的作用

主從複製的作用主要包括:

  1. 數據冗餘:主從複製實現了數據的熱備份,是持久化之外的一種數據冗餘方式。
  2. 故障恢復:當主節點出現問題時,可以由從節點提供服務,實現快速的故障恢復;實際上是一種服務的冗餘。
  3. 負載均衡:在主從複製的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務(即寫Redis數據時應用連接主節點,讀Redis數據時應用連接從節點),分擔服務器負載;尤其是在寫少讀多的場景下,通過多個從節點分擔讀負載,可以大大提高Redis服務器的併發量。
  4. 高可用、高併發基石:主從複製還是哨兵和集羣能夠實施的基礎,因此說主從複製是Redis高可用的基礎。

開啓主從複製的方式

需要注意,主從複製的開啓,完全是在從節點發起的;不需要我們在主節點做任何事情。

從節點開啓主從複製,有3種方式:

(1)配置文件

在從服務器的配置文件中加入:slaveof

(2)啓動命令

redis-server啓動命令後加入 --slaveof

(3)客戶端命令

Redis服務器啓動後,直接通過客戶端執行命令:slaveof ,則該Redis實例成爲從節點。

上述3種方式是等效的,下面以客戶端命令的方式爲例,看一下當執行了slaveof後,Redis主節點和從節點的變化。

主從複製實例

準備工作:啓動兩個節點

實驗所使用的主從節點是在一臺機器上的不同Redis實例,其中:

  • 主節點監聽6379端口,
  • 從節點監聽6380端口;
  • 從節點監聽的端口號可以在配置文件中修改:

img

啓動後可以看到:

img

兩個Redis節點啓動後(分別稱爲6379節點和6380節點),默認都是主節點。

建立複製關係

此時在6380節點執行slaveof命令,使之變爲從節點:

img

觀察效果

下面驗證一下,在主從複製建立後,主節點的數據會複製到從節點中。

(1)首先在從節點查詢一個不存在的key:

img

(2)然後在主節點中增加這個key:

img

(3)此時在從節點中再次查詢這個key,會發現主節點的操作已經同步至從節點:

img

(4)然後在主節點刪除這個key:

img

(5)此時在從節點中再次查詢這個key,會發現主節點的操作已經同步至從節點:

img

斷開復制

通過slaveof 命令建立主從複製關係以後,可以通過slaveof no one斷開。需要注意的是,從節點斷開復制後,不會刪除已有的數據,只是不再接受主節點新的數據變化。

從節點執行slaveof no one後,打印日誌如下所示;

img

可以看出斷開復制後,從節點又變回爲主節點。

斷開復制後,主節點打印日誌如下:

img

主從複製的核心原理

1 當啓動一個 slave node 的時候,它會發送一個 PSYNC 命令給 master node。

2 如果這是 slave node 初次連接到 master node,那麼會觸發一次 full resynchronization 全量複製。

master node 怎麼進行 full resynchronization 全量複製?

此時 master 會啓動一個後臺線程,開始生成一份 RDB 快照文件,同時還會將從客戶端 client 新收到的所有寫命令緩存在內存中。

RDB 文件生成完畢後, master 會將這個 RDB 發送給 slave,

slave node 接收到RDB ,幹啥呢?

會先寫入本地磁盤,然後再從本地磁盤加載到內存中,

3 數據同步階段完成後,主從節點進入命令傳播階段;在這個階段master 將自己執行的寫命令發送給從節點,從節點接收命令並執行,從而保證主從節點數據的一致性。

4 部分複製。如果slave node跟 master node 有網絡故障,斷開了連接,會自動重連,連接之後 master node 僅會複製給 slave 部分缺少的數據。

img點擊並拖拽以移動

主從複製的核心流程

主從複製過程大體可以分爲3個階段:

  • 連接建立階段(即準備階段)
  • 數據同步階段
  • 命令傳播階段;

下面分別進行介紹。

連接建立階段

該階段的主要作用是在主從節點之間建立連接,爲數據同步做好準備。

步驟1:保存主節點信息

從節點服務器內部維護了兩個字段,即masterhost和masterport字段,用於存儲主節點的ip和port信息。

需要注意的是,slaveof是異步命令,從節點完成主節點ip和port的保存後,向發送slaveof命令的客戶端直接返回OK,實際的複製操作在這之後纔開始進行。

這個過程中,可以看到從節點打印日誌如下:

img

步驟2:建立socket連接

slave 從節點每秒1次調用複製定時函數replicationCron(),如果發現了有主節點可以連接,便會根據主節點的ip和port,創建socket連接。

如果連接成功,則:

  • 從節點:

爲該socket建立一個專門處理複製工作的文件事件處理器,負責後續的複製工作,如接收RDB文件、接收命令傳播等。

  • 主節點:

接收到從節點的socket連接後(即accept之後),爲該socket創建相應的客戶端狀態,並將從節點看做是連接到主節點的一個客戶端,後面的步驟會以從節點向主節點發送命令請求的形式來進行。

這個過程中,從節點打印日誌如下:

img

步驟3:發送ping命令

從節點成爲主節點的客戶端之後,發送ping命令進行首次請求,目的是:檢查socket連接是否可用,以及主節點當前是否能夠處理請求。

從節點發送ping命令後,可能出現3種情況:

(1)返回pong:說明socket連接正常,且主節點當前可以處理請求,複製過程繼續。

(2)超時:一定時間後從節點仍未收到主節點的回覆,說明socket連接不可用,則從節點斷開socket連接,並重連。

(3)返回pong以外的結果:如果主節點返回其他結果,如正在處理超時運行的腳本,說明主節點當前無法處理命令,則從節點斷開socket連接,並重連。

在主節點返回pong情況下,從節點打印日誌如下:

img

步驟4:身份驗證

如果從節點中設置了masterauth選項,則從節點需要向主節點進行身份驗證;沒有設置該選項,則不需要驗證。

從節點進行身份驗證是通過向主節點發送auth命令進行的,auth命令的參數即爲配置文件中的master auth的值。

  • 則身份驗證通過,複製過程繼續;

  • 如果不一致,則從節點斷開socket連接,並重連。

步驟5:發送從節點端口信息

身份驗證之後,從節點會向主節點發送其監聽的端口號(前述例子中爲6380),主節點將該信息保存到該從節點對應的客戶端的slave_listening_port字段中;

該端口信息除了在主節點中執行info Replication時顯示以外,沒有其他作用。

數據同步階段

主從節點之間的連接建立以後,便可以開始進行數據同步,該階段可以理解爲從節點數據的初始化。

具體執行的方式是:從節點向主節點發送psync命令(Redis2.8以前是sync命令),開始同步。

數據同步階段是主從複製最核心的階段,根據主從節點當前狀態的不同,可以分爲全量複製和部分複製。

在Redis2.8以前,從節點向主節點發送sync命令請求同步數據,此時的同步方式是全量複製;

在Redis2.8及以後,從節點可以發送psync命令請求同步數據,此時根據主從節點當前狀態的不同,同步方式可能是全量複製或部分複製。後文介紹以Redis2.8及以後版本爲例。

  1. 全量複製:用於初次複製或其他無法進行部分複製的情況,將主節點中的所有數據都發送給從節點,是一個非常重型的操作。
  2. 部分複製:用於網絡中斷等情況後的複製,只將中斷期間主節點執行的寫命令發送給從節點,與全量複製相比更加高效。需要注意的是,如果網絡中斷時間過長,導致主節點沒有能夠完整地保存中斷期間執行的寫命令,則無法進行部分複製,仍使用全量複製。

全量複製的過程

Redis通過psync命令進行全量複製的過程如下:

(1)從節點判斷無法進行部分複製,向主節點發送全量複製的請求;或從節點發送部分複製的請求,但主節點判斷無法進行部分複製;

(2)主節點收到全量複製的命令後,執行bgsave,在後臺生成RDB文件,並使用一個緩衝區(稱爲複製緩衝區)記錄從現在開始執行的所有寫命令

(3)主節點的bgsave執行完成後,將RDB文件發送給從節點;從節點接收完成之後,首先清除自己的舊數據,然後載入接收的RDB文件,將數據庫狀態更新至主節點執行bgsave時的數據庫狀態

(4)主節點將前述複製緩衝區中的所有寫命令發送給從節點,從節點執行這些寫命令,將數據庫狀態更新至主節點的最新狀態

(5)如果從節點開啓了AOF,則會觸發bgrewriteaof的執行,從而保證AOF文件更新至主節點的最新狀態

下面是執行全量複製時,主從節點打印的日誌;可以看出日誌內容與上述步驟是完全對應的。

主節點的打印日誌如下:

img

從節點打印日誌如下圖所示:

img

其中,有幾點需要注意:

  • 從節點接收了來自主節點的89260個字節的數據;

  • 從節點在載入主節點的數據之前要先將老數據清除;

  • 從節點在同步完數據後,調用了bgrewriteaof。

通過全量複製的過程可以看出,全量複製是非常重型的操作:

(1)性能損耗:主節點通過bgsave命令fork子進程進行RDB持久化,該過程是非常消耗CPU、內存(頁表複製)、硬盤IO的;

(2)帶寬佔用:主節點通過網絡將RDB文件發送給從節點,對主從節點的帶寬都會帶來很大的消耗

(3)停服載入:從節點清空老數據、載入新RDB文件的過程是阻塞的,無法響應客戶端的命令;如果從節點執行bgrewriteaof,也會帶來額外的消耗

題外話:什麼是Redis Bgrewriteaof ?

Redis Bgrewriteaof 命令用於異步執行一個 AOF(AppendOnly File) 文件重寫操作。

Bgrewriteaof 重寫會創建一個當前 AOF 文件的體積優化版本。

即使 Bgrewriteaof 執行失敗,也不會有任何數據丟失,因爲舊的 AOF 文件在 Bgrewriteaof 成功之前不會被修改。

注意:從 Redis 2.4 開始, AOF 重寫由 Redis 自行觸發, BGREWRITEAOF 僅僅用於手動觸發重寫操作。

redis Bgrewriteaof 命令基本語法如下:

redis 127.0.0.1:6379> BGREWRITEAOF 

redis2.8 版本之前主從複製流程

redis2.8 版本之前主從複製流程:

img

  • 從服務器連接主服務器,發送SYNC命令;
  • 主服務器接收到SYNC命名後,開始執行BGSAVE命令生成RDB文件並使用緩衝區記錄此後執行的所有寫命令;
  • 主服務器BGSAVE執行完後,向所有從服務器發送快照文件,並在發送期間繼續記錄被執行的寫命令;
  • 從服務器收到快照文件後丟棄所有舊數據,載入收到的快照;
  • 主服務器快照發送完畢後開始向從服務器發送緩衝區中的寫命令;
  • 從服務器完成對快照的載入,開始接收命令請求,並執行來自主服務器緩衝區的寫命令;

全量複製的弊端:

場景:(1)新創建的slave,從主機master同步數據。(2)剛宕機一小會的slave,從主機master同步數據。

前者新建的slave則從主機master全量同步數據,這沒啥問題。但是後者slave可能只與主機master存在小量的數據差異,要是全量同步肯定沒有隻同步差異(部分複製)的那點數據性能高

部分複製

由於全量複製在主節點數據量較大時效率太低,因此Redis2.8開始提供部分複製,用於處理網絡中斷時的數據同步。

部分複製的實現,依賴於三個重要的概念:

(1)offset複製偏移量

  • 主節點和從節點分別維護一個複製偏移量(offset),代表的是主節點向從節點傳遞的字節數

  • 主節點每次向從節點傳播N個字節數據時,主節點的offset增加N;

  • 從節點每次收到主節點傳來的N個字節數據時,從節點的offset增加N。

offset複製偏移量的用途

offset用於判斷主從節點的數據庫狀態是否一致:如果二者offset相同,則一致;如果offset不同,則不一致,此時可以根據兩個offset找出從節點缺少的那部分數據。

例如,如果主節點的offset是1000,而從節點的offset是500,那麼部分複製就需要將offset爲501-1000的數據傳遞給從節點。

而offset爲501-1000的數據存儲的位置,就是下面要介紹的複製積壓緩衝區。

(2)複製積壓緩衝區( repl-backlog-buffer )

複製積壓緩衝區是由主節點維護的、固定長度的、先進先出(FIFO)隊列,默認大小1MB;

當主節點開始有從節點時, master創建一個積壓緩衝區,其作用是備份主節點最近收到的redis命令,後續會發送給從節點的數據。

注意,無論主節點有一個還是多個從節點,都只需要一個複製積壓緩衝區。

在命令傳播階段,主節點除了將寫命令發送給從節點,還會發送一份給複製積壓緩衝區,作爲寫命令的備份;

除了存儲寫命令,複製積壓緩衝區中還存儲了其中的每個字節對應的複製偏移量(offset)。

由於複製積壓緩衝區定長且是先進先出,所以它保存的是主節點最複製積壓緩衝區近執行的寫命令;時間較早的寫命令會被擠出緩衝區。

在這裏插入圖片描述

由於該緩衝區長度固定且有限,因此可以備份的寫命令也有限,當主從節點offset的差距過大超過緩衝區長度時,將無法執行部分複製,只能執行全量複製。

反過來說,爲了提高網絡中斷時部分複製執行的概率,可以根據需要增大複製積壓緩衝區的大小(通過配置repl-backlog-size);

例如如果網絡中斷的平均時間是60s,而主節點平均每秒產生的寫命令(特定協議格式)所佔的字節數爲100KB,則複製積壓緩衝區的平均需求爲6MB,保險起見,可以設置爲12MB,來保證絕大多數斷線情況都可以使用部分複製。

從節點將offset發送給主節點後,主節點根據offset和緩衝區大小決定能否執行部分複製:

  • 如果offset偏移量之後的數據,仍然都在複製積壓緩衝區裏,則執行部分複製;
  • 如果offset偏移量之後的數據已不在複製積壓緩衝區中(數據已被擠出),則執行全量複製。

(3)服務器運行ID(runid)

每個Redis節點(無論主從),在啓動時都會自動生成一個隨機ID(每次啓動都不一樣),由40個隨機的十六進制字符組成;runid用來唯一識別一個Redis節點。

通過info Server命令,可以查看節點的runid:

img

主從節點初次複製時,主節點將自己的runid發送給從節點,從節點將這個runid保存起來;當斷線重連時,從節點會將這個runid發送給主節點;主節點根據runid判斷能否進行部分複製:

  • 如果從節點保存的runid與主節點現在的runid相同,說明主從節點之前同步過,主節點會繼續嘗試使用部分複製(到底能不能部分複製還要看offset和複製積壓緩衝區的情況);
  • 如果從節點保存的runid與主節點現在的runid不同,說明從節點在斷線前同步的Redis節點並不是當前的主節點,只能進行全量複製。

slavof命令的執行流程

在瞭解了複製偏移量、複製積壓緩衝區、節點運行id之後,

接下來,看看slavof命令的執行流程

在這裏插入圖片描述

從節點收到slaveof命令之後,首先決定是使用全量複製還是部分複製:

(1)首先,從節點根據當前狀態,決定如何調用psync命令:

  • 如果從節點之前未執行過slaveof或最近執行了slaveof no one,則從節點發送命令爲psync ? -1,向主節點請求全量複製;
  • 如果從節點之前執行了slaveof,則發送命令爲psync {runid} {offset},其中runid爲上次複製的主節點的runid,offset爲上次複製截止時從節點保存的複製偏移量。

(2)主節點根據收到的psync命令,及當前服務器狀態,決定執行全量複製還是部分複製:

  • 如果主節點版本低於Redis2.8,則返回-ERR回覆,此時從節點重新發送sync命令執行全量複製;
  • 如果主節點版本夠新,且runid與從節點發送的runid相同,且從節點發送的offset之後的數據在複製積壓緩衝區中都存在,則回覆+CONTINUE,表示將進行部分複製,從節點等待主節點發送其缺少的數據即可;
  • 如果主節點版本夠新,但是runid與從節點發送的runid不同,或從節點發送的offset之後的數據已不在複製積壓緩衝區中(在隊列中被擠出了),則回覆+FULLRESYNC {runid} {offset},表示要進行全量複製,其中runid表示主節點當前的runid,offset表示主節點當前的offset,從節點保存這兩個值,以備使用。

重新連接之後的部分複製

部分複製主要是 Redis 針對全量複製的過高開銷做出的一種優化措施,使用 psync {runId} {offset} 命令實現。

當從節點正在複製主節點時,如果出現網絡閃斷或者命令丟失等異常情況時,從節點會向主節點要求補發丟失的命令數據,如果主節點的複製積壓緩衝區存在這部分數據,則直接發送給從節點,這樣就保證了主從節點複製的一致性。

補發的這部分數據一般遠遠小於全量數據,所以開銷很小。

  1. 當主從節點之間網絡出現中斷時,如果超過了 repl-timeout 時間,主節點會認爲從節點故障並中斷複製連接。

  2. 主從連接中斷期間主節點依然響應命令,但因複製連接中斷命令無法發送給從節點,不過主節點內部存在複製積壓緩衝區( repl-backlog-buffer ),依然可以保存最近一段時間的寫命令數據,默認最大緩存 1MB。

  3. 當主從節點網絡恢復後,從節點會再次連上主節點。

  4. 當主從連接恢復後,由於從節點之前保存了自身已複製的偏移量和主節點的運行ID。因此會把它們作爲 psync 參數發送給主節點,要求進行補發複製操作。

  5. 主節點接到 psync 命令後首先覈對參數 runId 是否與自身一致,如果一致,說明之前複製的是當前主節點;之後根據參數 offset 在自身複製積壓緩衝區查找,如果偏移量之後的數據存在緩衝區中,則對從節點發送 +CONTINUE 響應,表示可以進行部分複製。

  6. 主節點根據偏移量把複製積壓緩衝區裏的數據發送給從節點,保證主從複製進入正常狀態。

命令傳播階段

數據同步階段完成後,主從節點進入命令傳播階段;

在這個階段主節點將自己執行的寫命令發送給從節點,從節點接收命令並執行,從而保證主從節點數據的一致性。

在命令傳播階段,除了發送寫命令,主從節點還維持着心跳機制:PING和REPLCONF ACK。

心跳機制對於主從複製的超時判斷、數據安全等有作用。

1.主->從:PING

每隔指定的時間,主節點會向從節點發送PING命令,這個PING命令的作用,主要是爲了讓從節點進行超時判斷。

PING發送的頻率由 repl-ping-slave-period 參數控制,單位是秒,默認值是10s。

關於該PING命令究竟是由主節點發給從節點,還是相反,有一些爭議;

因爲在Redis的官方文檔中,對該參數的註釋中說明是從節點向主節點發送PING命令,如下圖所示:

img

但是通過源碼可以看到, PING命令是主節點會向從節點發送.

可能的原因是:代碼的迭代和註釋的迭代,沒有完全同步。 可能早期是 從發給主,後面改成了主發從,而並沒有配套修改註釋, 就像尼恩的很多代碼一樣。

2. 從->主:REPLCONF ACK

在命令傳播階段,從節點會向主節點發送REPLCONF ACK命令,頻率是每秒1次;

命令格式爲:REPLCONF ACK {offset},其中offset指從節點保存的複製偏移量。

REPLCONF ACK命令的作用包括:

(1)實時監測主從節點網絡狀態:該命令會被主節點用於複製超時的判斷。此外,在主節點中使用info Replication,可以看到其從節點的狀態中的lag值,代表的是主節點上次收到該REPLCONF ACK命令的時間間隔,在正常情況下,該值應該是0或1,如下圖所示:

img

(2)檢測命令丟失:從節點發送了自身的offset,主節點會與自己的offset對比,如果從節點數據缺失(如網絡丟包),主節點會推送缺失的數據(這裏也會利用複製積壓緩衝區)。

注意,offset和複製積壓緩衝區,不僅可以用於部分複製,也可以用於處理命令丟失等情形;區別在於前者是在斷線重連後進行的,而後者是在主從節點沒有斷線的情況下進行的。

(3)輔助保證從節點的數量和延遲:Redis主節點中使用min-slaves-to-write和min-slaves-max-lag參數,來保證主節點在不安全的情況下不會執行寫命令;所謂不安全,是指從節點數量太少,或延遲過高。

例如min-slaves-to-write和min-slaves-max-lag分別是3和10,含義是如果從節點數量小於3個,或所有從節點的延遲值都大於10s,則主節點拒絕執行寫命令。而這裏從節點延遲值的獲取,就是通過主節點接收到REPLCONF ACK命令的時間來判斷的,即前面所說的info Replication中的lag值。

數據分片(sharding)的基本原理

什麼是數據分片?

名詞說明:

數據分片(sharding)也叫數據分區

爲什麼要做數據分片?

全量數據較大的場景下,單節點無法滿足要求,需要數據分片

什麼是數據分片?

按照分片規則把數據分到若干個shard、partition當中

在這裏插入圖片描述

range 分片

一種是按照 range 來分,就是每個片,一段連續的數據,這個一般是按比如時間範圍/數據範圍來的,但是這種一般較少用,因爲很容易發生數據傾斜,大量的流量都打在最新的數據上了。

比如,安裝數據範圍分片,把1到100個數字,要保存在3個節點上

按照順序分片,把數據平均分配三個節點上

  • 1號到33號數據保存到節點1上
  • 34號到66號數據保存到節點2上
  • 67號到100號數據保存到節點3上

在這裏插入圖片描述

ID取模分片

此種分片規則將數據分成n份(通常dn節點也爲n),從而將數據均勻的分佈於各個表中,或者各節點上。

擴容方便。

ID取模分片常用在關係型數據庫的設計

具體請參見 秒殺視頻的 億級庫表架構設計

hash 哈希分佈

使用hash 算法,獲取key的哈希結果,再按照規則進行分片,這樣可以保證數據被打散,同時保證數據分佈的比較均勻

哈希分佈方式分爲三個分片方式:

  • 哈希取餘分片
  • 一致性哈希分片
  • 虛擬槽分片

哈希取餘模分片

例如1到100個數字,對每個數字進行哈希運算,然後對每個數的哈希結果除以節點數進行取餘,餘數爲1則保存在第1個節點上,餘數爲2則保存在第2個節點上,餘數爲0則保存在第3個節點,這樣可以保證數據被打散,同時保證數據分佈的比較均勻

比如有100個數據,對每個數據進行hash運算之後,與節點數進行取餘運算,根據餘數不同保存在不同的節點上

在這裏插入圖片描述

哈希取餘分片是非常簡單的一種分片方式

哈希取模分片有一個問題

即當增加或減少節點時,原來節點中的80%的數據會進行遷移操作,對所有數據重新進行分佈

哈希取餘分片,建議使用多倍擴容的方式,例如以前用3個節點保存數據,擴容爲比以前多一倍的節點即6個節點來保存數據,這樣只需要適移50%的數據。

數據遷移之後,第一次無法從緩存中讀取數據,必須先從數據庫中讀取數據,然後回寫到緩存中,然後才能從緩存中讀取遷移之後的數據

img

哈希取餘分片優點:

  • 配置簡單:對數據進行哈希,然後取餘

哈希取餘分片缺點:

  • 數據節點伸縮時,導致數據遷移
  • 遷移數量和添加節點數據有關,建議翻倍擴容

一致性哈希分片

一致性哈希原理:

將所有的數據當做一個token環,

token環中的數據範圍是0到2的32次方。

然後爲每一個數據節點分配一個token範圍值,這個節點就負責保存這個範圍內的數據。

img

對每一個key進行hash運算,被哈希後的結果在哪個token的範圍內,則按順時針去找最近的節點,這個key將會被保存在這個節點上。

img

一致性哈希分片的節點擴容

在下面的圖中:

  • 有4個key被hash之後的值在在n1節點和n2節點之間,按照順時針規則,這4個key都會被保存在n2節點上

  • 如果在n1節點和n2節點之間添加n5節點,當下次有key被hash之後的值在n1節點和n5節點之間,這些key就會被保存在n5節點上面了

下圖的例子裏,添加n5節點之後:

  • 數據遷移會在n1節點和n2節點之間進行
  • n3節點和n4節點不受影響
  • 數據遷移範圍被縮小很多

同理,如果有1000個節點,此時添加一個節點,受影響的節點範圍最多隻有千分之2。所以,一致性哈希一般用在節點比較多的時候,節點越多,擴容時受影響的節點範圍越少

img

分片方式:哈希 + 順時針(優化取餘)

一致性哈希分片優點:

  • 一致性哈希算法解決了分佈式下數據分佈問題。比如在緩存系統中,通過一致性哈希算法把緩存鍵映射到不同的節點上,由於算法中虛擬節點的存在,哈希結果一般情況下比較均勻。
  • 節點伸縮時,隻影響鄰近節點,但是還是有數據遷移

“但沒有一種解決方案是銀彈,能適用於任何場景。所以實踐中一致性哈希算法有哪些缺陷,或者有哪些場景不適用呢?”

一致性哈希分片缺點:

一致性哈希在大批量的數據場景下負載更加均衡,但是在數據規模小的場景下,會出現單位時間內某個節點完全空閒的情況出現。

虛擬槽分片 (範圍分片的變種)

Redis Cluster在設計中沒有使用一致性哈希(Consistency Hashing),而是使用數據分片引入哈希槽(hash slot)來實現;

虛擬槽分片是Redis Cluster採用的分片方式.

虛擬槽分片 ,可以理解爲範圍分片的變種, hash取模分片+範圍分片, 把hash值取餘數分爲n段,一個段給一個節點負責

在這裏插入圖片描述

虛擬槽分片 (範圍分片的變種)

Redis Cluster在設計中沒有使用一致性哈希(Consistency Hashing),而是使用數據分片引入哈希槽(hash slot)來實現;

虛擬槽分片是Redis Cluster採用的分片方式.

在該分片方式中:

  • 首先 預設虛擬槽,每個槽爲一個hash值,每個node負責一定槽範圍。
  • 每一個值都是key的hash值取餘,每個槽映射一個數據子集,一般比節點數大

Redis Cluster中預設虛擬槽的範圍爲0到16383

在這裏插入圖片描述

虛擬槽分片的映射步驟:

1.把16384槽按照節點數量進行平均分配,由節點進行管理
2.對每個key按照CRC16規則進行hash運算
3.把hash結果對16383進行取餘
4.把餘數發送給Redis節點
5.節點接收到數據,驗證是否在自己管理的槽編號的範圍

  • 如果在自己管理的槽編號範圍內,則把數據保存到數據槽中,然後返回執行結果
  • 如果在自己管理的槽編號範圍外,則會把數據發送給正確的節點,由正確的節點來把數據保存在對應的槽中

需要注意的是:Redis Cluster的節點之間會共享消息,每個節點都會知道是哪個節點負責哪個範圍內的數據槽

虛擬槽分佈方式中,由於每個節點管理一部分數據槽,數據保存到數據槽中。

當節點擴容或者縮容時,對數據槽進行重新分配遷移即可,數據不會丟失。

3個節點的Redis集羣虛擬槽分片結果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

虛擬槽分片特點:

虛擬槽分區巧妙地使用了哈希空間,使用分散度良好的哈希函數把所有數據映射到一個固定範圍的整數集合中,整數定義爲槽(slot)。槽是集羣內數據管理和遷移的基本單位。

槽的範圍一般遠遠大於節點數,比如Redis Cluster槽範圍是0~16383。

採用大範圍槽的主要目的是爲了方便數據拆分和集羣擴展,每個節點會負責一定數量的槽。

Redis虛擬槽分區的優點:

  • 解耦數據和節點之間的關係,簡化了節點擴容和收縮難度。

  • 節點自身維護槽的映射關係,不需要客戶端或者代理服務維護槽分區元數據。

  • 支持節點、槽、鍵之間的映射查詢,用於數據路由,在線伸縮等場景。

  • 無論數據規模大,還是小,Redis虛擬槽分區各個節點的負載,都會比較均衡 。而一致性哈希在大批量的數據場景下負載更加均衡,但是在數據規模小的場景下,會出現單位時間內某個節點完全空閒的情況出現。

Redis集羣如何高可用

要實現Redis高可用,前提條件之一,是需要進行Redis的節點集羣

集羣的必要性

所謂的集羣,就是通過添加服務節點的數量,不同的節點提供相同的服務,從而讓服務器達到高可用、自動failover的狀態。

面試題:單個redis節點,面臨哪些問題?

答:

(1)單個redis存在不穩定性。當redis服務宕機了,就沒有可用的服務了。

(2)單個redis的讀寫能力是有限的。單機的 redis,能夠承載的 QPS 大概就在上萬到幾萬不等。

對於緩存來說,一般都是用來支撐讀高併發、高可用。

單個redis節點,二者都做不到。

Redis集羣模式的分類,可以從下面角度來分:

  • 客戶端分片
  • 代理分片
  • 服務端分片
  • 代理模式和服務端分片相結合的模式

客戶端分片包括:

ShardedJedisPool

ShardedJedisPool是redis沒有集羣功能之前客戶端實現的一個數據分佈式方案,

使用shardedJedisPool實現redis集羣部署,由於shardedJedisPool的原理是通過一致性哈希進行切片實現的,不同點key被分別分配到不同的redis實例上。

代理分片包括:

  • Codis
  • Twemproxy

服務端分片包括:

  • Redis Cluster

從否中心化來劃分

它們還可以用是否中心化來劃分

  • 無中心化的集羣方案

其中客戶端分片、Redis Cluster屬於無中心化的集羣方案

  • 中心化的集羣方案

Codis、Tweproxy屬於中心化的集羣方案。

是否中心化是指客戶端訪問多個Redis節點時,是直接訪問還是通過一箇中間層Proxy來進行操作,直接訪問的就屬於無中心化的方案,通過中間層Proxy訪問的就屬於中心化的方案,它們有各自的優劣,下面分別來介紹。

如何學習redis集羣

說明:

 (1)redis集羣中,每一個redis稱之爲一個節點。
 (2)redis集羣中,有兩種類型的節點:主節點(master)、從節點(slave)。
  (3)redis集羣,是基於redis主從複製實現。

Docker方式部署redis-cluster步驟

1、redis容器初始化
2、redis容器集羣配置

這裏引用了別人的一個鏡像publicisworldwide/redis-cluster,方便快捷。

redis-cluster的節點端口共分爲2種,

  • 一種是節點提供服務的端口,如6379、6001;

  • 一種是節點間通信的端口,固定格式爲:10000+6379/10000+6001。

若不想使用host模式,也可以把network_mode去掉,但就要加ports映射。

這裏使用host(主機)網絡模式,把redis數據掛載到本機目錄/data/redis/800*下。

Docker網絡

Docker使用Linux橋接技術,在宿主機虛擬一個Docker容器網橋(docker0),Docker啓動一個容器時會根據Docker網橋的網段分配給容器一個IP地址,稱爲Container-IP,同時Docker網橋是每個容器的默認網關。

因爲在同一宿主機內的容器都接入同一個網橋,這樣容器之間就能夠通過容器的Container-IP直接通信。

Docker網橋是宿主機虛擬出來的,並不是真實存在的網絡設備,外部網絡是無法尋址到的,這也意味着外部網絡無法通過直接Container-IP訪問到容器。

如果容器希望外部訪問能夠訪問到,可以通過映射容器端口到宿主主機(端口映射),即docker run創建容器時候通過 -p 或 -P 參數來啓用,訪問容器的時候就通過[宿主機IP]:[容器端口]訪問容器。

Docker容器的四類網絡模式

Docker網絡模式 配置 說明
host模式 –net=host 容器和宿主機共享Network namespace。
container模式 –net=container:NAME_or_ID 容器和另外一個容器共享Network namespace。 kubernetes中的pod就是多個容器共享一個Network namespace。
none模式 –net=none 容器有獨立的Network namespace,但並沒有對其進行任何網絡設置,如分配veth pair 和網橋連接,配置IP等。
bridge模式 –net=bridge (默認爲該模式)

橋接模式(default)

Docker容器的默認網絡模式爲橋接模式,如圖所示:
在這裏插入圖片描述

Docker安裝時會創建一個名爲docker0的bridge虛擬網橋

bridge模式是docker的默認網絡模式,不寫--net參數,就是bridge模式。新創建的容器都會自動連接到這個虛擬網橋。

bridge網橋用於同一主機上的docker容器相互通信,連接到同一個網橋的docker容器可以相互通信。

bridge 對宿主機來講相當於一個單獨的網卡設備 ,對於運行在宿主機上的每個容器來說相當於一個交換機,所有容器的虛擬網線的一端都連接到docker0上。

容器通過本地主機進行上網,容器會創建名爲veth的虛擬網卡,網卡一端連接到docker0網橋,另一端連接容器,容器就可以通過網橋通過分配的IP地址進行上網。

在這裏插入圖片描述

docker exec -it rmqbroker-a cat /etc/hosts

[root@localhost ~]# docker exec -it rmqbroker-a cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.30.0.5      c55ea6edcc14

使用docker run -p時,docker實際是在iptables做了DNAT規則,實現端口轉發功能。

可以使用iptables -t nat -vnL查看。

 pkts bytes target     prot opt in     out     source               destination
15141  908K RETURN     all  --  br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0
 536K   32M RETURN     all  --  br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0
    0     0 RETURN     all  --  br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0
    0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0
   11   572 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:3306 to:172.19.0.2:3306
    0     0 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:3307 to:172.19.0.3:3306
    0     0 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:3308 to:172.19.0.4:3306
    3   156 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:23306 to:172.19.0.5:23306
    0     0 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:1080 to:172.19.0.5:1080
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8011 to:172.20.0.2:9555
    8   416 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8001 to:172.20.0.2:8001
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8013 to:172.20.0.3:9555
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8003 to:172.20.0.3:8003
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8012 to:172.20.0.4:9555
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8002 to:172.20.0.4:8002
   20  1040 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8848 to:172.20.0.5:8848
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:1082 to:172.20.0.5:1080
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:9877 to:172.30.0.2:9876
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:9876 to:172.30.0.3:9876
    5   260 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:9001 to:172.30.0.4:9001
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10912 to:172.30.0.5:10912
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10911 to:172.30.0.5:10911
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10922 to:172.30.0.6:10922
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10921 to:172.30.0.6:10921

我們也可以自定義自己的bridge網絡,docker文檔建議使用自定義bridge網絡

創建一個自定義網絡, 可以指定子網、IP地址範圍、網關等網絡配置

docker network create --driver bridge --subnet 172.22.16.0/24 --gateway 172.22.16.1 mynet2

查看docker網絡,是否創建成功。

docker network ls

在這裏插入圖片描述

總之:Docker網絡bridge橋接模式,是創建和運行容器時默認模式。這種模式會爲每個容器分配一個獨立的網卡,橋接到默認或指定的bridge上,同一個Bridge下的容器下可以互相通信的。我們也可以創建自定義bridge以滿足個性化的網絡需求。

HOST模式

在這裏插入圖片描述

Docker使用了Linux的Namespaces技術來進行資源隔離,如PID Namespace隔離進程,Mount Namespace隔離文件系統,Network Namespace隔離網絡等。

一個Network Namespace提供了一份獨立的網絡環境,包括網卡、路由、Iptable規則等都與其他的Network Namespace隔離。bridge模式下,一個Docker容器一般會分配一個獨立的Network Namespace。

host模式類似於Vmware的橋接模式,與宿主機在同一個網絡中,但沒有獨立IP地址。

一個Docker容器一般會分配一個獨立的Network Namespace。但如果啓動容器的時候使用host模式,那麼這個容器將不會獲得一個獨立的Network Namespace,而是和宿主機共用一個Network Namespace。容器將不會虛擬出自己的網卡,配置自己的IP等,而是使用宿主機的IP和端口。

容器與主機在相同的網絡命名空間下面,使用相同的網絡協議棧,容器可以直接使用主機的所有網絡接口

在這裏插入圖片描述

Container模式

在這裏插入圖片描述

None

獲取獨立的network namespace,但不爲容器進行任何網絡配置,之後用戶可以自己進行配置,容器內部只能使用loopback網絡設備,不會再有其他的網絡資源

創建文件目錄結構

mkdir -p /home/docker-compose/redis-cluster/conf/{6001,6002,6003,6004,6005,6006}/data

離線環境鏡像導入

從有公網的環境拉取鏡像,然後導出鏡像

  • publicisworldwide/redis-cluster redis-cluster鏡像

  • inem0o/redis-trib 集羣管理工具:自動執行節點握手,自動操作節點主從配置,自動給主節點分配槽

無公網的環境,上傳到到內網環境, 上傳鏡像到目標虛擬機

然後導入docker,load到docker

docker load   -i  /root/redis-cluster.tar
docker load   -i   /root/redis-trib.tar

導入後看到兩個image 鏡像:

[root@localhost ~]# docker image ls

publicisworldwide/redis-cluster   latest                         29e4f38e4475        2 years ago         94.9MB
inem0o/redis-trib                 latest                         0f7b910114d5        4 years ago         32MB

redis容器啓動

創建容器編排文件

使用docker-compose方式,先創建一個docker-compose.yml文件,容器的ip使用host模式,內容如下:

version: '3.5'
services:
 redis1:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6001/data:/data
  environment:
   - REDIS_PORT=6001

 redis2:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6002/data:/data
  environment:
   - REDIS_PORT=6002

 redis3:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6003/data:/data
  environment:
   - REDIS_PORT=6003

 redis4:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6004/data:/data
  environment:
   - REDIS_PORT=6004

 redis5:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6005/data:/data
  environment:
   - REDIS_PORT=6005

 redis6:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6006/data:/data
  environment:
   - REDIS_PORT=6006

作爲參考,如果容器的ip使用BRIDGE模式,docker-compose.yml文件內容如下:

version: '3'

services:
 redis1:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6001/data:/data
  environment:
   - REDIS_PORT=6001
  ports:
    - '6001:6001'       #服務端口
    - '16001:16001'   #集羣端口

 redis2:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6002/data:/data
  environment:
   - REDIS_PORT=6002
  ports:
    - '6002:6002'
    - '16002:16002'

 redis3:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6003/data:/data
  environment:
   - REDIS_PORT=6003
  ports:
    - '6003:6003'
    - '16003:16003'

 redis4:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6004/data:/data
  environment:
   - REDIS_PORT=6004
  ports:
    - '6004:6004'
    - '16004:16004'

 redis5:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6005/data:/data
  environment:
   - REDIS_PORT=6005
  ports:
    - '6005:6005'
    - '16005:16005'

 redis6:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6006/data:/data
  environment:
   - REDIS_PORT=6006
  ports:
    - '6006:6006'
    - '16006:16006'

啓動服務redis容器

創建文件後,直接啓動服務

窗口模式
docker-compose up
後臺進程
docker-compose up -d
1234

查看啓動的進程

CONTAINER ID        IMAGE                                     COMMAND                  CREATED             STATUS              PORTS                                                                        NAMES
2bdd27191859        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis4_1
afdf208c55f3        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis1_1
d14d7dbd207f        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis5_1
25070ed4a434        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis2_1
35e1ff66d2db        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis3_1
615bfbf336c0        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis6_1

狀態爲Up,說明服務均已啓動,鏡像無問題。

注意:以上鏡像不能設置永久密碼,其實redis一般是內網訪問,可以不需密碼。

建立redis集羣

這裏同樣使用了另一個鏡像inem0o/redis-trib,執行時會自動下載。

離線場景請提前load,或者導入到私有的restry。

使用redis-trib.rb創建redis 集羣

上面只是啓動了6個redis容器,並沒有設置集羣,通過下面的命令可以設置集羣。

使用 redis-trib.rb create 命令完成節點握手和槽分配過程

docker run --rm -it inem0o/redis-trib create --replicas 1 hostip:6001 hostip:6002 hostip:6003 hostip:6004 hostip:6005 hostip:6006

#hostip  換成 主機的ip

docker run --rm -it inem0o/redis-trib create --replicas 1 172.18.8.164:6001 172.18.8.164:6002 172.18.8.164:6003 172.18.8.164:6004 172.18.8.164:6005 172.18.8.164:6006

–replicas 參數指定集羣中每個主節點配備幾個從節點,這裏設置爲1,

redis-trib.rb 會盡可能保證主從節點不分配在同一機器下,因此會重新排序節點列表順序。

節點列表順序用於確定主從角色,先主節點之後是從節點。

創建過程中首先會給出主從節點角色分配的計劃,並且會生成報告

日誌如下:

[root@localhost redis-cluster]# docker run --rm -it inem0o/redis-trib create --replicas 1 172.18.8.164:6001 172.18.8.164:6002 172.18.8.164:6003 172.18.8.164:6004 172.18.8.164:6005 172.18.8.164:6006
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
172.18.8.164:6001
172.18.8.164:6002
172.18.8.164:6003
Adding replica 172.18.8.164:6004 to 172.18.8.164:6001
Adding replica 172.18.8.164:6005 to 172.18.8.164:6002
Adding replica 172.18.8.164:6006 to 172.18.8.164:6003
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:0-5460 (5461 slots) master
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:5461-10922 (5462 slots) master
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:10923-16383 (5461 slots) master
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
Can I set the above configuration? (type 'yes' to accept): yes

注意:出現Can I set the above configuration? (type ‘yes’ to accept): 是要輸入yes 不是Y

>>> 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 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

詳解redis-trib.rb 的命令

命令說明:
 redis-trib.rb help
Usage: redis-trib <command> <options> <arguments ...>

#創建集羣
create          host1:port1 ... hostN:portN  
                  --replicas <arg> #帶上該參數表示是否有從,arg表示從的數量
#檢查集羣
check           host:port
#查看集羣信息
info            host:port
#修復集羣
fix             host:port
                  --timeout <arg>
#在線遷移slot  
reshard         host:port       #個是必傳參數,用來從一個節點獲取整個集羣信息,相當於獲取集羣信息的入口
                  --from <arg>  #需要從哪些源節點上遷移slot,可從多個源節點完成遷移,以逗號隔開,傳遞的是節點的node id,還可以直接傳遞--from all,這樣源節點就是集羣的所有節點,不傳遞該參數的話,則會在遷移過程中提示用戶輸入
                  --to <arg>    #slot需要遷移的目的節點的node id,目的節點只能填寫一個,不傳遞該參數的話,則會在遷移過程中提示用戶輸入。
                  --slots <arg> #需要遷移的slot數量,不傳遞該參數的話,則會在遷移過程中提示用戶輸入。
                  --yes         #設置該參數,可以在打印執行reshard計劃的時候,提示用戶輸入yes確認後再執行reshard
                  --timeout <arg>  #設置migrate命令的超時時間。
                  --pipeline <arg> #定義cluster getkeysinslot命令一次取出的key數量,不傳的話使用默認值爲10。
#平衡集羣節點slot數量  
rebalance       host:port
                  --weight <arg>
                  --auto-weights
                  --use-empty-masters
                  --timeout <arg>
                  --simulate
                  --pipeline <arg>
                  --threshold <arg>
#將新節點加入集羣 
add-node        new_host:new_port existing_host:existing_port
                  --slave
                  --master-id <arg>
#從集羣中刪除節點
del-node        host:port node_id
#設置集羣節點間心跳連接的超時時間
set-timeout     host:port milliseconds
#在集羣全部節點上執行命令
call            host:port command arg arg .. arg
#將外部redis數據導入集羣
import          host:port
                  --from <arg>
                  --copy
                  --replace

通過客戶端命令使用集羣

檢查集羣狀態

 docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001

使用到的命令爲: redis-cli --cluster check

 

結果如下:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

redis-cli --cluster命令詳解

redis-cli --cluster命令參數詳解

redis-cli --cluster help
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN   #創建集羣
                 --cluster-replicas <arg>      #從節點個數
  check          host:port                     #檢查集羣
                 --cluster-search-multiple-owners #檢查是否有槽同時被分配給了多個節點
  info           host:port                     #查看集羣狀態
  fix            host:port                     #修復集羣
                 --cluster-search-multiple-owners #修復槽的重複分配問題
  reshard        host:port                     #指定集羣的任意一節點進行遷移slot,重新分slots
                 --cluster-from <arg>          #需要從哪些源節點上遷移slot,可從多個源節點完成遷移,以逗號隔開,傳遞的是節點的node id,還可以直接傳遞--from all,這樣源節點就是集羣的所有節點,不傳遞該參數的話,則會在遷移過程中提示用戶輸入
                 --cluster-to <arg>            #slot需要遷移的目的節點的node id,目的節點只能填寫一個,不傳遞該參數的話,則會在遷移過程中提示用戶輸入
                 --cluster-slots <arg>         #需要遷移的slot數量,不傳遞該參數的話,則會在遷移過程中提示用戶輸入。
                 --cluster-yes                 #指定遷移時的確認輸入
                 --cluster-timeout <arg>       #設置migrate命令的超時時間
                 --cluster-pipeline <arg>      #定義cluster getkeysinslot命令一次取出的key數量,不傳的話使用默認值爲10
                 --cluster-replace             #是否直接replace到目標節點
  rebalance      host:port                                      #指定集羣的任意一節點進行平衡集羣節點slot數量 
                 --cluster-weight <node1=w1...nodeN=wN>         #指定集羣節點的權重
                 --cluster-use-empty-masters                    #設置可以讓沒有分配slot的主節點參與,默認不允許
                 --cluster-timeout <arg>                        #設置migrate命令的超時時間
                 --cluster-simulate                             #模擬rebalance操作,不會真正執行遷移操作
                 --cluster-pipeline <arg>                       #定義cluster getkeysinslot命令一次取出的key數量,默認值爲10
                 --cluster-threshold <arg>                      #遷移的slot閾值超過threshold,執行rebalance操作
                 --cluster-replace                              #是否直接replace到目標節點
  add-node       new_host:new_port existing_host:existing_port  #添加節點,把新節點加入到指定的集羣,默認添加主節點
                 --cluster-slave                                #新節點作爲從節點,默認隨機一個主節點
                 --cluster-master-id <arg>                      #給新節點指定主節點
  del-node       host:port node_id                              #刪除給定的一個節點,成功後關閉該節點服務
  call           host:port command arg arg .. arg               #在集羣的所有節點執行相關命令
  set-timeout    host:port milliseconds                         #設置cluster-node-timeout
  import         host:port                                      #將外部redis數據導入集羣
                 --cluster-from <arg>                           #將指定實例的數據導入到集羣
                 --cluster-copy                                 #migrate時指定copy
                 --cluster-replace                              #migrate時指定replace
  help           

For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

  

參考的cluster命令

CLUSTER info:打印集羣的信息。
CLUSTER nodes:列出集羣當前已知的所有節點(node)的相關信息。
CLUSTER meet <ip> <port>:將ip和port所指定的節點添加到集羣當中。
CLUSTER addslots <slot> [slot ...]:將一個或多個槽(slot)指派(assign)給當前節點。
CLUSTER delslots <slot> [slot ...]:移除一個或多個槽對當前節點的指派。
CLUSTER slots:列出槽位、節點信息。
CLUSTER slaves <node_id>:列出指定節點下面的從節點信息。
CLUSTER replicate <node_id>:將當前節點設置爲指定節點的從節點。
CLUSTER saveconfig:手動執行命令保存保存集羣的配置文件,集羣默認在配置修改的時候會自動保存配置文件。
CLUSTER keyslot <key>:列出key被放置在哪個槽上。
CLUSTER flushslots:移除指派給當前節點的所有槽,讓當前節點變成一個沒有指派任何槽的節點。
CLUSTER countkeysinslot <slot>:返回槽目前包含的鍵值對數量。
CLUSTER getkeysinslot <slot> <count>:返回count個槽中的鍵。
CLUSTER setslot <slot> node <node_id> 將槽指派給指定的節點,如果槽已經指派給另一個節點,那麼先讓另一個節點刪除該槽,然後再進行指派。  
CLUSTER setslot <slot> migrating <node_id> 將本節點的槽遷移到指定的節點中。  
CLUSTER setslot <slot> importing <node_id> 從 node_id 指定的節點中導入槽 slot 到本節點。  
CLUSTER setslot <slot> stable 取消對槽 slot 的導入(import)或者遷移(migrate)。 

CLUSTER failover:手動進行故障轉移。
CLUSTER forget <node_id>:從集羣中移除指定的節點,這樣就無法完成握手,過期時爲60s,60s後兩節點又會繼續完成握手。
CLUSTER reset [HARD|SOFT]:重置集羣信息,soft是清空其他節點的信息,但不修改自己的id,hard還會修改自己的id,不傳該參數則使用soft方式。

CLUSTER count-failure-reports <node_id>:列出某個節點的故障報告的長度。
CLUSTER SET-CONFIG-EPOCH:設置節點epoch,只有在節點加入集羣前才能設置。

連接redis的某個節點

成功後可連接redis集羣中的摸個節點,用以下命令

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h 172.18.8.164 -p 6001

172.18.8.164:6001>

通過該redis cli 控制檯,可以輸入redis的操作命令

 

查看集羣信息和節點信息

  

# 查看集羣信息
cluster info
# 查看集羣結點信息
cluster nodes

查看集羣信息

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h172.18.8.164 -p 6001
172.18.8.164:6001> 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:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:2979
cluster_stats_messages_pong_sent:2904
cluster_stats_messages_sent:5883
cluster_stats_messages_ping_received:2899
cluster_stats_messages_pong_received:2979
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:5883

查看集羣結點信息

172.18.8.164:6001> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634365163922 6 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634365162000 2 connected 5461-10922
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634365163000 4 connected
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634365163000 5 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634365164023 3 connected 10923-16383
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 myself,master - 0 1634365163000 1 connected 0-5460

SET/GET

  

在 6001節點中執行寫入和讀取,命令如下:

進入容器並連接至集羣某個節點

docker exec -it redis-cluster_redis1_1 redis-cli -c -h 172.18.8.164 -p 6001


[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h 172.18.8.164 -p 6001
172.18.8.164:6001>

# 寫入數據
set name mrhelloworld
set aaa 111
set bbb 222
# 讀取數據
get name
get aaa
get bbb

第一個命令:set name mrhelloworld

172.18.8.164:6001> set name mrhelloworld
-> Redirected to slot [5798] located at 172.18.8.164:6002
OK
172.18.8.164:6002>

set 命令 set name mrhelloworldname 鍵根據哈希函數運算以後得到的值爲 [5798]

當前集羣環境的槽分配情況爲:[0-5460] 6001節點[5461-10922] 6002節點[10923-16383] 6003節點

該鍵的存儲就被分配到了 6002節點上;

第二個 set 命令 set aaa 111

172.18.8.164:6002>  set aaa  111
OK

再來看第二個 set 命令 set aaa,這裏大家可能會有一些疑問,爲什麼看不到 aaa 鍵根據哈希函數運算以後得到的值?

因爲剛纔重定向至 6002節點插入了數據,此時如果還有數據插入,正好鍵根據哈希函數運算以後得到的值也還在該節點的範圍內,那麼直接插入數據即可;

第三個 set 命令 set bbb 222

172.18.8.164:6002>  set bbb  222
-> Redirected to slot [5287] located at 172.18.8.164:6001
OK

接着是第三個 set 命令 set bbbbbb 鍵根據哈希函數運算以後得到的值爲 [5287],所以該鍵的存儲就被分配到了 6001 節點上;

第四個命令 get name

172.18.8.164:6001> get name
-> Redirected to slot [5798] located at 172.18.8.164:6002
"mrhelloworld"
172.18.8.164:6002>

第四個命令 get namename 鍵根據哈希函數運算以後得到的值爲 [5798],被重定向至 6002節點讀取;

第五個命令 get aaa

172.18.8.164:6002> get aaa
"111"

第六個命令 get bbb

172.18.8.164:6002> get bbb
-> Redirected to slot [5287] located at 172.18.8.164:6001
"222"

第六個命令 get bbbbbb 鍵根據哈希函數運算以後得到的值爲 [5287],被重定向至 6001 節點讀取。

客戶端連接

來一波客戶端連接操作,隨便哪個節點,看看可否通過外部訪問 Redis Cluster 集羣。

  

至此使用多機環境基於 Docker Compose 搭建 Redis Cluster 就到這裏。

Docker Compose 簡化了集羣的搭建,之前的方式就需要一個個去操作,而 Docker Compose 只需要一個 docker-compose up/down 命令的操作即可。

集羣維護

啓動兩個節點

規劃:一個作爲主,一個作爲從

爲新增的節點,創建文件目錄結構

mkdir -p /home/docker-compose/redis-cluster-ext/conf/{6007,6008}/data

準備compose編排文件,並且上傳到 /home/docker-compose/redis-cluster-ext 目錄

version: '3.5'
services:
 redis1:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster-ext/conf/6007/data:/data
  environment:
   - REDIS_PORT=6007

 redis2:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster-ext/conf/6008/data:/data
  environment:
   - REDIS_PORT=6008

啓動兩個新的redis節點

[root@localhost redis-cluster]# cd  /home/docker-compose/redis-cluster-ext
[root@localhost redis-cluster-ext]# docker-compose  up -d
Creating redis-cluster-ext_redis8_1 ... done
Creating redis-cluster-ext_redis7_1 ... done


添加一個主節點

通過任意容器的shell終端,都可以執行 --cluster add-node 指令,增加一個新的節點,如 6007節點

docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6007 127.0.0.1:6001  


第一個參數爲新增加的節點的IP和端口,第二個參數爲任意一個已經存在的節點的IP和端口。

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6007 127.0.0.1:6001
>>> Adding node 127.0.0.1:6007 to cluster 127.0.0.1:6001
>>> Performing Cluster Check (using node 127.0.0.1:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6007 to make it join the cluster.
[OK] New node added correctly.

查看集羣信息

此時該新節點已經成爲集羣的一份子

docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007

cluster nodes
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634369547601 1 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 myself,master - 0 1634369546000 0 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001@16001 master - 0 1634369547902 1 connected 0-5460
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634369546000 3 connected 10923-16383
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634369546900 2 connected
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634369546000 3 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634369546000 2 connected 5461-10922

但是該節點沒有包含任何的哈希槽,所以沒有數據會存到該主節點。

我們可以通過上面的集羣重新分片給該節點分配哈希槽,那麼該節點就成爲了一個真正的主節點了。

添加從節點到集羣

跟添加主節點一樣添加一個節點6008,然後連接上該節點並執行如下命令

通過任意容器的shell終端,都可以執行 --cluster add-node 指令,增加一個新的節點,如 6007節點

docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6008 127.0.0.1:6007 


第一個參數爲新增加的節點的IP和端口,第二個參數爲任意一個已經存在的節點的IP和端口。

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6008 127.0.0.1:6007
>>> Adding node 127.0.0.1:6008 to cluster 127.0.0.1:6007
>>> Performing Cluster Check (using node 127.0.0.1:6007)
M: 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007
   slots: (0 slots) master
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6008 to make it join the cluster.
[OK] New node added correctly.


測試一下:

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6008
172.18.8.164:6008> cluster replicate 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4
OK

設置主從關係

連接6008,成爲 6007的從節點

命令的格式

cluster replicate <nodeId>    
    

具體命令如下:

#進入從節點
docker exec -it redis-cluster-ext_redis8_1 redis-cli -c -h 172.18.8.164 -p 6008

cluster replicate 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4

這樣就可以指定該節點成爲哪個節點的從節點。

查看一下節點信息

172.18.8.164:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634369957000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 master - 0 1634369958089 0 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634369958000 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 172.18.8.164:6008@16008 myself,slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634369958000 7 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 master - 0 1634369958591 1 connected 0-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634369958000 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634369957000 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634369959092 2 connected 5461-10922

集羣重新分片

如果對默認的平均分配不滿意,我們可以對集羣進行重新分片。

執行如下命令,只需要指定集羣中的其中一個節點地址即可,它會自動找到集羣中的其他節點。

(如果設置了密碼則需要加上 -a ,沒有密碼則不需要,後面的命令我會省略這個,設置了密碼的自己加上就好)。

重新分片的命令的格式:

redis-cli -a <password> --cluster reshard ip:port

重新分片的命令式:

docker exec -it redis-cluster-ext_redis7_1  redis-cli  --cluster reshard 172.18.8.164:6001

輸入你想重新分配的哈希槽數量

docker exec -it redis-cluster-ext_redis7_1How many slots do you want to move (from 1 to 16384)?  1024

輸入你想接收這些哈希槽的節點ID

What is the receiving node ID?  6007的id

輸入想從哪個節點移動槽點,選擇all表示所有其他節點,也可以依次輸入節點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:   6001的id

輸入yes執行重新分片

省略常常的日誌,

查看集羣信息

此時該新節點已經成爲集羣的一份子

docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007

cluster nodes
172.18.8.164:6007> cluster nodes
4656b8b2e26dd290928f45f9e4e001123c7ae36d 127.0.0.1:6008@16008 slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634370982594 8 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634370983000 1 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 myself,master - 0 1634370981000 8 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001@16001 master - 0 1634370982594 1 connected 1024-5460
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634370982994 3 connected 10923-16383
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634370982594 2 connected
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634370983997 3 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634370982594 2 connected 5461-10922

failover故障轉移

auto-failover自動故障轉移

當運行中的master節點掛掉了,集羣會在該master節點的slave節點中選出一個作爲新的master節點。

容器停止

docker-compose stop 是停止yaml包含的所有容器

停止6007

docker-compose stop redis7

[root@localhost redis-cluster-ext]# docker-compose stop redis7
Stopping redis-cluster-ext_redis7_1 ... done

查看集羣信息

此時該新節點已經成爲集羣的一份子

docker exec -it redis-cluster-ext_redis8_1 redis-cli -c -h 172.18.8.164 -p 6008

cluster nodes
[root@localhost redis-cluster-ext]# docker exec -it redis-cluster-ext_redis8_1 redis-cli -c -h 172.18.8.164 -p 6008
172.18.8.164:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371307000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 master,fail - 1634371240604 1634371239000 8 disconnected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371307537 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 172.18.8.164:6008@16008 myself,master - 0 1634371306000 9 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 master - 0 1634371307537 1 connected 1024-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371308000 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634371308542 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634371308542 2 connected 5461-10922

重啓6007

docker-compose up -d redis7

查看狀態,變成了 6008的從節點

172.18.8.164:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371452000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 slave 4656b8b2e26dd290928f45f9e4e001123c7ae36d 0 1634371452531 9 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371453535 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 172.18.8.164:6008@16008 myself,master - 0 1634371453000 9 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 master - 0 1634371452000 1 connected 1024-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371453234 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634371452000 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634371452230 2 connected 5461-10922

manu-failover手動故障轉移

有的時候在主節點沒有任何問題的情況下,強制手動故障轉移也是很有必要的,比如想要升級主節點的Redis進程,我們可以通過故障轉移將master其轉爲slave,再進行升級操作來避免對集羣的可用性造成很大的影響。

Redis集羣使用 cluster failover 命令來進行故障轉移,不過要在被轉移的主節點的slave從節點上執行該命令

也就是說,使用redis-cli連接slave節點並執行 cluster failover命令進行轉移。

現在,6007 爲從, 6008爲主,在6007上進行故障轉移:

連接6007

docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007

執行cluster failover 的結果:

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007
172.18.8.164:6007> cluster failover
OK
172.18.8.164:6007> cluster nodes
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371888000 2 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634371888686 2 connected 5461-10922
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371888587 1 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001@16001 master - 0 1634371887583 1 connected 1024-5460
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 myself,master - 0 1634371888000 10 connected 0-1023
4656b8b2e26dd290928f45f9e4e001123c7ae36d 127.0.0.1:6008@16008 slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634371887000 10 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634371887000 3 connected 10923-16383
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371889189 3 connected


節點的移除

可以使用如下命令來移除節點

./src/redis-cli --cluster del-node 127.0.0.1:7001 <nodeId>

第一個參數是任意一個節點的地址,

第二個參數是你想要移除的節點ID。

移除6008

 docker exec -it  redis-cluster-ext_redis7_1 redis-cli --cluster  del-node  172.18.8.164:6007 4656b8b2e26dd290928f45f9e4e001123c7ae36d

結果:

[root@localhost redis-cluster-ext]#  docker exec -it  redis-cluster-ext_redis7_1 redis-cli --cluster  del-node  172.18.8.164:6007 4656b8b2e26dd290928f45f9e4e001123c7ae36d
>>> Removing node 4656b8b2e26dd290928f45f9e4e001123c7ae36d from cluster 172.18.8.164:6007
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.

如果是移除主節點,需要確保這個節點是空的,如果不是空的,則需要將這個節點上的數據重新分配到其他節點上。

Redis Cluster基本架構

數據分片架構

在單個的 redis節點中,我們都知道redis把數據已 k-v 結構存儲在內存中,使得 redis 對數據的讀寫非常之快。

Redis Cluster 是去中心化的,它將所有數據分區存儲。也就是說當多個 Redis 節點搭建成集羣后,每個節點只負責自己應該管理的那部分數據,相互之間存儲的數據是不同的。

Redis Cluster 將全部的鍵空間劃分爲16384塊,每一塊空間稱之爲槽(slot),又將這些槽及槽所對應的 k-v 劃分給集羣中的每個主節點負責。

3個節點的Redis集羣虛擬槽如下圖:

在這裏插入圖片描述

3個節點的Redis集羣虛擬槽分片結果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

key -> slot 的算法選擇

key -> slot 的算法選擇上,Redis Cluster 選擇的算法是 hash(key) mod 16383,即使用CRC16算法對key進行hash,然後再對16383取模,結果便是對應的slot。

hash(key) mod 16383

1 keyhash= hash(key) 
2 slot= keyhash  % 16383

把16384個槽平均分配給節點進行管理,每個節點只能對自己負責的槽進行讀寫操作

由於每個節點之間都彼此通信,每個節點都知道另外節點負責管理的槽範圍

img

客戶端訪問任意節點時,對數據key按照CRC16規則進行hash運算,然後對運算結果對16383進行取作,如果餘數在當前訪問的節點管理的槽範圍內,則直接返回對應的數據

節點之間的漫遊

如果不在當前節點負責管理的槽範圍內,則會告訴客戶端去哪個節點獲取數據,由客戶端去正確的節點獲取數據

img

節點間的通信架構

集羣中會有多個節點,每個節點負責一部分slot以及對應的k-v數據,並且通過直連具體節點的方式與客戶端通信。

那麼問題來了,你向我這裏請求一個key的value,這個key對應的slot並不歸我負責,但我又要需要告訴你MOVED到目標節點,我如何知道這個目標節點是誰呢?

Redis Cluster使用Gossip協議維護節點的元數據信息,這種協議是P2P模式的,主要指責就是信息交換。

節點間不停地去交換彼此的元數據信息,那麼總會在一段時間後,大家都知道彼此是誰,負責哪些數據,是否正常工作等等。

節點間信息交換是依賴於彼此發出的Gossip消息的。

集羣的元數據

Cluster中的每個節點都維護一份在自己看來當前整個集羣的元數據,主要包括:

  • 當前集羣狀態
  • 集羣中各節點所負責的slots信息,及其migrate狀態
  • 集羣中各節點的master-slave狀態
  • 集羣中各節點的存活狀態及不可達投票

P2P方式模式的元數據交互協議

回顧: es的元數據,是怎麼管理的

Redis集羣內採用的是P2P方式模式,沒有主節點。並且採用的是Gossip協議。

Gossip協議工作原理就是節點彼此不斷通信交換信息,一段時間後所有的節點都會知道集羣完整的信息,這種方式類似流言傳播。

gossip 協議(gossip protocol)又稱 epidemic 協議(epidemic protocol),是基於流行病傳播方式的節點或者進程之間信息交換的協議,在分佈式系統中被廣泛使用,比如我們可以使用 gossip 協議來確保網絡中所有節點的數據一樣。

gossip protocol 最初是由施樂公司帕洛阿爾託研究中心(Palo Alto Research Center)的研究員艾倫·德默斯(Alan Demers)於1987年創造的。

從 gossip 單詞就可以看到,其中文意思是八卦、流言等意思,我們可以想象下緋聞的傳播(或者流行病的傳播);gossip 協議的工作原理就類似於這個。

gossip 協議

Goosip 協議的信息傳播和擴散通常需要由種子節點發起。

整個傳播過程可能需要一定的時間,由於不能保證某個時刻所有節點都收到消息,但是理論上最終所有節點都會收到消息,因此它是一個最終一致性協議。

Gossip協議的特點

Gossip協議是一個P2P協議,所有寫操作可以由不同節點發起,並且同步給其他副本。

Gossip內組成的網絡節點都是對等節點,是非結構化網絡。

gossip 協議利用一種隨機的方式將信息傳播到整個網絡中,並在一定時間內使得系統內的所有節點數據一致。

Gossip 其實是一種去中心化思路的分佈式協議,解決狀態在集羣中的傳播和狀態一致性的保證兩個問題。

節點間的通訊消息

Redis集羣的Gossip消息

Redis集羣使用二進制協議進行節點到節點的數據交換,這更適合於使用很少的帶寬和處理時間在節點之間交換信息。

Gossip協議的主要職責就是信息交換。

信息交換的載體就是節點彼此發送的Gossip消息。

Redis集羣中每個redis實例(可能一臺機部署多個實例)會使用兩個Tcp端口,

  • 一個用於給客戶端(redis-cli或應用程序等)使用的端口,
  • 另一個是用於集羣中實例相互通信的內部總線端口,且第二個端口比第一個端口一定大10000。

內部總線端口通信使用特殊Gossip協議,以便實現集羣內部高帶寬低時延的數據交換。

所以配置redis實例時只需要指明第一個端口就可以了。

所以,每一個Redis羣集的節點都需要打開兩個TCP連接,由於這兩個連接就需要兩個端口,分別是用於爲客戶端提供服務的常規RedisTCP命令端口(例如6379)以及通過將10000和命令端口相加(10000+6379)而獲得的端口,就是集羣端口(例如16379)。

命令端口和集羣總線端口偏移量是固定的,始終爲10000。第二個大號端口用於羣集總線,即使用二進制協議的節點到節點通信通道。節點使用羣集總線進行故障檢測,配置更新,故障轉移授權等。

客戶端不應嘗試與羣集總線端口通信,爲了保證Redis命令端口的正常使用,請確保在防火牆中打開這兩個端口,否則Redis羣集節點將無法通信。

請注意,爲了讓Redis羣集正常工作,您需要爲每個節點:

1、用於與客戶端進行通信的普通客戶端通信端口(通常爲6379)對所有需要到達羣集的客戶端以及所有其他羣集節點(使用客戶端端口進行密鑰遷移)都是開放的。

2、集羣總線端口(客戶端端口+10000)必須可從所有其他集羣節點訪問。

Redis集羣常用的Gossip消息可分爲:ping消息、pong消息、meet消息、fail消息:

  • meet消息 會通知接收該消息的節點,發送節點要加入當前集羣,接收者進行響應。
  • ping消息 是集羣中的節點定期向集羣中其他節點(部分或全部)發送的連接檢測以及信息交換請求,消息包含發送節點信息以及發送節點知道的其他節點信息。
  • pong消息 是在節點接收到meet、ping消息後回覆給發送節點的響應消息,告訴發送方本次通信正常,消息包含當前節點狀態。
  • fail消息 是在節點認爲集羣內另外某一節點下線後向集羣內所有節點廣播的消息。

節點的握手消息

在集羣啓動的過程中,有一個重要的步驟是 節點握手 ,其本質就是在一個節點上向其他所有節點發送meet消息,消息中包含當前節點的信息(節點id,負責槽位,節點標識等等),接收方會將發送節點信息存儲至本地的節點列表中。

當發送者接到客戶端發送的CLUSTER MEET命令時,發送者會向接收者 發送MEET消息,請求接收者加入到發送者當前所處的集羣裏面

消息體中還會包含與發送節點通信的其他節點信息(節點標識、節點id、節點ip、port等),接收方也會解析這部分內容,如果本地節點列表中不存在,則會主動向新節點發送meet消息。

接收方處理完消息後,也會回覆pong消息給發送者節點,發送者也會解析pong消息更新本地存儲節點信息。

因此,雖然只是在一個節點向其他所有節點發送meet消息,最後所有節點都會有其他所有節點的信息。

節點之間會相互通信,meet操作是節點之間完成相互通信的基礎,meet操作有一定的頻率和規則

img

集羣內的心跳消息

集羣啓動後,集羣中各節點也會定時往 其他部分節點 發送ping消息,用來檢測:

  • 目標節點是否正常/
  • 以此來檢測被選中的節點是否在線/
  • 以及發送自己最新的節點負槽位信息。

接收方同樣響應pong消息,由發送方更新本地節點信息。

心跳時機:

Redis節點會記錄其向每一個節點上一次發出ping和收到pong的時間,心跳發送時機與這兩個值有關。

通過下面的方式既能保證及時更新集羣狀態,又不至於使心跳數過多,集羣的週期性執行clusterCron函數,每秒執行10次,100ms執行一次:

  • 每次clusterCron向所有未建立鏈接的節點發送ping或meet
  • 每1秒(10次當中某次)從所有已知節點中隨機選取5個,向其中上次收到pong最久遠的一個發送ping
  • 每次Cron向收到pong超過timeout/2的節點發送ping
  • 收到ping或meet,立即回覆pong

集羣裏的每個節點默認每隔一秒鐘就會從已知節點列表中隨機選出五個節點,然後對這五個節點中最長時間沒有發送過PING消息的節點發送PING消息。

除此之外,如果節點A最後一次收到節點B發送的PONG消息的時間,距離當前時間已經超過了節點A的cluster-node-timeout選項設置時長的一半,那麼節點A也會向節點B發送PING消息,這可以防止節點A因爲長時間沒有隨機選中節點B作爲PING消息的發送對象而導致對節點B的信息更新滯後

serverCron的核心邏輯如下,感興趣就看
serverCron{
...
if (server.cluster_enabled) clusterCron();
...
}
 
clusterCron函數執行如下操作:
(1)向其他節點發送MEET消息,將其加入集羣;
(2)每1s會隨機選擇一個節點,發送ping消息;
(3)如果一個節點在超時時間之內仍未收到ping包的響應(cluster-node-timeout配置項指定的時間),則將其
標記爲pfail;
(4)檢查是否需要進行主從切換,如果需要則執行切換;
(5)檢查是否需要進行副本漂移,如果需要,執行副本漂移操作.
 
注意:
a.對於步驟(1),當在一個集羣節點A執行CLUSTER MEET ip port命令時,會將“ip:port”指定的節點B加入該集
羣中,但該命令執行時只是將B的“ip:port”信息保存到A節點中,然後在clusterCron函數中爲A節點“ip:port”
指定的B節點建立連接併發送MEET類型的數據包.
 
b.對於步驟(3),Redis集羣中節點的故障狀態有兩種.一種爲pfail(Possible failure),當一個節點A未在
指定時間收到另一個節點B對ping包的響應時,A節點會將B節點標記爲pfail。另一種是,當大多數Master節點
確認B爲pfail之後,就會將B標記爲fail. fail狀態的節點纔會需要執行主從切換.
心跳數據
  • Header,發送者自己的信息
    • 所負責slots的信息
    • 主從信息
    • ip port信息
    • 狀態信息
  • Gossip,發送者所瞭解的部分其他節點的信息
    • ping_sent, pong_received
    • ip, port信息
    • 狀態信息,比如發送者認爲該節點已經不可達,會在狀態信息中標記其爲PFAIL或FAIL

考慮到頻繁地交換信息會加重帶寬(集羣節點越多越明顯)和計算的負擔,Redis Cluster內部的定時任務每秒執行10次,每100毫秒一次,每次遍歷本地節點列表,對最近一次接受到pong消息時間大於cluster_node_timeout/2的節點立馬發送ping消息,此外每秒隨機找5個節點,選裏面最久沒有通信的節點發送ping消息。

同時 ping 消息的消息投攜帶自身節點信息,消息體只會攜帶1/10的其他節點信息,避免消息過大導致通信成本過高。

cluster_node_timeout 參數影響發送消息的節點數量,調整要綜合考慮故障轉移、槽信息更新、新節點發現速度等方面。

一般帶寬資源特別緊張時,可以適當調大一點這個參數,降低通信成本。

fail消息

當集羣裏的節點A將節點B標記爲已下線(FAIL)時,節點A將向集羣廣播一條關於節點B的FAIL消息,所有接收到這條FAIL消息的節點都會將節點B標記爲已下線

fail消息演示案例

舉個例子,對於包含7000、7001、7002、7003四個主節點的集羣來說:

  • 如果主節點7001發現主節點7000已下線,那麼主節點7001將向主節點7002和主節點7003 發送FAIL消息,其中FAIL消息中包含的節點名字爲主節點7000的名字,以此來表示主節點 7000已下線
  • 當主節點7002和主節點7003都接收到主節點7001發送的FAIL消息時,它們也會將主節 點7000標記爲已下線
  • 因爲這時集羣已經有超過一半的主節點認爲主節點7000已下線,所以集羣剩下的幾個主節點可以判斷是否需要將該節點標記爲下線,又或者開始對主節點7000進行故障轉移

下圖展示了節點發送和接收FAIL消息的整個過程

img

在集羣的節點數量比較大的情況下,單純使用Gossip協議來傳播節點的已下線信息會給節點的信息更新帶來一定延遲,因爲Gossip協議消息通常需要一段時間才能傳播至整個集羣,而發送FAIL消息可以讓集羣裏的所有節點立即知道某個主節點已下線,從而儘快判斷是 否需要將集羣標記爲下線,又或者對下線主節點進行故障轉移 (slave提升爲新Master)

ping 時的節點選擇

這個地方,很複雜,能講清楚的培訓機構,全網不多,大家慢慢看看

Redis集羣的Gossip協議需要兼顧信息交換實時性和成本開銷。

  • ping 時要攜帶一些元數據,如果很頻繁,可能會加重網絡負擔。因此,Redis集羣內節點通信採用固定頻率(定時任務每秒執行10次),一般每個節點每秒會執行 10 次 ping,每次會選擇 5 個最久沒有通信的其它節點。

  • 當然如果發現某個節點通信延時達到了 cluster_node_timeout / 2,那麼立即發送 ping,避免數據交換延時過長導致信息嚴重滯後。

    比如說,兩個節點之間都 10 分鐘沒有交換數據了,那麼整個集羣處於嚴重的元數據不一致的情況,就會有問題。所以 cluster_node_timeout 可以調節,如果調得比較大,那麼會降低 ping 的頻率。

  • 每次 ping,會帶上自己節點的信息,還有就是帶上 1/10 其它節點的信息,發送出去,進行交換。至少包含 3 個其它節點的信息,最多包含 總節點數減 2 個其它節點的信息。

因此節點每次選擇需要通信的節點列表變得非常重要。通信節點選擇過多雖然可以做到信息及時交換但是成本過高。節點選擇過少會降低集羣內所有節點彼此信息交互頻率,從而影響故障判定、新節點發現等需求的速度。

ping 時,通信節點選擇的規則如圖所示:

img

根據通信節點選擇的流程可以看出:

消息交換的成本主要體現在單位時間選擇發送消息的節點數量和每個消息攜帶的數據量。

選擇發送消息的節點數量

  • 集羣內每個節點維護定時任務默認每秒執行10次,每秒會隨機選取5個節點找出最久沒有通信的節點發送ping消息,用於保證Gossip信息交換的隨機性。每100毫秒都會掃描本地節點列表,如果發現節點最近一次接受pong消息的時間大於 cluster_node_timeout / 2,則立刻發送ping消息,防止該節點信息太長時間未更新。

    根據以上規則得出每個節點每秒需要發送ping消息的數量,由此,根據以上規則得出每個節點/每秒需要發送ping消息的數量:

    5 + 10*num(num=node.pong_received>cluster_node_timeout/2 的節點數)

    所以: cluster_node_timeout參數對消息發送的節點數量影響非常大。

  • 當我們的帶寬資源緊張時,可以適當調大這個參數,如從默認15秒改爲30秒來降低帶寬佔用率。

  • 過度調大cluster_node_timeout會影響消息交換的頻率從而影響故障轉移、槽信息更新、新節點發現的速度。

  • 需要根據業務容忍度和資源消耗進行平衡,同時整個集羣消息總交換量也跟節點數成正比。

消息數據量

  • 每個ping消息的數據量體現在消息頭和消息體中,其中消息頭主要佔用 空間的字段是myslots[CLUSTER_SLOTS/8],佔用2KB,這塊空間佔用相對固定。
  • 消息體會攜帶一定數量的其他節點信息用於信息交換。消息體攜帶數據量跟集羣的節點數息息相關,更大的集羣每次消息通信的成本也就更高,因此對於Redis集羣來說並不是大而全的集羣更好。

redis虛擬槽位爲什麼是16384(2^14)個?

問題1

redis虛擬槽位爲什麼是16384(2^14)個?而不是 65535 (2^16)個?

問題2

CRC16算法產生的hash值有16bit,該算法可以產生2^16-=65536個值。換句話說,值是分佈在0~65535之間。那作者在做mod運算的時候,爲什麼不mod 65536,而選擇 mod 16384?

分片SLOT的計算公式

SLOT=CRC16.crc16(key.getBytes()) % MAX_SLOT

在這裏插入圖片描述

對於客戶端請求的key,根據公式HASH_SLOT=CRC16(key) mod 16384,計算出映射到哪個分片上,然後Redis會去相應的節點進行操作!

在這裏插入圖片描述

但是可能這個槽並不歸隨機找的這個節點管,節點如果發現不歸自己管,就會返回一個MOVED ERROR通知,引導客戶端去正確的節點訪問,這個時候客戶端就會去正確的節點操作數據。

CRC16算法產生的hash值有16bit,該算法可以產生2^16-=65536個值。換句話說,值是分佈在0~65535之間。那作者在做mod運算的時候,爲什麼不mod 65536,而選擇 mod 16384?

redis節點發送心跳包

在redis節點發送心跳包時需要把所有的槽放到這個心跳包裏,以便讓節點知道當前集羣信息,

img點擊並拖拽以移動

交換的數據信息,由消息體和消息頭組成。消息體無外乎是一些節點標識啊,IP啊,端口號啊,發送時間啊。

這裏不做展開,我們來看消息頭,結構如下

img點擊並拖拽以移動

消息頭裏面有個myslots的char數組,長度爲16383/8,這其實是一個bitmap,每一個位代表一個槽,如果該位爲1,表示這個槽是屬於這個節點的。在消息頭中,最佔空間的是myslots[CLUSTER_SLOTS/8]

這塊(2的十四次方)的大小是:
16384÷8÷1024=2kb

16384=16k,

在發送心跳包時使用char進行bitmap壓縮後是2k(2 * 8 (8 bit) * 1024(1k) = 16K)個char,也就是說使用2k個char的空間,能表達16k的槽數。

雖然使用CRC16算法最多可以分配65535(2^16-1)個槽位,65535=65k,壓縮後就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是說需要需要8k的心跳包,作者認爲這樣做不太值得;

集羣節點越多,心跳包的消息體內攜帶的數據越多。

如果節點過1000個,也會導致網絡擁堵。

因此redis作者,不建議redis cluster節點數量超過1000個。
那麼,對於節點數在1000以內的redis cluster集羣,16384個槽位夠用了。沒有必要拓展到65536個。

並且一般情況下一個redis集羣不會有超過1000個master節點,所以16k的槽位是個比較合適的選擇。

Redis Cluster的高可用架構

要保證高可用的前提是離不開從節點的,一旦某個主節點因爲某種原因不可用後,就需要一個一直默默當備胎的從節點頂上來了。

一般在集羣搭建時最少都需要6個實例,其中3個實例做主節點,各自負責一部分槽位,另外3個實例各自對應一個主節點做其從節點,對主節點的操作進行復制(對於主從複製的細節,前面已經進行詳細說明)。

完整的redis集羣架構圖( 請參見演示)

要求: 參見演示, 建議邊看視頻,邊自己畫一個,加深理解

3個節點的Redis集羣虛擬槽分片結果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

集羣中指定主從關係

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

Redis Cluster在給主節點添加從節點時,不是使用 slaveof 命令,而是通過在從節點上執行命令 :

cluster replicate masterNodeId 。

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

redis-cli -p 7000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
redis-cli -p 7001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
redis-cli -p 7002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1

failover故障發現與轉移

當集羣內少量節點出現故障時,通過自動故障轉移保證集羣可以正常對外提供服務。

作爲一個完整的集羣,每個負責處理槽的節點應該具有從節點,保證當它出現故障時可以自動進行故障轉移。

redis集羣自身實現了高可用,Redis Cluster通過ping/pong消息實現故障發現:不需要sentinel

首次啓動的節點和被分配槽的節點都是主節點,從節點負責複製主節點槽信息和相關的數據。

Redis Cluster通過ping/pong消息不僅能傳遞節點與槽的對應消息,也能傳遞其他狀態,比如:節點主從狀態,節點故障等

failover故障發現與轉移總體過程

Cluster的故障發現也是基於節點通信的。

完整的failover故障發現與轉移總體過程,

要求: 參見演示, 建議邊看視頻,邊自己畫一個,加深理解

每個節點在本地存儲有一個節點列表(其他節點信息),列表中每個 節點元素除了存儲其ID、ip、port、狀態標識(主從角色、是否下線等等)外,還有最後一次向該節點發送ping消息的時間、最後一次接收到該節點的pong消息的時間以及一個保存其他節點對該節點下線傳播的報告鏈表 。

節點與節點間會定時發送ping消息,彼此響應pong消息,成功後都會更新這個時間。

同時每個節點都有定時任務掃描本地節點列表裏這兩個消息時間,若發現pong響應時間減去ping發送時間超過cluster-node-timeout配置時間後,便會將本地列表中對應節點的狀態標識爲PFAIL,認爲其有可能下線。

cluster-node-timeout默認15秒,該參數用來設置節點間通信的超時時間

節點間通信(ping)時會攜帶本地節點列表中部分節點信息,如果其中包括標記爲PFAIL的節點.

那麼在消息接收方解析到該節點時,會找自己本地的節點列表中該節點元素的下線報告鏈表,看是否已經存在發送節點對於該故障節點的報告,如果有,就更新接收到發送ping消息節點對於故障節點的報告的時間,如果沒有,則將本次報告添加進鏈表。

下線報告鏈表的每個元素結構只有兩部分內容,一個是報告本地這個故障節點的發送節點信息,一個是本地接收到該報告的時間 (存儲該時間是因爲故障報告是有有效期的,避免誤報) 。

由於每個節點的下線報告鏈表都存在於各自的信息結構中,所以在瀏覽本地節點列表中每個節點元素時,可以清晰地知道,有其他哪些節點跟我說,兄弟,你正在看的這個節點我覺的涼涼了。

故障報告的有效期是 cluster-node-timeout * 2

消息接收方解析到PFAIL節點,並且更新本地列表中對應節點的故障報告鏈表後,會去查看該節點的故障報告鏈表中有效的報告節點是否超過所有主節點數的一半。

  • 如果沒超過,便繼續解析ping消息;

  • 如果超過,代表 超過半數的節點認爲這個節點可能下線了,當前節點就會將PFAIL節點本地的節點信息中的狀態標識標記爲FAIL ,然後向集羣內廣播一條fail消息,集羣內的所有節點接收到該fail消息後,會把各自本地節點列表中該節點的狀態標識修改爲FAIL。

在所有節點對其標記爲FAIL後,開始故障轉移:該FAIL節點對應的從節點就會發起轉正流程。

在轉正流程完成後,這個節點就會正式下線,等到其恢復後,發現自己的槽已經被分給某個節點,便會將自己轉換成這個節點的從節點並且ping集羣內其他節點,其他節點接到恢復節點的ping消息後,便會更新其狀態標識。

此外,恢復的節點若發現自己的槽還是由自己負責,就會跟其他節點通信,其他主節點發現該節點恢復後,就會拒絕其從節點的選舉,最終清除自己的FAIL狀態。

故障發現

故障發現就是通過這種模式來實現,分爲:

  • 主觀下線
  • 客觀下線

故障發現也是通過消息傳播機制實現的,主要環節包括:

(1)主觀下線(pfail)。

集羣中每個節點都會定期向其他節點發送ping消息,接收節點回復pong消息作爲響應。如果在cluster-node-timeout時間內通信一直失敗,則發送節點會認爲接收節點存在故障,把接收節點標記爲主觀下線(pfail)狀態。

相當於 自己認爲,別人下線了,

(2)客觀下線(fail)

當某個節點判斷另一個節點主觀下線後,相應的節點狀態會跟隨消息在集羣內傳播。

當接受節點發現消息體中含有主觀下線的節點狀態,且發送節點是主節點時,會在本地找到故障節點的ClusterNode結構,更新下線報告鏈表。

相當於 大家認爲,別人下線了

主觀下線

某個節點認爲另一個節點不可用,'偏見',只代表一個節點對另一個節點的判斷,不代表所有節點的認知

主觀下線流程:

完整主觀下線過程,

要求: 參見演示, 建議邊看視頻,邊自己畫一個,加深理解

1.節點1定期發送ping消息給節點2
2.如果發送成功,代表節點2正常運行,節點2會響應PONG消息給節點1,節點1更新與節點2的最後通信時間
3.如果發送失敗,則節點1與節點2之間的通信異常判斷連接,在下一個定時任務週期時,仍然會與節點2發送ping消息
4.如果節點1發現與節點2最後通信時間超過node-timeout,則把節點2標識爲pfail狀態

完整的客觀下線與主觀下線流程,

要求: 參見演示, 建議邊看視頻,邊自己畫一個,加深理解

客觀下線

當半數以上持有槽的主節點都標記某節點主觀下線時,可以保證判斷的公平性

集羣模式下,只有主節點(master)纔有讀寫權限和集羣槽的維護權限,從節點(slave)只有複製的權限

客觀下線流程:

完整客觀下線過程,

要求: 參見演示, 建議邊看視頻,邊自己畫一個,加深理解

1.某個節點接收到其他節點發送的ping消息,如果接收到的ping消息中包含了其他pfail節點,這個節點會將主觀下線的消息內容添加到自身的故障列表中,故障列表中包含了當前節點接收到的每一個節點對其他節點的狀態信息

當某個節點判斷另一個節點主觀下線後,相應的節點狀態會跟隨消息在集羣內傳播。

當接受節點發現消息體中含有主觀下線的節點狀態且發送節點是主節點時,會在本地找到故障節點的ClusterNode結構,更新下線報告鏈表。

  • 集羣中的節點每次接收到其他節點的pfail狀態,都會嘗試觸發客觀下線。首先統計有效的下線報告數量,當下線報告數量大於槽主節點數量一半時,標記對應故障節點爲客觀下線狀態。
  • 向集羣廣播一條fail消息,通知所有的節點將故障節點標記爲客觀下線,fail消息的消息體只包含故障節點的ID。通知故障節點的從節點觸發故障轉移流程。
struct clusterNode { /* 認爲是主觀下線的clusterNode結構 */
    list *fail_reports; /* 記錄了所有其他節點對該節點的下線報告 */
};

只有負責槽的主節點(master節點,而非slave)參與故障發現決策,

因爲集羣模式下只有處理槽的主節點才負責讀寫請求和集羣槽等關鍵信息維護,而從節點只進行master 主節點數據和狀態信息的複製。

故障列表的 檢查週期爲:

集羣的node-timeout * 2,保證以前的故障消息不會對週期內的故障消息造成影響,保證客觀下線的公平性和有效性

Redis節點failover(故障轉移、故障恢復)流程

故障節點變爲客觀下線後,如果下線節點是持有槽的主節點, 則需要在它的slave 從節點中選出一個替換它,從而保證集羣的高可用

誰來承擔故障恢復的職責:

下線主節點的所有從節點

下線主節點的所有從節點承擔故障恢復的義務,當從節點通過內部定時任務發現自身複製的主節點進入客觀下線時,將會觸發故障恢復流程:

(1) 資格檢查

每個從節點都要檢查最後與主節點斷線時間,判斷是否有資格替換故障的主節點。

如果從節點與主節點斷線時間超過cluster-node-time * cluster-slave-validity-factor,則當前從節點不具備故障轉移資格。

(2)準備選舉時間

當從節點符合故障轉移資格後,更新觸發故障選舉的時間,只有到達該時間後才能執行後續流程。

在多個從節點的場景

這裏之所以採用延遲觸發機制,主要是通過對多個從節點使用不同的延遲選舉時間來支持優先級問題。

複製偏移量越大,說明從節點延遲越低,那麼它應該具有更高的優先級來替換故障主節點。

複製偏移量越小,說明從節點延遲越高,那麼它應該具有更低的優先級來替換故障主節點。

(3)發起選舉

當從節點定時任務檢測到達故障選舉時間(failover_auth_time)到達後,發起選舉流程如下:會先更新配置紀元,再在集羣內廣播選舉消息,並記錄已發送過消息的狀態,保證該從節點在一個配置紀元內只能發起一次選舉。

(4)選舉投票

只有持有槽的主節點纔會處理故障選舉消息,因爲每個持有槽的節點在一個配置紀元內都有唯一的一張選票,當接到第一個請求投票的從節點消息時回覆FAILOVER_AUTH_ACK消息作爲投票,之後相同配置紀元內其他從節點的選舉消息將忽略。當從節點收集到N/2+1個持有槽的主節點投票時,從節點可以執行替換主機點操作。

(5)替換主節點

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

  • 當前從節點取消複製變爲主節點。
  • 執行clusterDelSlot操作撤銷故障主節點負責的槽,並執行clusterAddSlot把這些槽委派給自己。
  • 向集羣廣播自己的pong消息,通知集羣內所有的節點當前從節點變爲主節點並接管了故障主節點的槽信息。

資格檢查

每個從節點都要檢查最後與主節點斷線時間,判斷是否有資格替換故障的主節點。如果從節點與主節點斷線時間超過cluster-node-time * cluster-slave-validity-factor,則當前從節點不具備故障轉移資格。

  • 對從節點的資格進行檢查,只有通過檢查的從節點纔可以開始進行故障恢復

  • 每個從節點檢查與故障主節點的斷線時間

  • 超過cluster-node-timeout * cluster-slave-validity-factor數字,則取消資格

  • cluster-node-timeout默認爲15秒,cluster-slave-validity-factor默認值爲10

  • 如果這兩個參數都使用默認值,則每個節點都檢查與故障主節點的斷線時間,如果超過150秒,則這個節點就沒有成爲替換主節點的可能性

準備選舉時間

當從節點符合故障轉移資格後,更新觸發故障選舉的時間,只有到達該時間後才能執行後續流程。

這裏之所以採用延遲觸發機制,主要是通過對多個從節點使用不同的延遲選舉時間來支持優先級問題。

複製偏移量越大說明從節點延遲越低,那麼它應該具有更高的優先級來替換故障主節點。

  • 複製偏移量越大,說明從節點延遲越低,那麼它應該具有更高的優先級來替換故障主節點。

  • 複製偏移量越小,說明從節點延遲越高,那麼它應該具有更低的優先級來替換故障主節點。

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

使偏移量最大的從節點具備優先級成爲主節點的條件

img

發起選舉

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

(1).更新配置紀元:

配置紀元是一個只增不減的整數,每個主節點自身維護一個配置紀元 (clusterNode.configEpoch)標示當前主節點的版本,所有主節點的配置紀元都不相等,從節點會複製主節點的配置紀元。

整個集羣又維護一個全局的配 置紀元(clusterState.current Epoch),用於記錄集羣內所有主節點配置紀元 的最大版本。

執行cluster info命令可以查看配置紀元信息。

只要集羣發生重要的關鍵事件,紀元數就會增加,所以在選從的時候需要選擇一個紀元數最大的從。

(2).廣播選舉消息:

在集羣內廣播選舉消息(FAILOVER_AUTH_REQUEST),並記錄已發送過消息的狀態,保證該從節點在一個配置紀元內只能發起一次選舉。

消息 內容如同ping消息只是將type類型變爲FAILOVER_AUTH_REQUEST。

配置紀元的主要作用:
  • 標示集羣內每個主節點的不同版本和當前集羣最大的版本。
  • 每次集羣發生重要事件時,這裏的重要事件指出現新的主節點(新加入的或者由從節點轉換而來),從節點競爭選舉。都會遞增集羣全局的配置紀元並賦值給相關主節點,用於記錄這一關鍵事件。
  • 主節點具有更大的配置紀元代表了更新的集羣狀態,因此當節點間進行ping/pong消息交換時,如出現slots等關鍵信息不一致時,以配置紀元更大的一方爲準,防止過時的消息狀態污染集羣。

配置紀元的應用場景有:新節點加入、槽節點映射衝突檢測、從節點投票選舉衝突檢測。

選舉投票

只有持有哈希槽的主節點才能參與投票,每個主節點有一票的權利,如集羣內有N個主節點,那麼只要有一個從節點獲得了N/2+1的選票即認爲勝出。

故障主節點也算在投票數內,假設集羣內節點規模是3主3從,其中有2個主節點部署在一臺機器上,當這臺機器宕機時,由於從節點無法收集到 3/2+1個主節點選票將導致故障轉移失敗。

這個問題也適用於故障發現環 節。因此部署集羣時所有主節點最少需要部署在3臺物理機上才能避免單點問題。

投票作廢:每個配置紀元代表了一次選舉週期,如果在開始投票之後的 cluster-node-timeout*2時間內從節點沒有獲取足夠數量的投票,則本次選舉作廢。

從節點對配置紀元自增併發起下一輪投票,直到選舉成功爲止。

img

替換主節點

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

  • 當前從節點取消複製, 變爲主節點。
  • 執行clusterDelSlot操作, 撤銷故障主節點負責的槽,並執行clusterAddSlot把這些槽委派給自己。
  • 向集羣廣播自己的pong消息,通知集羣內所有的節點當前,從節點變爲主節點並接管了故障主節點的槽信息。

故障轉移時間預估

  • 主觀下線(pfail)識別時間 = cluster-node-timeout , 如果節點1發現與節點2最後通信時間超過node-timeout,則把節點2標識爲pfail狀態

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

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

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

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

cluster-node-timeout時間設置,需要平衡:

  • 當節點發現與其他節點最後通信時間超過cluster-node-timeout/2時會直接發送ping消息,適當提高cluster-node-timeout可以降低消息發送頻率,減少網絡IO的流量

  • 但同時cluster-node-timeout還影響故障轉移的速度,因此需要根據自身業務場景兼顧二者的平衡。

故障轉移演練

對某一個主節點執行kill -9 {pid}來模擬宕機的情況

客戶端高可用

客戶端高可用方案,包含:

  • 客戶端moved重定向和ask重定向
  • smart智能客戶端

客戶端moved重定向和ask重定向

moved重定向

1.每個節點通過通信都會共享Redis Cluster中槽和集羣中對應節點的關係
2.客戶端向Redis Cluster的任意節點發送命令,接收命令的節點會根據CRC16規則進行hash運算與16383取餘,計算自己的槽和對應節點
3.如果保存數據的槽被分配給當前節點,則去槽中執行命令,並把命令執行結果返回給客戶端
4.如果保存數據的槽不在當前節點的管理範圍內,則向客戶端返回moved重定向異常
5.客戶端接收到節點返回的結果,如果是moved異常,則從moved異常中獲取目標節點的信息
6.客戶端向目標節點發送命令,獲取命令執行結果

img

需要注意的是:客戶端不會自動找到目標節點執行命令

槽命中:直接返回

img

槽不命中:moved異常

img

ask重定向

img

在對集羣進行擴容和縮容時,需要對槽及槽中數據進行遷移

當客戶端向某個節點發送命令,節點向客戶端返回moved異常,告訴客戶端數據對應的槽的節點信息

如果此時正在進行集羣擴展或者縮空操作,當客戶端向正確的節點發送命令時,槽及槽中數據已經被遷移到別的節點了,就會返回ask,這就是ask重定向機制

img

步驟:

1.客戶端向目標節點發送命令,目標節點中的槽已經遷移支別的節點上了,此時目標節點會返回ask轉向給客戶端
2.客戶端向新的節點發送Asking命令給新的節點,然後再次向新節點發送命令
3.新節點執行命令,把命令執行結果返回給客戶端

moved異常與ask異常的相同點和不同點

兩者都是客戶端重定向
moved異常:槽已經確定遷移,即槽已經不在當前節點
ask異常:槽還在遷移中

smart智能客戶端

使用智能客戶端的首要目標:追求性能

從集羣中選一個可運行節點,使用Cluster slots初始化槽和節點映射

將Cluster slots的結果映射在本地,爲每個節點創建JedisPool,相當於爲每個redis節點都設置一個JedisPool,然後就可以進行數據讀寫操作

讀寫數據時的注意事項:

每個JedisPool中緩存了slot和節點node的關係
key和slot的關係:對key進行CRC16規則進行hash後與16383取餘得到的結果就是槽
JedisCluster啓動時,已經知道key,slot和node之間的關係,可以找到目標節點
JedisCluster對目標節點發送命令,目標節點直接響應給JedisCluster
如果JedisCluster與目標節點連接出錯,則JedisCluster會知道連接的節點是一個錯誤的節點
此時JedisCluster會隨機節點發送命令,隨機節點返回moved異常給JedisCluster
JedisCluster會重新初始化slot與node節點的緩存關係,然後向新的目標節點發送命令,目標命令執行命令並向JedisCluster響應
如果命令發送次數超過5次,則拋出異常"Too many cluster redirection!"

img

開發運維常見的高可用問題

集羣完整性

cluster-require-full-coverage默認爲yes,即是否集羣中的所有節點都是在線狀態且16384個槽都處於服務狀態時,集羣纔會提供服務

集羣中16384個槽全部處於服務狀態,保證集羣完整性

當某個節點故障或者正在故障轉移時獲取數據會提示:(error)CLUSTERDOWN The cluster is down

建議把cluster-require-full-coverage設置爲no

帶寬消耗

Redis Cluster節點之間會定期交換Gossip消息,以及做一些心跳檢測

官方建議Redis Cluster節點數量不要超過1000個,當集羣中節點數量過多時,會產生不容忽視的帶寬消耗

消息發送頻率:節點發現與其他節點最後通信時間超過cluster-node-timeout /2時,會直接發送PING消息

消息數據量:slots槽數組(2kb空間)和整個集羣1/10的狀態數據(10個節點狀態數據約爲1kb)

節點部署的機器規模:集羣分佈的機器越多且每臺機器劃分的節點數越均勻,則集羣內整體的可用帶寬越高

帶寬優化:

避免使用'大'集羣:避免多業務使用一個集羣,大業務可以多集羣
cluster-node-timeout:帶寬和故障轉移速度的均衡
儘量均勻分配到多機器上:保證高可用和帶寬

Pub/Sub廣播

在任意一個cluster節點執行publish,則發佈的消息會在集羣中傳播,集羣中的其他節點都會訂閱到消息,這樣節點的帶寬的開銷會很大

publish在集羣每個節點廣播,加重帶寬

解決辦法:需要使用Pub/Sub時,爲了保證高可用,可以單獨開啓一套Redis Sentinel

Redis集羣的一致性保證

Redis集羣不能保證強一致性。

一些已經向客戶端確認寫成功的操作,會在某些不確定的情況下丟失。

  • 主從節點切換導致的不一致性

  • 集羣腦裂、網絡問題導致的不一致性

主從節點切換導致的不一致性

面試問題:

主從節點切換導致的不一致性原因

產生寫操作丟失的第一個原因,是因爲主從節點之間使用了異步的方式來同步數據。

一個寫操作是這樣一個流程:

  • 1)客戶端向主節點B發起寫的操作

  • 2)主節點B迴應客戶端寫操作成功

  • 3)主節點B向它的從節點B1,B2,B3同步該寫操作

從上面的流程可以看出來,主節點B並沒有等從節點B1,B2,B3寫完之後再回復客戶端這次操作的結果。

所以,如果主節點B在通知客戶端寫操作成功之後,但同步給從節點之前,主節點B故障了,其中一個沒有收到該寫操作的從節點會晉升成主節點,該寫操作就這樣永遠丟失了。

就像傳統的數據庫,在不涉及到分佈式的情況下,它每秒寫回磁盤。爲了提高一致性,可以在寫盤完成之後再回復客戶端,但這樣就要損失性能。這種方式就等於Redis集羣使用同步複製的方式。

基本上,在性能和一致性之間,需要一個權衡。

如果真的需要,Redis集羣支持同步複製的方式,通過WAIT指令來實現,這可以讓丟失寫操作的可能性降到很低。

但就算使用了同步複製的方式,Redis集羣依然不是強一致性的:

在某些複雜的情況下,比如從節點在與主節點失去連接之後被選爲主節點,不一致性還是會發生。

WAIT numslaves timeout

起始版本:3.0.0

時間複雜度:O(1)

此命令阻塞當前客戶端,直到所有以前的寫命令都成功的傳輸和指定的slaves確認。如果超時,指定以毫秒爲單位,即使指定的slaves還沒有到達,命令任然返回。

命令始終返回之前寫命令發送的slaves的數量,無論是在指定slaves的情況還是達到超時。

注意點:

  1. 當’WAIT’返回時,所有之前的寫命令保證接收由WAIT返回的slaves的數量。
  2. 如果命令唄當做事務的一部分發送,該命令不阻塞,而是隻儘快返回先前寫命令的slaves的數量。
  3. 如果timeout是0那意味着永遠阻塞。
  4. 由於WAIT返回的是在失敗和成功的情況下的slaves的數量。客戶端應該檢查返回的slaves的數量是等於或更大的複製水平。

一致性(Consistency and WAIT)

WAIT 不能保證Redis強一致:儘管同步複製是複製狀態機的一個部分,但是還需要其他條件。

不過,在sentinel和Redis羣集故障轉移中,WAIT 能夠增強數據的安全性。

如果寫操作已經被傳送給一個或多個slave節點,當master發生故障我們極大概率(不保證100%)提升一個受到寫命令的slave節點爲master:不管是Sentinel還是Redis Cluster 都會嘗試選slave節點中最優(日誌最新)的節點,提升爲master。

儘管是選擇最優節點,但是仍然會有丟失一個同步寫操作可能行。

實現細節

因爲引入了部分同步,Redis slave節點在ping主節點時會攜帶已經處理的複製偏移量。 這被用在多個地方:

  1. 檢測超時的slaves
  2. 斷開連接後的部分複製
  3. 實現WAIT

WAIT實現的案例中,當客戶端執行完一個寫命令後,針對每一個複製客戶端,Redis會爲其記錄寫命令產生的複製偏移量。當執行命令WAIT時,Redis會檢測 slaves節點是否已確認完成該操作或更新的操作。

返回值

integer-reply: 當前連接的寫操作會產生日誌偏移,該命令會返回已處理至該偏移量的slaves的個數。

例子

> SET foo bar
OK
> WAIT 1 0
(integer) 1
> WAIT 2 1000
(integer) 1

在例子中,第一次調用WAIT並沒有使用超時設置,並且設置寫命令傳輸到一個slave節點,返回成功。第二次使用時,我們設置了超時值並要求寫命令傳輸到兩個節點。 因爲只有一個slave節點有效,1秒後WAIT解除阻塞並返回1–傳輸成功的slave節點數。

集羣腦裂、網絡問題導致的不一致性

什麼是redis的集羣腦裂?

redis的集羣腦裂是指因爲網絡問題,導致redis master節點跟redis slave節點和sentinel集羣處於不同的網絡分區,此時因爲sentinel集羣無法感知到master的存在,所以將slave節點提升爲master節點。此時存在兩個不同的master節點,就像一個大腦分裂成了兩個。

集羣腦裂問題中,如果客戶端還在基於原來的master節點繼續寫入數據,那麼新的master節點將無法同步這些數據,當網絡問題解決之後,sentinel集羣將原先的master節點降爲slave節點,此時再從新的master中同步數據,將會造成大量的數據丟失。

具體來說,這種不一致性發生的情況是這樣的:

當客戶端與少數的節點(至少含有一個主節點)網絡聯通,但他們與其他大多數節點網絡不通。

比如6個節點,A,B,C是主節點,A1,B1,C1分別是他們的從節點,一個客戶端稱之爲Z。

在這裏插入圖片描述

當網絡出問題時,他們被分成2組網絡,組內網絡聯通,但2組之間的網絡不通,假設A,C,A1,B1,C1彼此之間是聯通的,另一邊,B和Z的網絡是聯通的。

在這裏插入圖片描述

Z可以繼續往B發起寫操作,B也接受Z的寫操作。

當網絡恢復時,如果這個時間間隔足夠短,集羣仍然能繼續正常工作。如果時間比較長,以致B1在大多數的這邊被選爲主節點,那剛纔Z1發給B的寫操作都將丟失。

注意,Z1給B發送寫操作是有一個限制的,如果時間長度達到了大多數節點那邊可以選出一個新的主節點時,少數這邊的所有主節點都不接受寫操作。

這個時間的配置,稱之爲節點超時(node timeout)。

節點超時(node timeout)設置:

對集羣來說非常重要:

  • 當達到了這個節點超時的時間之後,主節點被認爲已經宕機,可以用它的一個從節點來代替。

  • 同樣,在節點超時時,如果主節點依然不能聯繫到其他主節點,它將進入錯誤狀態,不再接受寫操作。

在redis.conf中的參數說明:

cluster-node-timeout :

這是集羣中的節點能夠失聯的最大時間,超過這個時間,該節點就會被認爲故障。如果主節點超過這個時間還是不可達,則用它的從節點將啓動故障遷移,升級成主節點。注意,任何一個節點在這個時間之內如果還是沒有連上大部分的主節點,則此節點將停止接收任何請求。

cluster-node-timeout默認15s。這個參數建議不要設置太小或者太大 。

redis集羣沒有過半機制會有腦裂問題,網絡分區導致腦裂後多個主節點對外提供寫服務,一旦網絡分區恢復,會將其中一個主節點變爲從節點,這時會有大量數據丟失。

如果發生了腦裂,就會有cluster-node-timeout 的數據丟失。

Redis集羣故障恢復的幾個場景

問題1:如果主節點下線?從節點能否自動升爲主節點?

答:主節點下線,從節點自動升爲主節點。
在這裏插入圖片描述

問題2:主節點恢復後,主從關係會如何?

主節點恢復後,主節點變爲從節點!
在這裏插入圖片描述

問題3:如果所有某一段插槽的主從節點都宕掉,redis服務是否還能繼續?

答:服務是否繼續,可以通過redis.conf中的cluster-require-full-coverage參數(默認關閉)進行控制。

主從都宕掉,意味着有一片數據,會變成真空,沒法再訪問了!

  • 如果無法訪問的數據,是連續的業務數據,我們需要停止集羣,避免缺少此部分數據,造成整個業務的異常。

此時可以通過配置cluster-require-full-coverage爲yes.

當cluster-require-full-coverage爲no時,表示當負責一個插槽的主庫下線且沒有相應的從庫進行故障恢復時,集羣不可用

  • 如果無法訪問的數據,是相對獨立的,對於其他業務的訪問,並不影響,那麼可以繼續開啓集羣體提供服務。此時,可以配置cluster-require-full-coverage爲no。

當cluster-require-full-coverage爲no時,表示當負責一個插槽的主庫下線且沒有相應的從庫進行故障恢復時,集羣仍然可用

數據傾斜與流量傾斜

對於分佈式數據庫來說,存在傾斜問題是比較常見的

集羣傾斜也就是各個節點使用的內存不一致

數據傾斜與流量傾斜原因

1.節點和槽分配不均,如果使用redis-trib.rb工具構建集羣,則出現這種情況的機會不多

redis-trib.rb info ip:port查看節點,槽,鍵值分佈
redis-trib.rb rebalance ip:port進行均衡(謹慎使用)

2.不同槽對應鍵值數量差異比較大

CRC16算法正常情況下比較均勻
可能存在hash_tag
cluster countkeysinslot {slot}獲取槽對應鍵值個數

3.包含bigkey:例如大字符串,幾百萬的元素的hash,set等

在從節點:redis-cli --bigkeys
優化:優化數據結構

4.內存相關配置不一致

hash-max-ziplist-value:滿足一定條件情況下,hash可以使用ziplist
set-max-intset-entries:滿足一定條件情況下,set可以使用intset
在一個集羣內有若干個節點,當其中一些節點配置上面兩項優化,另外一部分節點沒有配置上面兩項優化
當集羣中保存hash或者set時,就會造成節點數據不均勻
優化:定期檢查配置一致性

5.請求傾斜:熱點key

重要的key或者bigkey
Redis Cluster某個節點有一個非常重要的key,就會存在熱點問題

集羣傾斜優化:

避免bigkey
避免hot key

hot key出現造成集羣訪問量傾斜

Hot key,即熱點 key,指的是在一段時間內,該 key 的訪問量遠遠高於其他的 redis key, 導致大部分的訪問流量在經過 proxy 分片之後,都集中訪問到某一個 redis 實例上。hot key 通常在不同業務中,存儲着不同的熱點信息。比如

  1. 新聞應用中的熱點新聞內容;
  2. 活動系統中某個用戶瘋狂參與的活動的活動配置;
  3. 商城秒殺系統中,最吸引用戶眼球,性價比最高的商品信息;

解決方案一:使用本地緩存

在 client 端使用本地緩存,從而降低了redis集羣對hot key的訪問量,但是同時帶來兩個問題:

1、如果對可能成爲 hot key 的 key 都進行本地緩存,那麼本地緩存是否會過大,從而影響應用程序本身所需的緩存開銷。

2、如何保證本地緩存和redis集羣數據的有效期的一致性。

解決方案二: 利用分片算法的特性,對key進行打散處理

我們知道 hot key 之所以是 hot key,是因爲它只有一個key,落地到一個實例上。

所以我們可以給hot key加上前綴或者後綴,把一個hotkey 的數量變成 redis 實例個數N的倍數M,從而由訪問一個 redis key 變成訪問 N * M 個redis key。
N*M 個 redis key 經過分片分佈到不同的實例上,將訪問量均攤到所有實例。

big key 造成集羣數據量傾斜

big key ,即數據量大的 key ,由於其數據大小遠大於其他key,導致經過分片之後,某個具體存儲這個 big key 的實例內存使用量遠大於其他實例,造成,內存不足,拖累整個集羣的使用。

big key 在不同業務上,通常體現爲不同的數據,比如:

  1. 論壇中的大型持久蓋樓活動;
  2. 聊天室系統中熱門聊天室的消息列表;

解決方案:對 big key 進行拆分

對 big key 存儲的數據 (big value)進行拆分,變成value1,value2… valueN,

大廠使用什麼樣的redis集羣:

redis 集羣方案主要有3類

第一是使用類 codis 的代理模式架構,按組劃分,實例之間互相獨立;

第二是基於官方的 redis cluster 的服務端分片方案;

第三是:代理模式和服務端分片相結合的模式

  • 基於官方 redis cluster 的服務端分片方案
  • 類 codis 的代理模式架構
  • 代理模式和服務端分片相結合的模式

類 codis 的代理模式架構

img

這套架構的特點:

  • 分片算法:基於 slot hash桶;
  • 分片實例之間相互獨立,每組 一個master 實例和多個slave;
  • 路由信息存放到第三方存儲組件,如 zookeeper 或etcd
  • 旁路組件探活

使用這套方案的公司:
阿里雲: ApsaraCache, RedisLabs、京東、百度等

阿里雲

AparaCache 的單機版已開源(開源版本中不包含slot等實現),集羣方案細節未知;ApsaraCache

百度 BDRP 2.0

主要組件:
proxy,基於twemproxy 改造,實現了動態路由表;
redis內核: 基於2.x 實現的slots 方案;
metaserver:基於redis實現,包含的功能:拓撲信息的存儲 & 探活;
最多支持1000個節點;

slot 方案:
redis 內核中對db劃分,做了16384個db; 每個請求到來,首先做db選擇;

數據遷移實現:
數據遷移的時候,最小遷移單位是slot,遷移中整個slot 處於阻塞狀態,只支持讀請求,不支持寫請求;
對比 官方 redis cluster/ codis 的按key粒度進行遷移的方案:按key遷移對用戶請求更爲友好,但遷移速度較慢;這個按slot進行遷移的方案速度更快;

京東proxy

主要組件:
proxy: 自主實現,基於 golang 開發;
redis內核:基於 redis 2.8
configServer(cfs)組件:配置信息存放;
scala組件:用於觸發部署、新建、擴容等請求;
mysql:最終所有的元信息及配置的存儲;
sentinal(golang實現):哨兵,用於監控proxy和redis實例,redis實例失敗後觸發切換;

slot 方案實現:
在內存中維護了slots的map映射表;

數據遷移:
基於 slots 粒度進行遷移;
scala組件向dst實例發送命令告知會接受某個slot;
dst 向 src 發送命令請求遷移,src開啓一個線程來做數據的dump,將這個slot的數據整塊dump發送到dst(未加鎖,只讀操作)
寫請求會開闢一塊緩衝區,所有的寫請求除了寫原有數據區域,同時雙寫到緩衝區中。
當一個slot遷移完成後,把這個緩衝區的數據都傳到dst,當緩衝區爲空時,更改本分片slot規則,不再擁有該slot,後續再請求這個slot的key返回moved;
上層proxy會保存兩份路由表,當該slot 請求目標實例得到 move 結果後,更新拓撲;

跨機房:跨機房使用主從部署結構;沒有多活,異地機房作爲slave;

基於官方 redis cluster 的服務端分片方案

img

和上一套方案比,所有功能都集成在 redis cluster 中,路由分片、拓撲信息的存儲、探活都在redis cluster中實現;各實例間通過 gossip 通信;這樣的好處是簡單,依賴的組件少,應對200個節點以內的場景沒有問題(按單實例8w read qps來計算,能夠支持 200 * 8 = 1600w 的讀多寫少的場景);但當需要支持更大的規模時,由於使用 gossip協議導致協議之間的通信消耗太大,redis cluster 不再合適;

使用這套方案的有:AWS, 百度貼吧

官方 redis cluster

數據遷移過程:
基於 key粒度的數據遷移;
遷移過程的讀寫衝突處理:
從A 遷移到 B;

  • 訪問的 key 所屬slot 不在節點 A 上時,返回 MOVED 轉向,client 再次請求B;
  • 訪問的 key 所屬 slot 在節點 A 上,但 key 不在 A上, 返回 ASK 轉向,client再次請求B;
  • 訪問的 key 所屬slot 在A上,且key在 A上,直接處理;(同步遷移場景:該 key正在遷移,則阻塞)

AWS ElasticCache

ElasticCache 支持主從和集羣版、支持讀寫分離;
集羣版用的是開源的Redis Cluster,未做深度定製;

代理模式和服務端分片相結合的模式

p2p和代理的混合模式: 基於redis cluster + twemproxy混合模式

百度貼吧的ksarch-saas:

基於redis cluster + twemproxy 實現;後被 BDRP 吞併;
twemproxy 實現了 smart client 功能;

使用 redis cluster後還加一層 proxy的好處:

  1. 對client友好,不需要client都升級爲smart client;(否則,所有語言client 都需要支持一遍)
  2. 加一層proxy可以做更多平臺策略;比如在proxy可做 大key、熱key的監控、慢查詢的請求監控、以及接入控制、請求過濾等;

即將發佈的 redis 5.0 中有個 feature,作者計劃給 redis cluster加一個proxy。

ksarch-saas 對 twemproxy的改造已開源:
https://github.com/ksarch-saas/r3proxy

總之,大廠使用代理分片的方案,還是更加廣泛一些。雖然,代理分片,中間增加一層Proxy進行轉發,必然會有一定的性能損耗(理論值20ms),但是那也是非常有限。

知乎爲什麼沒有使用官方 Redis 集羣方案

在 2015 年調研過多種集羣方案,綜合評估多種方案後,最終選擇了看起來較爲陳舊的 Twemproxy 而不是官方 Redis 集羣方案與 Codis,具體原因如下:

1)MIGRATE 造成的阻塞問題:

Redis 官方集羣方案使用 CRC16 算法計算哈希值並將 Key 分散到 16384 個 Slot 中,由使用方自行分配 Slot 對應到每個分片中,擴容時由使用方自行選擇 Slot 並對其進行遍歷,對 Slot 中每一個 Key 執行 MIGRATE 命令進行遷移。

調研後發現,MIGRATE 命令實現分爲三個階段:

a)DUMP 階段:由源實例遍歷對應 Key 的內存空間,將 Key 對應的 Redis Object 序列化,序列化協議跟 Redis RDB 過程一致;

b)RESTORE 階段:由源實例建立 TCP 連接到對端實例,並將 DUMP 出來的內容使用 RESTORE 命令到對端進行重建,新版本的 Redis 會緩存對端實例的連接;

c)DEL 階段(可選):如果發生遷移失敗,可能會造成同名的 Key 同時存在於兩個節點,此時 MIGRATE 的 REPLACE 參數決定是是否覆蓋對端的同名 Key,如果覆蓋,對端的 Key 會進行一次刪除操作,4.0 版本之後刪除可以異步進行,不會阻塞主進程。

經過調研,認爲這種模式MIGRATE 並不適合知乎的生產環境。

Redis 爲了保證遷移的一致性, MIGRATE 所有操作都是同步操作,執行 MIGRATE 時,兩端的 Redis 均會進入時長不等的 BLOCK 狀態。對於小 Key,該時間可以忽略不計,但如果一旦 Key 的內存使用過大,一個 MIGRATE 命令輕則導致尖刺,重則直接觸發集羣內的 Failover,造成不必要的切換

同時,遷移過程中訪問到處於遷移中間狀態的 Slot 的 Key 時,根據進度可能會產生 ASK 轉向,此時需要客戶端發送 ASKING 命令到 Slot 所在的另一個分片重新請求,請求時延則會變爲原來的兩倍。

同樣,方案調研期間的 Codis 採用的是相同的 MIGRATE 方案,但是使用 Proxy 控制 Redis 進行遷移操作而非第三方腳本(如 redis-trib.rb),基於同步的類似 MIGRATE 的命令,實際跟 Redis 官方集羣方案存在同樣的問題。

2)緩存模式下高可用方案不夠靈活:

還有,官方集羣方案的高可用策略僅有主從一種,高可用級別跟 Slave 的數量成正相關,如果只有一個 Slave,則只能允許一臺物理機器宕機, Redis 4.2 roadmap 提到了 cache-only mode,提供類似於 Twemproxy 的自動剔除後重分片策略,但是截至目前仍未實現。

3)內置 Sentinel 造成額外流量負載:

另外,官方 Redis 集羣方案將 Sentinel 功能內置到 Redis 內,這導致在節點數較多(大於 100)時在 Gossip 階段會產生大量的 PING/INFO/CLUSTER INFO 流量,根據 issue 中提到的情況,200 個使用 3.2.8 版本節點搭建的 Redis 集羣,在沒有任何客戶端請求的情況下,每個節點仍然會產生 40Mb/s 的流量,雖然到後期 Redis 官方嘗試對其進行壓縮修復,但按照 Redis 集羣機制,節點較多的情況下無論如何都會產生這部分流量,對於使用大內存機器但是使用千兆網卡的用戶這是一個值得注意的地方。

4)slot 存儲開銷:

最後,每個 Key 對應的 Slot 的存儲開銷,在規模較大的時候會佔用較多內存,4.x 版本以前甚至會達到實際使用內存的數倍,雖然 4.x 版本使用 rax 結構進行存儲,但是仍然佔據了大量內存,從非官方集羣方案遷移到官方集羣方案時,需要注意這部分多出來的內存。

總之,官方 Redis 集羣方案與 Codis 方案對於絕大多數場景來說都是非常優秀的解決方案,但是仔細調研發現並不是很適合集羣數量較多且使用方式多樣化的知乎,

總之,場景不同側重點也會不一樣,方案也需要調整,沒有最有,只有最適合。

中小廠使用什麼樣的redis集羣:

既然大廠傾向於選擇代理分片模式的集羣如Codis,那麼中小廠子該如何選擇呢?

Codis與Redis Cluster集羣方案對比

Codis Redis Cluster
數據庫數量 16 1
客戶端支持 All Smart Client
Redis版本 3.2.8分支開發 5.0.3
不支持的命令 KEYS等 SELECT、跨節點multi-key命令
Dashboard
可視化客戶端
集羣結構 代理 類中心化架構 集羣管理層與存儲層解耦 P2P模型 Gossip協議 去中心化
哈希槽 1024 16384
pipeline 支持 不支持
重新分片時multi-key操作 支持 不支持
主從複製 不負責 負責
可靠 經過線上服務驗證,可靠性較高 新推出,坑會比較多,遇到bug之後需要等官網升級
升級 後續升級無法保證 Redis官方推出,後續升級可保證
部署 較複雜 簡單

通過以上表格對比,發現Codis和Redis Cluster各有特點,可以根據項目實際需要進行選擇。

選擇Redis Cluster的場景:

  1. 需要redis的新特性,例如:Stream
  2. 需要更豐富的命令支持
  3. 資源緊張

選擇Codis的場景:

  1. Codis支持的命令可滿足需求
  2. 資源充裕
  3. 強調可靠性

Redis Cluster沒有采用中心化模式的Proxy方案,而是把請求轉發邏輯一部分放在客戶端,一部分放在了服務端,它們之間互相配合完成請求的處理。

Redis Cluster是在Redis 3.0推出的,但隨着Redis的版本迭代,Redis官方的Cluster也越來越穩定,更多人開始採用官方的集羣化方案。

Redis Cluster沒有了中間的Proxy代理層,那麼是如何進行請求的轉發呢?

Smart Client客戶端路由轉發

Redis把請求轉發的邏輯放在了Smart Client中,要想使用Redis Cluster,必須升級Client SDK,這個SDK中內置了請求轉發的邏輯,所以業務開發人員同樣不需要自己編寫轉發規則,Redis Cluster採用16384個槽位進行路由規則的轉發。

總之,對於中小項目來說,選擇 Redis Cluster 會更加合理。

對於大型集羣來說, 由於200 個使用 3.2.8 版本節點搭建的 Redis 集羣,在沒有任何客戶端請求的情況下,每個節點仍然會產生 40Mb/s 的流量, 所以不建議使用官方的 Redis Cluster ,建議採用 codis、twenproxy 等代理方案。

高可用Redis集羣的架構

集羣的性能和數據量參考指標

單節點redis推薦的容量 10-20G

單節點redis推薦的併發量 4-5WQPS

選型:哨兵模式

如果系統的緩存大小<10G

建議使用一主多從的哨兵模式。 從節點的數量,根據qps來擴展,比如10WQPS,可以有3-4個從節點。

選型: Redis Cluster模式

如果系統的緩存大小<2000G, 主節點數<200個,建議使用Redis Cluster模式

選型:proxy模式

對於大型集羣來說, 由於200 個使用 3.2.8 版本節點搭建的 Redis 集羣,在沒有任何客戶端請求的情況下,每個節點仍然會產生 40Mb/s 的流量, 所以不建議使用官方的 Redis Cluster ,建議採用 codis、twenproxy 等代理方案。

集羣緩存擊穿解決方案:

緩存擊穿是指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的併發請求過來,從而大量的請求打到db。

描述:某一個熱點 key,在緩存過期的一瞬間,同時有大量的請求打進來,由於此時緩存過期了,所以請求最終都會走到數據庫,造成瞬時數據庫請求量大、壓力驟增,甚至可能打垮數據庫。

  1. 設置熱點數據永遠不過期。
  2. 採用多級緩存架構,熱點數據,肯定數據量不大,可以使用 本地緩存
  3. 如果過期則或者在快過期之前更新,如有變化,主動刷新緩存數據,同時也能保障數據一致性

緩存穿透解決方案:

什麼是穿透?

緩存穿透是指查詢一個一定不存在的數據,由於緩存是不命中時需要從數據庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,進而給數據庫帶來壓力。

要點:訪問一個緩存和數據庫都不存在的 key,此時會直接打到數據庫上,並且查不到數據,沒法寫緩存,所以下一次同樣會打到數據庫上。

此時,緩存起不到作用,請求每次都會走到數據庫,流量大時數據庫可能會被打掛。此時緩存就好像被“穿透”了一樣,起不到任何作用。

解決方案

1、接口校驗。在正常業務流程中可能會存在少量訪問不存在 key 的情況,但是一般不會出現大量的情況,所以這種場景最大的可能性是遭受了非法攻擊。可以在最外層先做一層校驗:用戶鑑權、數據合法性校驗等,例如商品查詢中,商品的ID是正整數,則可以直接對非正整數直接過濾等等。

2、緩存空值。當訪問緩存和DB都沒有查詢到值時,可以將空值寫進緩存,但是設置較短的過期時間,該時間需要根據產品業務特性來設置。

3、hashmap 記錄存在性,存在去查redis,不存在直接返回。

4、布隆過濾器。使用布隆過濾器存儲所有可能訪問的 key,不存在的 key 直接被過濾,存在的 key 則再進一步查詢緩存和數據庫。

布隆過濾器由一個 bitSet 和 一組 Hash 函數(算法)組成,是一種空間效率極高的概率型算法和數據結構,主要用來判斷一個元素是否在集合中存在。布隆過濾器佔用多少空間,主要取決於 Hash 函數的個數,跟 key 本身的大小無關,這使得其在空間的優勢非常大,但是存在一定的誤判率。

緩存雪崩保障方案

什麼是雪崩?

緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至down機。

描述:大量的熱點 key 設置了相同的過期時間,導在緩存在同一時刻全部失效,造成瞬時數據庫請求量大、壓力驟增,引起雪崩,甚至導致數據庫被打掛。緩存雪崩其實有點像“升級版的緩存擊穿”,緩存擊穿是一個熱點 key,緩存雪崩是一組熱點 key。

1、過期時間打散。既然是大量緩存集中失效,那最容易想到就是讓他們不集中生效。可以給緩存的過期時間時加上一個隨機值時間,使得每個 key 的過期時間分佈開來,不會集中在同一時刻失效。

在做電商項目的時候,一般是採取不同分類商品,緩存不同週期。在同一分類中的商品,加上一個隨機因子。這樣能儘可能分散緩存過期時間,而且,熱門類目的商品緩存時間長一些,冷門類目的商品緩存時間短一些,也能節省緩存服務的資源。

2、熱點數據不過期。該方式和緩存擊穿一樣,也是要着重考慮刷新的時間間隔和數據異常如何處理的情況。

集羣與數據庫的數據一致性保障方案:

  • 方案1:biglog同步保障數據一致性
  • 方案2:使用程序方式發送更新消息,保障數據一致性

方案1:biglog同步保障數據一致性的架構:

方案1,可以通過biglog同步,來保障二級緩存的數據一致性,具體的架構如下

在這裏插入圖片描述

利用 rocketMQ是支持廣播消費的,增加消費端即可。

所以,必須設置 rocketMQ 客戶端的消費模式,爲 廣播模式;

@RocketMQMessageListener(topic = "seckillgood", consumerGroup = "UpdateGuava", messageModel = MessageModel.BROADCASTING)

增加一個更新redis緩存的實力,完成redis的更新。

對於更新Guava或者其他1級緩存來說,增加一個實例消費消息,就可以了。

方案2:使用程序方式保障數據一致性的架構

使用程序方式保障數據一致性的架構,可以編寫一個通用的2級緩存通用組件,當數據更新的時候,去發送消息,具體的架構如下:

在這裏插入圖片描述

方案2和方案1 的區別

方案2和方案1 的整體區別不大,只不過 方案2 需要自己寫代碼(或者中間組件)發送數據的變化通知。 並且可以進行延遲雙刪的操作,首先刪除一次,再發送到延遲隊列,再刪一次緩存。

方案1 的一個優勢:可以和 建立索引等其他的消費者,共用binlog的消息隊列。

其他的區別,大家可以自行探索。

Redis持久化導致的高可用問題分析及解決

Redis的持久化配置

redis的 rdb 和 aof 持久化的區別

aof,rdb是兩種 redis持久化的機制。用於crash後,redis的恢復。

redis將數據保存在內存中,一旦Redis服務器被關閉,或者運行Redis服務的主機本身被關閉的話,儲存在內存裏面的數據就會丟失

如果僅僅將redis用作緩存的話,那麼這種數據丟失帶來的問題並不是非常大,只需要重啓機器,然後再次將數據同步到緩存中就可以了

但如果將redis用作DB的話,那麼因爲一些原因導致數據丟失的情況就不能接受

Redis的持久化就是將儲存在內存裏面的數據以文件形式保存硬盤裏面,這樣即使Redis服務端被關閉,已經同步到硬盤裏面的數據也不會丟失

除此之外,持久化也可以使Redis服務器重啓時,通過載入同步的持久文件來還原之前的數據,或者使用持久化文件來進行數據備份和數據遷移等工作

RDB持久化功能

RDB持久化功能可以將Redis中所有數據生成快照並以二進行文件的形式保存到硬盤裏,文件名爲.RDB文件

在Redis啓動時載入RDB文件,Redis讀取RDB文件內容,還原服務器原有的數據庫數據

過程如下圖所示:

5bc3321a00012c2508280432.jpg

Redis服務端創建RDB文件,有三種方式

  • 使用SAVE命令手動同步創建RDB文件

  • 使用BGSAVE命令異步創建RDB文件

  • 自動創建RDB文件

使用SAVE命令手動同步創建RDB文件

客戶端向Redis服務端發送SAVE命令,服務端把當前所有的數據同步保存爲一個RDB文件

使用BGSAVE命令異步創建RDB文件

執行BGSAVE命令也會創建一個新的RDB文件

BGSAVE不會造成redis服務器阻塞:在執行BGSAVE命令的過程中,Redis服務端仍然可以正常的處理其他的命令請求

BGSAVE命令執行步驟:

自動創建RDB文件

打開Redis的配置文件/etc/redis.conf

save 900 1save 300 10save 60 10000

自動持久化配置解釋:

  • save 900 1表示:如果距離上一次創建RDB文件已經過去的900秒時間內,Redis中的數據發生了1次改動,則自動執行BGSAVE命令
  • save 300 10表示:如果距離上一次創建RDB文件已經過去的300秒時間內,Redis中的數據發生了10次改動,則自動執行BGSAVE命令
  • save 60 10000表示:如果距離上一次創建RDB文件已經過去了60秒時間內,Redis中的數據發生了10000次改動,則自動執行BGSAVE命令
    當三個條件中的任意一個條件被滿足時,Redis就會自動執行BGSAVE命令

rdb持久化的特性如下:

fork一個進程,遍歷hash table,利用copy on write,把整個db dump保存下來。
save, shutdown, slave 命令會觸發這個操作。
粒度比較大,如果save, shutdown, slave 之前crash了,則中間的操作沒辦法恢復。

AOF的功能

AOF持久化保存數據庫的方法是:每當有修改的數據庫的命令被執行時,服務器就會將執行的命令寫入到AOF文件的末尾。

因爲AOF文件裏面儲存了服務器執行過的所有數據庫修改的命令,所以Redis只要重新執行一遍AOF文件裏面保存的命令,就可以達到還原數據庫的目的

AOF安全性問題

雖然服務器執行一次修改數據庫的命令,執行的命令就會被寫入到AOF文件,但這並不意味着AOF持久化方式不會丟失任何數據

在linux系統中,系統調用write函數,將一些數據保存到某文件時,爲了提高效率,系統通常不會直接將內容寫入硬盤裏面,而是先把數據保存到硬盤的緩衝區之中。

等到緩衝區被填滿,或者用戶執行fsync調用和fdatasync調用時,操作系統纔會將儲存在緩衝區裏的內容真正的寫入到硬盤裏

對於AOF持久化來說,當一條命令真正的被寫入到硬盤時,這條命令纔不會因爲停機而意外丟失

因此,AOF持久化在遭遇停機時丟失命令的數量,取決於命令被寫入硬盤的時間

越早將命令寫入到硬盤,發生意外停機時丟失的數據就越少,而越遲將命令寫入硬盤,發生意外停機時丟失的數據就越多

AOF三種策略

爲了控制Redis服務器在遇到意外停機時丟失的數據量,Redis爲AOF持久化提供了appendfsync選項,這個選項的值可以是always,everysec或者no

  • appendfsync always:
    總是寫入aof文件,並通過事件循環磁盤同步,即使Redis遭遇意外停機時,最多隻丟失一事件循環內的執行的數據
  • appendfsync everysec:
    每一秒寫入aof文件,並完成磁盤同步,即使Redis遭遇意外停機時,最多隻丟失一秒鐘內的執行的數據
  • appendfsync no:
    服務器不主動調用fdatasync,由操作系統決定任何將緩衝區裏面的命令寫入到硬盤裏,這種模式下,服務器遭遇意外停機時,丟失的命令的數量是不確定的
AOF三種方式比較

運行速度:

  • always的速度慢,everysec和no都很快, always丟失的數據最少,但是硬盤IO開銷很多,一般的SATA硬盤一秒種只能寫入幾百次數據
  • everysec每秒同步一次數據,如果Redis發生故障,可能會丟失1秒鐘的數據
  • no則系統控制,不可控,不知道會丟失多少數據

可見,從持久化角度講,always是最安全的。

從效率上講,no是最快的。而redis默認設置進行了折中,選擇了everysec。合情合理。

配置文件中AOF相關選項
appendonly   yes                     # 改爲yes,開啓AOF功能
appendfilename  "appendonly.aof"    # 生成的AOF的文件名
appendfsync everysec                # AOF同步的策略
no-appendfsync-on-rewrite  yes      # AOF重寫時,是否做append的操作,yes是不做,在`rewrite`期間的`AOF`有丟失的風險。

配置文件中AOF相關選項
  • 建議把appendfsync選項設定爲everysec,進行持久化,這種情況下Redis宕機最多隻會丟失一秒鐘的數據

  • 如果使用Redis做爲緩存時,即使數據丟失也不會造成任何影響,只需要在下次加載時重新從數據源加載就可以了

  • 不要佔用100%的內存。一般分配服務器60%到70%的內存給Redis使用,剩餘的內存分留給類似fork的操作

aof與rdb持久化的區別:

把寫操作指令,持續的寫到一個類似日誌文件裏。(類似於從postgresql等數據庫導出sql一樣,只記錄寫操作)
粒度較小,crash之後,只有crash之前沒有來得及做日誌的操作沒辦法恢復。

兩種區別就是,

  • 一個是持續的用日誌記錄寫操作,crash後利用日誌恢復;

  • 一個是平時寫操作的時候不觸發寫,只有手動提交save命令,或者是關閉命令時,才觸發備份操作。

選擇的標準,就是看系統是願意犧牲一些性能,換取更高的緩存一致性(aof),還是願意寫操作頻繁的時候,不啓用備份來換取更高的性能,待手動運行save的時候,再做備份(rdb)。

rdb這個就更有些 eventually consistent的意思了。

AOF重寫出現Redis主進程阻塞,應用端響應超時的問題

問題背景

某個業務線使用Redis集羣保存用戶session數據,數據量大約在4千萬-5千萬,每天發生3-4次AOF重寫,每次時間持續30-40秒,AOF重寫期間出現Redis主進程阻塞,應用端響應超時的問題。

環境:Redis 2.8,一主一從。

什麼是AOF重寫

AOF重寫是AOF持久化的一個機制,用來壓縮AOF文件。

隨着服務器的不斷運行,爲了記錄Redis中數據的變化,Redis會將越來越多的命令寫入到AOF文件中,使得AOF文件的體積來斷增大

爲了讓AOF文件的大小控制在合理的範圍,redis提供了AOF重寫功能,通過這個功能,服務器可以產生一個新的AOF文件:

  • 新的AOF文件記錄的數據庫數據和原有AOF文件記錄的數據庫數據完全一樣
  • 新的AOF文件會使用盡可能少的命令來記錄數據庫數據,因此新的AOF文件的體積通常會比原有AOF文件的體積要小得多
  • AOF重寫期間,服務器不會被阻塞,可以正常處理客戶端發送的命令請求

AOF重寫功能就是把Redis中過期的,不再使用的,重複的以及一些可以優化的命令進行優化,重新生成一個新的AOF文件,從而達到減少硬盤佔用量和加速Redis恢復速度的目的

在這裏插入圖片描述

AOF重寫的目的

Redis 的rewrite策略,實現AOF文件的減肥,但是結果是冪等的

AOF重寫的流程

Redis通過fork一個子進程,重新寫一個新的AOF文件,該次重寫不是讀取舊的AOF文件進行復制,而是讀取內存中的Redis數據庫,重寫一份AOF文件,有點類似於RDB的快照方式。

在子進程進行AOF重寫期間,Redis主進程執行的命令會被保存在AOF重寫緩衝區裏面,這個緩衝區在服務器創建子進程之後開始使用,當Redis執行完一個寫命令之後,它會同時將這個寫命令發送給 AOF緩衝區和AOF重寫緩衝區。如下圖:

在這裏插入圖片描述

具體的步驟如下:

1.無論是執行bgrewriteaof命令手動開啓重寫,還是自動進行AOF重寫,實際上都是執行BGREWRITEAOF命令
2.執行bgrewriteaof命令,Redis會fork一個子進程,
3.子進程對內存中的Redis數據進行回溯,生成新的AOF文件
4.Redis主進程會處理正常的命令操作
5.同時Redis把會新的命令寫入到aof_rewrite_buf當中,當bgrewriteaof命令執行完成,新的AOF文件生成完畢,Redis主進程會把aof_rewrite_buf中的命令追加到新的AOF文件中
6.用新生成的AOF文件替換舊的AOF文件

在這裏插入圖片描述

AOF重寫導致主進程阻塞原因分析

當AOF重寫子進程完成AOF重寫工作之後,它會向父進程發送一個信號,父進程在接收到該信號之後,會調用一個信號處理函數,並執行以下工作:

  • 將AOF重寫緩衝區中的所有內容寫入到新的AOF文件中,保證新 AOF文件保存的數據庫狀態和服務器當前狀態一致。
  • 對新的AOF文件進行改名,原子地覆蓋現有AOF文件,完成新舊文件的替換
  • 繼續處理客戶端請求命令。

現在問題出現了,同時在執行bgrewriteaof操作和主進程寫aof文件的操作,兩者都會操作磁盤,

特別需要注意的是:

bgrewriteaof往往會涉及大量磁盤操作,這樣就會造成主進程在寫aof文件的時候,出現阻塞的情形,導致主進程阻塞。

根因分析與解決方案

這是當時的Redis配置:

127.0.0.1:6379> config get *append*
1) "no-appendfsync-on-rewrite"
2) "no"
3) "appendonly"
4) "yes"
5) "appendfsync"
6) "everysec"

從配置看,原因理論上就很清楚了:

  • 我們的這個Redis實例使用AOF進行持久化(appendonly)
  • appendfsync策略採用的是everysec刷盤。

但是AOF隨着時間推移,文件會越來越大,因此,Redis自動啓動一個rewrite策略,實現AOF文件的減肥,但是結果是冪等的

  • no-appendfsync-on-rewrite的策略是 no,這就會導致在進行rewrite操作時,appendfsync會寫入aof文件而可能被阻塞。

這不是什麼新問題,很多開啓AOF的業務場景都會遇到這個問題。

解決的辦法有這麼幾個:

  • 將no-appendfsync-on-rewrite設置爲yes.

yes表示在日誌AOF重寫時,不進行aof文件命令追加操作,而只是將命令放在重寫緩衝區裏,避免與命令的追加造成磁盤IO造成的阻塞。但是在rewrite期間的AOF有丟失的風險。

  • 給當前Redis實例添加slave節點,當前節點設置爲master, 然後master節點關閉AOF,slave節點開啓AOF。

這樣的方式的風險是如果master掛掉,尚沒有同步到slave的數據會丟失。

比較折中的方式:

  • 在master節點設置將no-appendfsync-on-rewrite設置爲yes,注意,還有後手,就是停止自動aof重寫,如何停止,將auto-aof-rewrite-percentage參數設置爲0,關閉主動重寫

    auto-aof-rewrite-percentage 參數說明

    aof文件增長比例,指當前aof文件比上次重寫的增長比例大小。aof重寫即在aof文件在一定大小之後,重新將整個內存寫到aof文件當中,以反映最新的狀態(相當於bgsave)。這樣就避免了,aof文件過大而實際內存數據小的問題(頻繁修改數據問題).

  • 爲了防止AOF文件越來越大,在任務調度配置在凌晨低峯期定時手動執行bgrewriteaof命令完成每日一次的AOF重寫

  • 在重寫時爲了避免硬盤空間不足或者IO使用率高影響重寫功能添加了硬盤空間報警和IO使用率報警保障重寫的正常進行

why:Redis不能保證100%數據不丟失

Redis能否保證100%數據不丟失,答案是no。

哪怕是在要求最高的持久化配置場景,將appendfsync值設置爲always,其實也會產生數據丟失。

儘管,很多博客都講,將appendfsync值設置爲always,Redis能保證100%數據不丟失,可能會打臉了。

圖解:redis的事件循環

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

在這裏插入圖片描述

flushAppendOnlyFile 的時機分析

一個while循環,我們把這個循環叫做事件循環, 從寫盤的角度來說:

  • 第N+1輪循環的第一階段,調用flushAppendOnlyFile 的,會將aof buffer寫到磁盤上。

  • 第N輪循環的第二階段,將讀取到的命令,寫入aof buffer,而不是直接落盤

所以:

redis即使在配製appendfsync=always的策略下,還是會可能丟失一個事件循環的aof_buf數據,

異步複製導致的數據丟失

在這裏插入圖片描述

因爲master->slave的數據同步是異步的,所以可能存在部分數據還沒有同步到slave,master就宕機了,此時這部分數據就丟失了。

(2)腦裂導致的數據丟失

img

當master所在的機器突然脫離的正常的網絡,與其他slave、sentinel失去了連接,但是master還在運行着。

此時sentinel就會認爲master宕機了,會開始選舉把slave提升爲新的master,這個時候集羣中就會出現兩個master,也就是所謂的腦裂。

此時雖然產生了新的master節點,但是客戶端可能還沒來得及切換到新的master,會繼續向舊的master寫入數據。

當網絡恢復正常時,舊的master會變成新的master的從節點,自己的數據會清空,重新從新的master上覆制數據。

解決方案

Redis提供了這兩個配置用來降低數據丟失的可能性

min-slaves-to-write 1 
min-slaves-max-lag 10

上面兩行配置的意思是,要求至少有1個slave,數據複製和同步的延遲不能超過10秒,如果不符合這個條件,那麼master將不會接收任何請求。

(1)減少異步複製的數據丟失

有了min-slaves-max-lag這個配置,就可以確保,一旦slave複製數據和ack延時太長,就認爲master宕機後損失的數據太多了,那麼就拒絕寫請求,這樣可以把master宕機時由於部分數據未同步到slave導致的數據丟失降低到可控範圍內。

(2)減少腦裂的數據丟失

如果一個master出現了腦裂,跟其他slave丟了連接,那麼上面兩個配置可以確保,如果不能繼續給指定數量的slave發送數據,而且slave超過10秒沒有給自己ack消息,那麼就直接拒絕客戶端的寫請求

這樣腦裂後的舊master就不會接受client的新數據,也就避免了數據丟失。

Redis並不能保證數據的強一致性,看官方文檔的說明

img

參考文獻

https://www.cnblogs.com/zjxiang/p/12484474.html

https://www.cnblogs.com/zjxiang/p/12484474.html

https://blog.csdn.net/crazymakercircle/article/details/116110302

http://www.redis-doc.com/

https://blog.csdn.net/crazymakercircle/article/details/116110302

https://www.cnblogs.com/kismetv/p/9236731.html#t31

https://blog.csdn.net/qq_35044419/article/details/117817563

https://blog.csdn.net/javarrr/article/details/92830952

https://www.cnblogs.com/ExMan/p/14447298.html

https://blog.csdn.net/tr1912/article/details/81265007

http://www.redis.cn/topics/cluster-tutorial.html

http://www.redis.cn/topics/sentinel.html

http://www.redis.cn/topics/replication.html

https://www.cnblogs.com/mrhelloworld/p/docker14.html

http://www.redis.cn/topics/cluster-tutorial.html]

https://my.oschina.net/dabird/blog/4291090

https://my.oschina.net/dabird/blog/4291090

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章