深入學(xué)習(xí)Redis高可用的基石:主從復(fù)制
在前面的兩篇文章中,分別介紹了 Redis 的內(nèi)存模型和 Redis 的持久化,今天我們來(lái)深入學(xué)習(xí) Redis 的主從復(fù)制。
在 Redis 的持久化中曾提到,Redis 高可用的方案包括持久化、主從復(fù)制(及讀寫(xiě)分離)、哨兵和集群。
其中持久化側(cè)重解決的是 Redis 數(shù)據(jù)的單機(jī)備份問(wèn)題(從內(nèi)存到硬盤(pán)的備份);而主從復(fù)制則側(cè)重解決數(shù)據(jù)的多機(jī)熱備。此外,主從復(fù)制還可以實(shí)現(xiàn)負(fù)載均衡和故障恢復(fù)。
我將從以下幾個(gè)部分詳細(xì)介紹 Redis 主從復(fù)制的方方面面:
- 主從復(fù)制概述
- 如何使用主從復(fù)制
- 主從復(fù)制的實(shí)現(xiàn)原理
- 應(yīng)用中的問(wèn)題
- 總結(jié)
主從復(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)(或沒(méi)有從節(jié)點(diǎn)),但一個(gè)從節(jié)點(diǎn)只能有一個(gè)主節(jié)點(diǎn)。
主從復(fù)制的作用主要包括:
- 數(shù)據(jù)冗余:主從復(fù)制實(shí)現(xiàn)了數(shù)據(jù)的熱備份,是持久化之外的一種數(shù)據(jù)冗余方式。
- 故障恢復(fù):當(dāng)主節(jié)點(diǎn)出現(xiàn)問(wèn)題時(shí),可以由從節(jié)點(diǎn)提供服務(wù),實(shí)現(xiàn)快速的故障恢復(fù);實(shí)際上是一種服務(wù)的冗余。
- 負(fù)載均衡:在主從復(fù)制的基礎(chǔ)上,配合讀寫(xiě)分離,可以由主節(jié)點(diǎn)提供寫(xiě)服務(wù),由從節(jié)點(diǎn)提供讀服務(wù)(即寫(xiě) Redis 數(shù)據(jù)時(shí)應(yīng)用連接主節(jié)點(diǎn),讀 Redis 數(shù)據(jù)時(shí)應(yīng)用連接從節(jié)點(diǎn)),分擔(dān)服務(wù)器負(fù)載;尤其是在寫(xiě)少讀多的場(chǎng)景下,通過(guò)多個(gè)從節(jié)點(diǎn)分擔(dān)讀負(fù)載,可以大大提高 Redis 服務(wù)器的并發(fā)量。
- 高可用基石:除了上述作用以外,主從復(fù)制還是哨兵和集群能夠?qū)嵤┑幕A(chǔ),因此說(shuō)主從復(fù)制是 Redis 高可用的基礎(chǔ)。
如何使用主從復(fù)制
為了更直觀的理解主從復(fù)制,在介紹其內(nèi)部原理之前,先說(shuō)明我們需要如何操作才能開(kāi)啟主從復(fù)制。
建立復(fù)制
需要注意,主從復(fù)制的開(kāi)啟,完全是在從節(jié)點(diǎn)發(fā)起的;不需要我們?cè)谥鞴?jié)點(diǎn)做任何事情。
從節(jié)點(diǎn)開(kāi)啟主從復(fù)制,有 3 種方式:
- 在從服務(wù)器的配置文件中加入:slaveof <masterip> <masterport>。
- redis-server 啟動(dòng)命令后加入 --slaveof <masterip> <masterport>。
- Redis 服務(wù)器啟動(dòng)后,直接通過(guò)客戶端執(zhí)行命令:slaveof <masterip> <masterport>,則該 Redis 實(shí)例成為從節(jié)點(diǎn)。
上述 3 種方式是等效的,下面以客戶端命令的方式為例,看一下當(dāng)執(zhí)行了 slaveof 后,Redis 主節(jié)點(diǎn)和從節(jié)點(diǎn)的變化。
實(shí)例
準(zhǔn)備工作:?jiǎn)?dòng)兩個(gè)節(jié)點(diǎn)。為了方便起見(jiàn),實(shí)驗(yàn)所使用的主從節(jié)點(diǎn)是在一臺(tái)機(jī)器上的不同 Redis 實(shí)例。
其中主節(jié)點(diǎn)監(jiān)聽(tīng) 6379 端口,從節(jié)點(diǎn)監(jiān)聽(tīng) 6380 端口;從節(jié)點(diǎn)監(jiān)聽(tīng)的端口號(hào)可以在配置文件中修改:
啟動(dòng)后可以看到:
兩個(gè) Redis 節(jié)點(diǎn)啟動(dòng)后(分別稱為6379節(jié)點(diǎn)和6380節(jié)點(diǎn)),默認(rèn)都是主節(jié)點(diǎn)。
建立復(fù)制:此時(shí)在 6380 節(jié)點(diǎn)執(zhí)行 slaveof 命令,使之變?yōu)閺墓?jié)點(diǎn)。
觀察效果:下面驗(yàn)證一下,在主從復(fù)制建立后,主節(jié)點(diǎn)的數(shù)據(jù)會(huì)復(fù)制到從節(jié)點(diǎn)中。
首先在從節(jié)點(diǎn)查詢一個(gè)不存在的 key:
然后在主節(jié)點(diǎn)中增加這個(gè) key:
此時(shí)在從節(jié)點(diǎn)中再次查詢這個(gè) key,會(huì)發(fā)現(xiàn)主節(jié)點(diǎn)的操作已經(jīng)同步至從節(jié)點(diǎn):
然后在主節(jié)點(diǎn)刪除這個(gè) key:
此時(shí)在從節(jié)點(diǎn)中再次查詢這個(gè) key,會(huì)發(fā)現(xiàn)主節(jié)點(diǎn)的操作已經(jīng)同步至從節(jié)點(diǎn):
斷開(kāi)復(fù)制
通過(guò) slaveof <masterip> <masterport> 命令建立主從復(fù)制關(guān)系以后,可以通過(guò) slaveof no one 斷開(kāi)。
需要注意的是,從節(jié)點(diǎn)斷開(kāi)復(fù)制后,不會(huì)刪除已有的數(shù)據(jù),只是不再接受主節(jié)點(diǎn)新的數(shù)據(jù)變化。
從節(jié)點(diǎn)執(zhí)行 slaveof no one 后,打印日志如下圖所示;可以看出斷開(kāi)復(fù)制后,從節(jié)點(diǎn)又變回為主節(jié)點(diǎn)。
主節(jié)點(diǎn)打印日志如下:
主從復(fù)制的實(shí)現(xiàn)原理
上面一節(jié)中,我們介紹了如何操作可以建立主從關(guān)系;本小節(jié)將介紹主從復(fù)制的實(shí)現(xiàn)原理。
主從復(fù)制過(guò)程大體可以分為 3 個(gè)階段:
- 連接建立階段(即準(zhǔn)備階段)
- 數(shù)據(jù)同步階段
- 命令傳播階段
連接建立階段
該階段的主要作用是在主從節(jié)點(diǎn)之間建立連接,為數(shù)據(jù)同步做好準(zhǔn)備。
步驟 1:保存主節(jié)點(diǎn)信息
從節(jié)點(diǎn)服務(wù)器內(nèi)部維護(hù)了兩個(gè)字段,即 masterhost 和 masterport 字段,用于存儲(chǔ)主節(jié)點(diǎn)的 ip 和 port 信息。
需要注意的是,slaveof 是異步命令,從節(jié)點(diǎn)完成主節(jié)點(diǎn) ip 和 port 的保存后,向發(fā)送 slaveof 命令的客戶端直接返回 OK,實(shí)際的復(fù)制操作在這之后才開(kāi)始進(jìn)行。
這個(gè)過(guò)程中,可以看到從節(jié)點(diǎn)打印日志如下:
步驟 2:建立 Socket 連接
從節(jié)點(diǎn)每秒 1 次調(diào)用復(fù)制定時(shí)函數(shù) replicationCron(),如果發(fā)現(xiàn)了有主節(jié)點(diǎn)可以連接,便會(huì)根據(jù)主節(jié)點(diǎn)的 ip 和 port,創(chuàng)建 socket 連接。
如果連接成功,則:
- 從節(jié)點(diǎn):為該 socket 建立一個(gè)專門(mén)處理復(fù)制工作的文件事件處理器,負(fù)責(zé)后續(xù)的復(fù)制工作,如接收 RDB 文件、接收命令傳播等。
- 主節(jié)點(diǎn):接收到從節(jié)點(diǎn)的 socket 連接后(即 accept 之后),為該 socket 創(chuàng)建相應(yīng)的客戶端狀態(tài),并將從節(jié)點(diǎn)看做是連接到主節(jié)點(diǎn)的一個(gè)客戶端,后面的步驟會(huì)以從節(jié)點(diǎn)向主節(jié)點(diǎn)發(fā)送命令請(qǐng)求的形式來(lái)進(jìn)行。
這個(gè)過(guò)程中,從節(jié)點(diǎn)打印日志如下:
步驟 3:發(fā)送 Ping 命令
從節(jié)點(diǎn)成為主節(jié)點(diǎn)的客戶端之后,發(fā)送 ping 命令進(jìn)行***請(qǐng)求,目的是:檢查 socket 連接是否可用,以及主節(jié)點(diǎn)當(dāng)前是否能夠處理請(qǐng)求。
從節(jié)點(diǎn)發(fā)送 ping 命令后,可能出現(xiàn) 3 種情況:
- 返回pong:說(shuō)明 socket 連接正常,且主節(jié)點(diǎn)當(dāng)前可以處理請(qǐng)求,復(fù)制過(guò)程繼續(xù)。
- 超時(shí):一定時(shí)間后從節(jié)點(diǎn)仍未收到主節(jié)點(diǎn)的回復(fù),說(shuō)明 socket 連接不可用,則從節(jié)點(diǎn)斷開(kāi) socket 連接,并重連。
- 返回 pong 以外的結(jié)果:如果主節(jié)點(diǎn)返回其他結(jié)果,如正在處理超時(shí)運(yùn)行的腳本,說(shuō)明主節(jié)點(diǎn)當(dāng)前無(wú)法處理命令,則從節(jié)點(diǎn)斷開(kāi) socket 連接,并重連。
在主節(jié)點(diǎn)返回 pong 情況下,從節(jié)點(diǎn)打印日志如下:
步驟 4:身份驗(yàn)證
如果從節(jié)點(diǎn)中設(shè)置了 masterauth 選項(xiàng),則從節(jié)點(diǎn)需要向主節(jié)點(diǎn)進(jìn)行身份驗(yàn)證;沒(méi)有設(shè)置該選項(xiàng),則不需要驗(yàn)證。
從節(jié)點(diǎn)進(jìn)行身份驗(yàn)證是通過(guò)向主節(jié)點(diǎn)發(fā)送 auth 命令進(jìn)行的,auth 命令的參數(shù)即為配置文件中的 masterauth 的值。
如果主節(jié)點(diǎn)設(shè)置密碼的狀態(tài),與從節(jié)點(diǎn) masterauth 的狀態(tài)一致(一致是指都存在,且密碼相同,或者都不存在),則身份驗(yàn)證通過(guò),復(fù)制過(guò)程繼續(xù);如果不一致,則從節(jié)點(diǎn)斷開(kāi) socket 連接,并重連。
步驟 5:發(fā)送從節(jié)點(diǎn)端口信息
身份驗(yàn)證之后,從節(jié)點(diǎn)會(huì)向主節(jié)點(diǎn)發(fā)送其監(jiān)聽(tīng)的端口號(hào)(前述例子中為 6380),主節(jié)點(diǎn)將該信息保存到該從節(jié)點(diǎn)對(duì)應(yīng)的客戶端的 slave_listening_port 字段中。
該端口信息除了在主節(jié)點(diǎn)中執(zhí)行 info Replication 時(shí)顯示以外,沒(méi)有其他作用。
數(shù)據(jù)同步階段
主從節(jié)點(diǎn)之間的連接建立以后,便可以開(kāi)始進(jìn)行數(shù)據(jù)同步,該階段可以理解為從節(jié)點(diǎn)數(shù)據(jù)的初始化。
具體執(zhí)行的方式是:從節(jié)點(diǎn)向主節(jié)點(diǎn)發(fā)送 psync 命令(Redis 2.8 以前是 sync 命令),開(kāi)始同步。
數(shù)據(jù)同步階段是主從復(fù)制最核心的階段,根據(jù)主從節(jié)點(diǎn)當(dāng)前狀態(tài)的不同,可以分為全量復(fù)制和部分復(fù)制。
需要注意的是,在數(shù)據(jù)同步階段之前,從節(jié)點(diǎn)是主節(jié)點(diǎn)的客戶端,主節(jié)點(diǎn)不是從節(jié)點(diǎn)的客戶端;而到了這一階段及以后,主從節(jié)點(diǎn)互為客戶端。
原因在于:在此之前,主節(jié)點(diǎn)只需要響應(yīng)從節(jié)點(diǎn)的請(qǐng)求即可,不需要主動(dòng)發(fā)請(qǐng)求,而在數(shù)據(jù)同步階段和后面的命令傳播階段,主節(jié)點(diǎn)需要主動(dòng)向從節(jié)點(diǎn)發(fā)送請(qǐng)求(如推送緩沖區(qū)中的寫(xiě)命令),才能完成復(fù)制。
命令傳播階段
數(shù)據(jù)同步階段完成后,主從節(jié)點(diǎn)進(jìn)入命令傳播階段;在這個(gè)階段主節(jié)點(diǎn)將自己執(zhí)行的寫(xiě)命令發(fā)送給從節(jié)點(diǎn),從節(jié)點(diǎn)接收命令并執(zhí)行,從而保證主從節(jié)點(diǎn)數(shù)據(jù)的一致性。
在命令傳播階段,除了發(fā)送寫(xiě)命令,主從節(jié)點(diǎn)還維持著心跳機(jī)制:PING 和 REPLCONF ACK。
由于心跳機(jī)制的原理涉及部分復(fù)制,因此將在介紹了部分復(fù)制的相關(guān)內(nèi)容后單獨(dú)介紹該心跳機(jī)制。
延遲與不一致:需要注意的是,命令傳播是異步的過(guò)程,即主節(jié)點(diǎn)發(fā)送寫(xiě)命令后并不會(huì)等待從節(jié)點(diǎn)的回復(fù);因此實(shí)際上主從節(jié)點(diǎn)之間很難保持實(shí)時(shí)的一致性,延遲在所難免。
數(shù)據(jù)不一致的程度,與主從節(jié)點(diǎn)之間的網(wǎng)絡(luò)狀況、主節(jié)點(diǎn)寫(xiě)命令的執(zhí)行頻率、以及主節(jié)點(diǎn)中的 repl-disable-tcp-nodelay 配置等有關(guān)。
repl-disable-tcp-nodelay no:該配置作用于命令傳播階段,控制主節(jié)點(diǎn)是否禁止與從節(jié)點(diǎn)的 TCP_NODELAY;默認(rèn) no,即不禁止 TCP_NODELAY。
當(dāng)設(shè)置為 yes 時(shí),TCP 會(huì)對(duì)包進(jìn)行合并從而減少帶寬,但是發(fā)送的頻率會(huì)降低,從節(jié)點(diǎn)數(shù)據(jù)延遲增加,一致性變差;具體發(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),帶寬增加但延遲變小。
一般來(lái)說(shuō),只有當(dāng)應(yīng)用對(duì) Redis 數(shù)據(jù)不一致的容忍度較高,且主從節(jié)點(diǎn)之間網(wǎng)絡(luò)狀況不好時(shí),才會(huì)設(shè)置為 yes;多數(shù)情況使用默認(rèn)值 no。
數(shù)據(jù)同步階段:全量復(fù)制和部分復(fù)制
在 Redis 2.8 以前,從節(jié)點(diǎn)向主節(jié)點(diǎn)發(fā)送 sync 命令請(qǐng)求同步數(shù)據(jù),此時(shí)的同步方式是全量復(fù)制。
在 Redis 2.8 及以后,從節(jié)點(diǎn)可以發(fā)送 psync 命令請(qǐng)求同步數(shù)據(jù),此時(shí)根據(jù)主從節(jié)點(diǎn)當(dāng)前狀態(tài)的不同,同步方式可能是全量復(fù)制或部分復(fù)制。后文介紹以 Redis 2.8 及以后版本為例。
全量復(fù)制:用于初次復(fù)制或其他無(wú)法進(jìn)行部分復(fù)制的情況,將主節(jié)點(diǎn)中的所有數(shù)據(jù)都發(fā)送給從節(jié)點(diǎn),是一個(gè)非常重型的操作。
部分復(fù)制:用于網(wǎng)絡(luò)中斷等情況后的復(fù)制,只將中斷期間主節(jié)點(diǎn)執(zhí)行的寫(xiě)命令發(fā)送給從節(jié)點(diǎn),與全量復(fù)制相比更加高效。
需要注意的是,如果網(wǎng)絡(luò)中斷時(shí)間過(guò)長(zhǎng),導(dǎo)致主節(jié)點(diǎn)沒(méi)有能夠完整地保存中斷期間執(zhí)行的寫(xiě)命令,則無(wú)法進(jìn)行部分復(fù)制,仍使用全量復(fù)制。
全量復(fù)制
Redis 通過(guò) psync 命令進(jìn)行全量復(fù)制的過(guò)程如下:
- 從節(jié)點(diǎn)判斷無(wú)法進(jìn)行部分復(fù)制,向主節(jié)點(diǎn)發(fā)送全量復(fù)制的請(qǐng)求;或從節(jié)點(diǎn)發(fā)送部分復(fù)制的請(qǐng)求,但主節(jié)點(diǎn)判斷無(wú)法進(jìn)行全量復(fù)制;具體判斷過(guò)程需要在講述了部分復(fù)制原理后再介紹。
- 主節(jié)點(diǎn)收到全量復(fù)制的命令后,執(zhí)行 bgsave,在后臺(tái)生成 RDB 文件,并使用一個(gè)緩沖區(qū)(稱為復(fù)制緩沖區(qū))記錄從現(xiàn)在開(kāi)始執(zhí)行的所有寫(xiě)命令
- 主節(jié)點(diǎn)的 bgsave 執(zhí)行完成后,將 RDB 文件發(fā)送給從節(jié)點(diǎn);從節(jié)點(diǎn)首先清除自己的舊數(shù)據(jù),然后載入接收的 RDB 文件,將數(shù)據(jù)庫(kù)狀態(tài)更新至主節(jié)點(diǎn)執(zhí)行 bgsave 時(shí)的數(shù)據(jù)庫(kù)狀態(tài)。
- 主節(jié)點(diǎn)將前述復(fù)制緩沖區(qū)中的所有寫(xiě)命令發(fā)送給從節(jié)點(diǎn),從節(jié)點(diǎn)執(zhí)行這些寫(xiě)命令,將數(shù)據(jù)庫(kù)狀態(tài)更新至主節(jié)點(diǎn)的***狀態(tài)
- 如果從節(jié)點(diǎn)開(kāi)啟了 AOF,則會(huì)觸發(fā) bgrewriteaof 的執(zhí)行,從而保證 AOF 文件更新至主節(jié)點(diǎn)的***狀態(tài)。
下面是執(zhí)行全量復(fù)制時(shí),主從節(jié)點(diǎn)打印的日志;可以看出日志內(nèi)容與上述步驟是完全對(duì)應(yīng)的。
主節(jié)點(diǎn)的打印日志如下:
從節(jié)點(diǎn)打印日志如下圖所示:
其中,有幾點(diǎn)需要注意:
- 從節(jié)點(diǎn)接收了來(lái)自主節(jié)點(diǎn)的 89260 個(gè)字節(jié)的數(shù)據(jù)。
- 從節(jié)點(diǎn)在載入主節(jié)點(diǎn)的數(shù)據(jù)之前要先將老數(shù)據(jù)清除。
- 從節(jié)點(diǎn)在同步完數(shù)據(jù)后,調(diào)用了 bgrewriteaof。
通過(guò)全量復(fù)制的過(guò)程可以看出,全量復(fù)制是非常重型的操作:
- 主節(jié)點(diǎn)通過(guò) bgsave 命令 fork 子進(jìn)程進(jìn)行 RDB 持久化,該過(guò)程是非常消耗 CPU、內(nèi)存(頁(yè)表復(fù)制)、硬盤(pán) IO 的;關(guān)于 bgsave 的性能問(wèn)題,可以參考深入學(xué)習(xí)Redis(2):持久化。
- 主節(jié)點(diǎn)通過(guò)網(wǎng)絡(luò)將 RDB 文件發(fā)送給從節(jié)點(diǎn),對(duì)主從節(jié)點(diǎn)的帶寬都會(huì)帶來(lái)很大的消耗。
- 從節(jié)點(diǎn)清空老數(shù)據(jù)、載入新 RDB 文件的過(guò)程是阻塞的,無(wú)法響應(yīng)客戶端的命令;如果從節(jié)點(diǎn)執(zhí)行 bgrewriteaof,也會(huì)帶來(lái)額外的消耗。
部分復(fù)制
由于全量復(fù)制在主節(jié)點(diǎn)數(shù)據(jù)量較大時(shí)效率太低,因此 Redis 2.8 開(kāi)始提供部分復(fù)制,用于處理網(wǎng)絡(luò)中斷時(shí)的數(shù)據(jù)同步。
部分復(fù)制的實(shí)現(xiàn),依賴于三個(gè)重要的概念:
復(fù)制偏移量:主節(jié)點(diǎn)和從節(jié)點(diǎn)分別維護(hù)一個(gè)復(fù)制偏移量(offset),代表的是主節(jié)點(diǎn)向從節(jié)點(diǎn)傳遞的字節(jié)數(shù)。
主節(jié)點(diǎn)每次向從節(jié)點(diǎn)傳播 N 個(gè)字節(jié)數(shù)據(jù)時(shí),主節(jié)點(diǎn)的 offset 增加 N;從節(jié)點(diǎn)每次收到主節(jié)點(diǎn)傳來(lái)的 N 個(gè)字節(jié)數(shù)據(jù)時(shí),從節(jié)點(diǎn)的 offset 增加 N。
offset 用于判斷主從節(jié)點(diǎn)的數(shù)據(jù)庫(kù)狀態(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ū)。
復(fù)制積壓緩沖區(qū):復(fù)制積壓緩沖區(qū)是由主節(jié)點(diǎn)維護(hù)的、固定長(zhǎng)度的、先進(jìn)先出(FIFO)隊(duì)列,默認(rèn)大小 1MB。
當(dāng)主節(jié)點(diǎn)開(kāi)始有從節(jié)點(diǎn)時(shí)創(chuàng)建,其作用是備份主節(jié)點(diǎn)最近發(fā)送給從節(jié)點(diǎn)的數(shù)據(jù)。注意,無(wú)論主節(jié)點(diǎn)有一個(gè)還是多個(gè)從節(jié)點(diǎn),都只需要一個(gè)復(fù)制積壓緩沖區(qū)。
在命令傳播階段,主節(jié)點(diǎn)除了將寫(xiě)命令發(fā)送給從節(jié)點(diǎn),還會(huì)發(fā)送一份給復(fù)制積壓緩沖區(qū),作為寫(xiě)命令的備份;除了存儲(chǔ)寫(xiě)命令,復(fù)制積壓緩沖區(qū)中還存儲(chǔ)了其中的每個(gè)字節(jié)對(duì)應(yīng)的復(fù)制偏移量(offset)。
由于復(fù)制積壓緩沖區(qū)定長(zhǎng)且是先進(jìn)先出,所以它保存的是主節(jié)點(diǎn)最近執(zhí)行的寫(xiě)命令;時(shí)間較早的寫(xiě)命令會(huì)被擠出緩沖區(qū)。
由于該緩沖區(qū)長(zhǎng)度固定且有限,因此可以備份的寫(xiě)命令也有限,當(dāng)主從節(jié)點(diǎn) offset 的差距過(guò)大超過(guò)緩沖區(qū)長(zhǎng)度時(shí),將無(wú)法執(zhí)行部分復(fù)制,只能執(zhí)行全量復(fù)制。
反過(guò)來(lái)說(shuō),為了提高網(wǎng)絡(luò)中斷時(shí)部分復(fù)制執(zhí)行的概率,可以根據(jù)需要增大復(fù)制積壓緩沖區(qū)的大小(通過(guò)配置repl-backlog-size)。
例如如果網(wǎng)絡(luò)中斷的平均時(shí)間是 60s,而主節(jié)點(diǎn)平均每秒產(chǎn)生的寫(xiě)命令(特定協(xié)議格式)所占的字節(jié)數(shù)為 100KB,則復(fù)制積壓緩沖區(qū)的平均需求為 6MB。
保險(xiǎn)起見(jiàn),可以設(shè)置為 12MB,來(lái)保證絕大多數(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ù)制。
服務(wù)器運(yùn)行 ID(runid):每個(gè) Redis 節(jié)點(diǎn)(無(wú)論主從),在啟動(dòng)時(shí)都會(huì)自動(dòng)生成一個(gè)隨機(jī) ID(每次啟動(dòng)都不一樣),由 40 個(gè)隨機(jī)的十六進(jìn)制字符組成;runid 用來(lái)唯一識(shí)別一個(gè) Redis 節(jié)點(diǎn)。
通過(guò) info Server 命令,可以查看節(jié)點(diǎn)的 runid:
主從節(jié)點(diǎn)初次復(fù)制時(shí),主節(jié)點(diǎn)將自己的 runid 發(fā)送給從節(jié)點(diǎn),從節(jié)點(diǎn)將這個(gè) runid 保存起來(lái);當(dāng)斷線重連時(shí),從節(jié)點(diǎn)會(huì)將這個(gè) runid 發(fā)送給主節(jié)點(diǎn)。
主節(jié)點(diǎn)根據(jù) runid 判斷能否進(jìn)行部分復(fù)制:
- 如果從節(jié)點(diǎn)保存的 runid 與主節(jié)點(diǎn)現(xiàn)在的 runid 相同,說(shuō)明主從節(jié)點(diǎn)之前同步過(guò),主節(jié)點(diǎn)會(huì)繼續(xù)嘗試使用部分復(fù)制(到底能不能部分復(fù)制還要看 offset 和復(fù)制積壓緩沖區(qū)的情況)。
- 如果從節(jié)點(diǎn)保存的 runid 與主節(jié)點(diǎn)現(xiàn)在的 runid 不同,說(shuō)明從節(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ù)和返回值,從而說(shuō)明 psync 命令執(zhí)行過(guò)程中,主從節(jié)點(diǎn)是如何確定使用全量復(fù)制還是部分復(fù)制的。
psync 命令的執(zhí)行過(guò)程可以參見(jiàn)下圖:
首先,從節(jié)點(diǎn)根據(jù)當(dāng)前狀態(tài),決定如何調(diào)用 psync 命令:
- 如果從節(jié)點(diǎn)之前未執(zhí)行過(guò) slaveof 或最近執(zhí)行了 slaveof no one,則從節(jié)點(diǎn)發(fā)送命令為 psync ? -1,向主節(jié)點(diǎn)請(qǐng)求全量復(fù)制。
- 如果從節(jié)點(diǎn)之前執(zhí)行了 slaveof,則發(fā)送命令為 psync <runid> <offset>,其中 runid 為上次復(fù)制的主節(jié)點(diǎn)的 runid,offset 為上次復(fù)制截止時(shí)從節(jié)點(diǎn)保存的復(fù)制偏移量。
主節(jié)點(diǎn)根據(jù)收到的psync命令,及當(dāng)前服務(wù)器狀態(tài),決定執(zhí)行全量復(fù)制還是部分復(fù)制:
- 如果主節(jié)點(diǎn)版本低于 Redis 2.8,則返回 -ERR 回復(fù),此時(shí)從節(jié)點(diǎn)重新發(fā)送 sync 命令執(zhí)行全量復(fù)制。
- 如果主節(jié)點(diǎn)版本夠新,且 runid 與從節(jié)點(diǎn)發(fā)送的 runid 相同,且從節(jié)點(diǎn)發(fā)送的 offset 之后的數(shù)據(jù)在復(fù)制積壓緩沖區(qū)中都存在,則回復(fù) +CONTINUE,表示將進(jìn)行部分復(fù)制,從節(jié)點(diǎn)等待主節(jié)點(diǎn)發(fā)送其缺少的數(shù)據(jù)即可。
- 如果主節(jié)點(diǎn)版本夠新,但是 runid 與從節(jié)點(diǎn)發(fā)送的 runid 不同,或從節(jié)點(diǎn)發(fā)送的 offset 之后的數(shù)據(jù)已不在復(fù)制積壓緩沖區(qū)中(在隊(duì)列中被擠出了),則回復(fù) +FULLRESYNC <runid> <offset>,表示要進(jìn)行全量復(fù)制。
其中 runid 表示主節(jié)點(diǎn)當(dāng)前的 runid,offset 表示主節(jié)點(diǎn)當(dāng)前的 offset,從節(jié)點(diǎn)保存這兩個(gè)值,以備使用。
部分復(fù)制演示:在下面的演示中,網(wǎng)絡(luò)中斷幾分鐘后恢復(fù),斷開(kāi)連接的主從節(jié)點(diǎn)進(jìn)行了部分復(fù)制;為了便于模擬網(wǎng)絡(luò)中斷,本例中的主從節(jié)點(diǎn)在局域網(wǎng)中的兩臺(tái)機(jī)器上。
網(wǎng)絡(luò)中斷一段時(shí)間后,主節(jié)點(diǎn)和從節(jié)點(diǎn)都會(huì)發(fā)現(xiàn)失去了與對(duì)方的連接(關(guān)于主從節(jié)點(diǎn)對(duì)超時(shí)的判斷機(jī)制,后面會(huì)有說(shuō)明)。
此后,從節(jié)點(diǎn)便開(kāi)始執(zhí)行對(duì)主節(jié)點(diǎn)的重連,由于此時(shí)網(wǎng)絡(luò)還沒(méi)有恢復(fù),重連失敗,從節(jié)點(diǎn)會(huì)一直嘗試重連。
主節(jié)點(diǎn)日志如下:
從節(jié)點(diǎn)日志如下:
網(wǎng)絡(luò)恢復(fù)后,從節(jié)點(diǎn)連接主節(jié)點(diǎn)成功,并請(qǐng)求進(jìn)行部分復(fù)制,主節(jié)點(diǎn)接收請(qǐng)求后,二者進(jìn)行部分復(fù)制以同步數(shù)據(jù)。
主節(jié)點(diǎn)日志如下:
從節(jié)點(diǎn)日志如下:
命令傳播階段:心跳機(jī)制
在命令傳播階段,除了發(fā)送寫(xiě)命令,主從節(jié)點(diǎn)還維持著心跳機(jī)制:PING 和 REPLCONF ACK。心跳機(jī)制對(duì)于主從復(fù)制的超時(shí)判斷、數(shù)據(jù)安全等有作用。
主->從:PING
每隔指定的時(shí)間,主節(jié)點(diǎn)會(huì)向從節(jié)點(diǎn)發(fā)送 PING 命令,這個(gè) PING 命令的作用,主要是為了讓從節(jié)點(diǎn)進(jìn)行超時(shí)判斷。
PING 發(fā)送的頻率由 repl-ping-slave-period 參數(shù)控制,單位是秒,默認(rèn)值是 10s。
關(guān)于該 PING 命令究竟是由主節(jié)點(diǎn)發(fā)給從節(jié)點(diǎn),還是相反,有一些爭(zhēng)議;因?yàn)樵?Redis 的官方文檔中,對(duì)該參數(shù)的注釋中說(shuō)明是從節(jié)點(diǎn)向主節(jié)點(diǎn)發(fā)送 PING 命令,如下圖所示:
但是根據(jù)該參數(shù)的名稱(含有 ping-slave),以及代碼實(shí)現(xiàn),我認(rèn)為該 PING 命令是主節(jié)點(diǎn)發(fā)給從節(jié)點(diǎn)的。
相關(guān)代碼如下:
從->主:REPLCONF ACK
在命令傳播階段,從節(jié)點(diǎn)會(huì)向主節(jié)點(diǎn)發(fā)送 REPLCONF ACK 命令,頻率是每秒 1 次;命令格式為:REPLCONF ACK {offset},其中 offset 指從節(jié)點(diǎn)保存的復(fù)制偏移量。
REPLCONF ACK 命令的作用包括:
實(shí)時(shí)監(jiān)測(cè)主從節(jié)點(diǎn)網(wǎng)絡(luò)狀態(tài):該命令會(huì)被主節(jié)點(diǎn)用于復(fù)制超時(shí)的判斷。此外,在主節(jié)點(diǎn)中使用 info Replication,可以看到其從節(jié)點(diǎn)的狀態(tài)中的 lag 值,代表的是主節(jié)點(diǎn)上次收到該 REPLCONF ACK 命令的時(shí)間間隔。
在正常情況下,該值應(yīng)該是 0 或 1,如下圖所示:
檢測(cè)命令丟失:從節(jié)點(diǎn)發(fā)送了自身的 offset,主節(jié)點(diǎn)會(huì)與自己的 offset 對(duì)比,如果從節(jié)點(diǎn)數(shù)據(jù)缺失(如網(wǎng)絡(luò)丟包),主節(jié)點(diǎn)會(huì)推送缺失的數(shù)據(jù)(這里也會(huì)利用復(fù)制積壓緩沖區(qū))。
注意,offset 和復(fù)制積壓緩沖區(qū),不僅可以用于部分復(fù)制,也可以用于處理命令丟失等情形;區(qū)別在于前者是在斷線重連后進(jìn)行的,而后者是在主從節(jié)點(diǎn)沒(méi)有斷線的情況下進(jìn)行的。
輔助保證從節(jié)點(diǎn)的數(shù)量和延遲:Redis 主節(jié)點(diǎn)中使用 min-slaves-to-write 和 min-slaves-max-lag 參數(shù),來(lái)保證主節(jié)點(diǎn)在不安全的情況下不會(huì)執(zhí)行寫(xiě)命令;所謂不安全,是指從節(jié)點(diǎn)數(shù)量太少,或延遲過(guò)高。
例如 min-slaves-to-write 和 min-slaves-max-lag 分別是 3 和 10,含義是如果從節(jié)點(diǎn)數(shù)量小于 3 個(gè),或所有從節(jié)點(diǎn)的延遲值都大于 10s,則主節(jié)點(diǎn)拒絕執(zhí)行寫(xiě)命令。
而這里從節(jié)點(diǎn)延遲值的獲取,就是通過(guò)主節(jié)點(diǎn)接收到 REPLCONF ACK 命令的時(shí)間來(lái)判斷的,即前面所說(shuō)的 info Replication 中的 lag 值。
應(yīng)用中的問(wèn)題
讀寫(xiě)分離及其中的問(wèn)題
在主從復(fù)制基礎(chǔ)上實(shí)現(xiàn)的讀寫(xiě)分離,可以實(shí)現(xiàn) Redis 的讀負(fù)載均衡。
由主節(jié)點(diǎn)提供寫(xiě)服務(wù),由一個(gè)或多個(gè)從節(jié)點(diǎn)提供讀服務(wù)(多個(gè)從節(jié)點(diǎn)既可以提高數(shù)據(jù)冗余程度,也可以***化讀負(fù)載能力);在讀負(fù)載較大的應(yīng)用場(chǎng)景下,可以大大提高 Redis 服務(wù)器的并發(fā)量。
下面介紹在使用 Redis 讀寫(xiě)分離時(shí),需要注意的問(wèn)題。
延遲與不一致問(wèn)題
前面已經(jīng)講到,由于主從復(fù)制的命令傳播是異步的,延遲與數(shù)據(jù)的不一致不可避免。
如果應(yīng)用對(duì)數(shù)據(jù)不一致的接受程度程度較低,可能的優(yōu)化措施包括:
- 優(yōu)化主從節(jié)點(diǎn)之間的網(wǎng)絡(luò)環(huán)境(如在同機(jī)房部署)。
- 監(jiān)控主從節(jié)點(diǎn)延遲(通過(guò) offset)判斷,如果從節(jié)點(diǎn)延遲過(guò)大,通知應(yīng)用不再通過(guò)該從節(jié)點(diǎn)讀取數(shù)據(jù)。
- 使用集群同時(shí)擴(kuò)展寫(xiě)負(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)用對(duì)數(shù)據(jù)一致性的要求有關(guān);如果對(duì)數(shù)據(jù)一致性要求很高,則應(yīng)設(shè)置為 no。
數(shù)據(jù)過(guò)期問(wèn)題
在單機(jī)版 Redis 中,存在兩種刪除策略:
- 惰性刪除:服務(wù)器不會(huì)主動(dòng)刪除數(shù)據(jù),只有當(dāng)客戶端查詢某個(gè)數(shù)據(jù)時(shí),服務(wù)器判斷該數(shù)據(jù)是否過(guò)期,如果過(guò)期則刪除。
- 定期刪除:服務(wù)器執(zhí)行定時(shí)任務(wù)刪除過(guò)期數(shù)據(jù),但是考慮到內(nèi)存和 CPU 的折中(刪除會(huì)釋放內(nèi)存,但是頻繁的刪除操作對(duì) CPU 不友好),該刪除的頻率和執(zhí)行時(shí)間都受到了限制。
在主從復(fù)制場(chǎng)景下,為了主從節(jié)點(diǎn)的數(shù)據(jù)一致性,從節(jié)點(diǎn)不會(huì)主動(dòng)刪除數(shù)據(jù),而是由主節(jié)點(diǎn)控制從節(jié)點(diǎn)中過(guò)期數(shù)據(jù)的刪除。
由于主節(jié)點(diǎn)的惰性刪除和定期刪除策略,都不能保證主節(jié)點(diǎn)及時(shí)對(duì)過(guò)期數(shù)據(jù)執(zhí)行刪除操作,因此,當(dāng)客戶端通過(guò) Redis 從節(jié)點(diǎn)讀取數(shù)據(jù)時(shí),很容易讀取到已經(jīng)過(guò)期的數(shù)據(jù)。
在 Redis 3.2 中,從節(jié)點(diǎn)在讀取數(shù)據(jù)時(shí),增加了對(duì)數(shù)據(jù)是否過(guò)期的判斷:如果該數(shù)據(jù)已過(guò)期,則不返回給客戶端;將 Redis 升級(jí)到 3.2 可以解決數(shù)據(jù)過(guò)期問(wèn)題。
故障切換問(wèn)題
在沒(méi)有使用哨兵的讀寫(xiě)分離場(chǎng)景下,應(yīng)用針對(duì)讀和寫(xiě)分別連接不同的 Redis 節(jié)點(diǎn)。
當(dāng)主節(jié)點(diǎn)或從節(jié)點(diǎn)出現(xiàn)問(wèn)題而發(fā)生更改時(shí),需要及時(shí)修改應(yīng)用程序讀寫(xiě) Redis 數(shù)據(jù)的連接;連接的切換可以手動(dòng)進(jìn)行,或者自己寫(xiě)監(jiān)控程序進(jìn)行切換,但前者響應(yīng)慢、容易出錯(cuò),后者實(shí)現(xiàn)復(fù)雜,成本都不算低。
總結(jié):在使用讀寫(xiě)分離之前,可以考慮其他方法增加 Redis 的讀負(fù)載能力:如盡量?jī)?yōu)化主節(jié)點(diǎn)(減少慢查詢、減少持久化等其他情況帶來(lái)的阻塞等)提高負(fù)載能力;使用 Redis 集群同時(shí)提高讀負(fù)載能力和寫(xiě)負(fù)載能力等。
如果使用讀寫(xiě)分離,可以使用哨兵,使主從節(jié)點(diǎn)的故障切換盡可能自動(dòng)化,并減少對(duì)應(yīng)用程序的侵入。
復(fù)制超時(shí)問(wèn)題
主從節(jié)點(diǎn)復(fù)制超時(shí)是導(dǎo)致復(fù)制中斷的最重要的原因之一,本小節(jié)單獨(dú)說(shuō)明超時(shí)問(wèn)題,下一小節(jié)說(shuō)明其他會(huì)導(dǎo)致復(fù)制中斷的問(wèn)題。
超時(shí)判斷意義
在復(fù)制連接建立過(guò)程中及之后,主從節(jié)點(diǎn)都有機(jī)制判斷連接是否超時(shí),其意義在于:
- 如果主節(jié)點(diǎn)判斷連接超時(shí),其會(huì)釋放相應(yīng)從節(jié)點(diǎn)的連接,從而釋放各種資源,否則無(wú)效的從節(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ù))。
- 如果從節(jié)點(diǎn)判斷連接超時(shí),則可以及時(shí)重新建立連接,避免與主節(jié)點(diǎn)數(shù)據(jù)長(zhǎng)期的不一致。
判斷機(jī)制
主從復(fù)制超時(shí)判斷的核心,在于 repl-timeout 參數(shù),該參數(shù)規(guī)定了超時(shí)時(shí)間的閾值(默認(rèn) 60s),對(duì)于主節(jié)點(diǎn)和從節(jié)點(diǎn)同時(shí)有效。
- 主從節(jié)點(diǎn)觸發(fā)超時(shí)的條件分別如下:
- 主節(jié)點(diǎn):每秒1次調(diào)用復(fù)制定時(shí)函數(shù) replicationCron(),在其中判斷當(dāng)前時(shí)間距離上次收到各個(gè)從節(jié)點(diǎn) REPLCONF ACK 的時(shí)間,是否超過(guò)了 repl-timeout 值,如果超過(guò)了則釋放相應(yīng)從節(jié)點(diǎn)的連接。
從節(jié)點(diǎn):從節(jié)點(diǎn)對(duì)超時(shí)的判斷同樣是在復(fù)制定時(shí)函數(shù)中判斷。
從節(jié)點(diǎn)的基本邏輯是:
- 如果當(dāng)前處于連接建立階段,且距離上次收到主節(jié)點(diǎn)的信息的時(shí)間已超過(guò) 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í)間已超過(guò) repl-timeout 值,則釋放與主節(jié)點(diǎn)的連接。
主從節(jié)點(diǎn)判斷連接超時(shí)的相關(guān)源代碼如下:
- /* Replication cron function, called 1 time per second. */
- void replicationCron(void) {
- static long long replication_cron_loops = 0;
- /* Non blocking connection timeout? */
- if (server.masterhost &&
- (server.repl_state == REDIS_REPL_CONNECTING ||
- slaveIsInHandshakeState()) &&
- (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
- {
- redisLog(REDIS_WARNING,"Timeout connecting to the MASTER...");
- undoConnectWithMaster();
- }
- /* Bulk transfer I/O timeout? */
- if (server.masterhost && server.repl_state == REDIS_REPL_TRANSFER &&
- (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
- {
- redisLog(REDIS_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value.");
- replicationAbortSyncTransfer();
- }
- /* Timed out master when we are an already connected slave? */
- if (server.masterhost && server.repl_state == REDIS_REPL_CONNECTED &&
- (time(NULL)-server.master->lastinteraction) > server.repl_timeout)
- {
- redisLog(REDIS_WARNING,"MASTER timeout: no data nor PING received...");
- freeClient(server.master);
- }
- //此處省略無(wú)關(guān)代碼……
- /* Disconnect timedout slaves. */
- if (listLength(server.slaves)) {
- listIter li;
- listNode *ln;
- listRewind(server.slaves,&li);
- while((ln = listNext(&li))) {
- redisClient *slave = ln->value;
- if (slave->replstate != REDIS_REPL_ONLINE) continue;
- if (slave->flags & REDIS_PRE_PSYNC) continue;
- if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)
- {
- redisLog(REDIS_WARNING, "Disconnecting timedout slave: %s",
- replicationGetSlaveName(slave));
- freeClient(slave);
- }
- }
- }
- //此處省略無(wú)關(guān)代碼……
- }
需要注意的坑
下面介紹與復(fù)制階段連接超時(shí)有關(guān)的一些實(shí)際問(wèn)題:
數(shù)據(jù)同步階段:在主從節(jié)點(diǎn)進(jìn)行全量復(fù)制 bgsave 時(shí),主節(jié)點(diǎn)需要首先 fork 子進(jìn)程將當(dāng)前數(shù)據(jù)保存到 RDB 文件中,然后再將 RDB 文件通過(guò)網(wǎng)絡(luò)傳輸?shù)綇墓?jié)點(diǎn)。
如果 RDB 文件過(guò)大,主節(jié)點(diǎn)在 fork 子進(jìn)程+保存 RDB 文件時(shí)耗時(shí)過(guò)多,可能會(huì)導(dǎo)致從節(jié)點(diǎn)長(zhǎng)時(shí)間收不到數(shù)據(jù)而觸發(fā)超時(shí)。
此時(shí)從節(jié)點(diǎn)會(huì)重連主節(jié)點(diǎn),然后再次全量復(fù)制,再次超時(shí),再次重連……這是個(gè)悲傷的循環(huán)。
為了避免這種情況的發(fā)生,除了注意 Redis 單機(jī)數(shù)據(jù)量不要過(guò)大,另一方面就是適當(dāng)增大 repl-timeout 值,具體的大小可以根據(jù) bgsave 耗時(shí)來(lái)調(diào)整。
命令傳播階段:如前所述,在該階段主節(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)也沒(méi)有向從節(jié)點(diǎn)發(fā)送數(shù)據(jù),則從節(jié)點(diǎn)很容易判斷超時(shí)。
慢查詢導(dǎo)致的阻塞:如果主節(jié)點(diǎn)或從節(jié)點(diǎn)執(zhí)行了一些慢查詢(如 keys * 或者對(duì)大數(shù)據(jù)的 hgetall 等),導(dǎo)致服務(wù)器阻塞;阻塞期間無(wú)法響應(yīng)復(fù)制連接中對(duì)方節(jié)點(diǎn)的請(qǐng)求,可能導(dǎo)致復(fù)制超時(shí)。
復(fù)制中斷問(wèn)題
主從節(jié)點(diǎn)超時(shí)是復(fù)制中斷的原因之一,除此之外,還有其他情況可能導(dǎo)致復(fù)制中斷,其中最主要的是復(fù)制緩沖區(qū)溢出問(wèn)題。
復(fù)制緩沖區(qū)溢出
前面曾提到過(guò),在全量復(fù)制階段,主節(jié)點(diǎn)會(huì)將執(zhí)行的寫(xiě)命令放到復(fù)制緩沖區(qū)中。
該緩沖區(qū)存放的數(shù)據(jù)包括了以下幾個(gè)時(shí)間段內(nèi)主節(jié)點(diǎn)執(zhí)行的寫(xiě)命令:
- bgsave 生成 RDB 文件。
- RDB 文件由主節(jié)點(diǎn)發(fā)往從節(jié)點(diǎn)。
- 從節(jié)點(diǎn)清空老數(shù)據(jù)并載入 RDB 文件中的數(shù)據(jù)。
當(dāng)主節(jié)點(diǎn)數(shù)據(jù)量較大,或者主從節(jié)點(diǎn)之間網(wǎng)絡(luò)延遲較大時(shí),可能導(dǎo)致該緩沖區(qū)的大小超過(guò)了限制,此時(shí)主節(jié)點(diǎn)會(huì)斷開(kāi)與從節(jié)點(diǎn)之間的連接。
這種情況可能引起全量復(fù)制→復(fù)制緩沖區(qū)溢出導(dǎo)致連接中斷→重連→全量復(fù)制→復(fù)制緩沖區(qū)溢出導(dǎo)致連接中斷……的循環(huán)。
復(fù)制緩沖區(qū)的大小由 client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds} 配置,默認(rèn)值為 client-output-buffer-limit slave 256MB 64MB 60。
其含義是:如果 buffer 大于 256MB,或者連續(xù) 60s 大于 64MB,則主節(jié)點(diǎn)會(huì)斷開(kāi)與該從節(jié)點(diǎn)的連接。該參數(shù)是可以通過(guò) config set 命令動(dòng)態(tài)配置的(即不重啟 Redis 也可以生效)。
當(dāng)復(fù)制緩沖區(qū)溢出時(shí),主節(jié)點(diǎn)打印日志如下所示:
需要注意的是,復(fù)制緩沖區(qū)是客戶端輸出緩沖區(qū)的一種,主節(jié)點(diǎn)會(huì)為每一個(gè)從節(jié)點(diǎn)分別分配復(fù)制緩沖區(qū);而復(fù)制積壓緩沖區(qū)則是一個(gè)主節(jié)點(diǎn)只有一個(gè),無(wú)論它有多少個(gè)從節(jié)點(diǎn)。
各場(chǎng)景下復(fù)制的選擇及優(yōu)化技巧
在介紹了 Redis 復(fù)制的種種細(xì)節(jié)之后,現(xiàn)在我們可以來(lái)總結(jié)一下,在下面常見(jiàn)的場(chǎng)景中,何時(shí)使用部分復(fù)制,以及需要注意哪些問(wèn)題。
***次建立復(fù)制
此時(shí)全量復(fù)制不可避免,但仍有幾點(diǎn)需要注意:如果主節(jié)點(diǎn)的數(shù)據(jù)量較大,應(yīng)該盡量避開(kāi)流量的高峰期,避免造成阻塞。
如果有多個(gè)從節(jié)點(diǎn)需要建立對(duì)主節(jié)點(diǎn)的復(fù)制,可以考慮將幾個(gè)從節(jié)點(diǎn)錯(cuò)開(kāi),避免主節(jié)點(diǎn)帶寬占用過(guò)大。
此外,如果從節(jié)點(diǎn)過(guò)多,也可以調(diào)整主從復(fù)制的拓?fù)浣Y(jié)構(gòu),由一主多從結(jié)構(gòu)變?yōu)闃?shù)狀結(jié)構(gòu)(中間的節(jié)點(diǎn)既是其主節(jié)點(diǎn)的從節(jié)點(diǎn),也是其從節(jié)點(diǎn)的主節(jié)點(diǎn))。
但使用樹(shù)狀結(jié)構(gòu)應(yīng)該謹(jǐn)慎:雖然主節(jié)點(diǎn)的直接從節(jié)點(diǎn)減少,降低了主節(jié)點(diǎn)的負(fù)擔(dān),但是多層從節(jié)點(diǎn)的延遲增大,數(shù)據(jù)一致性變差;且結(jié)構(gòu)復(fù)雜,維護(hù)相當(dāng)困難。
主節(jié)點(diǎn)重啟
主節(jié)點(diǎn)重啟可以分為兩種情況來(lái)討論,一種是故障導(dǎo)致宕機(jī),另一種則是有計(jì)劃的重啟。
主節(jié)點(diǎn)宕機(jī):主節(jié)點(diǎn)宕機(jī)重啟后,runid 會(huì)發(fā)生變化,因此不能進(jìn)行部分復(fù)制,只能全量復(fù)制。
實(shí)際上在主節(jié)點(diǎn)宕機(jī)的情況下,應(yīng)進(jìn)行故障轉(zhuǎn)移處理,將其中的一個(gè)從節(jié)點(diǎn)升級(jí)為主節(jié)點(diǎn),其他從節(jié)點(diǎn)從新的主節(jié)點(diǎn)進(jìn)行復(fù)制;且故障轉(zhuǎn)移應(yīng)盡量的自動(dòng)化,后面文章將要介紹的哨兵便可以進(jìn)行自動(dòng)的故障轉(zhuǎn)移。
安全重啟 debug reload:在一些場(chǎng)景下,可能希望對(duì)主節(jié)點(diǎn)進(jìn)行重啟,例如主節(jié)點(diǎn)內(nèi)存碎片率過(guò)高,或者希望調(diào)整一些只能在啟動(dòng)時(shí)調(diào)整的參數(shù)。
如果使用普通的手段重啟主節(jié)點(diǎn),會(huì)使得 runid 發(fā)生變化,可能導(dǎo)致不必要的全量復(fù)制。
為了解決這個(gè)問(wèn)題,Redis 提供了 debug reload 的重啟方式:重啟后,主節(jié)點(diǎn)的 runid 和 offset 都不受影響,避免了全量復(fù)制。
如下圖所示,debug reload 重啟后 runid 和 offset 都未受影響:
但 debug reload 是一柄雙刃劍:它會(huì)清空當(dāng)前內(nèi)存中的數(shù)據(jù),重新從 RDB 文件中加載,這個(gè)過(guò)程會(huì)導(dǎo)致主節(jié)點(diǎn)的阻塞,因此也需要謹(jǐn)慎。
從節(jié)點(diǎn)重啟
從節(jié)點(diǎn)宕機(jī)重啟后,其保存的主節(jié)點(diǎn)的 runid 會(huì)丟失,因此即使再次執(zhí)行 slaveof,也無(wú)法進(jìn)行部分復(fù)制。
網(wǎng)絡(luò)中斷
如果主從節(jié)點(diǎn)之間出現(xiàn)網(wǎng)絡(luò)問(wèn)題,造成短時(shí)間內(nèi)網(wǎng)絡(luò)中斷,可以分為多種情況討論。
***種情況:網(wǎng)絡(luò)問(wèn)題時(shí)間極為短暫,只造成了短暫的丟包,主從節(jié)點(diǎn)都沒(méi)有判定超時(shí)(未觸發(fā) repl-timeout);此時(shí)只需要通過(guò) REPLCONF ACK 來(lái)補(bǔ)充丟失的數(shù)據(jù)即可。
第二種情況:網(wǎng)絡(luò)問(wèn)題時(shí)間很長(zhǎng),主從節(jié)點(diǎn)判斷超時(shí)(觸發(fā)了 repl-timeout),且丟失的數(shù)據(jù)過(guò)多,超過(guò)了復(fù)制積壓緩沖區(qū)所能存儲(chǔ)的范圍;此時(shí)主從節(jié)點(diǎn)無(wú)法進(jìn)行部分復(fù)制,只能進(jìn)行全量復(fù)制。
為了盡可能避免這種情況的發(fā)生,應(yīng)該根據(jù)實(shí)際情況適當(dāng)調(diào)整復(fù)制積壓緩沖區(qū)的大小;此外及時(shí)發(fā)現(xiàn)并修復(fù)網(wǎng)絡(luò)中斷,也可以減少全量復(fù)制。
第三種情況:介于前述兩種情況之間,主從節(jié)點(diǎn)判斷超時(shí),且丟失的數(shù)據(jù)仍然都在復(fù)制積壓緩沖區(qū)中;此時(shí)主從節(jié)點(diǎn)可以進(jìn)行部分復(fù)制。
復(fù)制相關(guān)的配置
這一節(jié)總結(jié)一下與復(fù)制有關(guān)的配置,說(shuō)明這些配置的作用、起作用的階段,以及配置方法等。
通過(guò)了解這些配置,一方面加深對(duì) Redis 復(fù)制的了解,另一方面掌握這些配置的方法,可以優(yōu)化 Redis 的使用,少走坑。
配置大致可以分為主節(jié)點(diǎn)相關(guān)配置、從節(jié)點(diǎn)相關(guān)配置以及與主從節(jié)點(diǎn)都有關(guān)的配置,下面分別說(shuō)明。
與主從節(jié)點(diǎn)都有關(guān)的配置
首先介紹最特殊的配置,它決定了該節(jié)點(diǎn)是主節(jié)點(diǎn)還是從節(jié)點(diǎn):
- slaveof <masterip> <masterport>:Redis 啟動(dòng)時(shí)起作用;作用是建立復(fù)制關(guān)系,開(kāi)啟了該配置的 Redis 服務(wù)器在啟動(dòng)后成為從節(jié)點(diǎn)。該注釋默認(rèn)注釋掉,即 Redis 服務(wù)器默認(rèn)都是主節(jié)點(diǎn)。
- repl-timeout 60:與各個(gè)階段主從節(jié)點(diǎn)連接超時(shí)判斷有關(guān),見(jiàn)前面的介紹。
主節(jié)點(diǎn)相關(guān)配置
repl-diskless-sync no:作用于全量復(fù)制階段,控制主節(jié)點(diǎn)是否使用 diskless 復(fù)制(無(wú)盤(pán)復(fù)制)。
所謂 diskless 復(fù)制,是指在全量復(fù)制時(shí),主節(jié)點(diǎn)不再先把數(shù)據(jù)寫(xiě)入 RDB 文件,而是直接寫(xiě)入 slave 的 socket 中,整個(gè)過(guò)程中不涉及硬盤(pán)。
diskless 復(fù)制在磁盤(pán) IO 很慢而網(wǎng)速很快時(shí)更有優(yōu)勢(shì)。需要注意的是,截至 Redis 3.0,diskless 復(fù)制處于實(shí)驗(yàn)階段,默認(rèn)是關(guān)閉的。
repl-diskless-sync-delay 5:該配置作用于全量復(fù)制階段,當(dāng)主節(jié)點(diǎn)使用 diskless 復(fù)制時(shí),該配置決定主節(jié)點(diǎn)向從節(jié)點(diǎn)發(fā)送之前停頓的時(shí)間,單位是秒;只有當(dāng) diskless 復(fù)制打開(kāi)時(shí)有效,默認(rèn) 5s。
之所以設(shè)置停頓時(shí)間,是基于以下兩個(gè)考慮:
- 向 slave 的 socket 的傳輸一旦開(kāi)始,新連接的 slave 只能等待當(dāng)前數(shù)據(jù)傳輸結(jié)束,才能開(kāi)始新的數(shù)據(jù)傳輸。
- 多個(gè)從節(jié)點(diǎn)有較大的概率在短時(shí)間內(nèi)建立主從復(fù)制。
client-output-buffer-limit slave 256MB 64MB 60:與全量復(fù)制階段主節(jié)點(diǎn)的緩沖區(qū)大小有關(guān),見(jiàn)前面的介紹。
repl-disable-tcp-nodelay no:與命令傳播階段的延遲有關(guān),見(jiàn)前面的介紹。
masterauth <master-password>:與連接建立階段的身份驗(yàn)證有關(guān),見(jiàn)前面的介紹。
repl-ping-slave-period 10:與命令傳播階段主從節(jié)點(diǎn)的超時(shí)判斷有關(guān),見(jiàn)前面的介紹。
repl-backlog-size 1mb:復(fù)制積壓緩沖區(qū)的大小,見(jiàn)前面的介紹。
repl-backlog-ttl 3600:當(dāng)主節(jié)點(diǎn)沒(méi)有從節(jié)點(diǎn)時(shí),復(fù)制積壓緩沖區(qū)保留的時(shí)間,這樣當(dāng)斷開(kāi)的從節(jié)點(diǎn)重新連進(jìn)來(lái)時(shí),可以進(jìn)行全量復(fù)制;默認(rèn) 3600s。如果設(shè)置為 0,則永遠(yuǎn)不會(huì)釋放復(fù)制積壓緩沖區(qū)。
min-slaves-to-write 3 與 min-slaves-max-lag 10:規(guī)定了主節(jié)點(diǎn)的最小從節(jié)點(diǎn)數(shù)目,及對(duì)應(yīng)的***延遲,見(jiàn)前面的介紹。
從節(jié)點(diǎn)相關(guān)配置
slave-serve-stale-data yes:與從節(jié)點(diǎn)數(shù)據(jù)陳舊時(shí)是否響應(yīng)客戶端命令有關(guān),見(jiàn)前面的介紹。
slave-read-only yes:從節(jié)點(diǎn)是否只讀;默認(rèn)是只讀的。由于從節(jié)點(diǎn)開(kāi)啟寫(xiě)操作容易導(dǎo)致主從節(jié)點(diǎn)的數(shù)據(jù)不一致,因此該配置盡量不要修改。
單機(jī)內(nèi)存大小限制
在深入學(xué)習(xí) Redis 持久化一文中,講到了 fork 操作對(duì) Redis 單機(jī)內(nèi)存大小的限制。實(shí)際上在 Redis 的使用中,限制單機(jī)內(nèi)存大小的因素非常之多。
下面總結(jié)一下在主從復(fù)制中,單機(jī)內(nèi)存過(guò)大可能造成的影響:
- 切主:當(dāng)主節(jié)點(diǎn)宕機(jī)時(shí),一種常見(jiàn)的容災(zāi)策略是將其中一個(gè)從節(jié)點(diǎn)提升為主節(jié)點(diǎn),并將其他從節(jié)點(diǎn)掛載到新的主節(jié)點(diǎn)上,此時(shí)這些從節(jié)點(diǎn)只能進(jìn)行全量復(fù)制。
如果 Redis 單機(jī)內(nèi)存達(dá)到 10GB,一個(gè)從節(jié)點(diǎn)的同步時(shí)間在幾分鐘的級(jí)別;如果從節(jié)點(diǎn)較多,恢復(fù)的速度會(huì)更慢。
如果系統(tǒng)的讀負(fù)載很高,而這段時(shí)間從節(jié)點(diǎn)無(wú)法提供服務(wù),會(huì)對(duì)系統(tǒng)造成很大的壓力。
- 從庫(kù)擴(kuò)容:如果訪問(wèn)量突然增大,此時(shí)希望增加從節(jié)點(diǎn)分擔(dān)讀負(fù)載,如果數(shù)據(jù)量過(guò)大,從節(jié)點(diǎn)同步太慢,難以及時(shí)應(yīng)對(duì)訪問(wèn)量的暴增。
- 緩沖區(qū)溢出:(1)和(2)都是從節(jié)點(diǎn)可以正常同步的情形(雖然慢),但是如果數(shù)據(jù)量過(guò)大,導(dǎo)致全量復(fù)制階段主節(jié)點(diǎn)的復(fù)制緩沖區(qū)溢出,從而導(dǎo)致復(fù)制中斷。
則主從節(jié)點(diǎn)的數(shù)據(jù)同步會(huì)全量復(fù)制→復(fù)制緩沖區(qū)溢出導(dǎo)致復(fù)制中斷→重連→全量復(fù)制→復(fù)制緩沖區(qū)溢出導(dǎo)致復(fù)制中斷……的循環(huán)。
- 超時(shí):如果數(shù)據(jù)量過(guò)大,全量復(fù)制階段主節(jié)點(diǎn) fork+ 保存 RDB 文件耗時(shí)過(guò)大,從節(jié)點(diǎn)長(zhǎng)時(shí)間接收不到數(shù)據(jù)觸發(fā)超時(shí)。
主從節(jié)點(diǎn)的數(shù)據(jù)同步同樣可能陷入全量復(fù)制→超時(shí)導(dǎo)致復(fù)制中斷→重連→全量復(fù)制→超時(shí)導(dǎo)致復(fù)制中斷……的循環(huán)。
此外,主節(jié)點(diǎn)單機(jī)內(nèi)存除了絕對(duì)量不能太大,其占用主機(jī)內(nèi)存的比例也不應(yīng)過(guò)大:***只使用 50%-65% 的內(nèi)存,留下 30%-45% 的內(nèi)存用于執(zhí)行 bgsave 命令和創(chuàng)建復(fù)制緩沖區(qū)等。
info Replication
在 Redis 客戶端通過(guò) info Replication 可以查看與復(fù)制相關(guān)的狀態(tài),對(duì)于了解主從節(jié)點(diǎn)的當(dāng)前狀態(tài),以及解決出現(xiàn)的問(wèn)題都會(huì)有幫助。
主節(jié)點(diǎn):
從節(jié)點(diǎn):
對(duì)于從節(jié)點(diǎn),上半部分展示的是其作為從節(jié)點(diǎn)的狀態(tài),從 connectd_slaves 開(kāi)始,展示的是其作為潛在的主節(jié)點(diǎn)的狀態(tài)。
info Replication 中展示的大部分內(nèi)容在文章中都已經(jīng)講述,這里不再詳述。
總結(jié)
下面回顧一下本文的主要內(nèi)容:
- 主從復(fù)制的作用:宏觀的了解主從復(fù)制是為了解決什么樣的問(wèn)題,即數(shù)據(jù)冗余、故障恢復(fù)、讀負(fù)載均衡等。
- 主從復(fù)制的操作:即 slaveof 命令。
- 主從復(fù)制的原理:主從復(fù)制包括了連接建立階段、數(shù)據(jù)同步階段、命令傳播階段。
其中數(shù)據(jù)同步階段,有全量復(fù)制和部分復(fù)制兩種數(shù)據(jù)同步方式;命令傳播階段,主從節(jié)點(diǎn)之間有 PING 和 REPLCONF ACK 命令互相進(jìn)行心跳檢測(cè)。
- 應(yīng)用中的問(wèn)題:包括讀寫(xiě)分離的問(wèn)題(數(shù)據(jù)不一致問(wèn)題、數(shù)據(jù)過(guò)期問(wèn)題、故障切換問(wèn)題等)、復(fù)制超時(shí)問(wèn)題、復(fù)制中斷問(wèn)題等。
然后總結(jié)了主從復(fù)制相關(guān)的配置,其中 repl-timeout、client-output-buffer-limit slave 等對(duì)解決 Redis 主從復(fù)制中出現(xiàn)的問(wèn)題可能會(huì)有幫助。
主從復(fù)制雖然解決或緩解了數(shù)據(jù)冗余、故障恢復(fù)、讀負(fù)載均衡等問(wèn)題,但其缺陷仍很明顯:故障恢復(fù)無(wú)法自動(dòng)化;寫(xiě)操作無(wú)法負(fù)載均衡;存儲(chǔ)能力受到單機(jī)的限制。
這些問(wèn)題的解決,需要哨兵和集群的幫助,我將在后面的文章中介紹,歡迎關(guān)注。