當前位置: 妍妍網 > 碼農

Redis 主從復制原理,設計的真巧妙!

2024-03-29碼農

原文連結:https://www.linkinstars.com/post/9ddfbd5e.html

前言

今天繼續來看看有關 Redis 的一個問題,主從復制。通常,對於大多數的場景來說,讀比寫更多,於是對於緩存的水平擴充套件,其中的一個方式 「主從復制」 就是一個常見的思路。有了主從復制,那麽可以擴充套件出很多從節點來應對大量的讀請求。那麽問題來了 Redis 的主從復制是如何實作的呢?

PS:本文僅關心復制的機制,不關心主節點下線重新選等等異常情況。

前置知識

  • 你需要知道 Redis 的持久化方式,RDB 和 AOF

  • Redis 執行命令的基本思路

  • 審題

    題目本身不復雜,提問者問這個問題的想法可能會有下面幾個方面:

    1. 了解 Redis 的主從復制機制的話,如果在實際使用過程中出現問題就更容易排查。

    2. 在設計復制機制的時候需要註意和考慮什麽問題。

    3. 這樣的設計是否能套用在別的場景中。

    嘗試思考

    假設你完全沒有看過 Redis 源碼來思考這個問題,可以從下面幾個角度去嘗試分析,並猜測答案。

    1. 首先,想到一個關系戶,也就是我們常用的 Mysql,它也有主從復制,如果你了解 binlog 那麽可以嘗試從這裏著手,雖然不同,但思路應該是差不多的。

    2. 然後,簡化問題,主從復制,無非就是將數據發送過去,對方接受保存。

    3. 不可能每次都復制的是全量數據,那麽肯定需要有機制去確保如何每次復制增量的數據。

    4. 復制的是什麽?

      1. 復制的是數據本身?數據只要變動就將變動的 kv 直接扔給從節點?

      2. 復制的是執行命令?將客戶端執行的命令發送給子節點執行一次?

    解決

    有了上面的思考,其實實際也就有思路的。首先主從復制肯定有兩種情況,一種就是第一次復制,也就是要執行一次全量復制,將主節點的所有數據到復制到從節點上去;另一種就是增量復制,在數據同步之後後續的增量數據保持同步。

    全量同步

    持久化數據

    因為需要全量同步所有數據,我們知道 Redis 數據在記憶體裏面,既然要發送,那勢必需要先持久化一次。也就是先 SYNC 一遍,透過方法 startBgsaveForReplication 來完成的。
    程式碼位置在:https://github.com/redis/redis/blob/14f802b360ef52141c83d477ac626cc6622e4eda/src/replication.c#L855
    這個問題不大, 就是保存一個 RDB 檔。

    發送數據

    這個也很不難,就是將數據直接扔過去就好了。
    程式碼位置在:https://github.com/redis/redis/blob/14f802b360ef52141c83d477ac626cc6622e4eda/src/replication.c#L1402

    增量同步

    後續的任務就是增量同步後續產生的數據了。在猜測時我們想到有兩種復制方式,一種是直接復制數據,這種方式復制 RDB 是可行,在全量同步的時候用這個肯定更好,如果同步命令那麽從節點還需再執行一次過於復雜和麻煩,還耗時。而對於後續的增量同步來說,肯定是同步命令來的更高效(不過還是得看實際)。

    下面就是傳播命令的方法:

    /* Propagate the specified command (in the context of the specified database id) * to AOF and Slaves. * * flags are an xor between: * + PROPAGATE_NONE (no propagation of command at all) * + PROPAGATE_AOF (propagate into the AOF file if is enabled) * + PROPAGATE_REPL (propagate into the replication link) * * This is an internal low-level function and should not be called! * * The API for propagating commands is alsoPropagate(). * * dbid value of -1 is saved to indicate that the called do not want * to replicate SELECT for this command (used for database neutral commands). */staticvoidpropagateNow(int dbid, robj **argv, int argc, int target) {if (!shouldPropagate(target))return;/* This needs to be unreachable since the dataset should be fixed during * replica pause (otherwise data may be lost during a failover) */ serverAssert(!(isPausedActions(PAUSE_ACTION_REPLICA) && (!server.client_pause_in_transaction)));if (server.aof_state != AOF_OFF && target & PROPAGATE_AOF) feedAppendOnlyFile(dbid,argv,argc);if (target & PROPAGATE_REPL) replicationFeedSlaves(server.slaves,dbid,argv,argc);}

    這個方法就是將增量命令傳播給 AOF 和 Slaves,AOF 就是持久化的另一種方式,而 Slaves 就是我們需要同步的從節點了。具體 replicationFeedSlaves 方法就不具體看了。

    監控狀態

    這個其實是我們在猜測的時候漏掉的,想來也是,master 肯定需要知道 slave 的狀態,如果連不上了,肯定要處理,在 replication.c 中有這樣一個方法:

    /* Replication cron function, called 1 time per second. */voidreplicationCron(void) {

    看名字和註釋就秒懂了,每秒執行一次的同步定時任務。

    而其中呼叫了 replicationFeedSlaves 方法,也就是 PING 一下,看看活著沒:

    replicationFeedSlaves(server.slaves, -1, ping_argv, 1);

    可能導致的問題

    第一次同步 RDB 時間太長?

    如果我們 redis 存放的數據很多,第一次同步會有兩個時間,一個是 bgsave 的時間,這個時間其實還好,畢竟平時就是要執行的,而第二個時間就是傳輸數據的時間,這個時間就取決於頻寬了。

    不過首先這個操作時,主節點依舊可以被讀寫,只不過操作均被緩存了,所以倒是不必擔心這段時間無法被使用。難就在如果數據過多可能真的會導致一個問題就是,同步->超時->重試,然後不斷迴圈,所以為了避免這樣的情況出現,建議 Redis 前往別直接把主機全部記憶體吃完。通常 maxmemory 設定為 75% 就相對不會出現問題,也不容易 OOM。

    當然,有人肯定會問,能不能直接先手動拷貝 RDB 檔來減少同步時間,實際操作過我告訴你,不要手動操作,容易出現意想不到的問題,當出現問題之後,數據還是會不同步,還是會執行重新同步,還不如第一次就手動讓程式自己來。

    最佳化

    傳播 cache

    命令在傳播的階段設定了主從同步發送的緩沖區,透過維護一個緩沖區來保證當主節點無需等待,從節點自己憑實力拿就好了,即使有一段時間突然抖動了一下,也沒事,緩沖區裏面還有,繼續同步就行嘞。但當完全超過緩沖區的承受範圍,那麽還是需要執行一次全量同步來保證數據一致。

    無盤載入

    之前看程式碼的時候就註意到了一個參數 repl_diskless_sync 轉譯過來就是無盤同步,顯然這個最佳化是 Redis 註意到第一次同步的時候,如果馬上寫入 RDB 顯然是有點慢了,直接 dump 記憶體肯定會來的更快,所以這就是無盤,也就是不先落盤。

    總結

    最後用一張圖來總結整個過程:

    我們看著這個圖我們也可以想到,其實這樣復制的策略在絕大多數復制的場景中都是適用的,如果實際沒有命令這個說法,那就將數據拆分成小塊(chunk)來同步。需要註意點和最佳化點可能 Redis 都幫你想好了,對著抄就可以了。所以,我稱為一種設計為 」單向同步「,那麽如果什麽是多向同步呢?也就是多個人同時編輯或運算元據,互相同步的策略,此時就需要一些 diff 演算法和策略了,你也可以考慮設計看看,看具體會遇到什麽問題。

    往期推薦


    點亮,伺服器三年不宕機