淺談Redis主從復(fù)制
2013.09.27 11:27:00 來(lái)源: 京東 作者:張成遠(yuǎn) ( 0 條評(píng)論 )
Redis是一個(gè)開(kāi)源的嫉入,遵守BSD許可協(xié)議的key/value緩存系統(tǒng),并由其高效的響應(yīng)速度以及豐富的數(shù)據(jù)結(jié)構(gòu)而聞名觉至。Redis在京東的使用也是非常普遍的剔应,包括很多關(guān)鍵業(yè)務(wù)上的使用,由于Redis官方集群還未發(fā)布康谆,在使用Redis的過(guò)程中需要面對(duì)Redis的單點(diǎn)問(wèn)題领斥,京東采用的是一種比較通用的解決方案即由主從備份再加相應(yīng)的主從切換(在一些場(chǎng)景下可能進(jìn)行讀寫(xiě)分離),使主Redis出現(xiàn)失效的時(shí)候可以快速的切換到從Redis上沃暗。但Redis目前存在的一個(gè)問(wèn)題是主從復(fù)制在遇到網(wǎng)絡(luò)不穩(wěn)定的情況下,Slave和Master斷開(kāi)(包括閃斷)會(huì)導(dǎo)致Master需要將內(nèi)存中的數(shù)據(jù)全部重新生成rdb文件(快照文件)何恶,然后傳輸給Slave孽锥。Slave接收完Master傳遞過(guò)來(lái)的rdb文件以后會(huì)將自身的內(nèi)存清空,把rdb文件重新加載到內(nèi)存中细层。這種方式效率比較低下惜辑,尤其是在數(shù)據(jù)量大的情況下,畢竟網(wǎng)絡(luò)閃斷未必丟數(shù)據(jù)或者說(shuō)丟的數(shù)據(jù)只是少部分疫赎,但卻要為此付出將整個(gè)內(nèi)存數(shù)據(jù)都重新傳輸一次的代價(jià)盛撑。如果能夠?qū)㈤W斷過(guò)程的更新數(shù)據(jù)傳遞給Slave,那么就不需要將Master內(nèi)存中的所有數(shù)據(jù)都傳遞給Slave了捧搞。Redis作者在2.8的候選版(以下簡(jiǎn)稱(chēng)Redis2.8)中已經(jīng)將這個(gè)部分復(fù)制的思路實(shí)現(xiàn)了抵卫。
那么Redis2.4.16的全量復(fù)制與Redis2.8的部分復(fù)制是如何實(shí)現(xiàn)的呢?如下圖所示,這5個(gè)狀態(tài)是Slave在主從復(fù)制過(guò)程涉及到的幾個(gè)狀態(tài)狮荔,其中REDIS_REPL_NONE是Redis啟動(dòng)時(shí)候默認(rèn)的狀態(tài)。圖1-2所示的四個(gè)狀態(tài)表示站在Master的角度來(lái)看介粘,Slave所處于的狀態(tài)殖氏,因?yàn)镾lave在Master端看來(lái)就是一個(gè)特殊的client(同理Master在Slave端看來(lái)也是一個(gè)特殊的client)。
/* Slave replication state – Slave side */
define REDIS_REPL_NONE 0 /* No active replication */
define REDIS_REPL_CONNECT 1 /* Must connect to Master */
define REDIS_REPL_CONNECTING 2 /* Connecting to Master */
define REDIS_REPL_TRANSFER 3 /* Receiving .rdb from Master */
define REDIS_REPL_CONNECTED 4 /* Connected to Master */
Slave自身的狀態(tài)
define REDIS_REPL_WAIT_BGSAVE_START 3 /* Master waits bgsave to start feeding it */
define REDIS_REPL_WAIT_BGSAVE_END 4 /* Master waits bgsave to start bulk DB transmission */
define REDIS_REPL_SEND_BULK 5 /* Master is sending the bulk DB */
define REDIS_REPL_ONLINE 6 /* bulk DB already transmitted, receive updates */
Master端的Slave狀態(tài)
Redis在接收到“slaveof ip port”命令以后姻采,首先會(huì)將自身的狀態(tài)置為REDIS_REPL_CONNECT雅采,表示需要與自己的Master連接,此時(shí)Slave并沒(méi)有與Master做連接慨亲。Redis每隔100ms會(huì)調(diào)用serverCron()函數(shù)一次婚瓜,每10次serverCron()的調(diào)用會(huì)調(diào)用replicationCron()一次,即每1s會(huì)調(diào)用一次replication()函數(shù)刑棵。在replication()函數(shù)中巴刻,會(huì)檢查Slave的狀態(tài),如果是處于REDIS_REPL_CONNECT狀態(tài)铐望,就會(huì)建立syncWithMaster()的事件處理函數(shù)冈涧,并將Slave的狀態(tài)改成REDIS_REPL_CONNECTING。syncWithMaster()函數(shù)主要是向Master發(fā)送sync命令正蛙,當(dāng)該事件處理函數(shù)被觸發(fā)以后會(huì)將Slave的狀態(tài)改成REDIS_REPL_TRANSFER督弓,表示Slave已經(jīng)準(zhǔn)備就緒要接收Master生成的rdb文件。
回到Master的角色乒验,Master發(fā)現(xiàn)有一個(gè)Slave連接上來(lái)愚隧,如果此時(shí)的Master一個(gè)Slave都沒(méi)有且沒(méi)有后臺(tái)快照進(jìn)程,則啟動(dòng)一個(gè)后臺(tái)進(jìn)程將當(dāng)前內(nèi)存中的數(shù)據(jù)生成一個(gè)rdb文件锻全,同時(shí)將Slave的狀態(tài)置為REDIS_REPL_WAIT_BGSAVE_END狀態(tài)狂塘,表示該Slave等待Master的快照進(jìn)程結(jié)束。在后臺(tái)進(jìn)行生成rdb文件的時(shí)候鳄厌,如果有對(duì)redis的更新命令荞胡,Master會(huì)將這些更新命令存到該Slave的buffer中,如果buffer滿(mǎn)了會(huì)另外開(kāi)辟list來(lái)存儲(chǔ)這些更新命令了嚎。當(dāng)后臺(tái)快照進(jìn)程結(jié)束泪漂,Master會(huì)將該Slave的狀態(tài)改為REDIS_REPL_SEND_BULK,同時(shí)注冊(cè)sendBulkToSlave()事件處理函數(shù)用于將生成的rdb文件傳輸給Slave歪泳。等rdb傳輸結(jié)束以后萝勤,sendBulkToSlave()事件函數(shù)會(huì)被刪除,Slave的狀態(tài)會(huì)被更改為REDIS_REPL_ONLINE呐伞,另外再注冊(cè)sendReplyToClient()事件函數(shù)敌卓,將Master在快照內(nèi)過(guò)程中的所有更新操作(Slave的buffer里存的命令)發(fā)給Slave。
再回到Slave的角色伶氢,當(dāng)Master向Slave傳輸完rdb文件以后趟径,Slave自身會(huì)將狀態(tài)改為REDIS_REPL_CONNECTED瘪吏,表示復(fù)制已完成,處于與Master保持實(shí)時(shí)同步的狀態(tài)舵抹。
上述描述的狀態(tài)轉(zhuǎn)換如圖1-3所示肪虎,由圖中可知,站在Slave角色看惧蛹,當(dāng)出現(xiàn)網(wǎng)絡(luò)中斷的時(shí)候不管Slave本身是處于REDIS_REPL_CONNECTING扇救、REDIS_REPL_REPL_TRANSFER還是REDIS_REPL_CONNECTED,都會(huì)調(diào)用相應(yīng)的處理函數(shù)使Slave進(jìn)入REDIS_REPL_CONNECT狀態(tài)香嗓,這就意味著Slave需要重新向Master發(fā)送sync命令迅腔,重新進(jìn)行一次全量同步過(guò)程。圖中的REDIS_REPL_WAIT_BGSAVE_START狀態(tài)是在Slave連接上Master的時(shí)候(站在Master的角色看)靠娱,當(dāng)時(shí)Master剛好后臺(tái)有快照進(jìn)程且該快照進(jìn)程生成的rdb不適合直接傳給該Slave時(shí)出現(xiàn)的狀態(tài)沧烈,則將Slave的狀態(tài)置為REDIS_REPL_WAIT_BGSAVE_START。如果此時(shí)有快照進(jìn)程且找到了另外的發(fā)起快照進(jìn)程的Slave像云,只需要將另外的Slave的buffer內(nèi)容拷貝到該Slave的buffer中锌雀,然后直接進(jìn)入REDIS_REPL_WAIT_BGSAVE_END狀態(tài)。如果此時(shí)沒(méi)有后臺(tái)快照進(jìn)程迅诬,Slave直接進(jìn)入REDIS_REPL_WAIT_BGSAVE_END狀態(tài)腋逆,同時(shí)啟動(dòng)一個(gè)后臺(tái)快照進(jìn)程。
圖1:Redis-2.4.16主從復(fù)制狀態(tài)轉(zhuǎn)換圖
在上述狀態(tài)轉(zhuǎn)圖中存在的最大問(wèn)題在于任何網(wǎng)絡(luò)閃斷都會(huì)導(dǎo)致Slave與Master重連侈贷,然后重新進(jìn)入快照過(guò)程惩歉,需要花費(fèi)較長(zhǎng)的時(shí)間重新傳輸rdb文件,而Slave在接收完rdb文件以后試圖將rdb文件恢復(fù)到內(nèi)存的過(guò)程中是不能服務(wù)的(除info命令外)俏蛮。所以提供部分復(fù)制至少可以做到在網(wǎng)絡(luò)閃斷且更新命令不太多的情景下能夠盡量的避免全量復(fù)制的方案就顯得尤為重要撑蚌。
慶幸的是Redis2.8中里已經(jīng)能夠做到在網(wǎng)絡(luò)閃斷的情況下,Slave重新連接上Master以后搏屑,僅僅只傳輸閃斷期間的更新命令争涌。在Redis2.8中redisServer結(jié)構(gòu)中增加了一個(gè)成員:
char runid[REDIS_RUN_ID_SIZE+1]; /* ID always different at every exec. /
該runid是由一個(gè)getRandomHexChars()函數(shù)生成的每次不同的一個(gè)唯一標(biāo)識(shí),不同Redis實(shí)例之間該runid是不同的辣恋,同一個(gè)Redis重啟以后第煮,其runid和之前的runid也是不同的。
還增加了比較重要的幾項(xiàng)數(shù)據(jù)成員抑党,如下所示:
char repl_backlog; / Replication backlog for partial syncs /
long long repl_backlog_size; / Backlog circular buffer size /
long long repl_backlog_histlen; / Backlog actual data length /
long long repl_backlog_idx; / Backlog circular buffer current offset /
long long repl_backlog_off; / Replication offset of first byte in the backlog buffer. /
time_t repl_backlog_time_limit; / Time without Slaves after the backlog gets released. /
time_t repl_no_Slaves_since; / We have no Slaves since that time.
Only valid if server.Slaves len is 0. /
Redis2.8增加的數(shù)據(jù)成員
repl_backlog是redis用于存儲(chǔ)更新命令的一塊buffer,在部分復(fù)制的時(shí)候Slave會(huì)請(qǐng)求Master從這塊buffer中獲取閃斷情況下丟失的更新操作撵摆。repl_backlog在redis啟動(dòng)的時(shí)候初始化為NULL底靠,當(dāng)有Slave連接上來(lái)的時(shí)候,會(huì)被指向創(chuàng)建的buffer特铝,默認(rèn)為10241024(即1Mb)暑中。repl_backlog_size表示該buffer的大小(默認(rèn)10241024壹瘟,即1Mb)。該buffer是作為一個(gè)環(huán)形緩存區(qū)使用的鳄逾,當(dāng)有數(shù)據(jù)超過(guò)buffer的大小以后就會(huì)重新從buffer的頭部開(kāi)始寫(xiě)入稻轨。repl_backlog_idx表示當(dāng)前緩存數(shù)據(jù)的尾部(因?yàn)槭黔h(huán)形buffer)。repl_backlog_off是全局緩存的偏移量雕凹,從開(kāi)始緩存數(shù)據(jù)起一直在增長(zhǎng)殴俱。如果Master一個(gè)Slave都沒(méi)有,則超過(guò)一段時(shí)間以后repl_backlog會(huì)被釋放枚抵,默認(rèn)超時(shí)時(shí)間是1小時(shí)线欲。
Redis2.8的主從復(fù)制如圖1-5所示,Slave如果與Master的連接超時(shí)了汽摹,Slave會(huì)將調(diào)用freeClient(server.Master)把連接關(guān)閉李丰。該freeClient()函數(shù)與2.4版本的相比做了改動(dòng),會(huì)將Master對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)的一些信息存起來(lái)作為cache Master逼泣,其中后續(xù)被用于部分復(fù)制的最重要的兩個(gè)信息一個(gè)是Master runid趴泌,另一個(gè)是reploff。reploff是Slave端接收到Master端傳遞過(guò)來(lái)的命令以后不斷更新記錄的全局偏移量的值拉庶,該值和Master端的repl_backlog_off對(duì)應(yīng)嗜憔,正常情況下reploff<=repl_backlog_off。如果Slave嘗試部分復(fù)制失敗以后砍的,就會(huì)將該cache Master釋放痹筛。
Redis2.8中主從復(fù)制的過(guò)程增加了REDIS_RECIVE_PONG狀態(tài),該狀態(tài)作為試圖與Master同步的時(shí)候先ping一下的一個(gè)中間狀態(tài)廓鞠。當(dāng)ping通以后帚稠,Slave首先會(huì)嘗試部分復(fù)制,從cache Master中拿出Master runid和reploff傳給Master床佳,表示請(qǐng)求部分復(fù)制滋早。第一次的時(shí)候,由于Slave端的cache Master是NULL砌们,所以Slave向Master發(fā)送的runid是“?”杆麸,偏移量是“-1”,當(dāng)Master收到這兩個(gè)變量以后會(huì)將自身的runid和實(shí)際偏移量發(fā)送給Slave浪感,同時(shí)讓Slave發(fā)起一次全量同步昔头。
Slave與Master完全同步以后,maste的更新命令會(huì)被存到repl_backlog中影兽,同時(shí)不斷更新偏移量等相關(guān)變量揭斧。這些更新命令不斷地被發(fā)送到Slave端,Slave也隨之更改自己記錄的偏移量峻堰。當(dāng)期間再次有網(wǎng)絡(luò)斷開(kāi)的情況讹开,Slave會(huì)根據(jù)記錄的runid和reploff向Master請(qǐng)求部分復(fù)制盅视,Master檢查Slave請(qǐng)求的偏移量對(duì)應(yīng)的內(nèi)容是否還在repl_backlog中,即比較repl_backlog_off和Slave傳遞過(guò)來(lái)的reploff的值的差是否小于等于repl_backlog中實(shí)際數(shù)據(jù)的長(zhǎng)度旦万,如果滿(mǎn)足條件則將這部分內(nèi)容發(fā)送給Slave闹击,部分復(fù)制完成。否則讓Slave進(jìn)行全量復(fù)制成艘。
Redis2.8之前的版本沒(méi)有提供部分復(fù)制功能赏半,當(dāng)出現(xiàn)網(wǎng)絡(luò)閃斷的情況會(huì)導(dǎo)致主從之間的全量復(fù)制。Redis2.8增加了部分復(fù)制功能狰腌,在處理網(wǎng)絡(luò)閃斷的情況下是非常有效的除破,這也是出Redis集群之前需要提供的基本保證。默認(rèn)1Mb的repl_backlog在訪問(wèn)量大的情況下可能效果未必理想琼腔,這個(gè)可以通過(guò)更改配置文件中的repl-backlog-size的值實(shí)現(xiàn)repl_backlog的大小的調(diào)整瑰枫。還有repl_backlog在沒(méi)有Slave的情況下過(guò)多久再釋放的時(shí)間閾值也可以通過(guò)配置文件中的repl-backlog-ttl進(jìn)行調(diào)整。