Redis-淺談主從同步

2022-08-05 15:00:34

主從庫叢集

Redis 提供了主從庫模式,以保證資料副本的一致,在從庫執行一下命令可以建立主從庫關係:

replicaof <dst ip> <dst port>

Redis 的主從庫之間採用的是讀寫分離的方式:

  1. 讀操作:主庫、從庫都可以接收;
  2. 寫操作:到主庫執行,然後將寫操作同步給從庫。

寫操作只在主庫執行,主要是為了避免多範例寫導致的資料一致性問題,減少多範例之間資料一致的協商開銷。

主從同步是如何進行的

下圖是主從第一次同步的流程:

第一階段

第一階段,從庫會給主庫傳送 psync 命令,類似 tcp 的握手,該命令有兩個引數:主庫的 runid 和複製進度 offset。

  • runid 是每個 Redis 範例啟動時都會自動生成的一個隨機 ID,用來標記這個範例。從庫第一次和主庫同步時,不知道主庫的 runid ,所以將 runid 設定為 「?」;
  • offset 是從庫到目前為止處理的偏移量,第一次同步的時候會傳 -1 。

收到 psync 命令後,主庫會用 FULLRESYNC 響應帶上主庫 id 和當前的複製進度 offset 返回給從庫,從庫會記錄這兩個引數。

第二階段

主庫在收到 psync 後,會執行 bgsave 命令,生成 RDB 檔案,然後將檔案傳送給從庫。從庫收到檔案後,會先情況當前的資料,然後載入 RDB 檔案。

第三階段

在從庫處理完 RDB 檔案時,主庫會將期間處理的寫操作放在 replication buffer 中,等到從庫處理完 RDB 檔案後,主庫會將修改操作都傳送給從庫執行,從而完成主從同步。

基於長連線的命令傳播

主從庫的連線建立成功,並且完成第一次的全量同步之後,主從庫之間會維持一個長連結,主庫會將之後接收到的寫操作同步給從庫。

增量同步

在使用過程中,可能會出現主從庫之間網路閃斷的情況,如果恢復連線後採用全量同步的方式,必然會有很大的開銷。Redis 2.8 之後,採用增量同步的方式來完成這個操作。

當主從斷連之後,主庫會把期間收到的寫操作命令寫入 replication buffer,同時也會把這些操作命令也寫入 repl_backlog_buffer 這個緩衝區。

repl_backlog_buffer 是一個環形緩衝區,主庫會記錄自己的偏移量 master_repl_offset,從庫會記錄自己的偏移量 slave_repl_offset。用命令 info Replication 可以檢視對應的 offset。

主從庫恢復連線後,從庫會用 psync 傳送自己的 slave_repl_offset 給主庫,主庫對比自己的 master_repl_offset ,將兩個 offset 之間的寫操作同步給從庫。

因為 repl_backlog_buffer 是一個環形佇列,所以,如果從庫的讀取速度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋,如果主庫接收從庫的 psync 時發現從庫的 offset 已經被覆蓋,為了不丟失資料那麼就會發起全量同步。為了避免全量同步,這時候就需要增加 repl_backlog_size 的值,這個值和緩衝空間大小有關,快取空間大小 = 主庫寫入命令速度 * 操作大小 - 主從庫間網路傳輸命令速度 * 操作大小 。考慮到突發壓力,通常 repl_backlog_size 會設定為 計算結果的 2 到 4 倍。

級聯主從

如果一個主庫下有很多的從庫,這些從庫都要和主庫進行全量同步的時候,主庫的壓力會非常大,忙於 fork 子程序生成 RDB 檔案,影響主執行緒處理使用者端請求。這是可以將主從結構改成主→ 從 → 從 的聯機結構,緩解主庫的壓力。

實驗

我們可以用 docker-compose 實驗 Redis 主從同步:

version: "3"
services:
  redis-master:
    image: redis:7
    ports: 
    - "16379:6379"
    container_name: "redis-master"
    command: redis-server
    networks:
    - redis-replica
  redis-slave-1:
    image: redis:7
    ports:
    - "6380:6379"
    container_name: "redis-slave-1"
    command: redis-server --replicaof redis-master 6379 
    depends_on: 
    - redis-master
    networks:
    - redis-replica
  redis-slave-2:
    image: redis:7
    ports:
    - "6381:6379"
    container_name: "redis-slave-2"
    command: redis-server --replicaof redis-slave-1 6379 
    depends_on: 
    - redis-slave-1
    networks:
    - redis-replica
networks:
  redis-replica:

這裡定義了一個主庫 redis-master 以及兩個從庫 redis-slave-1、redis-slave-2。它們是一個級聯的主從關係 redis-master ← redis-slave-1 ← redis-slave-2

我預先在 master 中插入了一些資料之後在設定了兩個 slave 節點。啟動之後,可以看到 slave2 先向 salve1 發起了同步請求,但是 slave1 還沒和 master 完成同步,所以 salve2 一直在重試,直到 salve1 和 master 完成同步後才開始 slave2 和 salve1 之間的資料同步。