redis cluster 集羣 實操 (史上最全,5W長文)

文章很長,建議收藏起來慢慢讀!瘋狂創客圈總目錄 語雀版 | 總目錄 碼雲版| 總目錄 博客園版

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

入大廠 、做架構、大力提升Java 內功 必備的精彩博文 秋招漲薪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模式實戰(圖解+秒懂+史上最全)

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

推薦:尼恩Java面試寶典(持續更新 + 史上最全 + 面試必備)具體詳情,請點擊此鏈接

尼恩Java面試寶典,34個最新pdf,含2000多頁不斷更新、持續迭代 具體詳情,請點擊此鏈接

在這裏插入圖片描述

說明

這個是redis-cluster實操,建議大家把裏邊的 6大實操,都做一下哈, 實力猛漲

在這裏插入圖片描述

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當中

數據分片(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集羣虛擬槽分片結果:

[[email protected] redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 192.168.56.121:6001
192.168.56.121:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
192.168.56.121:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
192.168.56.121: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 192.168.56.121:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121: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

[[email protected] ~]# 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鏡像

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

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

然後導入docker,load到docker

docker load   -i  /vagrant/3G-middleware/redis-cluster.tar
docker load   -i   /vagrant/3G-middleware/redis-trib.tar

導入後看到兩個image 鏡像:

[[email protected] ~]# docker image ls

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

redis容器啓動集羣

節點規劃(三主三從)

容器名稱 容器IP地址 映射端口號
redis-master1 172.20.0.2 7001->7001
redis-master2 172.20.0.3 7002->7002
redis-master3 172.20.0.4 7003->7003
redis-slave-1 172.30.0.2 7004->7004
redis-slave-2 172.30.0.3 7005->7005
redis-slave-3 172.30.0.4 7006->7006

創建內部網絡

注意,首先創建 內部網絡

創建普通的網絡,即可

#創建網絡,指定網段

docker network create ha-network-overlay 
docker inspect ha-network-overlay   #查看網絡

如果需要指定網段,可以如下(此處忽略):

創建redis配置文件

daemonize no  
port 7001
pidfile /var/run/redis.pid 
dir "/data"
logfile "/data/redis.log" 
cluster‐enabled yes#啓動集羣模式
cluster‐config‐file nodes.conf
cluster‐node‐timeout 10000
#bind 127.0.0.1
protected‐mode no #關閉保護模式
appendonly yes #開啓aof
repl-timeout 600  #默認60
repl-ping-replica-period   100  #默認10
#如果要設置密碼需要增加如下配置: 
#requirepass 123321 #設置redis訪問密碼 
#masterauth 123321 #設置集羣節點間訪問密碼,跟上面一致
  • port:節點端口;

  • requirepass:添加訪問認證;

  • masterauth:如果主節點開啓了訪問認證,從節點訪問主節點需要認證;

  • protected-mode:保護模式,默認值 yes,即開啓。開啓保護模式以後,需配置 bind ip 或者設置訪問密碼;關閉保護模式,外部網絡可以直接訪問;

  • daemonize:是否以守護線程的方式啓動(後臺啓動),默認 no;

    當redis.conf配置文件中daemonize參數設置的yes,這使得redis是以後臺啓動的方式運行的,

    由於docker容器在啓動時,需要任務在前臺運行,否則會啓動後立即退出,

    因此導致redis容器啓動後立即退出問題。

    所以redis.conf中daemonize必須是no

  • appendonly:是否開啓 AOF 持久化模式,默認 no;

  • logfile "/data/redis.log"

    指定日誌文件路徑,默認值爲 logfile ’ ', 默認爲控制檯打印,並沒有日誌文件生成

  • bind 127.0.0.1(bind綁定的是自己機器網卡的ip,如果有多塊網卡可以配多個ip,代表允許客戶端通 過機器的哪些網卡ip去訪問,內網一般可以不配置bind,註釋掉即可)

  • cluster-enabled:是否開啓集羣模式,默認 no;

  • cluster-config-file:集羣節點信息文件;

  • cluster-node-timeout:集羣節點連接超時時間;

  • cluster-announce-ip:集羣節點 IP,填寫宿主機的 IP;

  • cluster-announce-port:集羣節點映射端口;

  • cluster-announce-bus-port:集羣節點總線端口。

    每個 Redis 集羣節點都需要打開兩個 TCP 連接。一個用於爲客戶端提供服務的正常 Redis TCP 端口,例如 6379。還有一個基於 6379 端口加 10000 的端口,比如 16379。

    第二個端口用於集羣總線,這是一個使用二進制協議的節點到節點通信通道。節點使用集羣總線進行故障檢測、配置更新、故障轉移授權等等。客戶端永遠不要嘗試與集羣總線端口通信,與正常的 Redis 命令端口通信即可,但是請確保防火牆中的這兩個端口都已經打開,否則 Redis 集羣節點將無法通信。

創建容器編排文件

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

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

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

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

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

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

 redis6:
  image: nien/redis-cluster:5.0.0
  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 down

    rm -rf  /home/docker-compose/redis-cluster
    rm -rf  /home/docker-compose/redis-cluster-ha
    mkdir -p  /home/docker-compose/redis-cluster-ha
    cp -rf /vagrant/3G-middleware/redis-cluster-ha  /home/docker-compose/
    ll /home/docker-compose/redis-cluster-ha

    cd /home/docker-compose/redis-cluster-ha

    chmod 777 -R /home/docker-compose/redis-cluster-ha/{7001,7002,7003,7004,7005,7006}/data
    chmod 777 -R /home/docker-compose/redis-cluster-ha/{7001,7002,7003,7004,7005,7006}/logs


    docker-compose up -d

    docker-compose logs

    docker-compose logs -f redis1

docker-compose logs -f redis2

 docker run --rm -it nien/redis-trib create --replicas 1 192.168.56.121:7001 192.168.56.121:7002 192.168.56.121:7003 192.168.56.121:7004 192.168.56.121:7005 192.168.56.121:7006


查看啓動的進程

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集羣

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

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

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

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

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

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

#hostip  換成 主機的ip

docker run --rm -it nien/redis-trib create --replicas 1 192.168.56.121:7001 192.168.56.121:7002 192.168.56.121:7003 192.168.56.121:7004 192.168.56.121:7005 192.168.56.121:7006

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

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

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

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

日誌如下:

[[email protected] redis-cluster]# docker run --rm -it nien/redis-trib create --replicas 1 192.168.56.121:6001 192.168.56.121:6002 192.168.56.121:6003 192.168.56.121:6004 192.168.56.121:6005 192.168.56.121:6006
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.56.121:6001
192.168.56.121:6002
192.168.56.121:6003
Adding replica 192.168.56.121:6004 to 192.168.56.121:6001
Adding replica 192.168.56.121:6005 to 192.168.56.121:6002
Adding replica 192.168.56.121:6006 to 192.168.56.121:6003
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:6001
   slots:0-5460 (5461 slots) master
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:6002
   slots:5461-10922 (5462 slots) master
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:6003
   slots:10923-16383 (5461 slots) master
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:6004
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:6005
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
S: a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121: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

docker添加 --rm 參數,意思是啓動容器,執行完成後,停止即刪除
>>> 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 192.168.56.121:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:6001
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected]
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected]
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected]
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected]
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected]
   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 run --rm -it nien/redis-trib info 192.168.56.121:6001 

docker run --rm -it nien/redis-trib info 192.168.56.121:6002 

docker run --rm -it nien/redis-trib info 192.168.56.121:6003

通過客戶端命令使用集羣

檢查集羣狀態

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

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

 

結果如下:

[[email protected] redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 192.168.56.121:6001
192.168.56.121:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
192.168.56.121:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
192.168.56.121: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 192.168.56.121:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121: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集羣中的摸個節點,用以下命令

[[email protected] redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h 192.168.56.121 -p 6001

192.168.56.121:6001>

docker exec -it redis-cluster-ha_redis2_1 bash

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

查看集羣信息和節點信息

  

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

查看集羣信息

[[email protected] redis-cluster]# docker exec -it redis-cluster-ha_redis1_1 redis-cli -c -h 192.168.56.121 -p 7001
192.168.56.121:7001> 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

查看集羣結點信息

192.168.56.121:7001> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected] slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634365163922 6 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected] master - 0 1634365162000 2 connected 5461-10922
5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected] slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634365163000 4 connected
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected] slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634365163000 5 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected] master - 0 1634365164023 3 connected 10923-16383
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:[email protected] myself,master - 0 1634365163000 1 connected 0-5460

SET/GET

  

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

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

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


[[email protected] redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h 192.168.56.121 -p 6001
192.168.56.121:6001>

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

第一個命令:set name mrhelloworld

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

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

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

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

第二個 set 命令 set aaa 111

192.168.56.121:6002>  set aaa  111
OK

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

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

第三個 set 命令 set bbb 222

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

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

第四個命令 get name

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

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

第五個命令 get aaa

192.168.56.121:6002> get aaa
"111"

第六個命令 get bbb

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

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

客戶端連接

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

  

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

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

redis cluster配置

redis cluster狀態

127.0.0.1:8001>cluster info
cluster_state:ok

如果當前redis發現有failed的slots,默認爲把自己cluster_state從ok個性爲fail, 寫入命令會失敗。如果設置cluster-require-full-coverage爲no,則無此限制。
cluster_slots_assigned:16384 #已分配的槽
cluster_slots_ok:16384 #槽的狀態是ok的數目
cluster_slots_pfail:0 #可能失效的槽的數目
cluster_slots_fail:0 #已經失效的槽的數目
cluster_known_nodes:6 #集羣中節點個數
cluster_size:3 #集羣中設置的分片個數
cluster_current_epoch:15 #集羣中的currentEpoch總是一致的,currentEpoch越高,代表節點的配置或者操作越新,集羣中最大的那個node epoch
cluster_my_epoch:12 #當前節點的config epoch,每個主節點都不同,一直遞增, 其表示某節點最後一次變成主節點或獲取新slot所有權的邏輯時間.
cluster_stats_messages_sent:270782059
cluster_stats_messages_received:270732696

cluster-enabled yes

如果配置yes則開啓集羣功能,此redis實例作爲集羣的一個節點,

否則,它是一個普通的單一的redis實例。

cluster-config-file nodes-6379.conf

雖然此配置的名字叫"集羣配置文件",但是此配置文件不能人工編輯,它是集羣節點自動維護的文件,主要用於記錄集羣中有哪些節點、他們的狀態以及一些持久化參數等,方便在重啓時恢復這些狀態。

通常是在收到請求之後這個文件就會被更新。

cluster-node-timeout 15000

這是集羣中的節點能夠失聯的最大時間,超過這個時間,該節點就會被認爲故障。

如果主節點超過這個時間還是不可達,則用它的從節點將啓動故障遷移,升級成主節點。

注意,任何一個節點在這個時間之內如果還是沒有連上大部分的主節點,則此節點將停止接收任何請求。

一般設置爲15秒即可。

cluster-node-timeout相關作用

你說了一個ping的最長不能容忍的時間的二分之一,是指超時時間爲15秒除以2=7.5秒?

也就是cluster-node-timeout=15000,ping的超時時間是7.5秒?

cluster-slave-validity-factor 10

如果設置成0,則無論從節點與主節點失聯多久,從節點都會嘗試升級成主節點。

如果設置成正數,則cluster-node-timeout乘以cluster-slave-validity-factor得到的時間,是從節點與主節點失聯後,此從節點數據有效的最長時間,超過這個時間,從節點不會啓動故障遷移。

假設cluster-node-timeout=5,cluster-slave-validity-factor=10,則如果從節點跟主節點失聯超過50秒,此從節點不能成爲主節點。

注意,如果此參數配置爲非0,將可能出現由於某主節點失聯卻沒有從節點能頂上的情況,從而導致集羣不能正常工作,在這種情況下,只有等到原來的主節點重新迴歸到集羣,集羣才恢復運作。

cluster-migration-barrier 1

主節點需要的最小從節點數,只有達到這個數,主節點失敗時,它從節點纔會進行遷移。

cluster-require-full-coverage yes

在部分key所在的節點不可用時,如果此參數設置爲"yes"(默認值), 則整個集羣停止接受操作;

如果此參數設置爲”no”,則集羣依然爲可達節點上的key提供讀操作。

replicaof <masterip> <masterport>

通過設置 master 的 ip 和 port ,可以使當前的 Redis 實例成爲另一臺 Redis 實例的副本。

在Redis啓動時,它會自動從 master 進行數據同步。

  • Redis 複製是異步的,可以通過修改 master 的配置,在 master 沒有與給定數量的 replica 連接時,主機停止接收寫入;

  • 如果複製鏈路丟失的時間相對較短,Redis replica 可以與 master 執行部分重新同步,可以使用合理的 backlog 值來進行配置(見下文);

  • 複製是自動的,不需要用戶干預。在網絡分區後,replica 會自動嘗試重新連接到 master 並與 master 重新同步;

主從複製,從 5.0.0 版本開始,Redis 正式將 SLAVEOF 命令改名成了 REPLICAOF 命令並逐漸廢棄原來的 SLAVEOF 命令

Redis使用默認的異步複製,其特點是低延遲高性能,是絕大多數 Redis 用例的自然複製模式。但是,replica 會異步地確認它從主 master 週期接收到的數據量。

主從拓撲架構

img

master 用來寫操作,replicas 用來讀取數據,適用於讀多寫少的場景。而對於寫併發量較高的場景,多個從節點會導致主節點寫命令的多次發送從而過度消耗網絡帶寬,同時也加重了 master 的負載影響服務穩定性。

img

replica 可以接受其它 replica 的連接。

除了多個 replica 可以連接到同一個 master 之外, replica 之間也可以像層疊狀的結構(cascading-like structure)連接到其他 replica 。

自 Redis 4.0 起,所有的 sub-replica 將會從 master 收到完全一樣的複製流。

當 master 需要多個 replica 時,爲了避免對 master 的性能干擾,可以採用樹狀主從結構降低主節點的壓力。

replica-read-only

可以將 replica 配置爲是否只讀,yes 代表爲只讀狀態,將會拒絕所有寫入命令;no 表示可以寫入。從 Redis 2.6 之後, replica 支持只讀模式且默認開啓。可以在運行時使用 CONFIG SET 來隨時開啓或者關閉。

對 replica 進行寫入可能有助於存儲一些臨時數據(因爲寫入 replica 的數據在與 master 重新同步後很容易被刪除),計算慢速集或排序集操作並將其存儲到本地密鑰是多次觀察到的可寫副本的一個用例。但如果客戶端由於配置錯誤而向其寫入數據,則也可能會導致問題。

級聯結構中即使 replica B 節點是可寫的,Sub-replica C 也不會看到 B 的寫入,而是將擁有和 master A 相同的數據集。

設置爲 yes 並不表示客戶端用集羣方式以 replica 爲入口連入集羣時,不可以進行 set 操作,且 set 操作的數據不會被放在 replica 的槽上,會被放到某 master 的槽上。

注意:只讀 replica 設計的目的不是爲了暴露於互聯網上不受信任的客戶端,它只是一個防止實例誤用的保護層。默認情況下,只讀副本仍會導出所有管理命令,如CONFIG、DEBUG 等。在一定程度上,可以使用rename-command來隱藏所有管理/危險命令,從而提高只讀副本的安全性

repl-diskless-sync

複製同步策略:磁盤(disk)或套接字(socket),默認爲 no 使用 disk 。

新的 replicas 和重新連接的 replicas 如果因爲接收到差異而無法繼續複製過程,則需要執行“完全同步”。RDB 文件從 master 傳送到 replicas,傳輸可以通過兩種不同的方式進行:

  1. Disk-backed:Redis master 節點創建一個新的進程並將 RDB 文件寫入磁盤,然後文件通過父進程增量傳輸給 replicas 節點;
  2. Diskless:Redis master 節點創建一個新的進程並直接將 RDB 文件寫入到 replicas 的 sockets 中,不寫到磁盤。
  • 當進行 disk-backed 複製時, RDB 文件生成完畢,多個 replicas 通過排隊來同步 RDB 文件。
  • 當進行 diskless 複製時,master 節點會等待一段時間(下邊的repl-diskless-sync-delay 配置)再傳輸以期望會有多個 replicas 連接進來,這樣 master 節點就可以同時同步到多個 replicas 節點。如果超出了等待時間,則需要排隊,等當前的 replica 處理完成之後在進行下一個 replica 的處理。

硬盤性能差,網絡性能好的情況下 diskless 效果更佳

警告:無盤複製目前處於試驗階段

repl-diskless-sync-delay

當啓用 diskless 複製後,可以通過此選項設置 master 節點創建子進程前等待的時間,即延遲啓動數據傳輸,目的可以在第一個 replica 就緒後,等待更多的 replica 就緒。單位爲秒,默認爲5秒

repl-ping-replica-period

Replica 發送 PING 到 master 的間隔,默認值爲 10 秒。

repl-timeout

默認值60秒,此選項用於設置以下情形的 timeout 判斷:

  • 從 replica 節點的角度來看的 SYNC 過程中的 I/O 傳輸 —— 沒有收到 master SYNC 傳輸的 rdb snapshot 數據;

  • 從 replica 節點的角度來看的 master 的 timeout(如 data,pings)—— replica 沒有收到master發送的數據包或者ping;

  • 從 master 節點角度來看的 replica 的 timeout(如 REPLCONF ACK pings)—— master 沒有收到 REPLCONF ACK 的確認信息;

    需要注意的是,此選項必須大於 repl-ping-replica-period,否則在 master 和 replica 之間存在低業務量的情況下會經常發生 timeout。

repl-disable-tcp-nodelay

master 和 replicas 節點的連接是否關掉 TCP_NODELAY 選項。

  • 如果選擇“yes”,Redis 將使用更少的 TCP 數據包和更少的帶寬向 replicas 發送數據。但這會增加數據在 replicas 端顯示的延遲,對於使用默認配置的 Linux 內核,延遲可達40毫秒。
  • 如果選擇“no”,則數據出現在 replicas 端的延遲將減少,但複製將使用更多帶寬。

這個實際影響的是 TCP 層的選項,裏面會用 setsockopt 設置,默認爲 no,表示 TCP 層會禁用 Nagle 算法,儘快將數據發出, 設置爲 yes 表示 TCP 層啓用 Nagle 算法,數據累積到一定程度,或者經過一定時間 TCP 層纔會將其發出。

默認情況下,我們會針對低延遲進行優化,但在流量非常高的情況下,或者當 master 和 replicas 距離多個 hops 時,將此選項改爲“yes”可能會更好。

repl-backlog-size

設置複製的 backlog 緩衝大小,默認 1mb。backlog 是一個緩衝區,當 replica 斷開一段時間連接時,它會累積 replica 數據,所以當 replica 想要再次重新連接時,一般不需要全量同步,只需要進行部分同步即可,只傳遞 replica 在斷開連接時丟失的部分數據。

更大的 backlog 緩衝大小,意味着 replicas 斷開重連後,依然可以進行續傳的時間越長(支持斷開更長時間)。

backlog 緩衝只有在至少一個 replica 節點連過來的時候 master 節點才需要創建。

repl-backlog-ttl

當 replicas 節點斷開連接後,master 節點會在一段時間後釋放 backlog 緩衝區。這個選項設置的是當最後一個 replica 斷開鏈接後,master 需要等待多少秒再釋放緩衝區。默認3600 秒,0表示永遠不釋放。

replicas 節點永遠都不會釋放這個緩衝區,因爲它有可能再次連接到 master 節點, 然後嘗試進行 “增量同步”。

replica-priority

replica-priority 是 Redis 通過 INFO 接口發佈的整數,默認值爲 100。

當 master 節點無法正常工作後 Redis Sentinel 通過這個值來決定將哪個 replica 節點提升爲 master 節點。

這個數值越小表示越優先進行提升。

如有三個 replica 節點其 priority 值分別爲 10,100,25, Sentinel 會選擇 priority 爲 10 的節點進行提升。這個值爲 0 表示 replica 節點永遠不能被提升爲 master 節點。

repl-ping-slave-period和repl-ping-replica-period

repl-ping-slave-period和repl-ping-replica-period這兩個重要參數,意思差不多,

即:SLAVE週期性的ping MASTER間隔,可直接理解成SLAVE -> MASTER間的心跳間隔(注意箭頭方向)。

實際上因爲一些非技術原因,很多軟件將slave改成了replica,Redis也同樣如此,所以replicaslave完全相同的。

常用命令變化,但5.0仍然兼容的配置項(實際上所有的slave都改成了replica,包括一些官方網站的文檔,不過代碼中的變量名保持未變,仍就爲slave):

<5.0版本 >=5.0版本
repl-ping-slave-period repl-ping-replica-period
slaveof replicaof
slave-priority replica-priority
slave-read-only replica-read-only
slave-serve-stale-data replica-serve-stale-data
cluster-slave-validity-factor cluster-replica-validity-factor

repl-timeout和repl-ping-replica-period的區別:

默認值 單位
repl-ping-replica-period 10 定義心跳(PING)間隔。
repl-timeout 60 這個參數一定不能小於repl-ping-replica-period,可以考慮爲repl-ping-replica-period的3倍或更大。定義時間內均PING不通時,判定心跳超時。對於redis集羣,達到這個值並不會發生主從切換,主從何時切換由參數cluster-node-timeout控制,只有master狀態爲fail後,它的slaves才能發起選舉。
cluster-node-timeout 15000 毫秒 集羣中的節點最大不可用時長,在這個時長內,不會被判定爲fail。對於master節點,當不可用時長超過此值時,它slave在延遲至少0.5秒後會發起選舉進行failover成爲master。Redis集羣的很多其它值與cluster-node-timeout有關。
cluster-slave-validity-factor 10 如果設置爲0,則slave總是嘗試成爲master,無論slave和master間的鏈接斷開時間的長短。如果是一個大於0的值,則最大可斷開時長爲:(cluster-slave-validity-factor * cluster-node-timeout)。例如:當cluster-node-timeout值爲5,cluster-slave-validity-factor值爲10時,slave和master間的連接斷開50秒內,slave不會嘗試成爲master。

repl-timeout和cluster-node-timeout的區別:

默認值 單位
repl-timeout 60 決定複製超時,並不能決定slave發起選舉,也不決定master何時爲fail
cluster-node-timeout 15000 毫秒 決定master何時爲fail,在fail後,slave會發起選舉

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值。

集羣維護實操

啓動兩個節點

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

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

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節點

[[email protected] redis-cluster]# cd  /home/docker-compose/redis-cluster-ext
[[email protected] 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和端口。

[[email protected] 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 192.168.56.121:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121: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 192.168.56.121 -p 6007

cluster nodes
5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected] slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634369547601 1 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:[email protected] myself,master - 0 1634369546000 0 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:[email protected] master - 0 1634369547902 1 connected 0-5460
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected] master - 0 1634369546000 3 connected 10923-16383
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected] slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634369546900 2 connected
a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected] slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634369546000 3 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected] 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和端口。

[[email protected] 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 192.168.56.121: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 192.168.56.121:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
S: a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121: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.


測試一下:

[[email protected] redis-cluster-ext]# docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 192.168.56.121 -p 6008
192.168.56.121:6008> cluster replicate 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4
OK

設置主從關係

連接6008,成爲 6007的從節點

命令的格式

cluster replicate <nodeId>    
    

具體命令如下:

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

cluster replicate 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4

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

查看一下節點信息

192.168.56.121:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected] slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634369957000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:[email protected] master - 0 1634369958089 0 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected] slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634369958000 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 192.168.56.121:[email protected] myself,slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634369958000 7 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:[email protected] master - 0 1634369958591 1 connected 0-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected] slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634369958000 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected] master - 0 1634369957000 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected] 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 192.168.56.121: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 192.168.56.121 -p 6007

cluster nodes
192.168.56.121:6007> cluster nodes
4656b8b2e26dd290928f45f9e4e001123c7ae36d 127.0.0.1:[email protected] slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634370982594 8 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected] slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634370983000 1 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:[email protected] myself,master - 0 1634370981000 8 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:[email protected] master - 0 1634370982594 1 connected 1024-5460
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected] master - 0 1634370982994 3 connected 10923-16383
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected] slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634370982594 2 connected
a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected] slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634370983997 3 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected] master - 0 1634370982594 2 connected 5461-10922

failover故障轉移

auto-failover自動故障轉移

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

容器停止

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

停止6007

docker-compose stop redis7

[[email protected] 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 192.168.56.121 -p 6008

cluster nodes
[[email protected] redis-cluster-ext]# docker exec -it redis-cluster-ext_redis8_1 redis-cli -c -h 192.168.56.121 -p 6008
192.168.56.121:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected] slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371307000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:[email protected] master,fail - 1634371240604 1634371239000 8 disconnected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected] slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371307537 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 192.168.56.121:[email protected] myself,master - 0 1634371306000 9 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:[email protected] master - 0 1634371307537 1 connected 1024-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected] slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371308000 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected] master - 0 1634371308542 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected] master - 0 1634371308542 2 connected 5461-10922

重啓6007

docker-compose up -d redis7

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

192.168.56.121:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected] slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371452000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:[email protected] slave 4656b8b2e26dd290928f45f9e4e001123c7ae36d 0 1634371452531 9 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected] slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371453535 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 192.168.56.121:[email protected] myself,master - 0 1634371453000 9 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:[email protected] master - 0 1634371452000 1 connected 1024-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected] slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371453234 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected] master - 0 1634371452000 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected] 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 192.168.56.121 -p 6007

執行cluster failover 的結果:

[[email protected] redis-cluster-ext]# docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 192.168.56.121 -p 6007
192.168.56.121:6007> cluster failover
OK
192.168.56.121:6007> cluster nodes
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:[email protected] slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371888000 2 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:[email protected] master - 0 1634371888686 2 connected 5461-10922
5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:[email protected] slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371888587 1 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:[email protected] master - 0 1634371887583 1 connected 1024-5460
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:[email protected] myself,master - 0 1634371888000 10 connected 0-1023
4656b8b2e26dd290928f45f9e4e001123c7ae36d 127.0.0.1:[email protected] slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634371887000 10 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121:[email protected] master - 0 1634371887000 3 connected 10923-16383
a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:[email protected] 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  192.168.56.121:6007 4656b8b2e26dd290928f45f9e4e001123c7ae36d

結果:

[[email protected] redis-cluster-ext]#  docker exec -it  redis-cluster-ext_redis7_1 redis-cli --cluster  del-node  192.168.56.121:6007 4656b8b2e26dd290928f45f9e4e001123c7ae36d
>>> Removing node 4656b8b2e26dd290928f45f9e4e001123c7ae36d from cluster 192.168.56.121: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集羣虛擬槽分片結果:

[[email protected] redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 192.168.56.121:6001
192.168.56.121:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
192.168.56.121:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
192.168.56.121: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 192.168.56.121:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121: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

redis cluster報文抓包

如何使用nsenter來抓包呢?

獲取容器進程id,即PID
docker ps | grep xxx 獲取容器id/name
docker inspect --format "{{.State.Pid}}" container_id/name 獲取PID

使用nsenter切換網絡命名空間
nsenter -n -t container_id/name

可在切換前後執行ifconfig來對比變化


[[email protected] redis-cluster-ha]# docker ps
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS               NAMES
a74e4037614d        nien/redis-cluster:5.0.0   "/usr/local/bin/entr…"   About an hour ago   Up About an hour                        redis-cluster-ha_redis1_1
8d0b69ec4fac        nien/redis-cluster:5.0.0   "/usr/local/bin/entr…"   About an hour ago   Up About an hour                        redis-cluster-ha_redis6_1
d0b2566f7e7d        nien/redis-cluster:5.0.0   "/usr/local/bin/entr…"   About an hour ago   Up About an hour                        redis-cluster-ha_redis4_1
78aea2e5ef3f        nien/redis-cluster:5.0.0   "/usr/local/bin/entr…"   About an hour ago   Up About an hour                        redis-cluster-ha_redis2_1
576e7039f38d        nien/redis-cluster:5.0.0   "/usr/local/bin/entr…"   About an hour ago   Up About an hour                        redis-cluster-ha_redis3_1
2c8184785b04        nien/redis-cluster:5.0.0   "/usr/local/bin/entr…"   About an hour ago   Up About an hour                        redis-cluster-ha_redis5_1



[[email protected] redis-cluster-ha]# docker inspect --format "{{.State.Pid}}"  redis-cluster-ha_redis1_1
9053


[[email protected] redis-cluster-ha]# nsenter -n -t10944

現在就進入進程的命名空間了。



ifconfig就可以看到pod的ip了,然後就可以使用tcpdump

現在可以愉快的抓包了。

[[email protected] redis-cluster-ha]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.18.0.2  netmask 255.255.0.0  broadcast 172.18.255.255
        ether 02:42:ac:12:00:02  txqueuelen 0  (Ethernet)
        RX packets 1251  bytes 1386271 (1.3 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1225  bytes 1375002 (1.3 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 0  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

現在就已進入容器的網絡命名空間,就可以使用宿主機上的tcpdump來對容器進行抓包了

如果宿主機上已安裝了tcpdump抓包工具,那我們就可以通過宿主機上的nsenter工具來對docker容器進行抓包。

nsenter 包含在絕大部分 Linux 發行版預置的 util-linux 工具包中。使用它可以進入指定進程的關聯命名空間。包括文件命名空間(mount namespace)、主機名命名空間(UTS namespace)、IPC 命名空間(IPC namespace)、網絡命名空間(network namespace)、進程命名空間(pid namespace)和用戶命名空間(user namespace)。

what is nsenter ?

nsenter命令是一個可以在指定進程的命令空間下運行指定程序的命令。它位於util-linux包中。

一個最典型的用途就是進入容器的網絡命令空間。相當多的容器爲了輕量級,是不包含較爲基礎的命令的,比如說ip address,ping,telnet,ss,tcpdump等等命令,這就給調試容器網絡帶來相當大的困擾:只能通過docker inspect ContainerID命令獲取到容器IP,以及無法測試和其他網絡的連通性。這時就可以使用nsenter命令僅進入該容器的網絡命名空間,使用宿主機的命令調試容器網絡。

此外,nsenter也可以進入mnt, uts, ipc, pid, user命令空間,以及指定根目錄和工作目錄。

[[email protected] redis-cluster-ha]# nsenter --help

Usage:
 nsenter [options] <program> [<argument>...]

Run a program with namespaces of other processes.

Options:
 -t, --target <pid>     target process to get namespaces from
 -m, --mount[=<file>]   enter mount namespace
 -u, --uts[=<file>]     enter UTS namespace (hostname etc)
 -i, --ipc[=<file>]     enter System V IPC namespace
 -n, --net[=<file>]     enter network namespace
 -p, --pid[=<file>]     enter pid namespace
 -U, --user[=<file>]    enter user namespace
 -S, --setuid <uid>     set uid in entered namespace
 -G, --setgid <gid>     set gid in entered namespace
     --preserve-credentials do not touch uids or gids
 -r, --root[=<dir>]     set the root directory
 -w, --wd[=<dir>]       set the working directory
 -F, --no-fork          do not fork before exec'ing <program>
 -Z, --follow-context   set SELinux context according to --target PID

 -h, --help     display this help and exit
 -V, --version  output version information and exit

$ nsenter -n -t6700

退出命名空間

$ exit


namespace原理

namespace是Linux中一些進程的屬性的作用域,使用命名空間,可以隔離不同的進程。

Linux在不斷的添加命名空間,目前有:

mount:掛載命名空間,使進程有一個獨立的掛載文件系統,始於Linux 2.4.19
ipc:ipc命名空間,使進程有一個獨立的ipc,包括消息隊列,共享內存和信號量,始於Linux 2.6.19
uts:uts命名空間,使進程有一個獨立的hostname和domainname,始於Linux 2.6.19
net:network命令空間,使進程有一個獨立的網絡棧,始於Linux 2.6.24
pid:pid命名空間,使進程有一個獨立的pid空間,始於Linux 2.6.24
user:user命名空間,是進程有一個獨立的user空間,始於Linux 2.6.23,結束於Linux 3.8
cgroup:cgroup命名空間,使進程有一個獨立的cgroup控制組,始於Linux 4.6

Linux的每個進程都具有命名空間,可以在/proc/PID/ns目錄中看到命名空間的文件描述符。
以上面pid爲例

$ ls -l /proc/6700/ns

在這裏插入圖片描述

2)clone

clone是Linux的系統調用函數,用於創建一個新的進程。
clone和fork比較類似,但更爲精細化,比如說使用clone創建出的子進程可以共享父進程的虛擬地址空間,文件描述符表,信號處理表等等。

不過這裏要強調的是,clone函數還能爲新進程指定命名空間。

3)setns

clone用於創建新的命令空間,而setns則用來讓當前線程(單線程即進程)加入一個命名空間。

4)nsenter

那麼,最後就是nsenter了,

nsenter相當於在setns的示例程序之上做了一層封裝,使我們無需指定命名空間的文件描述符,而是指定進程號即可。

tcpdump

例子:抓取網卡eht0 及192.168.168.18ip和8081端口;

命令:

tcpdump -i eth0   tcp port 6001

tcpdump -i eth0  -w file.cap host 192.168.168.18 and tcp port 8081;

 -w :參數指定將監聽到的數據包寫入文件中保存,file.cap就是該文件。

 -i   :參數指定tcpdump監聽的網絡界面。

注意:每個服務器的網卡不一定是eth0,先使用ipconfig查看清楚自己又幾個網卡,要監聽那個 叫什麼名字等。

注意:每個服務器的網卡不一定是eht0,先使用ipconfig查看清楚自己又幾個網卡,要監聽那個 叫什麼名字等。

img

然後再查看保存的文件就可以了!

tcpdump 核心參數圖解

網絡上的流量、數據包,非常的多,因此要想抓到我們所需要的數據包,就需要我們定義一個精準的過濾器,把這些目標數據包,從巨大的數據包網絡中抓取出來。

所以學習抓包工具,其實就是學習如何定義過濾器的過程。

而在 tcpdump 的世界裏,過濾器的實現,都是通過一個又一個的參數組合起來,一個參數不夠精準,那就再加一個,直到我們能過濾掉無用的數據包,只留下我們感興趣的數據包。

安裝

yum install -y tcpdump




tcpdump -i enp0s8 -n -c 10 port 16001


tcpdump -i docker0 -n -c 10 port 16001

tcpdump -i eth0

$$

$$

理解 tcpdump 的輸出

tcpdump 輸出的內容雖然多,卻很規律。

這裏以我隨便抓取的一個 tcp 包爲例來看一下

21:26:49.013621 IP 172.20.20.1.15605 > 172.20.20.2.5920: Flags [P.], seq 49:97, ack 106048, win 4723, length 48

從上面的輸出來看,可以總結出:

第一列:時分秒毫秒 21:26:49.013621

第二列:網絡協議 IP

第三列:發送方的ip地址+端口號,其中172.20.20.1是 ip,而15605 是端口號

第四列:箭頭 >, 表示數據流向

第五列:接收方的ip地址+端口號,其中 172.20.20.2 是 ip,而5920 是端口號

第六列:冒號

第七列:數據包內容,包括Flags 標識符,seq 號,ack 號,win 窗口,數據長度 length,其中 [P.] 表示 PUSH 標誌位爲 1,更多標識符見下面

然後按ctrl+c停止tcpdump執行,把數據保存的文件,使用wireshark打開分析

image

TCP協議中的tcp push標誌位

在TCP層,有個FLAGS字段,FLAGS字段中有6個標誌位,五個字段的含義是:

  • SYN表示建立連接,

  • FIN表示關閉連接,

  • ACK表示響應,

  • PSH表示有 DATA數據傳輸,

  • RST表示連接重

  • URG表示緊急

 TCP(Transmission Control Protocol)傳輸控制協議,是主機對主機層的傳輸控制協議,提供可靠的連接服務,採用三次握手確認建立一個連接。

  位碼,即tcp標誌位,有6種標示:SYN(synchronous 建立聯機)、ACK(acknowledgement 確認)、PSH(push 傳送)、FIN(finish 結束)、RST(reset 重置)、URG(urgent 緊急)、Sequence number(順序號碼)、Acknowledge number(確認號碼)。、

(1)第一次握手:主機A發送位碼爲syn=1,隨機產生seq number=1234567的數據包到服務器,主機B由SYN=1知道,A要求建立聯機;

(2)第二次握手:主機B收到請求後要確認聯機信息,向A發送ack number=(主機A的seq+1),syn=1,ack=1,隨機產生seq=7654321的包;

(3)第三次握手:主機A收到後檢查ack number是否正確,即第一次發送的seq number+1,以及位碼ack是否爲1,若正確,主機A會再發送ack number=(主機B的seq+1),ack=1,主機B收到後確認seq值與ack=1則連接建立成功。

完成三次握手,主機A與主機B開始傳送數據。

img

 

 在TCP/IP協議中,TCP協議提供可靠的連接服務,採用三次握手建立一個連接。

第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;

第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;

第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。

但是PUSH這個標誌位表示的是什麼含義呢? 在什麼時候用呢?

PUSH標誌位所表達的是發送方通知接收方傳輸層應該儘快的將這個報文段交給應用層。

傳輸層及以下的數據往往是由系統所帶的協議棧進行處理的,客戶端在收到一個個報文之後,經由協議棧解封裝之後會立馬把數據交給應用層去處理嗎?如果說在收到報文之後立馬就交給上層,這時候應用層由於數據不全,可能也不會進行處理。而且每來一個報文就交一次,效率很低。因此傳輸層一般會是隔幾個報文,統一上交數據。什麼時候上交數據呢,就是在發送方將PUSH標誌位置1的時候。那麼什麼時候標誌位會置1呢,通常是發送端覺得傳輸的數據應用層可以進行處理了的時候。

舉個例子來說,TLS 協議中的的證書交換部分,通常證書鏈的大小在3K-4K左右,一般分三個報文來進行傳輸。只有當這3K-4K的報文傳輸完畢之後,那麼數據形成完整的證書鏈,這個時候對於接收方纔是有意義的(可以進行證書鏈的驗證),單純的一個報文無異於亂碼。因此在TLS連接中,通常會發現證書的第三個報文同上設置了push位,是發送方來告知接收方,可以把數據送往tcp的上層了,因爲這些報文已經組成了有意義的內容了。同樣接收方在解析了TCP的PUSH字段後,也會清空自己的緩衝區,向上層交數據。圖1是使用百度搜索"CSDN 村中少年"關鍵詞同時抓取報文中的一條數據流,表示的就是上述所述的場景:

這裏寫圖片描述
下面再以一個HTTP報文爲例說明PUSH的作用。

這裏寫圖片描述
圖2表示的是發送端在一個圖片傳輸結束,可以看到PUSH字段被置爲1,因爲該報文是該圖片流的最後一個報文,接下來就是四次揮手結束該流了。因此這個時候就需要將該報文交給應用層,讓應用層進行顯示等處理。

這裏寫圖片描述

看一下圖片流傳輸過程中哪些報文PUSH字段被設置爲1了。

對於http來說,多媒體文件,像圖片等一般來說比較大,不可能像證書鏈完全傳輸完成之後,僅僅在最後一個報文在再通知接收方向上層扔數據。因此我們看到傳輸過程中每隔一些報文,PUSH字段就設置上了。

由於通常網絡較好的時候,數據會以滿包狀態進行傳輸,當然這裏面是1494個字節,通常當一段數據傳輸完畢就會出現包長度下降,這時候PUSH就置1,提示傳輸層儘快刷新數據交由應用層處理

上述就是對於PUSH標誌位的理解,有可能在看TCP/IP協議的時候並不是對此很清晰,但是結合實際的傳輸過程,理解起來應該很容易。

節點間的通信架構

集羣中會有多個節點,每個節點負責一部分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狀態的節點纔會需要執行主從切換.
/* -----------------------------------------------------------------------------
 * CLUSTER cron job
 * -------------------------------------------------------------------------- */
 
/* This is executed 10 times every second */
/* 集羣的週期性執行函數,每秒執行10次,100ms執行一次 */
void clusterCron(void) {
    dictIterator *di;
    dictEntry *de;
    int update_state = 0;
    /* 沒有從節點的主節點的個數-光桿司令的個數*/
    int orphaned_masters; /* How many masters there are without ok slaves. */
    /* 所有從節點從屬的主節點個數 */
    int max_slaves; /* Max number of ok slaves for a single master. */
    /* 如果myself是從節點,該從節點對應的主節點下有多少個主節點 */
    int this_slaves; /* Number of ok slaves for our master (if we are slave). */
    
    mstime_t min_pong = 0, now = mstime();
    clusterNode *min_pong_node = NULL;
    /* 局部靜態變量,表示該函數執行了多少次 */
    static unsigned long long iteration = 0;
    mstime_t handshake_timeout;
    /* 每執行一次,對iteration做加加的操作 */
    iteration++; /* Number of times this function was called so far. */
 
    /* We want to take myself->ip in sync with the cluster-announce-ip option.
     * The option can be set at runtime via CONFIG SET, so we periodically check
     * if the option changed to reflect this into myself->ip. */
    /*
       我們想要將myself->ip設置地與cluster-announce-ip配置中的是一致的.
       這個配置是可以在運行時的時候通過CONFIG SET來改變的,所以我們間斷性地
       檢測這個選項配置是否是否能夠真實地被寫入到myself->ip中.
     */
    {
        static char *prev_ip = NULL;
        char *curr_ip = server.cluster_announce_ip;
        int changed = 0;
 
        if (prev_ip == NULL && curr_ip != NULL) changed = 1;
        else if (prev_ip != NULL && curr_ip == NULL) changed = 1;
        else if (prev_ip && curr_ip && strcmp(prev_ip,curr_ip)) changed = 1;
 
        if (changed) {
            if (prev_ip) zfree(prev_ip);
            prev_ip = curr_ip;
 
            if (curr_ip) {
                /* We always take a copy of the previous IP address, by
                 * duplicating the string. This way later we can check if
                 * the address really changed. */
                prev_ip = zstrdup(prev_ip);
                strncpy(myself->ip,server.cluster_announce_ip,NET_IP_STR_LEN);
                myself->ip[NET_IP_STR_LEN-1] = '\0';
            } else {
                myself->ip[0] = '\0'; /* Force autodetection. */
            }
        }
    }
 
    /* The handshake timeout is the time after which a handshake node that was
     * not turned into a normal node is removed from the nodes. Usually it is
     * just the NODE_TIMEOUT value, but when NODE_TIMEOUT is too small we use
     * the value of 1 second. */
    /*  獲取握手的超時時間,如果事件太短以至於小於1秒的話,就將其設置成1秒 */
    handshake_timeout = server.cluster_node_timeout;
    if (handshake_timeout < 1000) handshake_timeout = 1000;
 
    /* Update myself flags. */
    /* 更新當前節點的標誌 */
    clusterUpdateMyselfFlags();
 
    /* Check if we have disconnected nodes and re-establish the connection.
     * Also update a few stats while we are here, that can be used to make
     * better decisions in other part of the code. */
    
    /* 獲取安全迭代器 */
    di = dictGetSafeIterator(server.cluster->nodes);
     /* 初始化stats_pfail_nodes(狀態爲pfail的節點的個數)爲0 */
    server.cluster->stats_pfail_nodes = 0;
    /* 遍歷所有集羣中的節點,如果有未建立連接的節點,那麼發送PING或PONG消息,建立連接 */
    while((de = dictNext(di)) != NULL) {
        /* 獲取節點 */
        clusterNode *node = dictGetVal(de);
 
        /* Not interested in reconnecting the link with myself or nodes
         * for which we have no address. */
        /* 對於是自己的節點和沒有地址的節點不感興趣直接跳過 */
        if (node->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR)) continue;
        /* 遇到狀態是CLUSTER_NODE_PFAIL的節點,對stats_pfail_nodes統計的變量加1,
           CLUSTER_NODE_PFAIL是疑似下線的節點
        */
        if (node->flags & CLUSTER_NODE_PFAIL)
            server.cluster->stats_pfail_nodes++;
 
        /* A Node in HANDSHAKE state has a limited lifespan equal to the
         * configured node timeout. */
        /* 
            如果node節點處於握手狀態,但是從建立連接開始到現在已經超時,
            那麼從集羣中刪除該節點,遍歷下一個節點
        */
        if (nodeInHandshake(node) && now - node->ctime > handshake_timeout) {
            clusterDelNode(node);
            continue;
        }
        /* 如果節點的連接對象爲空 */
        if (node->link == NULL) {
            /* 爲節點創建一個連接對象 */
            clusterLink *link = createClusterLink(node);
            /* 
            通過判斷tls_cluster的信息來判斷是調用connCreateTLS還是調用connCreateSocket
            來創建連接
            */
            link->conn = server.tls_cluster ? connCreateTLS() : connCreateSocket();
            /* 將link設置爲link->conn這個連接的私有信息 */
            connSetPrivateData(link->conn, link);
            /* 建立當前節點與node這個節點的連接 */
            if (connConnect(link->conn, node->ip, node->cport, NET_FIRST_BIND_ADDR,
                        clusterLinkConnectHandler) == -1) {
                /* We got a synchronous error from connect before
                 * clusterSendPing() had a chance to be called.
                 * If node->ping_sent is zero, failure detection can't work,
                 * so we claim we actually sent a ping now (that will
                 * be really sent as soon as the link is obtained). */
                /*
                    如果ping_sent【最近一次發送PING的時間】爲0,察覺故障無法執行,
                    因此要設置發送PING的時間,當建立連接後會真正的的發送PING命令,
                    如果連接出錯,那麼跳過該節點.
                */
                
                if (node->ping_sent == 0) node->ping_sent = mstime();
                serverLog(LL_DEBUG, "Unable to connect to "
                    "Cluster Node [%s]:%d -> %s", node->ip,
                    node->cport, server.neterr);
                /* 釋放節點,繼續循環 */
                freeClusterLink(link);
                continue;
            }
            /* 爲node設置連接對象 */
            node->link = link;
        }
    }
    /* 釋放安全迭代器 */
    dictReleaseIterator(di);
 
    /* Ping some random node 1 time every 10 iterations, so that we usually ping
     * one random node every second. */
     /*
         在十次執行此函數中有一次會隨機PING一些節點,這樣我們通常就可以1秒鐘能夠ping
         到一個隨機的節點
     */
    if (!(iteration % 10)) {
        int j;
 
        /* Check a few random nodes and ping the one with the oldest
         * pong_received time. */
        /*
            隨機抽查5個節點,向pong_received值最小的發送PING消息
            pong_received【接收到PONG的時間】
         */
        for (j = 0; j < 5; j++) {
            /* 隨機抽查一個節點 */
            de = dictGetRandomKey(server.cluster->nodes);
            clusterNode *this = dictGetVal(de);
 
            /* Don't ping nodes disconnected or with a ping currently active. */
            /* 跳過無連接或已經發送過PING的節點 */
            if (this->link == NULL || this->ping_sent != 0) continue;
            /*  跳過myself節點和處於握手狀態的節點 */
            if (this->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_HANDSHAKE))
                continue;
            
            /* 需要再研究,這裏是什麼意思? */
            /* 當min_pong_node爲NULL或者min_pong大於當前節點收到的pong的時間的情況下 */
            /* menwen-查找出這個5個隨機抽查的節點,接收到PONG回覆過去最久的節點 */
            if (min_pong_node == NULL || min_pong > this->pong_received) {
                min_pong_node = this;
                min_pong = this->pong_received;
            }
        }
        /* 如果min_pong_node不爲NULL,
           向接收到PONG回覆過去最久的節點發送PING消息,判斷是否可達
         */
        if (min_pong_node) {
            serverLog(LL_DEBUG,"Pinging node %.40s", min_pong_node->name);
            clusterSendPing(min_pong_node->link, CLUSTERMSG_TYPE_PING);
        }
    }
 
    /* Iterate nodes to check if we need to flag something as failing.
     * This loop is also responsible to:
     * 1) Check if there are orphaned masters (masters without non failing
     *    slaves).
     * 2) Count the max number of non failing slaves for a single master.
     * 3) Count the number of slaves for our master, if we are a slave. */
    /*
        迭代所有的節點,檢查是否需要標記某個節點下線的狀態:
        (1)檢查是否有孤立的主節點(主節點的從節點全部下線);
        (2)計算單個主節點沒下線從節點的最大個數;
        (3)如果myself是從節點,計算該從節點的主節點有多少個從節點.
        追加註釋:
        (1)孤立的主節點個數用orphaned_masters記錄;
        (2)計算單個主節點沒下線從節點的最大個數用max_slaves記錄;
        (3)如果myself是從節點,計算該從節點的主節點有多少個從節點用this_slaves記錄.
    */
    orphaned_masters = 0;
    max_slaves = 0;
    this_slaves = 0;
    di = dictGetSafeIterator(server.cluster->nodes);
    while((de = dictNext(di)) != NULL) {
        /* 迭代所有的節點 */
        clusterNode *node = dictGetVal(de);
        now = mstime(); /* Use an updated time at every iteration. */
        /* 跳過myself節點,無地址NOADDR節點,和處於握手狀態的節點 */
        if (node->flags &
            (CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE))
                continue;
 
        /* Orphaned master check, useful only if the current instance
         * is a slave that may migrate to another master. */
        /*
           對無從節點的主節點進行判斷,僅僅在當前節點是一個可能快要變成另外一個主節點
           的時候有效
        */
        /* 如果myself是從節點並且node節點是主節點並且該主節點不處於下線狀態 */
        if (nodeIsSlave(myself) && nodeIsMaster(node) && !nodeFailed(node)) {
            
            /* 獲取node主節點有多少個正常的從節點*/
            int okslaves = clusterCountNonFailingSlaves(node);
 
            /* A master is orphaned if it is serving a non-zero number of
             * slots, have no working slaves, but used to have at least one
             * slave, or failed over a master that used to have slaves. */
             /*
              node主節點沒有ok的從節點,
              並且node節點負責有槽位,
              並且node節點指定了槽遷移標識
              */
      
            if (okslaves == 0 && node->numslots > 0 &&
                node->flags & CLUSTER_NODE_MIGRATE_TO)
            {
                   /* 孤立的主節點數加1,光桿司令的數量加一 */
                   orphaned_masters++;
            }
            /* 更新一個主節點最多ok從節點的數量 */
            if (okslaves > max_slaves) max_slaves = okslaves;
            /* 如果myself是從節點, 並且從屬於當前node主節點,
               更新該從節點的主節點有多少個從節點的值 
            */
            if (nodeIsSlave(myself) && myself->slaveof == node)
                this_slaves = okslaves;
        }
 
        /* If we are not receiving any data for more than half the cluster
         * timeout, reconnect the link: maybe there is a connection
         * issue even if the node is alive. */
        /*
            如果等待PONG回覆的時間超過cluster_node_timeout的一半,則重新建立連接.
           即使節點正常,但是它的連接出問題
        */
        /* 計算ping延遲和數據延遲 */
        mstime_t ping_delay = now - node->ping_sent;
        mstime_t data_delay = now - node->data_received;
      
        /* 如果node->link不爲NULL(表明是連接着的)
           且server.cluster_node_timeout不爲0(表明還沒有重連)
           且node->ping_sent不爲0(表明節點還在等待PONG回覆)
           且node->pong_received小於node->ping_sent(表明節點仍然在等待PONG回覆)
           且ping_delay > server.cluster_node_timeout/2(表明等到PONG回覆的時間已經超過了 
           timeout/2)
           且data_delay > server.cluster_node_timeout/2(表明現在已經超過timeout/2的時間 
           沒有看到數據的傳送)
         */
        if (node->link && /* is connected */
            now - node->link->ctime >
            server.cluster_node_timeout && /* was not already reconnected */
            node->ping_sent && /* we already sent a ping */
            node->pong_received < node->ping_sent && /* still waiting pong */
            /* and we are waiting for the pong more than timeout/2 */
            ping_delay > server.cluster_node_timeout/2 &&
            /* and in such interval we are not seeing any traffic at all. */
            data_delay > server.cluster_node_timeout/2)
        {
            /* Disconnect the link, it will be reconnected automatically. */
            /* 釋放連接,等待下個週期的自動重連 */
            freeClusterLink(node->link);
        }
 
        /* If we have currently no active ping in this instance, and the
         * received PONG is older than half the cluster timeout, send
         * a new ping now, to ensure all the nodes are pinged without
         * a too big delay. */
 
        /* 如果當前沒有發送PING消息,並且在一定時間內也沒有收到PONG回覆 */
        if (node->link &&
            node->ping_sent == 0 &&
            (now - node->pong_received) > server.cluster_node_timeout/2)
        {
            /*  給node節點發送一個PING消息 */
            clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
            continue;
        }
 
        /* If we are a master and one of the slaves requested a manual
         * failover, ping it continuously. */
         /*
            如果當前節點是一個主節點且有從節點請求手動故障轉移,那麼就持續
            地PING它
         */
         /*
            mf_end-如果爲0,表示沒有正在進行手動的故障轉移.否則表示手動故障轉移的時間限制.
            如果有從節點手動請求故障轉移且當前節點是主節點且當前節點是手動請求故障轉移的
            節點且當前節點的節點不爲NULL
         */
        if (server.cluster->mf_end &&
            nodeIsMaster(myself) &&
            server.cluster->mf_slave == node &&
            node->link)
        {
            clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
            continue;
        }
 
        /* Check only if we have an active ping for this instance. */
        /* 
           如果當前還沒有發送PING消息,則跳過,
           只有發送了PING消息之後,纔會執行以下操作
         */
        if (node->ping_sent == 0) continue;
 
        /* Check if this node looks unreachable.
         * Note that if we already received the PONG, then node->ping_sent
         * is zero, so can't reach this code at all, so we don't risk of
         * checking for a PONG delay if we didn't sent the PING.
         *
         * We also consider every incoming data as proof of liveness, since
         * our cluster bus link is also used for data: under heavy data
         * load pong delays are possible. */
         /*
            檢查一下當前的節點是否看起來不可到達.
            要注意一下如果我們之前就有收到過PING,那麼node->ping_sent這個字段是0,
            所以不可能到達這個狀態.所以我們不想在沒有發送PING消息的情況下冒着一定
            的風險去檢測PONG回覆的延遲.
         */
 
 
        /*取ping_delay和data_delay中較小的值作爲節點的延遲 */
        mstime_t node_delay = (ping_delay < data_delay) ? ping_delay :
                                                          data_delay;
        /* 如果節點的延遲超過了配置文件中設置的cluster-node-timeout
         */
        if (node_delay > server.cluster_node_timeout) {
            /* Timeout reached. Set the node as possibly failing if it is
             * not already in this state. */
             /*
             */
            if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) {
                serverLog(LL_DEBUG,"*** NODE %.40s possibly failing",
                    node->name);
                node->flags |= CLUSTER_NODE_PFAIL;
                update_state = 1;
            }
        }
    }
    dictReleaseIterator(di);
 
    /* If we are a slave node but the replication is still turned off,
     * enable it if we know the address of our master and it appears to
     * be up. */
    /*
          myself->slaveof【pointer to the master node】
          
          如果myself是從節點
          且server.masterhost爲NULL
          且myself->slaveof不爲NULL
          且myself->slaveof(mysql對應的從節點的指針)的地址是存在的
          那麼設置當前服務器的主節點的地址,即IP和端口號.
    */
    
    if (nodeIsSlave(myself) &&
        server.masterhost == NULL &&
        myself->slaveof &&
        nodeHasAddr(myself->slaveof))
    {
        replicationSetMaster(myself->slaveof->ip, myself->slaveof->port);
    }
 
    /* Abourt a manual failover if the timeout is reached. */
    /* 終止一個超時的手動故障轉移操作 */
    manualFailoverCheckTimeout();
    
    /* 如果當前節點是從節點 */
    if (nodeIsSlave(myself)) {
        
        /* 設置手動故障轉移的狀態 */
        clusterHandleManualFailover();
         
        /* 
          如果當前節點沒有被設置成不允許進行故障轉移,那麼
          調用clusterHandleSlaveFailover執行從節點的自動或手動故障轉移
        */
        if (!(server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_FAILOVER))
            clusterHandleSlaveFailover();
        /* If there are orphaned slaves, and we are a slave among the masters
         * with the max number of non-failing slaves, consider migrating to
         * the orphaned masters. Note that it does not make sense to try
         * a migration if there is no master with at least *two* working
         * slaves. */
        if (orphaned_masters && max_slaves >= 2 && this_slaves == max_slaves)
            clusterHandleSlaveMigration(max_slaves);
    }
    /* 
       如果存在孤立的主節點,並且集羣中的某一主節點有超過2個正常的從節點,
       並且該主節點正好是myself節點的主節點
    */
    if (update_state || server.cluster->state == CLUSTER_FAIL)
        /* 更新集羣狀態 */
        clusterUpdateState();
}
心跳數據

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集羣虛擬槽分片結果:

[[email protected] redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 192.168.56.121:6001
192.168.56.121:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
192.168.56.121:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
192.168.56.121: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 192.168.56.121:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 192.168.56.121:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 192.168.56.121:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 192.168.56.121:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 192.168.56.121:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 192.168.56.121:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 192.168.56.121: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結構,更新下線報告鏈表。

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

只有負責槽的主節點(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)到達後,發起選舉流程如下:

更新配置紀元:

配置紀元是一個只增不減的整數,每個主節點自身維護一個配置紀元 (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 cluster

Redis集羣的一致性保證

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

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

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

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

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

面試問題:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oc8zneiR-1655967980609)(./基礎設施部署圖片/redis-1.jpg)]

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

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

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

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

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

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

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

所以,如果主節點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節點將無法同步這些數據,

當網絡問題解決之後,集羣將原先的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、熱點數據不過期。該方式和緩存擊穿一樣,也是要着重考慮刷新的時間間隔和數據異常如何處理的情況。

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

集羣與數據庫的數據一致性保障方案:

  • 方案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緩存的三個經典的緩存模式

緩存可以提升性能、緩解數據庫壓力,但是使用緩存也會導致數據不一致性的問題。一般我們是如何使用緩存呢?有三種經典的緩存模式:

  • Cache-Aside Pattern
  • Read-Through/Write through
  • Write behind

Cache-Aside Pattern

Cache-Aside Pattern,即旁路緩存模式,它的提出是爲了儘可能地解決緩存與數據庫的數據不一致問題。

Cache-Aside的讀流程

Cache-Aside Pattern的讀請求流程如下:

在這裏插入圖片描述

讀的時候,先讀緩存,緩存命中的話,直接返回數據;

緩存沒有命中的話,就去讀數據庫,從數據庫取出數據,放入緩存後,同時返回響應。

Cache-Aside 寫流程

Cache-Aside Pattern的寫請求流程如下:

在這裏插入圖片描述

更新的時候,先更新數據庫,然後再刪除緩存。

Read-Through/Write-Through(讀寫穿透)

Read/Write Through模式中,服務端把緩存作爲主要數據存儲。應用程序跟數據庫緩存交互,都是通過抽象緩存層完成的。

Read-Through讀流程

Read-Through的簡要讀流程如下
在這裏插入圖片描述

從緩存讀取數據,讀到直接返回
如果讀取不到的話,從數據庫加載,寫入緩存後,再返回響應。

這個簡要流程是不是跟Cache-Aside很像呢?

其實Read-Through就是多了一層Cache-Provider,流程如下:

在這裏插入圖片描述

Read-Through的優點

Read-Through實際只是在Cache-Aside之上進行了一層封裝,它會讓程序代碼變得更簡潔,同時也減少數據源上的負載。

Write-Through寫流程

Write-Through模式下,當發生寫請求時,也是由緩存抽象層完成數據源和緩存數據的更新,流程如下:

在這裏插入圖片描述

Write behind (異步緩存寫入)

Write behind跟Read-Through/Write-Through有相似的地方,都是由Cache Provider來負責緩存和數據庫的讀寫。它兩又有個很大的不同:Read/Write Through是同步更新緩存和數據的,Write Behind則是隻更新緩存,不直接更新數據庫,通過批量異步的方式來更新數據庫。

加粗樣式

這種方式下,緩存和數據庫的一致性不強,對一致性要求高的系統要謹慎使用。

但是它適合頻繁寫的場景,MySQL的InnoDB Buffer Pool機制就使用到這種模式。

三種模式的比較

Cache Aside 更新模式實現起來比較簡單,但是需要維護兩個數據存儲:

  • 一個是緩存(Cache)
  • 一個是數據庫(Repository)。

Read/Write Through 的寫模式需要維護一個數據存儲(緩存),實現起來要複雜一些。

Write Behind Caching 更新模式和Read/Write Through 更新模式類似,區別是Write Behind Caching 更新模式的數據持久化操作是異步的,但是Read/Write Through 更新模式的數據持久化操作是同步的

Write Behind Caching 的優點是直接操作內存速度快,多次操作可以合併持久化到數據庫。缺點是數據可能會丟失,例如系統斷電等。

Cache-Aside的問題

更新數據的時候,Cache-Aside是刪除緩存呢,還是應該更新緩存?

有些小夥伴可能會問, Cache-Aside在寫入請求的時候,爲什麼是刪除緩存而不是更新緩存呢?

在這裏插入圖片描述

我們在操作緩存的時候,到底應該刪除緩存還是更新緩存呢?我們先來看個例子:
在這裏插入圖片描述

操作的次序如下:

線程A先發起一個寫操作,第一步先更新數據庫
線程B再發起一個寫操作,第二步更新了數據庫

現在,由於網絡等原因,線程B先更新了緩存, 線程A更新緩存。

這時候,緩存保存的是A的數據(老數據),數據庫保存的是B的數據(新數據),數據不一致了,髒數據出現啦。如果是刪除緩存取代更新緩存則不會出現這個髒數據問題。

更新緩存相對於刪除緩存,還有兩點劣勢:

1 如果你寫入的緩存值,是經過複雜計算纔得到的話。 更新緩存頻率高的話,就浪費性能啦。

2 在寫多讀少的情況下,數據很多時候還沒被讀取到,又被更新了,這也浪費了性能呢(實際上,寫多的場景,用緩存也不是很划算了)

任何的措施,也不是絕對的好, 只有分場景看是不是適合,更新緩存的措施,也是有用的:

在讀多寫少的場景,價值大。

雙寫的情況下,先操作數據庫還是先操作緩存?

美團二面:Redis與MySQL雙寫一致性如何保證?

Cache-Aside緩存模式中,有些小夥伴還是有疑問,在寫入請求的時候,爲什麼是先操作數據庫呢?爲什麼不先操作緩存呢?
假設有A、B兩個請求,請求A做更新操作,請求B做查詢讀取操作。
在這裏插入圖片描述

A、B兩個請求的操作流程如下:

  1. 線程A發起一個寫操作,第一步del cache
  2. 此時線程B發起一個讀操作,cache miss
  3. 線程B繼續讀DB,讀出來一個老數據
  4. 然後線程B把老數據設置入cache
  5. 線程A寫入DB最新的數據

醬紫就有問題啦,緩存和數據庫的數據不一致了。

緩存保存的是老數據,數據庫保存的是新數據。因此,Cache-Aside緩存模式,選擇了先操作數據庫而不是先操作緩存。

redis分佈式緩存與數據庫的數據一致性

重要:緩存是通過犧牲強一致性來提高性能的。

這是由CAP理論決定的。緩存系統適用的場景就是非強一致性的場景,它屬於CAP中的AP。

強一致性還是弱一致性

CAP理論,指的是在一個分佈式系統中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區容錯性),三者不可得兼。

CAP理論作爲分佈式系統的基礎理論,它描述的是一個分佈式系統在以下三個特性中:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分區容錯性(Partition tolerance)

最多滿足其中的兩個特性。也就是下圖所描述的。分佈式系統要麼滿足CA,要麼CP,要麼AP。無法同時滿足CAP。

        img

I. 什麼是 一致性、可用性和分區容錯性

分區容錯性:指的分佈式系統中的某個節點或者網絡分區出現了故障的時候,整個系統仍然能對外提供滿足一致性和可用性的服務。也就是說部分故障不影響整體使用。

事實上我們在設計分佈式系統是都會考慮到bug,硬件,網絡等各種原因造成的故障,所以即使部分節點或者網絡出現故障,我們要求整個系統還是要繼續使用的

(不繼續使用,相當於只有一個分區,那麼也就沒有後續的一致性和可用性了)

可用性: 一直可以正常的做讀寫操作。簡單而言就是客戶端一直可以正常訪問並得到系統的正常響應。用戶角度來看就是不會出現系統操作失敗或者訪問超時等問題。

一致性:在分佈式系統完成某寫操作後任何讀操作,都應該獲取到該寫操作寫入的那個最新的值。相當於要求分佈式系統中的各節點時時刻刻保持數據的一致性。

所以,如果需要數據庫和緩存數據保持強一致,就不適合使用緩存。

所以使用緩存提升性能,就是會有數據更新的延遲。這需要我們在設計時結合業務仔細思考是否適合用緩存。然後緩存一定要設置過期時間,這個時間太短、或者太長都不好:

  • 太短的話請求可能會比較多的落到數據庫上,這也意味着失去了緩存的優勢。
  • 太長的話緩存中的髒數據會使系統長時間處於一個延遲的狀態,而且系統中長時間沒有人訪問的數據一直存在內存中不過期,浪費內存。

但是,通過一些方案優化處理,是可以保證弱一致性,最終一致性的。

3種方案保證數據庫與緩存的一致性

3種方案保證數據庫與緩存的一致性

  • 延時雙刪策略
  • 刪除緩存重試機制
  • 讀取biglog異步刪除緩存

緩存延時雙刪

有些小夥伴可能會說,不一定要先操作數據庫呀,採用緩存延時雙刪策略就好啦?

什麼是延時雙刪呢?

延時雙刪的步驟:

1 先刪除緩存
2 再更新數據庫
3 休眠一會(比如1秒),再次刪除緩存。

在這裏插入圖片描述

參考代碼如下:

在這裏插入圖片描述

這個休眠一會,一般多久呢?都是1秒?

這個休眠時間 = 讀業務邏輯數據的耗時 + 幾百毫秒。

爲了確保讀請求結束,寫請求可以刪除讀請求可能帶來的緩存髒數據。

刪除緩存重試機制

不管是延時雙刪還是Cache-Aside的先操作數據庫再刪除緩存,如果第二步的刪除緩存失敗呢?

刪除失敗會導致髒數據哦~

刪除失敗就多刪除幾次呀,保證刪除緩存成功呀~ 所以可以引入刪除緩存重試機制

在這裏插入圖片描述

刪除緩存重試機制的大致步驟:

  • 寫請求更新數據庫
  • 緩存因爲某些原因,刪除失敗
  • 把刪除失敗的key放到消息隊列
  • 消費消息隊列的消息,獲取要刪除的key
  • 重試刪除緩存操作

同步biglog異步刪除緩存

重試刪除緩存機制還可以,就是會造成好多業務代碼入侵。

其實,還可以通過數據庫的binlog來異步淘汰key。

在這裏插入圖片描述

以mysql爲例 可以使用阿里的canal將binlog日誌採集發送到MQ隊列裏面,然後編寫一個簡單的緩存刪除消息者訂閱binlog日誌,根據更新log刪除緩存,並且通過ACK機制確認處理這條更新log,保證數據緩存一致性

如何確保消費成功

PushConsumer爲了保證消息肯定消費成功,只有使用方明確表示消費成功,RocketMQ纔會認爲消息消費成功。中途斷電,拋出異常等都不會認爲成功——即都會重新投遞。首先,消費的時候,我們需要注入一個消費回調,具體sample代碼如下:

consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
	System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
	delcache(key);//執行真正刪除
	return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;//返回消費成功
 }
});

業務實現消費回調的時候,當且僅當此回調函數返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ纔會認爲這批消息(默認是1條)是消費完成的。

如果這時候消息消費失敗,例如數據庫異常,餘額不足扣款失敗等一切業務認爲消息需要重試的場景,只要返回ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ就會認爲這批消息消費失敗了。

爲了保證消息是肯定被至少消費成功一次,RocketMQ會把這批消費失敗的消息重發回Broker(topic不是原topic而是這個消費租的RETRY topic),在延遲的某個時間點(默認是10秒,業務可設置)後,再次投遞到這個ConsumerGroup。而如果一直這樣重複消費都持續失敗到一定次數(默認16次),就會投遞到DLQ死信隊列。應用可以監控死信隊列來做人工干預。

pub/sub的訂閱實現

Pub/Sub功能(means Publish, Subscribe)即發佈及訂閱功能。Pub/Sub是目前廣泛使用的通信模型,它採用事件作爲基本的通信機制,提供大規模系統所要求的鬆散耦合的交互模式:訂閱者(如客戶端)以事件訂閱的方式表達出它有興趣接收的一個事件或一類事件;發佈者(如服務器)可將訂閱者感興趣的事件隨時通知相關訂閱者。熟悉設計模式的朋友應該瞭解這與23種設計模式中的觀察者模式極爲相似。

Redis 的 pub/sub訂閱實現

Redis通過publish和subscribe命令實現訂閱和發佈的功能。訂閱者可以通過subscribe向redis server訂閱自己感興趣的消息類型。redis將信息類型稱爲通道(channel)。當發佈者通過publish命令向redis server發送特定類型的信息時,訂閱該消息類型的全部訂閱者都會收到此消息。

主從數據庫通過biglog異步刪除

但是呢還有個問題, 「如果是主從數據庫呢」

因爲主從DB同步存在延時時間。如果刪除緩存之後,數據同步到備庫之前已經有請求過來時, 「會從備庫中讀到髒數據」,如何解決呢?解決方案如下流程圖:

在這裏插入圖片描述

緩存與數據的一致性的保障策略總結

綜上所述,在分佈式系統中,緩存和數據庫同時存在時,如果有寫操作的時候,「先操作數據庫,再操作緩存」。如下:

1.讀取緩存中是否有相關數據
2.如果緩存中有相關數據value,則返回
3.如果緩存中沒有相關數據,則從數據庫讀取相關數據放入緩存中key->value,再返回
4.如果有更新數據,則先更新數據庫,再刪除緩存
5.爲了保證第四步刪除緩存成功,使用binlog異步刪除
6.如果是主從數據庫,binglog取自於從庫
7.如果是一主多從,每個從庫都要採集binlog,然後消費端收到最後一臺binlog數據才刪除緩存,或者爲了簡單,收到一次更新log,刪除一次緩存

實戰:Canal+RocketMQ同步MySQL到Redis/ES

在很多業務情況下,我們都會在系統中加入redis緩存做查詢優化, 使用es 做全文檢索。

如果數據庫數據發生更新,這時候就需要在業務代碼中寫一段同步更新redis的代碼。這種數據同步的代碼跟業務代碼糅合在一起會不太優雅,能不能把這些數據同步的代碼抽出來形成一個獨立的模塊呢,答案是可以的。

biglog同步保障數據一致性的架構

在這裏插入圖片描述

技術棧

如果你還對SpringBootcanalRocketMQMySQLElasticSearch 不是很瞭解的話,這裏我爲大家整理個它們的官網網站,如下

這裏主要介紹一下canal,其他的自行學習。

canal工作原理

canal [kə’næl],譯意爲水道/管道/溝渠,主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據訂閱和消費.。

canal工作原理

canal是一個僞裝成slave訂閱mysql的binlog,實現數據同步的中間件。

在這裏插入圖片描述

  • canal 模擬 MySQL slave 的交互協議,僞裝自己爲 MySQL slave ,向 MySQL master 發送 dump 協議
  • MySQL master 收到 dump 請求,開始推送 binary log 給 slave (即 canal )
  • canal 解析 binary log 對象(原始爲 byte 流)
canal架構

在這裏插入圖片描述
說明:

  • server代表一個canal運行實例,對應於一個jvm
  • instance對應於一個數據隊列 (1個server對應1…n個instance)

instance模塊:

  • eventParser (數據源接入,模擬db的slave協議和master進行交互,協議解析)
  • eventSink (Parser和Store鏈接器,進行數據過濾,加工,分發的工作)
  • eventStore (數據存儲)
  • metaManager (增量訂閱&消費信息管理器)

到這裏我們對canal有了一個初步的認識,接下我們就進入實戰環節。

環境準備

MySQL 配置

對於自建 MySQL , 需要先開啓 Binlog寫入功能,配置binlog-formatROW 模式,my.cnf 中配置如下

[mysqld]
log-bin=mysql-bin # 開啓 binlog
binlog-format=ROW # 選擇 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定義,不要和 canal 的 slaveId 重複

注意:針對阿里雲 RDS for MySQL , 默認打開了 binlog , 並且賬號默認具有 binlog dump 權限 , 不需要任何權限或者 binlog 設置,可以直接跳過這一步

授權canal 連接 MySQL 賬號具有作爲 MySQL slave的權限, 如果已有賬戶可直接 使用grant 命令授權。

#創建用戶名和密碼都爲canal
CREATE USER canal IDENTIFIED BY 'canal';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;

3.2 canal的安裝和配置

canal.admin安裝和配置

canal提供web ui 進行Server管理、Instance管理。

下載 canal.admin, 訪問 release 頁面 , 選擇需要的包下載, 如以 1.1.4版本爲例
wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.admin-1.1.4.tar.gz

在這裏插入圖片描述

解壓完成可以看到如下結構:

在這裏插入圖片描述

我們先配置canal.admin之後。通過web ui來配置 cancal server,這樣使用界面操作非常的方便。

配置修改
vi conf/application.yml
server:
  port: 8089
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

spring.datasource:
  address: 127.0.0.1:3306
  database: canal_manager
  username: canal
  password: canal
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  hikari:
    maximum-pool-size: 30
    minimum-idle: 1

canal:
  adminUser: admin
  adminPasswd: admin
初始化元數據庫

初始化元數據庫

mysql -h127.0.0.1 -uroot -p

# 導入初始化SQL
> source conf/canal_manager.sql
  • 初始化SQL腳本里會默認創建canal_manager的數據庫,建議使用root等有超級權限的賬號進行初始化
  • canal_manager.sql默認會在conf目錄下,也可以通過鏈接下載 canal_manager.sql
啓動
sh bin/startup.sh
啓動成功,使用瀏覽器輸入http://ip:8089/ 會跳轉到登錄界面

在這裏插入圖片描述

使用用戶名:admin 密碼爲:123456 登錄
登錄成功,會自動跳轉到如下界面。這時候我們的canal.admin就搭建成功了。

在這裏插入圖片描述

canal.deployer部署和啓動

下載 canal.deployer, 訪問 release 頁面 , 選擇需要的包下載, 如以 1.1.4版本爲例

wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.deployer-1.1.4.tar.gz

在這裏插入圖片描述

解壓完成可以看到如下結構:

在這裏插入圖片描述

進入conf 目錄。可以看到如下的配置文件。

在這裏插入圖片描述

我們先對canal.properties 不做任何修改。

使用canal_local.properties的配置覆蓋canal.properties

# register ip
canal.register.ip =

# canal admin config
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441

# admin auto register
canal.admin.register.auto = true
canal.admin.register.cluster =

使用如下命令啓動canal server

sh bin/startup.sh local

啓動成功。同時我們在canal.admin web ui中刷新 server 管理,可以到canal server 已經啓動成功。

在這裏插入圖片描述

這時候我們的canal.server 搭建已經成功。

在canal admin ui 中配置Instance管理

新建 Instance

選擇Instance 管理-> 新建Instance
填寫 Instance名稱:cms_article

大概的步驟
  • 選擇 選擇所屬主機集羣
  • 選擇 載入模板
  • 修改默認信息
#mysql serverId
canal.instance.mysql.slaveId = 1234
#position info,需要改成自己的數據庫信息
canal.instance.master.address = 127.0.0.1:3306 
canal.instance.master.journal.name = 
canal.instance.master.position = 
canal.instance.master.timestamp = 
#canal.instance.standby.address = 
#canal.instance.standby.journal.name =
#canal.instance.standby.position = 
#canal.instance.standby.timestamp = 
#username/password,需要改成自己的數據庫信息
canal.instance.dbUsername = canal  
canal.instance.dbPassword = canal
#改成自己的數據庫信息(需要監聽的數據庫)
canal.instance.defaultDatabaseName = cms-manage
canal.instance.connectionCharset = UTF-8
#table regex 需要過濾的表 這裏數據庫的中所有表
canal.instance.filter.regex = .\*\\..\*

# MQ 配置 日誌數據會發送到cms_article這個topic上
canal.mq.topic=cms_article
# dynamic topic route by schema or table regex
#canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..*
#單分區處理消息
canal.mq.partition=0

我們這裏爲了演示之創建一張表。
在這裏插入圖片描述

配置好之後,我需要點擊保存。此時在Instances 管理中就可以看到此時的實例信息。
在這裏插入圖片描述

修改canal server 的配置文件,選擇消息隊列處理binlog

canal 1.1.1版本之後, 默認支持將canal server接收到的binlog數據直接投遞到MQ, 目前默認支持的MQ系統有:

本案例以RocketMQ爲例

我們仍然使用web ui 界面操作。點擊 server 管理 - > 點擊配置
在這裏插入圖片描述
修改配置文件

# ...
# 可選項: tcp(默認), kafka, RocketMQ
canal.serverMode = RocketMQ
# ...
# kafka/rocketmq 集羣配置: 192.168.1.117:9092,192.168.1.118:9092,192.168.1.119:9092 
canal.mq.servers = 192.168.0.200:9078
canal.mq.retries = 0
# flagMessage模式下可以調大該值, 但不要超過MQ消息體大小上限
canal.mq.batchSize = 16384
canal.mq.maxRequestSize = 1048576
# flatMessage模式下請將該值改大, 建議50-200
canal.mq.lingerMs = 1
canal.mq.bufferMemory = 33554432
# Canal的batch size, 默認50K, 由於kafka最大消息體限制請勿超過1M(900K以下)
canal.mq.canalBatchSize = 50
# Canal get數據的超時時間, 單位: 毫秒, 空爲不限超時
canal.mq.canalGetTimeout = 100
# 是否爲flat json格式對象
canal.mq.flatMessage = false
canal.mq.compressionType = none
canal.mq.acks = all
# kafka消息投遞是否使用事務
canal.mq.transaction = false

修改好之後保存。會自動重啓。

此時我們就可以在rocketmq的控制檯看到一個cms_article topic已經自動創建了。

在這裏插入圖片描述

更新Redis的MQ消息者開發

引入依賴

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.4</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

<!-- 根據個人需要依賴 -->
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>persistence-api</artifactId>
</dependency>

canal消息的通用解析代碼

package com.crazymaker.springcloud.stock.consumer;

import com.alibaba.otter.canal.protocol.FlatMessage;
import com.crazymaker.springcloud.common.exception.BusinessException;
import com.crazymaker.springcloud.common.util.JsonUtil;
import com.crazymaker.springcloud.standard.redis.RedisRepository;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.ReflectionUtils;

import javax.annotation.Resource;
import javax.persistence.Id;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * 抽象CanalMQ通用處理服務
 **/

@Slf4j
public abstract class AbstractCanalMQ2RedisService<T> implements CanalSynService<T> {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    RedisRepository redisRepository;


    private Class<T> classCache;

    /**
     * 獲取Model名稱
     *
     * @return Model名稱
     */
    protected abstract String getModelName();

    @Override
    public void process(FlatMessage flatMessage) {

        if (flatMessage.getIsDdl()) {
            ddl(flatMessage);
            return;
        }

        Set<T> data = getData(flatMessage);

        if (SQLType.INSERT.equals(flatMessage.getType())) {
            insert(data);
        }

        if (SQLType.UPDATE.equals(flatMessage.getType())) {
            update(data);
        }

        if (SQLType.DELETE.equals(flatMessage.getType())) {
            delete(data);
        }

    }

    @Override
    public void ddl(FlatMessage flatMessage) {
        //TODO : DDL需要同步,刪庫清空,更新字段處理

    }

    @Override
    public void insert(Collection<T> list) {
        insertOrUpdate(list);
    }

    @Override
    public void update(Collection<T> list) {
        insertOrUpdate(list);
    }

    private void insertOrUpdate(Collection<T> list) {
        redisTemplate.executePipelined((RedisConnection redisConnection) -> {
            for (T data : list) {
                String key = getWrapRedisKey(data);
                RedisSerializer keySerializer = redisTemplate.getKeySerializer();
                RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
                redisConnection.set(keySerializer.serialize(key), valueSerializer.serialize(data));
            }
            return null;
        });
    }

    @Override
    public void delete(Collection<T> list) {

        Set<String> keys = Sets.newHashSetWithExpectedSize(list.size());

        for (T data : list) {
            keys.add(getWrapRedisKey(data));
        }

        //Set<String> keys = list.stream().map(this::getWrapRedisKey).collect(Collectors.toSet());
        redisRepository.delAll(keys);
    }

    /**
     * 封裝redis的key
     *
     * @param t 原對象
     * @return key
     */
    protected String getWrapRedisKey(T t) {
//        return new StringBuilder()
//                .append(ApplicationContextHolder.getApplicationName())
//                .append(":")
//                .append(getModelName())
//                .append(":")
//                .append(getIdValue(t))
//                .toString();

        throw new IllegalStateException(
                "基類 方法 'getWrapRedisKey' 尚未實現!");
    }

    /**
     * 獲取類泛型
     *
     * @return 泛型Class
     */
    protected Class<T> getTypeArguement() {
        if (classCache == null) {
            classCache = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        }
        return classCache;
    }

    /**
     * 獲取Object標有@Id註解的字段值
     *
     * @param t 對象
     * @return id值
     */
    protected Object getIdValue(T t) {
        Field fieldOfId = getIdField();
        ReflectionUtils.makeAccessible(fieldOfId);
        return ReflectionUtils.getField(fieldOfId, t);
    }

    /**
     * 獲取Class標有@Id註解的字段名稱
     *
     * @return id字段名稱
     */
    protected Field getIdField() {

        Class<T> clz = getTypeArguement();
        Field[] fields = clz.getDeclaredFields();
        for (Field field : fields) {
            Id annotation = field.getAnnotation(Id.class);

            if (annotation != null) {
                return field;
            }
        }

        log.error("PO類未設置@Id註解");
        throw new BusinessException("PO類未設置@Id註解");
    }

    /**
     * 轉換Canal的FlatMessage中data成泛型對象
     *
     * @param flatMessage Canal發送MQ信息
     * @return 泛型對象集合
     */
    protected Set<T> getData(FlatMessage flatMessage) {
        List<Map<String, String>> sourceData = flatMessage.getData();
        Set<T> targetData = Sets.newHashSetWithExpectedSize(sourceData.size());
        for (Map<String, String> map : sourceData) {
            T t = JsonUtil.mapToPojo(map, getTypeArguement());
            targetData.add(t);
        }
        return targetData;
    }

}

canal消息的訂閱代碼

rocketMQ是支持廣播消費的,只需要在消費端進行配置即可,默認情況下使用的是集羣消費,這就意味着如果我們配置了多個消費者實例,只會有一個實例消費消息。

對於更新Redis來說,一個實例消費消息,完成redis的更新,這就夠了。

package com.crazymaker.springcloud.stock.consumer;

import com.alibaba.otter.canal.protocol.FlatMessage;
import com.crazymaker.springcloud.seckill.dao.po.SeckillGoodPO;
import com.google.common.collect.Sets;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Set;

@Slf4j
@Service
//廣播模式
//@RocketMQMessageListener(topic = "seckillgood", consumerGroup = "UpdateRedis", messageModel = MessageModel.BROADCASTING)
//集羣模式
@RocketMQMessageListener(topic = "seckillgood", consumerGroup = "UpdateRedis")
@Data
public class UpdateRedisGoodConsumer extends AbstractCanalMQ2RedisService<SeckillGoodPO> implements RocketMQListener<FlatMessage> {

    private String modelName = "seckillgood";

    @Override
    public void onMessage(FlatMessage s) {
        process(s);
    }

//    @Cacheable(cacheNames = {"seckill"}, key = "'seckillgood:' + #goodId")

    /**
     * 封裝redis的key
     *
     * @param t 原對象
     * @return key
     */
    protected String getWrapRedisKey(SeckillGoodPO t) {
        return new StringBuilder()
//                .append(ApplicationContextHolder.getApplicationName())
                .append("seckill")
                .append(":")
//                .append(getModelName())
                .append("seckillgood")
                .append(":")
                .append(t.getId())
                .toString();

    }

    /**
     * 轉換Canal的FlatMessage中data成泛型對象
     *
     * @param flatMessage Canal發送MQ信息
     * @return 泛型對象集合
     */
    protected Set<SeckillGoodPO> getData(FlatMessage flatMessage) {
        List<Map<String, String>> sourceData = flatMessage.getData();
        Set<SeckillGoodPO> targetData = Sets.newHashSetWithExpectedSize(sourceData.size());
        for (Map<String, String> map : sourceData) {
            SeckillGoodPO po = new SeckillGoodPO();
            po.setId(Long.valueOf(map.get("id")));
            //省略其他的屬性
            targetData.add(po);
        }
        return targetData;
    }

}

注意事項

根據需要可以重寫裏面的方法,DDL處理暫時還沒完成,只是整個Demo,完整的實戰活兒,還是留給大家自己幹吧。

尼恩的忠實建議:

  • 理論水平的提升,看看視頻、看看書,只有兩個字,就是需要:多看。
  • 實戰水平的提升,只有兩個字,就是需要:多幹。

L2級緩存與數據庫的數據一致性

集中式緩存需要考慮的問題

瞭解到了我們爲什麼要使用緩存,以及緩存能解決我們什麼樣的問題。但是使用緩存時也需要注意一些問題:

如果只是單純的整合Redis緩存,那麼可能出現如下的問題

  • 熱點數據的大量訪問,能對系統造成各種網絡開銷,影響系統的性能
  • 一旦集中式緩存發生雪崩了,或者緩存被擊穿了,能造成數據庫的壓力增大,可能會被打死,造成數據庫掛機狀態,進而造成服務宕機
  • 緩存雪崩,訪問全部打在數據庫上,數據庫也可能會被打死

爲了解決以上可能出現的問題,讓緩存層更穩定,健壯,我們使用二級緩存架構

  • 1級爲本地緩存,或者進程內的緩存(如 Ehcache) —— 速度快,進程內可用
  • 2級爲集中式緩存(如 Redis)—— 可同時爲多節點提供服務
二級緩存架構圖:

在這裏插入圖片描述

爲什麼要引入本地緩存

相對於IO操作 速度快,效率高 相對於Redis Redis是一種優秀的分佈式緩存實現,受限於網卡等原因,遠水救不了近火

所以:

DB + Redis + LocalCache = 高效存儲,高效訪問

本地緩存的適用場景

本地緩存一般適合於緩存只讀、量少、高頻率訪問的數據。如秒殺商品數據。

或者每個部署節點獨立的數據,如長連接服務中,每個部署節點由於都是維護了不同的連接,每個連接的數據都是獨立的,並且隨着連接的斷開而刪除。如果數據在集羣的不同部署節點需要共享和保持一致,則需要使用分佈式緩存來統一存儲,實現應用集羣的所有應用進程都在該統一的分佈式緩存中進行數據存取即可。

本地緩存的優缺點

1. 訪問速度快,但無法進行大數據存儲
  • 本地緩存位於同一個JVM的堆中,相對於分佈式緩存的好處是,故性能更好,減少了跨網絡傳輸,
  • 但是本地緩存由於佔用 JVM 內存空間 (或者進程的內存空間),故不能進行大數據量的數據存儲。
2. 數據一致性問題

本地緩存只支持被該應用進程訪問,一般無法被其他應用進程訪問,如果對應的數據庫數據,存在數據更新,則需要同步更新不同節點的本地緩存副本,來保證數據一致性

本地緩存的更新,複雜度較高並且容易出錯,如基於 Redis 的發佈訂閱機制、或者消息隊列MQ來同步更新各個部署節點。

數據庫、本地緩存及分佈式緩存的區別

數據庫 本地緩存 分佈式緩存
存儲位置 存盤,數據不丟失 不存盤,之前的數據丟失 不存盤,數據丟失
持久化 可以 不可以 不可以
訪問速度 最快
可擴展 可存在其他機器的硬盤 只能存在本機內存 可存在其他機器的內存
使用場景 需要實現持久化保存 需要快速訪問,但需要考慮內存大小 1)需要快速訪問,不需要考慮內存大小 2)需要實現持久化,但會丟失一些數據 3)需要讓緩存集中在一起,訪問任一機器上內存中的數據都可以從緩存中得到

本地緩存與集中式緩存的結合的需求場景

單獨使用本地緩存與集中式緩存,都會有各自的短板。

  1. 使用本地緩存時,一旦應用重啓後,由於緩存數據丟失,緩存雪崩,給數據庫造成巨大壓力,導致應用堵塞
  2. 使用本地緩存時,多個應用節點無法共享緩存數據
  3. 使用集中式緩存,由於大量的數據通過緩存獲取,導致緩存服務的數據吞吐量太大,帶寬跑滿。現象就是 Redis 服務負載不高,但是由於機器網卡帶寬跑滿,導致數據讀取非常慢

有這麼一個網站,某個頁面每天的訪問量是 1000萬,每個頁面從緩存讀取的數據是 50K。緩存數據存放在一個 Redis 服務,機器使用千兆網卡。那麼這個 Redis 一天要承受 500G 的數據流,相當於平均每秒鐘是 5.78M 的數據。而網站一般都會有高峯期和低峯期,兩個時間流量的差異可能是百倍以上。我們假設高峯期每秒要承受的流量比平均值高 50 倍,也就是說高峯期 Redis 服務每秒要傳輸超過 250 兆的數據。請注意這個 250 兆的單位是 byte,而千兆網卡的單位是“bit” ,你懂了嗎? 這已經遠遠超過 Redis 服務的網卡帶寬。

所以如果你能發現這樣的問題,一般你會這麼做:

  1. 升級到萬兆網卡 —— 這個有多麻煩,相信很多人知道,特別是一些雲主機根本沒有萬兆網卡給你使用(有些運維工程師會給這樣的建議)
  2. 多個 Redis 搭建集羣,將流量分攤多多臺機器上

如果你採用第2種方法來解決上述的場景中碰到的問題,那麼你最好準備 5 個 Redis 服務來支撐。

在緩存服務這塊成本直接攀升了 5 倍。你有錢當然沒任何問題,但是結構就變得非常複雜了,而且可能你緩存的數據量其實不大,1000 萬高頻次的緩存讀寫 Redis 也能輕鬆應付,可是因爲帶寬的問題,你不得不付出 5 倍的成本。

按照80/20原則,如果我們把20%的熱點數據,放在本地緩存,如果我們不用每次頁面訪問的時候都去 Redis 讀取數據,那麼 Redis 上的數據流量至少降低 80%的帶寬流量,甚至於一個很小的 Redis 集羣可以輕鬆應付。

本地緩存與集中式緩存的結合的使用案例

秒殺的商品數據

作爲需要超高併發的訪問數據,屬於 20% 的熱點數據

這屬於提前預測靜態熱點數據類型。

億級IM系統中用戶路由數據

具體參參見瘋狂創客圈的 億級 IM中臺實戰

這屬於提前預測靜態熱點數據類型。

通過流計算識別出來的熱點數據

還有的是提前不能識別出來的,如電商系統中的熱點商品那就完美了。

通過流計算識別出來的熱點數據,能夠動態地實時發現熱點。

這屬於實時預測動態熱點數據類型。由於數據量大,可以通過流計算框架 storm 或者 fink 實現,

不夠,此項工作,一般屬於大數據團隊的工作。

本地緩存與集中式緩存的2級緩存架構

第一級緩存使用內存(同時支持 Ehcache 2.x、Ehcache 3.x 、Guava、 Caffeine),第二級緩存使用 Redis(推薦)/Memcached

本地緩存與集中式緩存的結合架構,大致的架構圖,如下:

在這裏插入圖片描述

L2級緩存的數據讀取和更新

讀取流程

在這裏插入圖片描述

數據更新

通過消息隊列,或者其他廣播模式的發佈訂閱,保持各個一級緩存的數據一致性。

這一點,與Cache-Aside模式不同,Cache-Aside只是刪除緩存即可。但是熱點數據,如果刪除,很容易導致緩存擊穿。

對於秒殺這樣的場景,瞬間有十幾萬甚至上百萬的請求要同時讀取商品。如果沒有緩存,每一個請求連帶的數據操作都需要應用與數據庫生成connection,而數據庫的最大連接數是有限的,一旦超過數據庫會直接宕機。這就是緩存擊穿。

緩存擊穿與 緩存穿透的簡單區別:

  • 緩存擊穿是指數據庫中有數據,但是緩存中沒有,大量的請求打到數據庫;
  • 緩存穿透是指緩存和數據庫中都沒有的數據,而用戶不斷髮起請求,如發起爲id爲“-1”的數據或id爲特別大不存在的數據。這時的用戶很可能是攻擊者,攻擊會導致數據庫壓力過大。

二級緩存緩存擊穿解決方案:

  1. 設置熱點數據永遠不過期。
  2. 如果過期則或者在快過期之前更新,如有變化,主動刷新緩存數據,同時也能保障數據一致性
  3. 加互斥鎖,保障緩存中的數據,被第一次請求回填。此方案不適用於超高併發場景

L2級緩存與數據庫的數據一致性保障方案:

  • 方案1:biglog同步保障數據一致性
  • 方案2:使用程序方式發送更新消息,保障數據一致性

方案1:biglog同步保障數據一致性的架構:

方案1,可以通過biglog同步,來保障二級緩存的數據一致性,具體的架構如下

在這裏插入圖片描述

rocketMQ是支持廣播消費的,只需要在消費端進行配置即可,rocketMQ默認情況下使用的是集羣消費,這就意味着如果我們配置了多個消費者實例,只會有一個實例消費消息。

對於更新Redis來說,一個實例消費消息,完成redis的更新,這就夠了。

對於更新Guava或者其他1級緩存來說,一個實例消費消息,是不夠的,需要每一個實例都消息,所以,必須設置 rocketMQ 客戶端的消費模式,爲 廣播模式;

@RocketMQMessageListener(topic = "seckillgood", consumerGroup = "UpdateGuava", messageModel = MessageModel.BROADCASTING)

方案2:使用程序方式保障數據一致性的架構

使用程序方式保障數據一致性的架構,可以編寫一個通用的2級緩存通用組件,當數據更新的時候,去發送消息,具體的架構如下:

在這裏插入圖片描述

方案2和方案1 的區別

方案2和方案1 的整體區別不大,只不過 方案2 需要自己寫代碼(或者中間組件)發送數據的變化通知。

方案1 的一個優勢:可以和 建立索引等其他的消費者,共用binlog的消息隊列。

其他的區別,大家可以自行探索。

三級緩存與數據一致性

對於高併發的請求,接入層Nginx有着巨大的作用,能反向代理,負載均衡,動靜分離以及和Lua整合,可以實現請求定向分發等非常有用的功能,同理Nginx層可以實現緩存的功能

可以利用接入層Nginx的進程內緩存,緩存極熱數據的高併發訪問,在接入層,當請求過來時,判斷本地緩存中是否存在,如果存在着直接返回請求結果(或者展現靜態資源的數據),這樣的請求不會直接發送到後端服務層

爲了解決以上可能出現的問題,讓緩存層更穩定,健壯,我們引入三級緩存架構

  • 1級爲本地緩存,或者進程內的緩存(如 Ehcache) —— 速度快,進程內可用
  • 2級爲集中式緩存(如 Redis)—— 可同時爲多節點提供服務
  • 3級爲接入層Nginx本地緩存—— 速度快,進程內可用

三級緩存的架構

三級緩存架構 圖: 具體如下圖所示

在這裏插入圖片描述

使用Nginx Lua共享字典作爲L3本地緩存

lua_shared_dict 指令介紹

原文: lua_shared_dict

syntax:lua_shared_dict <name> <size>
default: no
context: http
phase: depends on usage

聲明一個共享內存區域 name,以充當基於 Lua 字典 ngx.shared.<name> 的共享存儲。

lua_shared_dict 指令定義的共享內存總是被當前 Nginx 服務器實例中所有的 Nginx worker 進程所共享。

size 參數接受大小單位,如 k,m:

http {
    #指定緩存信息
  lua_shared_dict seckill_cache 128m;
    ...
}

詳細參見: ngx.shared.DICT

Lua共享內存的使用

然後在lua腳本中使用:

local shared_memory = ngx.shared.seckill_cache

即可以取到放在共享內存中的數據。對共享內存的操作也是如set ,get 之類。

--優先從緩存獲取,否則訪問上游接口
local seckill_cache = ngx.shared.seckill_cache
local goodIdCacheKey = "goodId_" .. goodId
local goodCache = seckill_cache:get(goodIdCacheKey)

if goodCache == "" or goodCache == nil then

    ngx.log(ngx.DEBUG,"cache not hited " .. goodId)

    --回源上游接口,比如Java 後端rest接口
    local res = ngx.location.capture("/stock-provider/api/seckill/good/detail/v1", {
        method = ngx.HTTP_POST,
        -- args = requestBody ,  -- 重要:將請求參數,原樣向上遊傳遞
        always_forward_body = false, -- 也可以設置爲false 僅轉發put和post請求方式中的body.
    })

    --返回上游接口的響應體 body
    goodCache = res.body;

    --單位爲s
    seckill_cache:set(goodIdCacheKey, goodCache, 10 * 60 * 60)

end
ngx.say(goodCache);

Lua共享內存的淘汰機制

ngx.shared.DICT的實現是採用紅黑樹實現,當申請的緩存被佔用完後如果有新數據需要存儲則採用 LRU 算法淘汰掉“多餘”數據。

LRU原理

LRU的設計原理就是,當數據在最近一段時間經常被訪問,那麼它在以後也會經常被訪問。這就意味着,如果經常訪問的數據,我們需要然其能夠快速命中,而不常訪問的數據,我們在容量超出限制內,要將其淘汰。

L3本地緩存的優缺點

L3與L2一樣,都是本地緩存,優點和缺點如下:

1. 訪問速度快,但無法進行大數據存儲
  • 本地緩存位於同一個JVM的堆中,相對於分佈式緩存的好處是,故性能更好,減少了跨網絡傳輸,
  • 但是本地緩存由於佔用 JVM 內存空間 (或者進程的內存空間),故不能進行大數據量的數據存儲。
2. 數據一致性問題

本地緩存只支持被該應用進程訪問,一般無法被其他應用進程訪問,如果對應的數據庫數據,存在數據更新,則需要同步更新不同節點的本地緩存副本,來保證數據一致性

本地緩存的更新,複雜度較高並且容易出錯,如基於 Redis 的發佈訂閱機制、或者消息隊列MQ來同步更新各個部署節點。

L3級緩存的數據一致性保障

L3級緩存主要用於極熱數據,如秒殺的商品數據(對於秒殺這樣的場景,瞬間有十幾萬甚至上百萬的請求要同時讀取商品。如果沒有命中本地緩存,可能導致緩存擊穿。

緩存擊穿與 緩存穿透的簡單區別:

  • 緩存擊穿是指數據庫中有數據,但是緩存中沒有,大量的請求打到數據庫;
  • 緩存穿透是指緩存和數據庫中都沒有的數據,而用戶不斷髮起請求,如發起爲id爲“-1”的數據或id爲特別大不存在的數據。這時的用戶很可能是攻擊者,攻擊會導致數據庫壓力過大。

爲了防止緩存擊穿,同時也保持數據一致性,具體的方案爲:

L3級緩存的數據一致性保障以及防止緩存擊穿方案:

1.數據預熱(或者叫預加載)

2.設置熱點數據永遠不過期,通過 ngx.shared.DICT的緩存的LRU機制去淘汰

3.如果緩存主動更新,在快過期之前更新,如有變化,通過訂閱變化的機制,主動本地刷新

4.提供兜底方案,如果本地緩存沒有,則通過後端服務獲取數據,然後緩存起來

參考文獻

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

https://www.bilibili.com/read/cv12567436/

https://blog.csdn.net/Aquester/article/details/85936568

https://jishuin.proginn.com/p/763bfbd6d2ab

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章