Redis的主從複製是如何作的?複製過程當中也會產生各類問題?

若是Redis的讀寫請求量很大,那麼單個實例頗有可能承擔不了這麼大的請求量,如何提升Redis的性能呢?你也許已經想到了,能夠部署多個副本節點,業務採用讀寫分離的方式,把讀請求分擔到多個副本節點上,提升訪問性能。要實現讀寫分離,就必須部署多個副本,每一個副本須要實時同步主節點的數據。redis

Redis也提供了完善的主從複製機制,使用很是簡單的命令,就能夠構建一個多副本節點的集羣。數據庫

同時,當主節點故障宕機時,咱們能夠把一個副本節點提高爲主節點,提升Redis的可用性。可見,對於故障恢復,也依賴Redis的主從複製,它們都是Redis高可用的一部分。安全

這篇文章咱們就來介紹一下Redis主從複製流程和原理,以及在複製過程當中有可能產生的各類問題。服務器

構建主從複製集羣

假設咱們如今有一個節點A,它通過寫入一段時間的數據寫入後,內存中保存了一些數據。網絡

此時咱們再部署一個節點B,須要讓節點B成爲節點A的數據副本,而且以後與節點A保持實時同步,如何作呢?運維

Redis提供了很是簡單的命令:slaveof。咱們只須要在節點B上執行如下命令,就可讓節點B成爲節點A的數據副本:異步

slaveof 節點A_host:節點A_port

節點B就會自動與節點A創建數據同步,若是節點A的數據量不大,等待片刻,就能看到節點B擁有與節點A相同的數據,同時在節點A上產生的數據變動,也會實時同步到節點B上。ide

經過這樣簡單的方式,咱們能夠很是方便地構建一個master-slave集羣,業務能夠在master上進行寫入,在slave上讀取數據,實現讀寫分離,提升訪問性能。性能

那麼主從節點的複製是如何進行的?下面咱們就來分析其中的原理。優化

主從複製流程

爲了方便下面講解,咱們這裏把節點A叫作master節點,節點B叫作slave節點。

當咱們在slave上執行slaveof命令時,這個複製流程會通過如下階段:

  • slave發送psync $runid $offset給master,請求同步數據
  • master檢查slave發來的runidoffset參數,決定是發送全量數據仍是部分數據
  • 若是slave是第一次與master同步,或者master-slave斷開復制過久,則進行全量同步
    • master在後臺生成RDB快照文件,經過網絡發給slave
    • slave接收到RDB文件後,清空本身本地數據庫
    • slave加載RDB數據到內存中
  • 若是master-slave以前已經創建過數據同步,只是由於某些緣由斷開了複製,此時只同步部分數據
    • master根據slave發來的數據位置offset,只發送這個位置以後的數據給slave
    • slave接收這些差別數據,更新本身的數據,與maser保持一致
  • 以後master產生的寫入,都會傳播一份給slave,slave與master保持實時同步

下面分別介紹全量同步和部分同步的詳細流程。

全量同步

當咱們在節點B上執行slaveof命令後,節點B會與節點A創建一個TCP鏈接,而後發送psync $runid $offset命令,告知節點A須要開始同步數據。

這兩個參數的具體含義以下:

  • runid:master節點的惟一標識
  • offset:slave須要從哪一個位置開始同步數據

什麼是runid?在啓動Redis實例時,Redis會爲每一個實例隨機分配一個長度爲40位的十六進制字符串,用來標識實例的惟一性,也就是說,runid就是這個實例的惟一標識。

因爲是第一次同步,slave並不知道master的runid,因此slave會r發送psync ? -1,表示須要全量同步數據。

master在收到slave發來的psync後,會給slave回覆+fullsync $runid $offset,這個runid就是master的惟一標識,slave會記錄這個runid,用於後續斷線重連同步請求。

以後master會在後臺生成一個RDB快照文件。

RDB文件生成以後,master把這個RDB文件經過網絡發送給slave,slave收到RDB文件後,清空整個實例,而後加載這個RDB數據到內存中,此時slave擁有了與master接近一致的數據。

Redis的主從複製是如何作的?複製過程當中也會產生各類問題?

爲何是接近一致?由於master在生成RDB和slave加載RDB的過程是比較耗時的,在這個過程當中,master產生新的寫入,這些新寫入的命令目前在slave上是沒有執行的。這些命令master如何與slave保持一致呢?

Redis會把這些增量數據寫入到一個叫作複製緩衝區(repl_baklog)的地方暫存下來,這個複製緩衝區是一個固定大小的隊列,由配置參數repl-backlog-size決定,默認1MB,能夠經過配置文件修改它的大小。

因爲是固定大小的隊列,因此若是這個緩衝區被寫滿,那麼它以前的內容會被覆蓋掉。

注意:不管slave有多少個,master的複製緩衝區只有一份,它實際上就是暫存master最近寫入的命令,供多個slave部分同步時使用。

Redis的主從複製是如何作的?複製過程當中也會產生各類問題?

待slave加載RDB文件完成以後,master會把複製緩衝區的這些增量數據發送給slave,slave依次執行這些命令,就能保證與master擁有相同的數據。

以後master再收到的寫命令,會實時傳播給slave節點,slave與master執行一樣的命令,這樣slave就能夠與master保持實時數據的同步。

部分同步

若是在複製過程當中,由於網絡抖動或其餘緣由,致使主從鏈接斷開,等故障恢復時,slave是否須要從新同步master的數據呢?

在Redis的2.8版本以前,確實是這麼幹的,每次主從斷開復制,從新鏈接後,就會觸發一次全量數據的同步。

可見,這麼作的代價是很是大的,並且耗時耗力。後來在Redis在這方面進行了改進,在2.8版本以後,Redis支持部分同步數據

當主從斷開從新創建鏈接後,slave向master發送同步請求:psync $runid $offset,由於以前slave在第一次全量同步時,已經記錄下了master的runid,而且slave也知道目前本身複製到了哪一個位置offset

這時slave就會告知master,以前已經同步過數據了,此次只須要把offset這個位置以後的數據發送過來就能夠了。

master收到psync命令以後,檢查slave發來的runid與自身的runid一致,說明以前已經同步過數據,此次只須要同步部分數據便可。

可是slave須要的offset以後的數據,master還保存着嗎?

前面咱們介紹了master自身會有一個複製緩衝區(repl-backlog),這個緩衝區暫存了最近寫入的命令,同時記錄了這些命令的offset位置。此時master就會根據slave發來的這個offset在複製緩衝區中查詢是否還保留着這個位置以後的數據。

若是有,那麼master給slave回覆+continue,表示此次只同步部分數據。以後master把複製緩衝區offset以後的數據給slave便可,slave執行這些命令後就與master達到一致。

Redis的主從複製是如何作的?複製過程當中也會產生各類問題?

若是master複製緩衝區找不到offset以後的數據,說明斷開的時間過久,複製緩衝區的內容已經被新的內容覆蓋了,此時master只能觸發全量數據同步。

命令傳播

slave通過全量同步或部分同步後,以後master實時產生的寫入,是如何實時同步的?

很簡單,master每次執行完新的寫入命令後,也會把這個命令實時地傳播給slave,slave執行與master相同的操做,就能夠實時與master保持一致。

須要注意的是,master傳播給slave的命令是異步執行的,也就是說在master上寫入後,立刻在slave上查詢是有可能查不到的,由於異步執行存在必定的延遲。

slave與master創建鏈接後,slave就屬於master的一個client,master會爲每一個client分配一個client output buffer,master和每一個client通訊都會先把數據寫入到這個內存buffer中,再經過網絡發送給這個client。

可是,因爲這個buffer是佔用Redis實例內存的,因此不能無限大。因此Redis提供了控制buffer大小的參數限制:

# 普通client buffer限制 
client-output-buffer-limit normal 0 0 0
# slave client buffer限制
client-output-buffer-limit slave 256mb 64mb 60
# pubsub client buffer限制
client-output-buffer-limit pubsub 32mb 8mb 60

這個參數的格式爲:client-output-buffer-limit $type $hard_limit $soft_limit $soft_seconds,其含義爲:若是client的buffer大小達到了hard_limit或在達到了soft_limit並持續了soft_seconds時間,那麼Redis會強制斷開與client的鏈接。

對於slave的client,默認的限制是,若是buffer達到了256MB,或者達到64MB並持續了1分鐘,那麼master就會強制斷開slave的鏈接。

這個配置的大小在某些場景下,也會影響到主從的數據同步,咱們下面會具體介紹到。

心跳機制

在命令傳播階段,爲了保證master-slave數據同步的穩定進行,Redis還設計了一些機制維護這個複製鏈路,這種機制主要經過心跳來完成,主要包括兩方面:

  • master定時向slave發送ping,檢查slave是否正常
  • slave定時向master發送replconf ack $offset,告知master本身複製的位置

在master這一側,master向slave發送ping的頻率由repl-ping-slave-period參數控制,默認10秒,它的主要做用是讓slave節點進行超時判斷,若是slave在規定時間內沒有收到master的心跳,slave會自動釋放與master的鏈接,這個時間由repl-timeout決定,默認60秒。

一樣,在slave這邊,它也會定時向master發送replconf ack $offset命令,頻率爲每1秒一次,其中offset是slave當前複製到的數據偏移量,這麼作的主要做用以下:

  • 讓master檢測slave的狀態:若是master超過repl-timeout時間未收到slave的replconf ack $offset命令,則master主動斷開與slave的鏈接
  • master檢測slave丟失的命令:master根據slave發送的offset並與本身對比,若是發現slave發生了數據丟失,master會從新發送丟失的數據,前提是master的複製緩衝區中還保留這些數據,不然會觸發全量同步
  • 數據安全性保障:Redis提供了min-slaves-to-writemin-slaves-max-lag參數,用於保障master在不安全的狀況下禁止寫入,min-slaves-to-write表示至少存在N個slave節點,min-slaves-max-lag表示slave延遲必須小於這個時間,那麼master纔會接收寫命令,不然master認爲slave節點太少或延遲過大,這種狀況下是數據不安全的,實現這個機制就依賴slave定時發送replconf ack $offset讓master知曉slave的狀況,通常狀況下,咱們不會開啓這個配置,瞭解一下就好

可見,master和slave節點經過心跳機制共同維護它們之間數據同步的穩定性,並在同步過程當中發生問題時能夠及時自動恢復。

咱們能夠能夠在master上執行info命令查看當前全部slave的同步狀況:

role:master         # redis的角色
connected_slaves:1  # slave節點數
slave0:ip=127.0.0.1,port=6480,state=online,offset=22475,lag=0   # slave信息、slave複製到的偏移位置、距離上一次slave發送心跳的時間間隔(秒)
master_repl_offset:22475    # master當前的偏移量
repl_backlog_active:1       # master有可用的複製緩衝區
repl_backlog_size:1048576   # master複製緩衝區大小

經過這些信息,咱們能看到slave與master的數據同步狀況,例如延遲了多大的數據,slave多久沒有發送心跳給master,以及master的複製緩衝區大小。

複製過程當中的問題

在整個數據複製的過程當中,故障是時有發生的,例如網絡延遲過大、網絡故障、機器故障等。

因此在複製過程當中,有一些狀況須要咱們格外注意,必要時須要針對性進行參數配置的調整,不然同步過程當中會發生不少意外問題。

主要問題分爲如下幾個方面,下面依次來介紹。

主從斷開復制後,從新複製觸發了全量同步?

上面咱們有提到,主從創建同步時,優先檢測是否能夠嘗試只同步部分數據,這種狀況就是針對於以前已經創建好了複製鏈路,只是由於故障致使臨時斷開,故障恢復後從新創建同步時,爲了不全量同步的資源消耗,Redis會優先嚐試部分數據同步,若是條件不符合,纔會觸發全量同步。

這個判斷依據就是在master上維護的複製緩衝區大小,若是這個緩衝區配置的太小,頗有可能在主從斷開復制的這段時間內,master產生的寫入致使複製緩衝區的數據被覆蓋,從新創建同步時的slave須要同步的offset位置在master的緩衝區中找不到,那麼此時就會觸發全量同步。

如何避免這種狀況?解決方案就是適當調大複製緩衝區repl-backlog-size的大小,這個緩衝區的大小默認爲1MB,若是實例寫入量比較大,能夠針對性調大此配置。

但這個配置不能調的無限大,由於它會額外佔用內存空間。若是主從斷開復制的時間過長,那麼觸發全量複製在所不免的,咱們須要保證主從節點的網絡質量,避免頻繁斷開復制的狀況發生。

master寫入量很大,主從斷開復制?

主從通過全量同步和部分同步後,以後master產生了寫入命令,會實時傳播給slave節點,若是在這個過程當中發生了複製斷開,那麼必定是在這個過程當中產生了問題。咱們來分析這個過程是如何處理命令傳播的。

上面咱們也提到了,主從創建同步鏈路後,因爲slave也是master的一個client,master會對每一個client維護一個client output buffer,master產生寫命令執行完成後,會把這個命令寫入到這個buffer中,而後等待Redis的網絡循環事件把buffer中數據經過Socket發送給slave,發送成功後,master釋放buffer中的內存。

若是master在寫入量很是大的狀況下,可能存在如下狀況會致使master的client output buffer內存持續增加:

  • 主從節點機器存在必定網絡延遲(例如機器網卡負載比較高),master沒法及時的把數據發送給slave
  • slave因爲一些緣由沒法及時處理master發來的命令(例如開啓了AOF並實時刷盤,磁盤IO負載高)

當遇到上面狀況時,master的client output buffer持續增加,直到觸發默認配置的閾值限制client-output-buffer-limit slave 256mb 64mb 60,那麼master則會把這個slave鏈接強制斷開,這就會致使複製中斷。

以後slave從新發送複製請求,可是以上緣由可能依舊存在,通過一段時間後又產生上述問題,主從鏈接再次被斷開,周而復始,主從頻繁斷開連接,沒法正常複製數據

解決方案是,適當調大client-output-buffer-limit的閾值,而且解決slave寫入慢的狀況,保證master發給slave的數據能夠很快得處理完成,這樣才能避免頻繁斷開復制的問題。

添加slave節點,master發生阻塞?

當主從創建同步進行全量同步數據時,master會fork出一個子進程,掃描全量數據寫入到RDB文件中。

這個fork操做,並非沒有代價的。fork在建立子進程時,須要父進程拷貝一分內存頁表給子進程,若是master佔用的內存過大,那麼fork時須要拷貝的內存頁表也會比較耗時,在完成fork以前,Redis整個進程都會阻塞住,沒法處理任何的請求,因此業務會發現Redis忽然變慢了,甚至發生超時的狀況。

咱們能夠執行info能夠看到latest_fork_usec參數,單位微妙。這就是最後一次fork的耗時時間,咱們能夠根據這個時間來評估fork時間是否符合預期。

對於這種狀況,能夠優化方案以下:

  • 必定保證機器擁有足夠的CPU資源和內存資源
  • 單個Redis實例內存不要太大,大實例拆分紅小實例

經過以上方式避免fork引起的父進程長時間阻塞問題。

主從全量同步數據時很慢?

以前咱們已經瞭解到,主從全量複製會通過3個階段:

  • master生成RDB文件
  • master把RDB文件發送給slave
  • slave清空數據庫,加載RDB文件到內存

若是發現全量同步數據很是耗時,咱們根據以上階段來分析緣由:

  • master實例數據比較大,而且機器的CPU負載較高時,在生成RDB時耗大量CPU資源,致使RDB生成很慢
  • master和slave的機器網絡帶寬被打滿,致使master發送給slave的RDB文件網絡傳輸時變慢
  • slave機器內存不夠用,但開啓了swap機制,致使內存不足以加載RDB文件,數據被寫入到磁盤上,致使數據加載變慢

經過以上狀況能夠看出,主從複製時,會消耗CPU、內存、網卡帶寬各方面的資源,咱們須要合理規劃服務器資源,保證資源的充足。而且針對大實例進行拆分,能避免不少複製中的問題。

總結

這篇文章咱們介紹了Redis主從複製的流程和工做原理,以及在複製過程當中可能引起的問題。

雖然搭建一個複製集羣很簡單,但其中涉及到的細節也不少。Redis在複製過程也可能存在各類問題,咱們須要設置合適的配置參數和合理運維Redis,才能保證Redis有穩定可用的副本數據,爲咱們的高可用提供基礎。

相關文章
相關標籤/搜索