圖解 Redis | 差點崩潰了,還好有主從複製

小林coding發表於2021-07-01

大家好,我是小林哥,又來圖解 Redis 啦。

我在前兩篇已經給大家圖解了 AOF 和 RDB,這兩個持久化技術保證了即使在伺服器重啟的情況下也不會丟失資料(或少量損失)。

不過,由於資料都是儲存在一臺伺服器上,如果出事就完犢子了,比如:

  • 如果伺服器發生了當機,由於資料恢復是需要點時間,那麼這個期間是無法服務新的請求的;
  • 如果這臺伺服器的硬碟出現了故障,可能資料就都丟失了。

要避免這種單點故障,最好的辦法是將資料備份到其他伺服器上,讓這些伺服器也可以對外提供服務,這樣即使有一臺伺服器出現了故障,其他伺服器依然可以繼續提供服務。

多臺伺服器要儲存同一份資料,這裡問題就來了。

這些伺服器之間的資料如何保持一致性呢?資料的讀寫操作是否每臺伺服器都可以處理?

Redis 提供了主從複製模式,來避免上述的問題。

這個模式可以保證多臺伺服器的資料一致性,且主從伺服器之間採用的是「讀寫分離」的方式。

主伺服器可以進行讀寫操作,當發生寫操作時自動將寫操作同步給從伺服器,而從伺服器一般是隻讀,並接受主伺服器同步過來寫操作命令,然後執行這條命令。

也就是說,所有的資料修改只在主伺服器上進行,然後將最新的資料同步給從伺服器,這樣就使得主從伺服器的資料是一致的。

同步這兩個字說的簡單,但是這個同步過程並沒有想象中那麼簡單,要考慮的事情不是一兩個。

我們先來看看,主從伺服器間的第一次同步是如何工作的?

第一次同步

多臺伺服器之間要通過什麼方式來確定誰是主伺服器,或者誰是從伺服器呢?

我們可以使用 replicaof(Redis 5.0 之前使用 slaveof)命令形成主伺服器和從伺服器的關係。

比如,現在有伺服器 A 和 伺服器 B,我們在伺服器 B 上執行下面這條命令:

# 伺服器 B 執行這條命令
replicaof <伺服器 A 的 IP 地址> <伺服器 A 的 Redis 埠號>

接著,伺服器 B 就會變成 伺服器 A 的「從伺服器」,然後與主伺服器進行第一次同步。

主從伺服器間的第一次同步的過程可分為三個階段:

  • 第一階段是建立連結、協商同步;
  • 第二階段是主伺服器同步資料給從伺服器;
  • 第三階段是主伺服器傳送新寫操作命令給從伺服器。

為了讓你更清楚瞭解這三個階段,我畫了一張圖。

接下來,我在具體介紹每一個階段都做了什麼。

第一階段:建立連結、協商同步

執行了 replicaof 命令後,從伺服器就會給主伺服器傳送 psync 命令,表示要進行資料同步。

psync 命令包含兩個引數,分別是主伺服器的 runID複製進度 offset

  • runID,每個 Redis 伺服器在啟動時都會自動生產一個隨機的 ID 來唯一標識自己。當從伺服器和主伺服器第一次同步時,因為不知道主伺服器的 run ID,所以將其設定為 "?"。
  • offset,表示複製的進度,第一次同步時,其值為 -1。

主伺服器收到 psync 命令後,會用 FULLRESYNC 作為響應命令返回給對方。

並且這個響應命令會帶上兩個引數:主伺服器的 runID 和主伺服器目前的複製進度 offset。從伺服器收到響應後,會記錄這兩個值。

FULLRESYNC 響應命令的意圖是採用全量複製的方式,也就是主伺服器會把所有的資料都同步給從伺服器。

所以,第一階段的工作時為了全量複製做準備。

那具體怎麼全量同步呀呢?我們可以往下看第二階段。

第二階段:主伺服器同步資料給從伺服器

接著,主伺服器會執行 bgsave 命令來生成 RDB 檔案,然後把檔案傳送給從伺服器。

從伺服器收到 RDB 檔案後,會先清空當前的資料,然後載入 RDB 檔案。

這裡有一點要注意,主伺服器生成 RDB 這個過程是不會阻塞主執行緒的,也就是說 Redis 依然可以正常處理命令。

但是這期間的寫操作命令並沒有記錄到剛剛生成的 RDB 檔案中,這時主從伺服器間的資料就不一致了。

那麼為了保證主從伺服器的資料一致性,主伺服器會將在 RDB 檔案生成後收到的寫操作命令,寫入到 replication buffer 緩衝區裡。

第三階段:主伺服器傳送新寫操作命令給從伺服器

在主伺服器生成的 RDB 檔案傳送後,然後將 replication buffer 緩衝區裡所記錄的寫操作命令傳送給從伺服器,然後從伺服器重新執行這些操作。

至此,主從伺服器的第一次同步的工作就完成了。

命令傳播

主從伺服器在完成第一次同步後,雙方之間就會維護一個 TCP 連線。

後續主伺服器可以通過這個連線繼續將寫操作命令傳播給從伺服器,然後從伺服器執行該命令,使得與主伺服器的資料庫狀態相同。

而且這個連線是長連線的,目的是避免頻繁的 TCP 連線和斷開帶來的效能開銷。

上面的這個過程被稱為基於長連線的命令傳播,通過這種方式來保證第一次同步後的主從伺服器的資料一致性。

分攤主伺服器的壓力

在前面的分析中,我們可以知道主從伺服器在第一次資料同步的過程中,主伺服器會做兩件耗時的操作:生成 RDB 檔案和傳輸 RDB 檔案。

主伺服器是可以有多個從伺服器的,如果從伺服器數量非常多,而且都與主伺服器進行全量同步的話,就會帶來兩個問題:

  • 由於是通過 bgsave 命令來生成 RDB 檔案的,那麼主伺服器就會忙於使用 fork() 建立子程式,如果主伺服器的記憶體資料非大,在執行 fork() 函式時是會阻塞主執行緒的,從而使得 Redis 無法正常處理請求;
  • 傳輸 RDB 檔案會佔用主伺服器的網路頻寬,會對主伺服器響應命令請求產生影響。

這種情況就好像,剛創業的公司,由於人不多,所以員工都歸老闆一個人管,但是隨著公司的發展,人員的擴充,老闆慢慢就無法承擔全部員工的管理工作了。

要解決這個問題,老闆就需要設立經理職位,由經理管理多名普通員工,然後老闆只需要管理經理就好。

Redis 也是一樣的,從伺服器可以有自己的從伺服器,我們可以把擁有從伺服器的從伺服器當作經理角色,它不僅可以接收主伺服器的同步資料,自己也可以同時作為主伺服器的形式將資料同步給從伺服器,組織形式如下圖:

通過這種方式,主伺服器生成 RDB 和傳輸 RDB 的壓力可以分攤到充當經理角色的從伺服器

那具體怎麼做到的呢?

其實很簡單,我們在「從伺服器」上執行下面這條命令,使其作為目標伺服器的從伺服器:

replicaof <目標伺服器的IP> 6379

此時如果目標伺服器本身也是「從伺服器」,那麼該目標伺服器就會成為「經理」的角色,不僅可以接受主伺服器同步的資料,也會把資料同步給自己旗下的從伺服器,從而減輕主伺服器的負擔。

增量複製

主從伺服器在完成第一次同步後,就會基於長連線進行命令傳播。

可是,網路總是不按套路出牌的嘛,說延遲就延遲,說斷開就斷開。

如果主從伺服器間的網路連線斷開了,那麼就無法進行命令傳播了,這時從伺服器的資料就沒辦法和主伺服器保持一致了,客戶端就可能從「從伺服器」讀到舊的資料。

那麼問題來了,如果此時斷開的網路,又恢復正常了,要怎麼繼續保證主從伺服器的資料一致性呢?

在 Redis 2.8 之前,如果主從伺服器在命令同步時出現了網路斷開又恢復的情況,從伺服器就會和主伺服器重新進行一次全量複製,很明顯這樣的開銷太大了,必須要改進一波。

所以,從 Redis 2.8 開始,網路斷開又恢復後,從主從伺服器會採用增量複製的方式繼續同步,也就是隻會把網路斷開期間主伺服器接收到的寫操作命令,同步給從伺服器。

網路恢復後的增量複製過程如下圖:

主要有三個步驟:

  • 從伺服器在恢復網路後,會傳送 psync 命令給主伺服器,此時的 psync 命令裡的 offset 引數不是 -1;
  • 主伺服器收到該命令後,然後用 CONTINUE 響應命令告訴從伺服器接下來採用增量複製的方式同步資料;
  • 然後主服務將主從伺服器斷線期間,所執行的寫命令傳送給從伺服器,然後從伺服器執行這些命令。

那麼關鍵的問題來了,主伺服器怎麼知道要將哪些增量資料傳送給從伺服器呢?

答案藏在這兩個東西里:

  • repl_backlog_buffer,是一個「環形」緩衝區,用於主從伺服器斷連後,從中找到差異的資料;
  • replication offset,標記上面那個緩衝區的同步進度,主從伺服器都有各自的偏移量,主伺服器使用 master_repl_offset 來記錄自己「」到的位置,從伺服器使用 slave_repl_offset 來記錄自己「」到的位置。

那repl_backlog_buffer 緩衝區是什麼時候寫入的呢?

在主伺服器進行命令傳播時,不僅會將寫命令傳送給從伺服器,還會將寫命令寫入到 repl_backlog_buffer 緩衝區裡,因此 這個緩衝區裡會儲存著最近傳播的寫命令。

網路斷開後,當從伺服器重新連上主伺服器時,從伺服器會通過 psync 命令將自己的複製偏移量 slave_repl_offset 傳送給主伺服器,主伺服器根據自己的 master_repl_offset 和 slave_repl_offset 之間的差距,然後來決定對從伺服器執行哪種同步操作:

  • 如果判斷出從伺服器要讀取的資料還在 repl_backlog_buffer 緩衝區裡,那麼主伺服器將採用增量同步的方式;
  • 相反,如果判斷出從伺服器要讀取的資料已經不存在
    repl_backlog_buffer 緩衝區裡,那麼主伺服器將採用全量同步的方式。

當主伺服器在 repl_backlog_buffer 中找到主從伺服器差異(增量)的資料後,就會將增量的資料寫入到 replication buffer 緩衝區,這個緩衝區我們前面也提到過,它是快取將要傳播給從伺服器的命令。

repl_backlog_buffer 緩行緩衝區的預設大小是 1M,
並且由於它是一個環形緩衝區,所以當緩衝區寫滿後,主伺服器繼續寫入的話,就會覆蓋之前的資料。

因此,當主伺服器的寫入速度遠超於從伺服器的讀取速度,緩衝區的資料一下就會被覆蓋,在網路恢復時,如果從伺服器想讀的資料已經被覆蓋了,主伺服器就會採用全量同步,這個方式比增量同步的效能損耗要大很多。

因此,為了避免在網路恢復時,主伺服器頻繁地使用全量同步的方式,我們應該調整下 repl_backlog_buffer 緩衝區大小,儘可能的大一些,減少出現從伺服器要讀取的資料被覆蓋的概率,從而使得主伺服器採用增量同步的方式。

那 repl_backlog_buffer 緩衝區具體要調整到多大呢?

repl_backlog_buffer 最小的大小可以根據這面這個公式估算。

我來解釋下這個公式的意思:

  • second 為從伺服器斷線後重新連線上主伺服器所需的平均 時間(以秒計算)。
  • write_size_per_second 則是主伺服器平均每秒產生的寫命令資料量大小。

舉個例子,如果主伺服器平均每秒產生 1 MB 的寫命令,而從伺服器斷線之後平均要 5 秒才能重新連線主伺服器。

那麼 repl_backlog_buffer 大小就不能低於 5 MB,否則新寫地命令就會覆蓋舊資料了。

當然,為了應對一些突發的情況,可以將 repl_backlog_buffer 的大小設定為此基礎上的 2 倍,也就是 10 MB。

關於 repl_backlog_buffer 大小修改的方法,只需要修改配置檔案裡下面這個引數項的值就可以。

repl-backlog-size 1mb

總結

主從複製共有三種模式:全量複製、基於長連線的命令傳播、增量複製

主從伺服器第一次同步的時候,就是採用全量複製,此時主伺服器會兩個耗時的地方,分別是生成 RDB 檔案和傳輸 RDB 檔案。為了避免過多的從伺服器和主伺服器進行全量複製,可以把一部分從伺服器升級為「經理角色」,讓它也有自己的從伺服器,通過這樣可以分攤主伺服器的壓力。

第一次同步完成後,主從伺服器都會維護著一個長連線,主伺服器在接收到寫操作命令後,就會通過這個連線將寫命令傳播給從伺服器,來保證主從伺服器的資料一致性。

如果遇到網路斷開,增量複製就可以上場了,不過這個還跟 repl_backlog_size 這個大小有關係。

如果它配置的過小,主從伺服器網路恢復時,可能發生「從伺服器」想讀的資料已經被覆蓋了,那麼這時就會導致主伺服器採用全量複製的方式。所以為了避免這種情況的頻繁發生,要調大這個引數的值,以降低主從伺服器斷開後全量同步的概率。


參考資料
  • 《Redis核心技術與實戰》
  • 《Redis設計與實現》
  • 《Redis原始碼分析》

往期圖解

圖解 Reids | 讀者問題答疑

圖解 Reids | RDB 快照

圖解 Reids | AOF 日誌

圖解 Reids | 快取雪崩、擊穿、穿透


我是小林,今天的你,比昨天更博學了嗎?

我們下次見啦。

相關文章