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