徹底搞懂 Redis 主從復(fù)制機(jī)制

主從復(fù)制概述

主從復(fù)制,是指將一臺(tái)Redis服務(wù)器的數(shù)據(jù),復(fù)制到其他的Redis服務(wù)器。前者稱為主節(jié)點(diǎn)(master),后者稱為從節(jié)點(diǎn)(slave);

數(shù)據(jù)的復(fù)制是單向的,只能由主節(jié)點(diǎn)到從節(jié)點(diǎn)。

默認(rèn)情況下,每臺(tái)Redis服務(wù)器都是主節(jié)點(diǎn),且一個(gè)主節(jié)點(diǎn)可以有多個(gè)從節(jié)點(diǎn)(或沒有從節(jié)點(diǎn)),但一個(gè)從節(jié)點(diǎn)只能有一個(gè)主節(jié)點(diǎn)。

目前很多中小企業(yè)都沒有使用到 Redis 的集群,但是至少都做了主從。有了主從,當(dāng) master 掛掉的時(shí)候,運(yùn)維讓從庫過來接管,服務(wù)就可以繼續(xù),否則 master 需要經(jīng)過數(shù)據(jù)恢復(fù)和重啟的過程,這就可能會(huì)拖很長的時(shí)間,影響線上業(yè)務(wù)的持續(xù)服務(wù)。

Redis主從復(fù)制服務(wù)器架構(gòu)圖如下:

Redis主從復(fù)制架構(gòu)圖

主從復(fù)制的作用

主從復(fù)制的作用主要包括:

  • 數(shù)據(jù)冗余:主從復(fù)制實(shí)現(xiàn)了數(shù)據(jù)的熱備份,是持久化之外的一種數(shù)據(jù)冗余方式。
  • 故障恢復(fù):當(dāng)主節(jié)點(diǎn)出現(xiàn)問題時(shí),可以由從節(jié)點(diǎn)提供服務(wù),實(shí)現(xiàn)快速的故障恢復(fù);實(shí)際上是一種服務(wù)的冗余。
  • 負(fù)載均衡:在主從復(fù)制的基礎(chǔ)上,配合讀寫分離,可以由主節(jié)點(diǎn)提供寫服務(wù),由從節(jié)點(diǎn)提供讀服務(wù)(即寫Redis數(shù)據(jù)時(shí)應(yīng)用連接主節(jié)點(diǎn),讀Redis數(shù)據(jù)時(shí)應(yīng)用連接從節(jié)點(diǎn)),分擔(dān)服務(wù)器負(fù)載;尤其是在寫少讀多的場景下,通過多個(gè)從節(jié)點(diǎn)分擔(dān)讀負(fù)載,可以大大提高Redis服務(wù)器的并發(fā)量。
  • 高可用基石:主從復(fù)制還是哨兵和集群能夠?qū)嵤┑幕A(chǔ),因此說主從復(fù)制是Redis高可用的基礎(chǔ)。

CAP 原理

在了解 Redis 的主從復(fù)制之前,讓我們先來理解一下現(xiàn)代分布式系統(tǒng)的理論基石——CAP 原理。

CAP 原理就好比分布式領(lǐng)域的牛頓定律,它是分布式存儲(chǔ)的理論基石。自打 CAP 的論文發(fā)表之后,分布式存儲(chǔ)中間件猶如雨后春筍般一個(gè)一個(gè)涌現(xiàn)出來。理解這個(gè)原理其實(shí)很簡單,本節(jié)我們首先對這個(gè)原理進(jìn)行一些簡單的講解。

  • C - Consistent ,一致性
  • A - Availability ,可用性
  • P - Partition tolerance ,分區(qū)容忍性
    分布式系統(tǒng)的節(jié)點(diǎn)往往都是分布在不同的機(jī)器上進(jìn)行網(wǎng)絡(luò)隔離開的,這意味著必然會(huì)有網(wǎng)絡(luò)斷開的風(fēng)險(xiǎn),這個(gè)網(wǎng)絡(luò)斷開的場景的專業(yè)詞匯叫著「網(wǎng)絡(luò)分區(qū)」。

在網(wǎng)絡(luò)分區(qū)發(fā)生時(shí),兩個(gè)分布式節(jié)點(diǎn)之間無法進(jìn)行通信,我們對一個(gè)節(jié)點(diǎn)進(jìn)行的修改操作將無法同步到另外一個(gè)節(jié)點(diǎn),所以數(shù)據(jù)的「一致性」將無法滿足,因?yàn)閮蓚€(gè)分布式節(jié)點(diǎn)的數(shù)據(jù)不再保持一致。除非我們犧牲「可用性」,也就是暫停分布式節(jié)點(diǎn)服務(wù),在網(wǎng)絡(luò)分區(qū)發(fā)生時(shí),不再提供修改數(shù)據(jù)的功能,直到網(wǎng)絡(luò)狀況完全恢復(fù)正常再繼續(xù)對外提供服務(wù)。

一句話概括 CAP 原理就是——網(wǎng)絡(luò)分區(qū)發(fā)生時(shí),一致性和可用性兩難全。

Redis 主從同步最終一致性

Redis 的主從數(shù)據(jù)是異步同步的,所以分布式的 Redis 系統(tǒng)并不滿足「一致性」要求。當(dāng)客戶端在 Redis 的主節(jié)點(diǎn)修改了數(shù)據(jù)后,立即返回,即使在主從網(wǎng)絡(luò)斷開的情況下,主節(jié)點(diǎn)依舊可以正常對外提供修改服務(wù),所以 Redis 滿足「可用性」。

Redis 保證「最終一致性」,從節(jié)點(diǎn)會(huì)努力追趕主節(jié)點(diǎn),最終從節(jié)點(diǎn)的狀態(tài)會(huì)和主節(jié)點(diǎn)的狀態(tài)將保持一致。如果網(wǎng)絡(luò)斷開了,主從節(jié)點(diǎn)的數(shù)據(jù)將會(huì)出現(xiàn)大量不一致,一旦網(wǎng)絡(luò)恢復(fù),從節(jié)點(diǎn)會(huì)采用多種策略努力追趕上落后的數(shù)據(jù),繼續(xù)盡力保持和主節(jié)點(diǎn)一致。

主從復(fù)制的實(shí)現(xiàn)原理

總的來說主從復(fù)制功能的詳細(xì)步驟可以分為7個(gè)步驟:

  1. 設(shè)置主節(jié)點(diǎn)的地址和端口
  2. 建立套接字連接
  3. 發(fā)送PING命令
  4. 權(quán)限驗(yàn)證
  5. 同步
  6. 命令傳播

接下來分別敘述每個(gè)步驟,整個(gè)流程圖如下:


在這里插入圖片描述

為了測試,我在本地機(jī)開啟兩個(gè)Redis節(jié)點(diǎn),分別監(jiān)聽:
127.0.0.1 6379(主)
127.0.0.1 6380(從)


在這里插入圖片描述

1. 設(shè)置主服務(wù)器的地址和端口

第一步首先是在從服務(wù)器設(shè)置需要同步的主服務(wù)器信息,包括機(jī)器IP, 端口。
主從復(fù)制的開啟,完全是在從節(jié)點(diǎn)發(fā)起的;不需要我們在主節(jié)點(diǎn)做任何事情。

從節(jié)點(diǎn)開啟主從復(fù)制,有3種方式:

(1)配置文件

在從服務(wù)器的配置文件中加入:slaveof masterip masterport

(2)啟動(dòng)命令

redis-server啟動(dòng)命令后加入 --slaveof masterip masterport

(3)客戶端命令

Redis服務(wù)器啟動(dòng)后,直接通過客戶端執(zhí)行命令:slaveof masterip masterport,則該Redis實(shí)例成為從節(jié)點(diǎn)。

上述3種方式是等效的,下面以客戶端命令的方式為例,看一下當(dāng)執(zhí)行了slaveof后,Redis主節(jié)點(diǎn)和從節(jié)點(diǎn)的變化。

完成上面的配置后, 從服務(wù)器會(huì)將主服務(wù)器的ip地址和端口號(hào)保存到服務(wù)器狀態(tài)的屬性里面??梢訰edis使用info Replication 命令分別查看從服務(wù)器和主服務(wù)器的主從信息

2. 建立套接字連接

在slaveof命令執(zhí)行之后,從服務(wù)器會(huì)根據(jù)設(shè)置的ip和端口,向主服務(wù)器簡歷socket連接。
在6380從服務(wù)器里面執(zhí)行完slave of 127.0.0.1 6379后意味著,從服務(wù)器向主服務(wù)器發(fā)起socket連接
在執(zhí)行info Replication 命令,可以看到6380服務(wù)器的角色是slave了

我們在

而6379 服務(wù)器已經(jīng)成為主服務(wù)器角色:
在這里插入圖片描述

3. 發(fā)送PING命令

從節(jié)點(diǎn)成為主節(jié)點(diǎn)的客戶端之后,發(fā)送ping命令進(jìn)行首次請求,目的是:檢查socket連接是否可用,以及主節(jié)點(diǎn)當(dāng)前是否能夠處理請求。

從節(jié)點(diǎn)發(fā)送ping命令后,可能出現(xiàn)3種情況:

(1)返回pong:說明socket連接正常,且主節(jié)點(diǎn)當(dāng)前可以處理請求,復(fù)制過程繼續(xù)。

(2)超時(shí):一定時(shí)間后從節(jié)點(diǎn)仍未收到主節(jié)點(diǎn)的回復(fù),說明socket連接不可用,則從節(jié)點(diǎn)斷開socket連接,并重連。

(3)返回pong以外的結(jié)果:如果主節(jié)點(diǎn)返回其他結(jié)果,如正在處理超時(shí)運(yùn)行的腳本,說明主節(jié)點(diǎn)當(dāng)前無法處理命令,則從節(jié)點(diǎn)斷開socket連接,并重連。

主從發(fā)送PING命令流程圖如下:


在這里插入圖片描述

4. 身份驗(yàn)證

如果從節(jié)點(diǎn)中設(shè)置了masterauth選項(xiàng),則從節(jié)點(diǎn)需要向主節(jié)點(diǎn)進(jìn)行身份驗(yàn)證;沒有設(shè)置該選項(xiàng),則不需要驗(yàn)證。從節(jié)點(diǎn)進(jìn)行身份驗(yàn)證是通過向主節(jié)點(diǎn)發(fā)送auth命令進(jìn)行的,auth命令的參數(shù)即為配置文件中的masterauth的值。

如果主節(jié)點(diǎn)設(shè)置密碼的狀態(tài),與從節(jié)點(diǎn)masterauth的狀態(tài)一致(一致是指都存在,且密碼相同,或者都不存在),則身份驗(yàn)證通過,復(fù)制過程繼續(xù);如果不一致,則從節(jié)點(diǎn)斷開socket連接,并重連。

主從身份驗(yàn)證流程圖如下:


在這里插入圖片描述

5. 同步

同步就是將從節(jié)點(diǎn)的數(shù)據(jù)庫狀態(tài)更新成主節(jié)點(diǎn)當(dāng)前的數(shù)據(jù)庫狀態(tài)。具體執(zhí)行的方式是:從節(jié)點(diǎn)向主節(jié)點(diǎn)發(fā)送psync命令(Redis2.8以前是sync命令),開始同步。
數(shù)據(jù)同步階段是主從復(fù)制最核心的階段,根據(jù)主從節(jié)點(diǎn)當(dāng)前狀態(tài)的不同,可以分為全量復(fù)制部分復(fù)制
下面會(huì)有詳細(xì)介紹全量復(fù)制和部分復(fù)制內(nèi)容,這里暫不詳述

6. 命令傳播

經(jīng)過上面同步操作,此時(shí)主從的數(shù)據(jù)庫狀態(tài)其實(shí)已經(jīng)一致了,但這種一致的狀態(tài)的并不是一成不變的。
在完成同步之后,也許主服務(wù)器馬上就接受到了新的寫命令,執(zhí)行完該命令后,主從的數(shù)據(jù)庫狀態(tài)又不一致。

數(shù)據(jù)同步階段完成后,主從節(jié)點(diǎn)進(jìn)入命令傳播階段;在這個(gè)階段主節(jié)點(diǎn)將自己執(zhí)行的寫命令發(fā)送給從節(jié)點(diǎn),從節(jié)點(diǎn)接收命令并執(zhí)行,從而保證主從節(jié)點(diǎn)數(shù)據(jù)的一致性。

另外命令轉(zhuǎn)播我們需要關(guān)注兩個(gè)點(diǎn): 延遲與不一致心跳機(jī)制 我們下面介紹一下

延遲與不一致
需要注意的是,命令傳播是異步的過程,即主節(jié)點(diǎn)發(fā)送寫命令后并不會(huì)等待從節(jié)點(diǎn)的回復(fù);因此實(shí)際上主從節(jié)點(diǎn)之間很難保持實(shí)時(shí)的一致性,延遲在所難免。數(shù)據(jù)不一致的程度,與主從節(jié)點(diǎn)之間的網(wǎng)絡(luò)狀況、主節(jié)點(diǎn)寫命令的執(zhí)行頻率、以及主節(jié)點(diǎn)中的repl-disable-tcp-nodelay配置等有關(guān)。

repl-disable-tcp-nodelay 配置如下:

  • 假如設(shè)置成yes,則redis會(huì)合并小的TCP包從而節(jié)省帶寬,但會(huì)增加同步延遲(40ms),造成master與slave數(shù)據(jù)不一致
  • 假如設(shè)置成no,則redis master會(huì)立即發(fā)送同步數(shù)據(jù),沒有延遲

概括來說就是:前者關(guān)注性能,后者關(guān)注一致性

具體發(fā)送頻率與Linux內(nèi)核的配置有關(guān),默認(rèn)配置為40ms。當(dāng)設(shè)置為no時(shí),TCP會(huì)立馬將主節(jié)點(diǎn)的數(shù)據(jù)發(fā)送給從節(jié)點(diǎn),帶寬增加但延遲變小。

一般來說,只有當(dāng)應(yīng)用對Redis數(shù)據(jù)不一致的容忍度較高,且主從節(jié)點(diǎn)之間網(wǎng)絡(luò)狀況不好時(shí),才會(huì)設(shè)置為yes;多數(shù)情況使用默認(rèn)值no

Redis是如何保證主從服務(wù)器一致處于連接狀態(tài)以及命令是否丟失?
答:命令傳播階段,從服務(wù)器會(huì)利用心跳檢測機(jī)制定時(shí)的向主服務(wù)發(fā)送消息。
心跳機(jī)制我們下面再詳細(xì)說。

全量復(fù)制和部分復(fù)制

在Redis2.8以前,從節(jié)點(diǎn)向主節(jié)點(diǎn)發(fā)送sync命令請求同步數(shù)據(jù),此時(shí)的同步方式是全量復(fù)制;在Redis2.8及以后,從節(jié)點(diǎn)可以發(fā)送psync命令請求同步數(shù)據(jù),此時(shí)根據(jù)主從節(jié)點(diǎn)當(dāng)前狀態(tài)的不同,同步方式可能是全量復(fù)制或部分復(fù)制。后文介紹以Redis2.8及以后版本為例。

  1. 全量復(fù)制:用于初次復(fù)制或其他無法進(jìn)行部分復(fù)制的情況,將主節(jié)點(diǎn)中的所有數(shù)據(jù)都發(fā)送給從節(jié)點(diǎn),是一個(gè)非常重型的操作。
  2. 部分復(fù)制:用于網(wǎng)絡(luò)中斷等情況后的復(fù)制,只將中斷期間主節(jié)點(diǎn)執(zhí)行的寫命令發(fā)送給從節(jié)點(diǎn),與全量復(fù)制相比更加高效。需要注意的是,如果網(wǎng)絡(luò)中斷時(shí)間過長,導(dǎo)致主節(jié)點(diǎn)沒有能夠完整地保存中斷期間執(zhí)行的寫命令,則無法進(jìn)行部分復(fù)制,仍使用全量復(fù)制。

全量復(fù)制

Redis通過psync命令進(jìn)行全量復(fù)制的過程如下:

(1)從節(jié)點(diǎn)判斷無法進(jìn)行部分復(fù)制,向主節(jié)點(diǎn)發(fā)送全量復(fù)制的請求;或從節(jié)點(diǎn)發(fā)送部分復(fù)制的請求,但主節(jié)點(diǎn)判斷無法進(jìn)行部分復(fù)制;具體判斷過程需要在講述了部分復(fù)制原理后再介紹。

(2)主節(jié)點(diǎn)收到全量復(fù)制的命令后,執(zhí)行bgsave,在后臺(tái)生成RDB文件,并使用一個(gè)緩沖區(qū)(稱為復(fù)制緩沖區(qū))記錄從現(xiàn)在開始執(zhí)行的所有寫命令

(3)主節(jié)點(diǎn)的bgsave執(zhí)行完成后,將RDB文件發(fā)送給從節(jié)點(diǎn);從節(jié)點(diǎn)首先清除自己的舊數(shù)據(jù),然后載入接收的RDB文件,將數(shù)據(jù)庫狀態(tài)更新至主節(jié)點(diǎn)執(zhí)行bgsave時(shí)的數(shù)據(jù)庫狀態(tài)

(4)主節(jié)點(diǎn)將前述復(fù)制緩沖區(qū)中的所有寫命令發(fā)送給從節(jié)點(diǎn),從節(jié)點(diǎn)執(zhí)行這些寫命令,將數(shù)據(jù)庫狀態(tài)更新至主節(jié)點(diǎn)的最新狀態(tài)

(5)如果從節(jié)點(diǎn)開啟了AOF,則會(huì)觸發(fā)bgrewriteaof的執(zhí)行,從而保證AOF文件更新至主節(jié)點(diǎn)的最新狀態(tài)


Redis全量復(fù)制流程

通過全量復(fù)制的過程可以看出,全量復(fù)制是非常重型的操作:

(1)主節(jié)點(diǎn)通過bgsave命令fork子進(jìn)程進(jìn)行RDB持久化,該過程是非常消耗CPU、內(nèi)存(頁表復(fù)制)、硬盤IO的;關(guān)于bgsave的性能問題,可以參考我另外一篇文章: 深入剖析Redis高可用系列:持久化 AOF和RDB

(2)主節(jié)點(diǎn)通過網(wǎng)絡(luò)將RDB文件發(fā)送給從節(jié)點(diǎn),對主從節(jié)點(diǎn)的帶寬都會(huì)帶來很大的消耗

(3)從節(jié)點(diǎn)清空老數(shù)據(jù)、載入新RDB文件的過程是阻塞的,無法響應(yīng)客戶端的命令;如果從節(jié)點(diǎn)執(zhí)行bgrewriteaof,也會(huì)帶來額外的消耗

部分復(fù)制

由于全量復(fù)制在主節(jié)點(diǎn)數(shù)據(jù)量較大時(shí)效率太低,因此Redis2.8開始提供部分復(fù)制,用于處理網(wǎng)絡(luò)中斷時(shí)的數(shù)據(jù)同步。

部分復(fù)制的實(shí)現(xiàn),依賴于三個(gè)重要的概念:

  1. 復(fù)制偏移量
  2. 復(fù)制積壓緩沖區(qū)
  3. 服務(wù)器運(yùn)行ID(runid)
    下面我們分別講解一下這三個(gè)概念:

1. 復(fù)制偏移量:

執(zhí)行復(fù)制的雙方,主從節(jié)點(diǎn),分別會(huì)維護(hù)一個(gè)復(fù)制偏移量offset:
主節(jié)點(diǎn)每次向從節(jié)點(diǎn)同步了N字節(jié)數(shù)據(jù)后,將修改自己的復(fù)制偏移量offset+N
從節(jié)點(diǎn)每次從主節(jié)點(diǎn)同步了N字節(jié)數(shù)據(jù)后,將修改自己的復(fù)制偏移量offset+N

offset用于判斷主從節(jié)點(diǎn)的數(shù)據(jù)庫狀態(tài)是否一致:
如果二者offset相同,則一致;
如果offset不同,則不一致,此時(shí)可以根據(jù)兩個(gè)offset找出從節(jié)點(diǎn)缺少的那部分?jǐn)?shù)據(jù)。

例如,如果主節(jié)點(diǎn)的offset是1000,而從節(jié)點(diǎn)的offset是500,那么部分復(fù)制就需要將offset為501-1000的數(shù)據(jù)傳遞給從節(jié)點(diǎn)。而offset為501-1000的數(shù)據(jù)存儲(chǔ)的位置,就是下面要介紹的復(fù)制積壓緩沖區(qū)。

2. 復(fù)制積壓緩沖區(qū):

主節(jié)點(diǎn)內(nèi)部維護(hù)了一個(gè)固定長度的、先進(jìn)先出(FIFO)隊(duì)列 作為復(fù)制積壓緩沖區(qū),其默認(rèn)大小為1MB
在主節(jié)點(diǎn)進(jìn)行命令傳播時(shí),不僅會(huì)將寫命令同步到從節(jié)點(diǎn),還會(huì)將寫命令寫入復(fù)制積壓緩沖區(qū)。

由于復(fù)制積壓緩沖區(qū)定長且是先進(jìn)先出,所以它保存的是主節(jié)點(diǎn)最近執(zhí)行的寫命令;時(shí)間較早的寫命令會(huì)被擠出緩沖區(qū)。因此,當(dāng)主從節(jié)點(diǎn)offset的差距過大超過緩沖區(qū)長度時(shí),將無法執(zhí)行部分復(fù)制,只能執(zhí)行全量復(fù)制。

為了提高網(wǎng)絡(luò)中斷時(shí)部分復(fù)制執(zhí)行的概率,可以根據(jù)需要增大復(fù)制積壓緩沖區(qū)的大小(通過配置repl-backlog-size);例如如果網(wǎng)絡(luò)中斷的平均時(shí)間是60s,而主節(jié)點(diǎn)平均每秒產(chǎn)生的寫命令(特定協(xié)議格式)所占的字節(jié)數(shù)為100KB,則復(fù)制積壓緩沖區(qū)的平均需求為6MB,保險(xiǎn)起見,可以設(shè)置為12MB,來保證絕大多數(shù)斷線情況都可以使用部分復(fù)制。

從節(jié)點(diǎn)將offset發(fā)送給主節(jié)點(diǎn)后,主節(jié)點(diǎn)根據(jù)offset和緩沖區(qū)大小決定能否執(zhí)行部分復(fù)制:

  • 如果offset偏移量之后的數(shù)據(jù),仍然都在復(fù)制積壓緩沖區(qū)里,則執(zhí)行部分復(fù)制;
  • 如果offset偏移量之后的數(shù)據(jù)已不在復(fù)制積壓緩沖區(qū)中(數(shù)據(jù)已被擠出),則執(zhí)行全量復(fù)制。

復(fù)制積壓緩沖區(qū)示意圖:


在這里插入圖片描述

3. 服務(wù)器運(yùn)行ID(runid):

每個(gè)Redis節(jié)點(diǎn),都有其運(yùn)行ID,運(yùn)行ID由節(jié)點(diǎn)在啟動(dòng)時(shí)自動(dòng)生成,主節(jié)點(diǎn)會(huì)將自己的運(yùn)行ID發(fā)送給從節(jié)點(diǎn),從節(jié)點(diǎn)會(huì)將主節(jié)點(diǎn)的運(yùn)行ID存起來。
從節(jié)點(diǎn)Redis斷開重連的時(shí)候,就是根據(jù)運(yùn)行ID來判斷同步的進(jìn)度:

  • 如果從節(jié)點(diǎn)保存的runid與主節(jié)點(diǎn)現(xiàn)在的runid相同,說明主從節(jié)點(diǎn)之前同步過,主節(jié)點(diǎn)會(huì)繼續(xù)嘗試使用部分復(fù)制(到底能不能部分復(fù)制還要看offset和復(fù)制積壓緩沖區(qū)的情況);
  • 如果從節(jié)點(diǎn)保存的runid與主節(jié)點(diǎn)現(xiàn)在的runid不同,說明從節(jié)點(diǎn)在斷線前同步的Redis節(jié)點(diǎn)并不是當(dāng)前的主節(jié)點(diǎn),只能進(jìn)行全量復(fù)制。

psync 命令的執(zhí)行

在了解了復(fù)制偏移量、復(fù)制積壓緩沖區(qū)、節(jié)點(diǎn)運(yùn)行id之后,本節(jié)將介紹psync命令的參數(shù)和返回值,從而說明psync命令執(zhí)行過程中,主從節(jié)點(diǎn)是如何確定使用全量復(fù)制還是部分復(fù)制的。
psync命令流程圖如下:


在這里插入圖片描述

psync命令的大體流程如下:

  • 如果從節(jié)點(diǎn)之前沒有復(fù)制過任何主節(jié)點(diǎn),或者之前執(zhí)行過slaveof no one命令,從節(jié)點(diǎn)就會(huì)向主節(jié)點(diǎn)發(fā)送psync命令,請求主節(jié)點(diǎn)進(jìn)行數(shù)據(jù)的全量同步
  • 如果前面從節(jié)點(diǎn)已經(jīng)同步過部分?jǐn)?shù)據(jù),此時(shí)從節(jié)點(diǎn)就會(huì)發(fā)送psync {runid} {offset}命令給主節(jié)點(diǎn),其中runid是上一次主節(jié)點(diǎn)的運(yùn)行ID,offset是當(dāng)前從節(jié)點(diǎn)的復(fù)制偏移量

主節(jié)點(diǎn)收到psync命令后,會(huì)出現(xiàn)以下三種可能:

  • 主節(jié)點(diǎn)返回 fullresync {runid} {offset}回復(fù),表示主節(jié)點(diǎn)要求與從節(jié)點(diǎn)進(jìn)行數(shù)據(jù)的完整全量復(fù)制,其中runid表示主節(jié)點(diǎn)的運(yùn)行ID,offset表示當(dāng)前主節(jié)點(diǎn)的復(fù)制偏移量
  • 如果主服務(wù)器返回 +continue,表示主節(jié)點(diǎn)與從節(jié)點(diǎn)會(huì)進(jìn)行部分?jǐn)?shù)據(jù)的同步操作,將從服務(wù)器缺失的數(shù)據(jù)復(fù)制過來即可
  • 如果主服務(wù)器返回 -err,表示主服務(wù)器的Redis版本低于2.8,無法識(shí)別psync命令,此時(shí)從服務(wù)器會(huì)向主服務(wù)器發(fā)送sync命令,進(jìn)行完整的數(shù)據(jù)全量復(fù)制

心跳檢測機(jī)制

心跳檢測機(jī)制的作用有三個(gè):

  1. 檢查主從服務(wù)器的網(wǎng)絡(luò)連接狀態(tài)
  2. 輔助實(shí)現(xiàn)min-slaves選項(xiàng)
  3. 檢測命令丟失

檢查主從服務(wù)器的網(wǎng)絡(luò)連接狀態(tài)

主節(jié)點(diǎn)信息中可以看到所屬的從節(jié)點(diǎn)的連接信息:

  • state 表示從節(jié)點(diǎn)狀態(tài)
  • offset 表示復(fù)制偏移量
  • lag 表示延遲值(幾秒之前有過心跳檢測機(jī)制)
  • 在這里插入圖片描述

輔助實(shí)現(xiàn)min-slaves選項(xiàng)

Redis.conf配置文件中有下方兩個(gè)參數(shù)

# 未達(dá)到下面兩個(gè)條件時(shí),寫操作就不會(huì)被執(zhí)行
# 最少包含的從服務(wù)器
# min-slaves-to-write 3
# 延遲值
# min-slaves-max-lag 10

如果將兩個(gè)參數(shù)的注釋取消,那么如果從服務(wù)器的數(shù)量少于3個(gè),或者三個(gè)從服務(wù)器的延遲(lag)大于等于10秒時(shí),主服務(wù)器都會(huì)拒絕執(zhí)行寫命令。

檢測命令丟失

在從服務(wù)器的連接信息中可以看到復(fù)制偏移量,如果此時(shí)主服務(wù)器的復(fù)制偏移量與從服務(wù)器的復(fù)制偏移量不一致時(shí),主服務(wù)器會(huì)補(bǔ)發(fā)缺失的數(shù)據(jù)。

實(shí)踐中的問題

1.讀寫分離及其中的問題

在主從復(fù)制基礎(chǔ)上實(shí)現(xiàn)的讀寫分離,可以實(shí)現(xiàn)Redis的讀負(fù)載均衡:由主節(jié)點(diǎn)提供寫服務(wù),由一個(gè)或多個(gè)從節(jié)點(diǎn)提供讀服務(wù)(多個(gè)從節(jié)點(diǎn)既可以提高數(shù)據(jù)冗余程度,也可以最大化讀負(fù)載能力);在讀負(fù)載較大的應(yīng)用場景下,可以大大提高Redis服務(wù)器的并發(fā)量。下面介紹在使用Redis讀寫分離時(shí),需要注意的問題。

1. 延遲與不一致問題
前面已經(jīng)講到,由于主從復(fù)制的命令傳播是異步的,延遲與數(shù)據(jù)的不一致不可避免。如果應(yīng)用對數(shù)據(jù)不一致的接受程度程度較低,可能的優(yōu)化措施包括:優(yōu)化主從節(jié)點(diǎn)之間的網(wǎng)絡(luò)環(huán)境(如在同機(jī)房部署);監(jiān)控主從節(jié)點(diǎn)延遲(通過offset)判斷,如果從節(jié)點(diǎn)延遲過大,通知應(yīng)用不再通過該從節(jié)點(diǎn)讀取數(shù)據(jù);使用集群同時(shí)擴(kuò)展寫負(fù)載和讀負(fù)載等。

在命令傳播階段以外的其他情況下,從節(jié)點(diǎn)的數(shù)據(jù)不一致可能更加嚴(yán)重,例如連接在數(shù)據(jù)同步階段,或從節(jié)點(diǎn)失去與主節(jié)點(diǎn)的連接時(shí)等。從節(jié)點(diǎn)的slave-serve-stale-data參數(shù)便與此有關(guān):它控制這種情況下從節(jié)點(diǎn)的表現(xiàn);如果為yes(默認(rèn)值),則從節(jié)點(diǎn)仍能夠響應(yīng)客戶端的命令,如果為no,則從節(jié)點(diǎn)只能響應(yīng)info、slaveof等少數(shù)命令。該參數(shù)的設(shè)置與應(yīng)用對數(shù)據(jù)一致性的要求有關(guān);如果對數(shù)據(jù)一致性要求很高,則應(yīng)設(shè)置為no。

2. 數(shù)據(jù)過期問題
在單機(jī)版Redis中,存在兩種刪除策略:

惰性刪除:服務(wù)器不會(huì)主動(dòng)刪除數(shù)據(jù),只有當(dāng)客戶端查詢某個(gè)數(shù)據(jù)時(shí),服務(wù)器判斷該數(shù)據(jù)是否過期,如果過期則刪除。
定期刪除:服務(wù)器執(zhí)行定時(shí)任務(wù)刪除過期數(shù)據(jù),但是考慮到內(nèi)存和CPU的折中(刪除會(huì)釋放內(nèi)存,但是頻繁的刪除操作對CPU不友好),該刪除的頻率和執(zhí)行時(shí)間都受到了限制。
在主從復(fù)制場景下,為了主從節(jié)點(diǎn)的數(shù)據(jù)一致性,從節(jié)點(diǎn)不會(huì)主動(dòng)刪除數(shù)據(jù),而是由主節(jié)點(diǎn)控制從節(jié)點(diǎn)中過期數(shù)據(jù)的刪除。由于主節(jié)點(diǎn)的惰性刪除和定期刪除策略,都不能保證主節(jié)點(diǎn)及時(shí)對過期數(shù)據(jù)執(zhí)行刪除操作,因此,當(dāng)客戶端通過Redis從節(jié)點(diǎn)讀取數(shù)據(jù)時(shí),很容易讀取到已經(jīng)過期的數(shù)據(jù)。

Redis 3.2中,從節(jié)點(diǎn)在讀取數(shù)據(jù)時(shí),增加了對數(shù)據(jù)是否過期的判斷:如果該數(shù)據(jù)已過期,則不返回給客戶端;將Redis升級到3.2可以解決數(shù)據(jù)過期問題。

3. 故障切換問題
在沒有使用哨兵的讀寫分離場景下,應(yīng)用針對讀和寫分別連接不同的Redis節(jié)點(diǎn);當(dāng)主節(jié)點(diǎn)或從節(jié)點(diǎn)出現(xiàn)問題而發(fā)生更改時(shí),需要及時(shí)修改應(yīng)用程序讀寫Redis數(shù)據(jù)的連接;連接的切換可以手動(dòng)進(jìn)行,或者自己寫監(jiān)控程序進(jìn)行切換,但前者響應(yīng)慢、容易出錯(cuò),后者實(shí)現(xiàn)復(fù)雜,成本都不算低。

4. 總結(jié)
在使用讀寫分離之前,可以考慮其他方法增加Redis的讀負(fù)載能力:如盡量優(yōu)化主節(jié)點(diǎn)(減少慢查詢、減少持久化等其他情況帶來的阻塞等)提高負(fù)載能力;使用Redis集群同時(shí)提高讀負(fù)載能力和寫負(fù)載能力等。如果使用讀寫分離,可以使用哨兵,使主從節(jié)點(diǎn)的故障切換盡可能自動(dòng)化,并減少對應(yīng)用程序的侵入。

2. 復(fù)制超時(shí)問題

主從節(jié)點(diǎn)復(fù)制超時(shí)是導(dǎo)致復(fù)制中斷的最重要的原因之一,本小節(jié)單獨(dú)說明超時(shí)問題,下一小節(jié)說明其他會(huì)導(dǎo)致復(fù)制中斷的問題。

超時(shí)判斷意義

在復(fù)制連接建立過程中及之后,主從節(jié)點(diǎn)都有機(jī)制判斷連接是否超時(shí),其意義在于:

  1. 如果主節(jié)點(diǎn)判斷連接超時(shí),其會(huì)釋放相應(yīng)從節(jié)點(diǎn)的連接,從而釋放各種資源,否則無效的從節(jié)點(diǎn)仍會(huì)占用主節(jié)點(diǎn)的各種資源(輸出緩沖區(qū)、帶寬、連接等);此外連接超時(shí)的判斷可以讓主節(jié)點(diǎn)更準(zhǔn)確的知道當(dāng)前有效從節(jié)點(diǎn)的個(gè)數(shù),有助于保證數(shù)據(jù)安全(配合前面講到的min-slaves-to-write等參數(shù))。

  2. 如果從節(jié)點(diǎn)判斷連接超時(shí),則可以及時(shí)重新建立連接,避免與主節(jié)點(diǎn)數(shù)據(jù)長期的不一致。

判斷機(jī)制

主從復(fù)制超時(shí)判斷的核心,在于repl-timeout參數(shù),該參數(shù)規(guī)定了超時(shí)時(shí)間的閾值(默認(rèn)60s),對于主節(jié)點(diǎn)和從節(jié)點(diǎn)同時(shí)有效;主從節(jié)點(diǎn)觸發(fā)超時(shí)的條件分別如下:

  1. 主節(jié)點(diǎn):每秒1次調(diào)用復(fù)制定時(shí)函數(shù)replicationCron(),在其中判斷當(dāng)前時(shí)間距離上次收到各個(gè)從節(jié)點(diǎn)REPLCONF ACK的時(shí)間,是否超過了repl-timeout值,如果超過了則釋放相應(yīng)從節(jié)點(diǎn)的連接。

  2. 從節(jié)點(diǎn):從節(jié)點(diǎn)對超時(shí)的判斷同樣是在復(fù)制定時(shí)函數(shù)中判斷,基本邏輯是:

    • 如果當(dāng)前處于連接建立階段,且距離上次收到主節(jié)點(diǎn)的信息的時(shí)間已超過repl-timeout,則釋放與主節(jié)點(diǎn)的連接;
    • 如果當(dāng)前處于數(shù)據(jù)同步階段,且收到主節(jié)點(diǎn)的RDB文件的時(shí)間超時(shí),則停止數(shù)據(jù)同步,釋放連接;
    • 如果當(dāng)前處于命令傳播階段,且距離上次收到主節(jié)點(diǎn)的PING命令或數(shù)據(jù)的時(shí)間已超過repl-timeout值,則釋放與主節(jié)點(diǎn)的連接。

需要注意的坑

下面介紹與復(fù)制階段連接超時(shí)有關(guān)的一些實(shí)際問題:

  1. 數(shù)據(jù)同步階段:在主從節(jié)點(diǎn)進(jìn)行全量復(fù)制bgsave時(shí),主節(jié)點(diǎn)需要首先fork子進(jìn)程將當(dāng)前數(shù)據(jù)保存到RDB文件中,然后再將RDB文件通過網(wǎng)絡(luò)傳輸?shù)綇墓?jié)點(diǎn)。如果RDB文件過大,主節(jié)點(diǎn)在fork子進(jìn)程+保存RDB文件時(shí)耗時(shí)過多,可能會(huì)導(dǎo)致從節(jié)點(diǎn)長時(shí)間收不到數(shù)據(jù)而觸發(fā)超時(shí);此時(shí)從節(jié)點(diǎn)會(huì)重連主節(jié)點(diǎn),然后再次全量復(fù)制,再次超時(shí),再次重連……這是個(gè)悲傷的循環(huán)。為了避免這種情況的發(fā)生,除了注意Redis單機(jī)數(shù)據(jù)量不要過大,另一方面就是適當(dāng)增大repl-timeout值,具體的大小可以根據(jù)bgsave耗時(shí)來調(diào)整。

  2. 命令傳播階段:如前所述,在該階段主節(jié)點(diǎn)會(huì)向從節(jié)點(diǎn)發(fā)送PING命令,頻率由repl-ping-slave-period控制;該參數(shù)應(yīng)明顯小于repl-timeout值(后者至少是前者的幾倍)。否則,如果兩個(gè)參數(shù)相等或接近,網(wǎng)絡(luò)抖動(dòng)導(dǎo)致個(gè)別PING命令丟失,此時(shí)恰巧主節(jié)點(diǎn)也沒有向從節(jié)點(diǎn)發(fā)送數(shù)據(jù),則從節(jié)點(diǎn)很容易判斷超時(shí)。

  3. 慢查詢導(dǎo)致的阻塞:如果主節(jié)點(diǎn)或從節(jié)點(diǎn)執(zhí)行了一些慢查詢(如keys *或者對大數(shù)據(jù)的hgetall等),導(dǎo)致服務(wù)器阻塞;阻塞期間無法響應(yīng)復(fù)制連接中對方節(jié)點(diǎn)的請求,可能導(dǎo)致復(fù)制超時(shí)。

總結(jié)

Redis 高可用系列的第二彈:主從復(fù)制的核心內(nèi)容已經(jīng)講解完畢了,希望大家閱讀完會(huì)有一點(diǎn)收獲,另外如果大家覺得看完文章有幫助的話,可以點(diǎn)贊,收藏微微表示一下支持,好讓我能有動(dòng)力繼續(xù)寫技術(shù)文章。

另外:我開了公眾號(hào)【碼農(nóng)富哥】(id: coder2025),專注于寫后端,服務(wù)端的原創(chuàng)干文,持續(xù)更新手法文章,大家有興趣也可以關(guān)注一下!

歡迎關(guān)注公眾號(hào):「碼農(nóng)富哥」,致力于分享后端技術(shù) (高并發(fā)架構(gòu),分布式集群系統(tǒng),消息隊(duì)列中間件,網(wǎng)絡(luò),微服務(wù),Linux, TCP/IP, HTTP, MySQL, Redis), Python 等 原創(chuàng)干貨 和 面試指南!

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容