淺談Redis主從復制
2013.09.27 11:27:00 來源: 京東 作者:張成遠 ( 0 條評論 )
Redis是一個開源的,遵守BSD許可協(xié)議的key/value緩存系統(tǒng),并由其高效的響應速度以及豐富的數(shù)據(jù)結構而聞名。Redis在京東的使用也是非常普遍的,包括很多關鍵業(yè)務上的使用,由于Redis官方集群還未發(fā)布,在使用Redis的過程中需要面對Redis的單點問題,京東采用的是一種比較通用的解決方案即由主從備份再加相應的主從切換(在一些場景下可能進行讀寫分離),使主Redis出現(xiàn)失效的時候可以快速的切換到從Redis上。但Redis目前存在的一個問題是主從復制在遇到網(wǎng)絡不穩(wěn)定的情況下,Slave和Master斷開(包括閃斷)會導致Master需要將內存中的數(shù)據(jù)全部重新生成rdb文件(快照文件),然后傳輸給Slave。Slave接收完Master傳遞過來的rdb文件以后會將自身的內存清空,把rdb文件重新加載到內存中。這種方式效率比較低下,尤其是在數(shù)據(jù)量大的情況下,畢竟網(wǎng)絡閃斷未必丟數(shù)據(jù)或者說丟的數(shù)據(jù)只是少部分,但卻要為此付出將整個內存數(shù)據(jù)都重新傳輸一次的代價。如果能夠將閃斷過程的更新數(shù)據(jù)傳遞給Slave,那么就不需要將Master內存中的所有數(shù)據(jù)都傳遞給Slave了。Redis作者在2.8的候選版(以下簡稱Redis2.8)中已經(jīng)將這個部分復制的思路實現(xiàn)了。
那么Redis2.4.16的全量復制與Redis2.8的部分復制是如何實現(xiàn)的呢?如下圖所示,這5個狀態(tài)是Slave在主從復制過程涉及到的幾個狀態(tài),其中REDIS_REPL_NONE是Redis啟動時候默認的狀態(tài)。圖1-2所示的四個狀態(tài)表示站在Master的角度來看,Slave所處于的狀態(tài),因為Slave在Master端看來就是一個特殊的client(同理Master在Slave端看來也是一個特殊的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”命令以后,首先會將自身的狀態(tài)置為REDIS_REPL_CONNECT,表示需要與自己的Master連接,此時Slave并沒有與Master做連接。Redis每隔100ms會調用serverCron()函數(shù)一次,每10次serverCron()的調用會調用replicationCron()一次,即每1s會調用一次replication()函數(shù)。在replication()函數(shù)中,會檢查Slave的狀態(tài),如果是處于REDIS_REPL_CONNECT狀態(tài),就會建立syncWithMaster()的事件處理函數(shù),并將Slave的狀態(tài)改成REDIS_REPL_CONNECTING。syncWithMaster()函數(shù)主要是向Master發(fā)送sync命令,當該事件處理函數(shù)被觸發(fā)以后會將Slave的狀態(tài)改成REDIS_REPL_TRANSFER,表示Slave已經(jīng)準備就緒要接收Master生成的rdb文件。
回到Master的角色,Master發(fā)現(xiàn)有一個Slave連接上來,如果此時的Master一個Slave都沒有且沒有后臺快照進程,則啟動一個后臺進程將當前內存中的數(shù)據(jù)生成一個rdb文件,同時將Slave的狀態(tài)置為REDIS_REPL_WAIT_BGSAVE_END狀態(tài),表示該Slave等待Master的快照進程結束。在后臺進行生成rdb文件的時候,如果有對redis的更新命令,Master會將這些更新命令存到該Slave的buffer中,如果buffer滿了會另外開辟list來存儲這些更新命令。當后臺快照進程結束,Master會將該Slave的狀態(tài)改為REDIS_REPL_SEND_BULK,同時注冊sendBulkToSlave()事件處理函數(shù)用于將生成的rdb文件傳輸給Slave。等rdb傳輸結束以后,sendBulkToSlave()事件函數(shù)會被刪除,Slave的狀態(tài)會被更改為REDIS_REPL_ONLINE,另外再注冊sendReplyToClient()事件函數(shù),將Master在快照內過程中的所有更新操作(Slave的buffer里存的命令)發(fā)給Slave。
再回到Slave的角色,當Master向Slave傳輸完rdb文件以后,Slave自身會將狀態(tài)改為REDIS_REPL_CONNECTED,表示復制已完成,處于與Master保持實時同步的狀態(tài)。
上述描述的狀態(tài)轉換如圖1-3所示,由圖中可知,站在Slave角色看,當出現(xiàn)網(wǎng)絡中斷的時候不管Slave本身是處于REDIS_REPL_CONNECTING、REDIS_REPL_REPL_TRANSFER還是REDIS_REPL_CONNECTED,都會調用相應的處理函數(shù)使Slave進入REDIS_REPL_CONNECT狀態(tài),這就意味著Slave需要重新向Master發(fā)送sync命令,重新進行一次全量同步過程。圖中的REDIS_REPL_WAIT_BGSAVE_START狀態(tài)是在Slave連接上Master的時候(站在Master的角色看),當時Master剛好后臺有快照進程且該快照進程生成的rdb不適合直接傳給該Slave時出現(xiàn)的狀態(tài),則將Slave的狀態(tài)置為REDIS_REPL_WAIT_BGSAVE_START。如果此時有快照進程且找到了另外的發(fā)起快照進程的Slave,只需要將另外的Slave的buffer內容拷貝到該Slave的buffer中,然后直接進入REDIS_REPL_WAIT_BGSAVE_END狀態(tài)。如果此時沒有后臺快照進程,Slave直接進入REDIS_REPL_WAIT_BGSAVE_END狀態(tài),同時啟動一個后臺快照進程。
圖1:Redis-2.4.16主從復制狀態(tài)轉換圖
在上述狀態(tài)轉圖中存在的最大問題在于任何網(wǎng)絡閃斷都會導致Slave與Master重連,然后重新進入快照過程,需要花費較長的時間重新傳輸rdb文件,而Slave在接收完rdb文件以后試圖將rdb文件恢復到內存的過程中是不能服務的(除info命令外)。所以提供部分復制至少可以做到在網(wǎng)絡閃斷且更新命令不太多的情景下能夠盡量的避免全量復制的方案就顯得尤為重要。
慶幸的是Redis2.8中里已經(jīng)能夠做到在網(wǎng)絡閃斷的情況下,Slave重新連接上Master以后,僅僅只傳輸閃斷期間的更新命令。在Redis2.8中redisServer結構中增加了一個成員:
char runid[REDIS_RUN_ID_SIZE+1]; /* ID always different at every exec. /
該runid是由一個getRandomHexChars()函數(shù)生成的每次不同的一個唯一標識,不同Redis實例之間該runid是不同的,同一個Redis重啟以后,其runid和之前的runid也是不同的。
還增加了比較重要的幾項數(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用于存儲更新命令的一塊buffer,在部分復制的時候Slave會請求Master從這塊buffer中獲取閃斷情況下丟失的更新操作。repl_backlog在redis啟動的時候初始化為NULL,當有Slave連接上來的時候,會被指向創(chuàng)建的buffer,默認為10241024(即1Mb)。repl_backlog_size表示該buffer的大小(默認10241024,即1Mb)。該buffer是作為一個環(huán)形緩存區(qū)使用的,當有數(shù)據(jù)超過buffer的大小以后就會重新從buffer的頭部開始寫入。repl_backlog_idx表示當前緩存數(shù)據(jù)的尾部(因為是環(huán)形buffer)。repl_backlog_off是全局緩存的偏移量,從開始緩存數(shù)據(jù)起一直在增長。如果Master一個Slave都沒有,則超過一段時間以后repl_backlog會被釋放,默認超時時間是1小時。
Redis2.8的主從復制如圖1-5所示,Slave如果與Master的連接超時了,Slave會將調用freeClient(server.Master)把連接關閉。該freeClient()函數(shù)與2.4版本的相比做了改動,會將Master對應的數(shù)據(jù)結構的一些信息存起來作為cache Master,其中后續(xù)被用于部分復制的最重要的兩個信息一個是Master runid,另一個是reploff。reploff是Slave端接收到Master端傳遞過來的命令以后不斷更新記錄的全局偏移量的值,該值和Master端的repl_backlog_off對應,正常情況下reploff<=repl_backlog_off。如果Slave嘗試部分復制失敗以后,就會將該cache Master釋放。
Redis2.8中主從復制的過程增加了REDIS_RECIVE_PONG狀態(tài),該狀態(tài)作為試圖與Master同步的時候先ping一下的一個中間狀態(tài)。當ping通以后,Slave首先會嘗試部分復制,從cache Master中拿出Master runid和reploff傳給Master,表示請求部分復制。第一次的時候,由于Slave端的cache Master是NULL,所以Slave向Master發(fā)送的runid是“?”,偏移量是“-1”,當Master收到這兩個變量以后會將自身的runid和實際偏移量發(fā)送給Slave,同時讓Slave發(fā)起一次全量同步。
Slave與Master完全同步以后,maste的更新命令會被存到repl_backlog中,同時不斷更新偏移量等相關變量。這些更新命令不斷地被發(fā)送到Slave端,Slave也隨之更改自己記錄的偏移量。當期間再次有網(wǎng)絡斷開的情況,Slave會根據(jù)記錄的runid和reploff向Master請求部分復制,Master檢查Slave請求的偏移量對應的內容是否還在repl_backlog中,即比較repl_backlog_off和Slave傳遞過來的reploff的值的差是否小于等于repl_backlog中實際數(shù)據(jù)的長度,如果滿足條件則將這部分內容發(fā)送給Slave,部分復制完成。否則讓Slave進行全量復制。
Redis2.8之前的版本沒有提供部分復制功能,當出現(xiàn)網(wǎng)絡閃斷的情況會導致主從之間的全量復制。Redis2.8增加了部分復制功能,在處理網(wǎng)絡閃斷的情況下是非常有效的,這也是出Redis集群之前需要提供的基本保證。默認1Mb的repl_backlog在訪問量大的情況下可能效果未必理想,這個可以通過更改配置文件中的repl-backlog-size的值實現(xiàn)repl_backlog的大小的調整。還有repl_backlog在沒有Slave的情況下過多久再釋放的時間閾值也可以通過配置文件中的repl-backlog-ttl進行調整。