一、前言 在之前的文章已經詳細介紹了redis入門基礎已經持久化相關內容包括redis4.0所提供的混合持久化。 通過持久化功能,Redis保證了即使在伺服器宕機情況下數據的丟失非常少。但是如果這台伺服器出現了硬碟故障、系統崩潰等等,不僅僅是數據丟失,很可能對業務造成災難性打擊。為了避免單點故障通常 ...
一、前言
在之前的文章已經詳細介紹了redis入門基礎已經持久化相關內容包括redis4.0所提供的混合持久化。
通過持久化功能,Redis保證了即使在伺服器宕機情況下數據的丟失非常少。但是如果這台伺服器出現了硬碟故障、系統崩潰等等,不僅僅是數據丟失,很可能對業務造成災難性打擊。為了避免單點故障通常的做法是將數據複製多個副本保存在不同的伺服器上,這樣即使有其中一臺伺服器出現故障,其他伺服器依然可以繼續提供服務。當然Redis提供了多種高可用方案包括:主從複製、哨兵模式的主從複製、以及集群。
本文將詳細介紹Redis從2.6以到4.0提供複製方案的演進,也包括:主從複製、複製原理以及相關實踐。
二、主從複製
簡介
在主從複製中,資料庫分為兩類,一類是主庫(master),另一類是同步主庫數據的從庫(slave)。主庫可以進行讀寫操作,當寫操作導致數據變化時會自動同步到從庫。而從庫一般是只讀的(特定情況也可以寫,通過參數slave-read-only指定),並接受來自主庫的數據,一個主庫可擁有多個從庫,而一個從庫只能有一個主庫。這樣就使得redis的主從架構有了兩種模式:一類是一主多從如下圖1,二類是“鏈式主從複製”--主->從->主-從如下圖2。
對於一主多從的複製架構不必多說,這裡解釋下鏈式主從複製:如上圖2,主庫A的數據會同步到從庫B和從庫C,而B又是從庫D和從庫E的主庫,所以B的數據會同步到從庫D和從庫E。如果向B中寫數據,數據只能同步到D和E中,所以對於這種架構數據的一致性將不能保持,也不推薦這種架構。
搭建配置主從
由於沒有過多的機器,這裡將使用一臺機器上啟動多個redis實例實現主從複製。
對於redis來說搭建主從非常容易,引用官網一句話來說:there is a very simple to use and configure leader follower (master-slave) replication。
本次實踐分別以 10.1.210.68:6379 作為主,兩個從伺服器分別是 10.1.210.69:6380 和 10.1.210.69:6381。
搭建步驟:
- 將redis.conf文件拷貝三份,名稱最好有顯示的區別,我這裡採用名字為 6379.conf、 6380.conf、 6381.conf;
- 分別修改三個文件的ip(預設127.0.0.1可以不用修改)、埠(儘量和配置文件一致)、pid文件,日誌文件,持久化數據目錄(dir)、後臺運行(daemonize yes);
- 使用啟動命令腳本啟動每個redis服務;
- 設置主從關係、驗證主從同步;
示例:
步驟一:
#建立三個redis目錄 mkdir -p /opt/db/{redis6379,redis6380,redis6381} #從源碼中拷貝配置文件 cp redis-stable/redis.conf /opt/db/redis6379/6379.conf cp redis-stable/redis.conf /opt/db/redis6380/6380.conf cp redis-stable/redis.conf /opt/db/redis6381/6381.conf
步驟二:
修改配置項如下:找到對應的參數修改即可,下麵是每個配置文件修改部分、本機器IP地址是10.1.210.69;
daemonize yes #修改redis為後臺運行模式 pidfile /var/run/redis_6379.pid #修改運行的redis實例的pid,不能重覆 logfile "/opt/db/redis6379/6379.log" #指明日誌文件 dir "/opt/db/redis6379" #工作目錄,存放持久化數據的目錄 bind 10.1.210.69 #監聽地址,如果是單機多個示例可以不用修改 port 6379 #監聽埠,保持和配置文件名稱埠一致6379.conf
daemonize yes #修改redis為後臺運行模式 pidfile /var/run/redis_6380.pid #修改運行的redis實例的pid,不能重覆 logfile "/opt/db/redis6380/6380.log" #指明日誌文件 dir "/opt/db/redis6380" #工作目錄,存放持久化數據的目錄 bind 10.1.210.69 #監聽地址,如果是單機多個示例可以不用修改 port 6380 #監聽埠,保持和配置文件名稱埠一致6380.conf
daemonize yes #修改redis為後臺運行模式 pidfile /var/run/redis_6381.pid #修改運行的redis實例的pid,不能重覆 logfile "/opt/db/redis6379/6381.log" #指明日誌文件 dir "/opt/db/redis6381" #工作目錄,存放持久化數據的目錄 bind 10.1.210.69 #監聽地址,如果是單機多個實例可以不用修改使用127.0.0.1 port 6381 #監聽埠,保持和配置文件名稱埠一致6381.conf
步驟三:
啟動每個redis實例
redis-server /opt/db/redis6379/6379.conf redis-server /opt/db/redis6380/6380.conf redis-server /opt/db/redis6381/6381.conf
步驟四:
設置主從關係,當然你可以直接指明從庫配置文件直接使用slaveof <masterip> <masterport>指定,這裡我在用客戶端修改,分別使用客戶端redis-cli命令連入埠為6380、6381的redis。
連入6380資料庫,使用redis-cli -h 10.1.210.69 -p 6380,其中-h代表ip地址,-p代表埠,並執行slaveof 10.1.210.69 6379,並寫入配置文件config rewrite,如下:
同樣我們在從庫6381執行相同操作:
此時我們在使用info Replication 查看相關主從信息:
同時,還可以測試主從功能,在6379上創建key,在從庫查看:
主庫:
從庫:
三、複製原理
瞭解redis複製原理對日後運維有很大幫助,包括如何規劃節點,如何處理節點故障,redis複製過程可分為三個階段:
- 複製初始化階段
- 數據同步階段
- 命令傳播階段
複製初始化階段
當執行完slaveof masterip port 命令時候,從庫根據指明的master節點ip和port向主庫發起socket連接,主庫收到socket連接之後將連接信息保存,此時連接建立;
當socket連接建立完成以後,從庫向主庫發送ping命令,以確認主庫是否可用,此時的結果返回如果是PONG則代表主庫可以用,否則可能出現超時或者主庫此時在處理其他任務阻塞那麼此時從庫將斷開socket連接,然後進行重試;
如果主庫連接設置了密碼,則從庫需要設置masterauth參數,此時從庫會發送auth命令,命令格式為“auth + 密碼”進行密碼驗證,其中密碼為masterauth參數配置的密碼,需要註意的是如果主庫設置了密碼驗證,從庫未配置masterauth參數則報錯,socket連接斷開。
當身份驗證完成以後,從節點發送自己的監聽埠,主庫保存其埠信息,此時進入下一個階段:數據同步階段。
數據同步階段
主庫和從庫都確認對方信息以後,便可開始數據同步,此時從庫向主庫發送psync命令(需要註意的是redis4.0版本對2.8版本的psync做了優化,後續會進行說明),主庫收到該命令後判斷是進行增量複製還是全量複製,然後根據策略進行數據的同步,當主庫有新的寫操作時候,此時進入複製第三階段:命令傳播階段。
命令傳播階段
當數據同步完成以後,在此後的時間里主從維護著心跳檢查來確認對方是否線上,每隔一段時間(預設10秒,通過repl-ping-slave-period參數指定)主節點向從節點發送PING命令判斷從節點是否線上,而從節點每秒1次向主節點發送REPLCONF ACK命令,命令格式為:REPLCONF ACK {offset},其中offset指從節點保存的複製偏移量,作用一是彙報自己複製偏移量,主節點會對比複製偏移量向從節點發送未同步的命令,作用二在於判斷主節點是否線上,從庫接送命令並執行,最終實現與主庫數據相同。
樂觀複製
redis採用量樂觀複製策略,容忍在一定時間內主從數據內容是不同的,但是兩者的數據最終會同步。
四、redis複製演進
sync&psync1&psync2
從redis2.6到4.0開發人員對其複製流程進行逐步的優化,以下是演進過程:
- redis版本<=2.6<2.8,複製採用sync命令,無論是第一次主從複製還是斷線重連進行複製都採用全量複製;
- 2.8<=redis版本<4.0,複製採用psync,從redis2.8開始,redis複製從sync過渡到psync,這一特性主要添加了redis在斷線重新時候可使用部分複製;
- redis版本>=4.0,也採用psync,相比與2.8版本的psync優化了增量複製,這裡我們稱為psync2,2.8版本的psync稱為psync1。
以下將分別說明各個版本的複製演進。
sync
在redis2.6以及以前的版本,複製採用sync命令,當一個從庫啟動後,會向主庫發送sync命令,主庫收到sync命令後執行bgsave後臺保存RDB快照(該過程在上一篇已經詳細介紹),同時將保存快照的將快照保存期間接受的寫命令保存到緩衝隊列。當快照完成以後,主庫將快照文件已經緩存的所有命令發送給從庫,從庫接受到快照文件並載入,再將執行主庫發送的命令,也就是上面我們介紹的複製初始化階段和數據同步階段,其後就是命令增量同步,最終主庫與從庫保持數據一直。
當從庫在某些情況斷線重連(如從庫重啟、由於網路原因主從連接超時),重覆上述過程,進行數據同步。由此可見,redis2.6版本以及2.6以前複製過程全部採用全量複製。
sync雖然解決了數據同步問題,但是在數據量比較大情況下,從庫斷線從來依然採用全量複製機制,無論是從數據恢復、寬頻占用來說,sync所帶來的問題還是很多的。於是redis從2.8開始,引入新的命令psync。
psync1
在redis2.8版本,redis引入psync命令來進行主從的數據同步,這裡我們稱該命令為psync1。psync1實現依賴以下三個關鍵點:
1.offset(複製偏移量):
主庫和從庫分別各自維護一個複製偏移量(可以使用info replication查看),用於標識自己複製的情況,在主庫中代表主節點向從節點傳遞的位元組數,在從庫中代表從庫同步的位元組數。每當主庫向從節點發送N個位元組數據時,主節點的offset增加N,從庫每收到主節點傳來的N個位元組數據時,從庫的offset增加N。因此offset總是不斷增大,這也是判斷主從數據是否同步的標誌,若主從的offset相同則表示數據同步量,不通則表示數據不同步。以下圖示分別代表某個時刻兩個主從的同步情況(以下是4.0版本截圖):
2.replication backlog buffer(複製積壓緩衝區):
複製積壓緩衝區是一個固定長度的FIFO隊列,大小由配置參數repl-backlog-size指定,預設大小1MB。需要註意的是該緩衝區由master維護並且有且只有一個,所有slave共用此緩衝區,其作用在於備份最近主庫發送給從庫的數據。
在主從命令傳播階段,主節點除了將寫命令發送給從節點外,還會發送一份到複製積壓緩衝區,作為寫命令的備份。除了存儲最近的寫命令,複製積壓緩衝區中還存儲了每個位元組相應的複製偏移量(如下圖),由於複製積壓緩衝區固定大小先進先出的隊列,所以它總是保存的是最近redis執行的命令。
3.run_id(伺服器運行的唯一ID)
每個redis實例在啟動時候,都會隨機生成一個長度為40的唯一字元串來標識當前運行的redis節點,查看此id可通過命令info server查看。
當主從複製在初次複製時,主節點將自己的runid發送給從節點,從節點將這個runid保存起來,當斷線重連時,從節點會將這個runid發送給主節點。主節點根據runid判斷能否進行部分複製:
- 如果從節點保存的runid與主節點現在的runid相同,說明主從節點之前同步過,主節點會更具offset偏移量之後的數據判斷是否執行部分複製,如果offset偏移量之後的數據仍然都在複製積壓緩衝區里,則執行部分複製,否則執行全量複製;
- 如果從節點保存的runid與主節點現在的runid不同,說明從節點在斷線前同步的redis節點並不是當前的主節點,只能進行全量複製;
介紹完三個概念以後,接下來就可以介紹redis2.8提供的psync命令實現過程,如下圖:
圖文說明:
- 如果從伺服器以前沒有複製過任何主伺服器,或者之前執行過SLAVEOF no one命令,那麼從伺服器在開始一次新的複製時將向主伺服器發送PSYNC ? -1命令,主動請求主伺服器進行完整重同步(因為這時不可能執行部分重同步);
- 相反地,如果從伺服器已經複製過某個主伺服器,那麼從伺服器在開始一次新的複製時將向主伺服器發送PSYNC <runid> <offset>命令:其中runid是上一次複製的主伺服器的運行ID,而offset則是從伺服器當前的複製偏移量,接收到這個命令的主伺服器會通過這兩個參數來判斷應該對從伺服器執行哪種同步操作,如何判斷已經在介紹runid時進行詳細說明。
根據情況,接收到PSYNC命令的主伺服器會向從伺服器返回以下三種回覆的其中一種:
- 如果主伺服器返回+FULLRESYNC <runid> <offset>回覆,那麼表示主伺服器將與從伺服器執行完整重同步操作:其中runid是這個主伺服器的運行ID,從伺服器會將這個ID保存起來,在下一次發送PSYNC命令時使用;而offset則是主伺服器當前的複製偏移量,從伺服器會將這個值作為自己的初始化偏移量;
- 如果主伺服器返回+CONTINUE回覆,那麼表示主伺服器將與從伺服器執行部分同步操作,從伺服器只要等著主伺服器將自己缺少的那部分數據發送過來就可以了;
- 如果主伺服器返回-ERR回覆,那麼表示主伺服器的版本低於Redis 2.8,它識別不了PSYNC命令,從伺服器將向主伺服器發送SYNC命令,並與主伺服器執行完整同步操作。
由此可見psync也有不足之處,當從庫重啟以後runid發生變化,也就意味者從庫還是會進行全量複製,而在實際的生產中進行從庫的維護很多時候會進行重啟,而正是有由於全量同步需要主庫執行快照,以及數據傳輸會帶不小的影響。因此在4.0版本,psync命令做了改進,以下說明。
psync2
redis4.0新版本除了增加混合持久化,還優化了psync(以下稱psync2)並實現即使redis實例重啟的情況下也能實現部分同步,下麵主要介紹psync2實現過程。psync2在psync1基礎上新增兩個複製id(可使用info replication 查看如下圖):
-
master_replid: 複製id1(後文簡稱:replid1),一個長度為41個位元組(40個隨機串+’0’)的字元串,每個redis實例都有,和runid沒有直接關聯,但和runid生成規則相同。當實例變為從實例後,自己的replid1會被主實例的replid1覆蓋。
-
master_replid2:複製id2(後文簡稱:replid2),預設初始化為全0,用於存儲上次主實例的replid1。
在4.0之前的版本,redis複製信息完全丟失,所以每個實例重啟後只能進行全量複製,到了4.0版本,任然可以使用部分同步,其實現過程:
第一步:存儲複製信息
redis在關閉時,通過shutdown save,都會調用rdbSaveInfoAuxFields函數,把當前實例的repl-id和repl-offset保存到RDB文件中,當前的RDB存儲的數據內容和複製信息是一致性的可通過redis-check-rdb命令查看。
第二步:重啟後載入RDB文件中的複製信息
redis載入RDB文件,會專門處理文件中輔助欄位(AUX fields)信息,把其中repl_id和repl_offset載入到實例中,分別賦給master_replid和master_repl_offset兩個變數值,特別註意當從庫開啟了AOF持久化,redis載入順序發生變化優先載入AOF文件,但是由於aof文件中沒有複製信息,所以導致重啟後從實例依舊使用全量複製!
第三步:向主庫上報複制信息,判斷是否進行部分同步
從實例向主庫上報master_replid和master_repl_offset+1;從實例同時滿足以下兩條件,就可以部分重新同步,否則執行全量同步:
- 從實例上報master_replid串,與主實例的master_replid1或replid2有一個相等,用於判斷主從未發生改變;
- 從實例上報的master_repl_offset+1位元組,還存在於主實例的複製積壓緩衝區中,用於判斷從庫丟失部分是否在複製緩衝區中;
psync2除瞭解決redis重啟使用部分同步外,還為解決在主庫故障時候從庫切換為主庫時候使用部分同步機制。redis從庫預設開啟複製積壓緩衝區功能,以便從庫故障切換變化master後,其他落後該從庫可以從緩衝區中獲取缺少的命令。該過程的實現通過兩組replid、offset替換原來的master runid和offset變數實現:
- 第一組:master_replid和master_repl_offset:如果redis是主實例,則表示為自己的replid和複製偏移量; 如果redis是從實例,則表示為自己主實例的replid1和同步主實例的複製偏移量。
- 第二組:master_replid2和second_repl_offset:無論主從,都表示自己上次主實例repid1和複製偏移量;用於兄弟實例或級聯複製,主庫故障切換psync。
判斷是否使用部分複製條件:如果從庫提供的master_replid與master的replid不同,且與master的replid2不同,或同步速度快於master; 就必須進行全量複製,否則執行部分複製。
以下常見的主從切換都可以使用部分複製:
- 一主一從發生切換,A->B 切換變成 B->A ;
- 一主多從發生切換,兄弟節點變成父子節點時;
- 級別複製發生切換, A->B->C 切換變成 B->C->A;
用一句redis開發者話來說psync2,儘管它不是非常完美,但是已經非常適用。
五、馬上實踐
為了演示4.0的psync2功能,這裡做實踐演示。主從實例採用上述搭建的主從架構,主庫:10.1.210.69:6379 、從庫:10.1.210.69:6380和10.1.210.69:6381。首先關閉一臺從節點10.1.210.69:6380:
查看日誌從庫執行了RDB快照:
查看RDB文件,裡面記錄了相關複製信息:
此時我們在啟動從庫,查看對應日誌,此時啟用部分複製來恢複數據:
之前提到過,當從庫開啟來AOF持久化時候,重啟時載入文件從AOF文件中載入,但是AOF文件中沒有對應的複製信息,所以從實例依舊採用全量複製。以下是從庫開啟AOF持久化後,重啟日誌,可以看到是全量同步:
六、總結
複製演進中解決的問題
早起版本才用的sync同步方法,雖然實現了簡單的主從同步,但是在從庫斷線或重啟時無法實現部分同步,由此在2.8版本推出psync1,redis2.8的部分同步機制,有效解決了網路環境不穩定、redis執行高時間複雜度的命令引起的複製中斷,從而導致全量同步。但在應對從庫重啟和主庫故障切換的場景時,psync1還是需進行全量同步。於是在4.0新的psync得到了加強,redis4.0通過在關閉時候執行RDB快照,將複製信息保存在RDB中等到重新啟動時載入RDB文件載入複製信息,通過對比複製信息啟用部分複製,有效的解決了psync1情形下從庫重啟複製信息丟失而導致全量同步的問題。同時引入兩組replid、offset,主從切換時交換兩組值來實現主從故障切換時候依舊採用部分複製。
再次強調,在上述的過程的實現是從庫不開啟AOF持久化情況下,如果從庫開啟的AOF持久化,重啟時候依然使用全量複製。
故障切換
在實際生產環境中,在沒有哨兵的主從架構里如果要重啟從庫,比較好的方式是先動態調配主庫中的複製積壓緩衝隊列,調整大小應大於某個N值,N值計算公式:backlog_size = 重啟從實例時長 * 主實例offset每秒寫入量,這樣好處在於,即使從庫重啟斷線一段時間後再啟動任然保持部分複製。調整方式通過config set repl-backlog-size <位元組數>,當我們重啟完成後又可以將
repl-backlog-size重新調回原有值。當然在數據量小或者重啟時間短情況下,也可以直接重啟從節點。
當主庫宕機時候,在沒有哨兵情況下,需要現將從節點中的某一臺提升為主庫,我們需要在所有從節點中找到當前的offset最大值的從庫(這樣代表該庫相對其他從庫數據較全),然後執行slaveof no one將該從庫提升為主庫,最後將所有其他重庫依次執行slaveof ip port(ip和port是新主庫的IP地址和埠),最後完成故障切換。此外,redis4.0中這種切換任然採用部分複製進行數據同步。
主從配置參數
########從庫############## slaveof <masterip> <masterport> #設置該資料庫為其他資料庫的從資料庫 masterauth <master-password> #主從複製中,設置連接master伺服器的密碼(前提master啟用了認證) slave-serve-stale-data yes # 當從庫同主庫失去連接或者複製正在進行,從庫有兩種運行方式: # 1) 如果slave-serve-stale-data設置為yes(預設設置),從庫會繼續相應客戶端的請求 # 2) 如果slave-serve-stale-data設置為no,除了INFO和SLAVOF命令之外的任何請求都會返回一個錯誤"SYNC with master in progress" slave-priority 100 #當主庫發生宕機時候,哨兵會選擇優先順序最高的一個稱為主庫,從庫優先順序配置預設100,數值越小優先順序越高 slave-read-only yes #從節點是否只讀;預設yes只讀,為了保持數據一致性,應保持預設 ####主庫############## repl-disable-tcp-nodelay no #在slave和master同步後(發送psync/sync),後續的同步是否設置成TCP_NODELAY假如設置成yes,則redis會合併小的TCP包從而節省帶寬,但會增加同步延遲(40ms),造成master與slave數據不一致假如設置成no,則redis master會立即發送同步數據,沒有延遲 #前者關註性能,後者關註一致性 repl-ping-slave-period 10 #從庫會按照一個時間間隔向主庫發送PING命令來判斷主伺服器是否線上,預設是10秒 repl-backlog-size 1mb #複製積壓緩衝區大小設置 repl-backlog-ttl 3600 #master沒有slave一段時間會釋放複製緩衝區的記憶體,repl-backlog-ttl用來設置該時間長度。單位為秒。 min-slaves-to-write 3 min-slaves-max-lag 10 #設置某個時間斷內,如果從庫數量小於該某個值則不允許主機進行寫操作,以上參數表示10秒內如果主庫的從節點小於3個,則主庫不接受寫請求,min-slaves-to-write 0代表關閉此功能。