2020年4月19日 星期日

Replications

本篇為 Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems [1] 的讀書筆記。

Replication 指的是在分散式系統中將一份資料複製並存放在多個節點上,以帶來幾個可能的好處:

  • 將資料複製到距離使用者較近的節點,以減少 latency
  • 提高 availability
  • 增加讀取的 throughput

一般討論分散式系統的 replication 時,關注的困難點都是如何處理資料的變化。因為若資料不會變化,那只要整個複製貼上就完工了,沒什麼困難點。但若資料會變化,那麼就會需要考慮多個節點間要如何控管資料的變化了。

    Leaders and Followers

    當資料在分散式系統中,被複製到多個節點時,如果我們允許每個節點都各自自由地讀寫,那麼資料很容易就會不一致。所以通常我們會採取某些限制,以讓每個節點上的資料具有某些規則,方便我們做資料的管理。

    在分類上,書中主要將作法分成三種:

    • 單一 leader
    • 多個 leader
    • 沒有 leader

    其中 leader 的稱呼也有像是 master node 等等,代表的是所有其他非 leader 的節點,會以 leader 節點的資料為基礎做資料同步。

    Leader-based Replication

    在書中提到 Leader-based Replication 時,實際上在指的都是只有單一 leader 的架構。運作起來如下圖所示,leader 節點負責接受寫入的 request,然後透過再將資料更新同步到其他的 follower 節點。

    圖片節自 [1]

    同時,在討論 replica 的時候,也會一併討論到我們會在何時使用 synchronous replication、何時使用 asynchronous replication

    圖片節自 [1]

    以上圖來說,當 leader 收到寫入的 request 後,它會將更新寫入自己的 storage,同時會把 request 發送給其他的 follower。這裡可以注意到有兩種不同的行為:

    • follower 1 寫入完成並回覆 leader 時,leader 才會回覆 user 說寫入完成了。此時 follower 1 的寫入操作就是 synchronous replication。
    • follower 2 雖然也會收到寫入的 request,但 leader 完全不管 follower 2 到底何時才寫完。此時 follower 2 的寫入操作是 asynchronous replication。

    synchronous replication 可以保障整個系統中至少有幾份 replication 是跟 leader 一致的,所以當 leader 死掉的時候,我們可以確定在系統中必然還有其他節點擁有那些資料。然而缺點就在於 leader 與 synchronous follower 之間存在關聯性,synchronous follower 無法回覆、斷線等等的各種異常都會直接影響到 leader 無法完成正常的寫入。因此現實上雖然我們通常會想要保障一定程度的 synchronous replication,但絕不會想要所有節點都是 synchronous replication,因為這樣整個系統會過於脆弱,任何節點的一點小問題都會導致整個系統無法運作。

    反過來說,如果整個系統所有 follower 全都是 asynchronous replication,則一旦 leader 掛掉時,有可能 leader 的某些資料尚未發生在 follower 上,導致系統產生 durability 的問題(已送出的資料應該要永久存在,但結果並沒有)。

    處理節點離線的狀況

    就像在 microservice 當中會談到的,分散式系統當中應該假設所有東西都可能會壞,任何節點的 CPU、記憶體、硬碟、網路等都有可能故障,或者是因為預期的維護計畫而將節點關閉等,這時我們就會需要處理當節點離線時的狀況。而大致來說,我們會需要處理的大概就是以下幾種狀況:

    • follower 離線 – 讓 follower 再次與 leader 同步,補回離線期間沒跟到的更新。
    • leader 離線 – 若一定時間內 leader 沒有回來,則需要透過像是選舉機制等方式,在剩餘節點中挑出一個新的 leader。並且必須確保舊的 leader 一旦意外回歸時,能夠確實知道自己已經不再是 leader 了。
    如何實現 Replication Logs?

    Replication Logs 是在 Leader-based Replication 架構中常使用的複製手法,也就是在談要如何使更新能夠複製到其他 follower 上。書中談到一般會有以下幾種選擇。

    Statement-based replication

    這是最簡單的方式,就是 leader 收到的每個更新的指令(例如 INSERT/UPDATE/DELETE 的 SQL statement),全部一條一條發送到 follower,在 follower 端全部 replay 一次。然而這個方法存在一些潛在的問題:

    1. 如果指令有包含像是 NOW()、RAND() 這類不同時間在不同機器上執行會有不同結果的指令,會導致在不同 replica 上會產生不同的結果。
    2. 如果指令需要依據既有的資料,像是依賴 auto-increment 的 ID 等,那麼 replay 指令時,指令的順序就會非常重要。
    3. 如果指令存在 side effect(例如需要使用 trigger、stored procedure、自定義的 function 等)時,也不一定能保障每個 replica replay 後能獲得相同的結果。

    雖然說我們可以讓 leader 先執行指令,然後再產出能夠創造固定結果的更新指令送給 follower,以確保所有 follower 的結果皆一致,但實務上因為可能有很多 edge case,因此多數狀況下還是會比較傾向其他作法。但這個方法無疑是最簡單的作法。

    Write-ahead log (WAL) shipping

    把 leader 的 log 丟給 follower,讓 follower 整個 replay 一次。跟 Statement-based replication 不同的地方是,WAL 指的是很低階的 log,不像 statement 是高階的指令。不過缺點也是在於這些 log 很低階,例如可能是在某個 block 的某個 byte 被變更了之類的,這會導致如果不同節點存在像是磁碟格式不同、甚至軟體版本不同等等的因素時,WAL 複製很容易無法成功。

    PostgreSQL 和 Oracle 是使用這個 replication 的方法。

    Logical (row-based) log replication

    Logical log replication 跟上面的 WAL 是一樣的,差別只是在於由於想必開 WAL 的問題,因此讓 log 的格式改為是邏輯上的格式,而不是實際存放在 storage engine 上的格式。例如新建的 row 的話,log 會包含所有新建的欄位內容;刪除的 row 則需要有足以代表這行 row 的 identifier;更新 row 時則同時有代表 row 的 identifier 以及被更新的欄位內容。

    MySQL 的 binlog 就是使用這個方式。

    PS. 不過我個人目前還沒看懂 Logical log replication 和 Statement-based replication 之間的差別….。

    Trigger-based replication

    前三種方法基本上都是資料庫本身就能提供的方法,快速有效但侷限。如果我們想要做更靈活的 replication,例如我只想要一部分的資料的 replication,這時通常就會採取利用資料庫的 trigger 或者 stored procedure 等手法來觸發自定義的行為了。雖然這麼做通常比較容易有 bug、效率也大多比較差,但考慮到可客製化程度的話,這個方法也是個常見的選擇。

    Replication 延遲的問題

    概要來說,在 Leader-based Replication 的架構中,因為寫入會先流入 leader,之後才會反應到 follower,如果此時使用者的行為是剛完成寫入後,馬上就讀取他剛剛寫入的資料,此時就會發生使用者可能看不見他剛剛寫入的內容,因為寫入可能還沒反應到他讀取的那個 follower 上。這種問題依據使用情境,可能有幾種不同的 workaround 可以選擇:

    • Reading Your Own Writes:想辦法讓使用者遇到要讀取可能剛剛才由他自己變更的資料時,都是從 leader 讀取;而其他資料則照常從 follower 讀取。不過這個選擇通常有很多限制,例如使用者有多個裝置時,從 PC 寫入但從手機讀取等狀況,依據情況可能不容易判定是否剛剛才變更過。
    • Monotonic Reads:Monotonic Reads 定位上是一種低於強一致性strong consistency)、但高於最終一致性eventually consistency)的一致性等級。概念是說特定使用者永遠只會從特定的 follower 讀取資料,從而避免使用者一下讀取 follower #1、一下讀取 follower #2,導致使用者可能會看到剛剛出現過的更新又不見了的狀況。
    • Consistent Prefix Reads:確保具有因果關係的資料,都會寫入到相同的分區,從而保證資料的順序具有一致性,不會產生順序錯置等問題。

    另外在其他書 [4] 上面,針對延遲的處理可能還有一些不同角度的思考方式。

    • 寫入後的讀取都指定發給 leader:….我個人覺得這是萬萬不可的作法,因為這樣往往會建立出多個系統之間的強耦合。例如前端需要知道如何指定發給 leader、或者後端的 application service 需要明確地了解底下的資料庫是如何分配工作的。
    • 二次讀取:對 follower 的讀取如果讀不到資料,下一次就改成向 leader 讀取。不過缺點在於特殊狀況像是被駭客攻擊時,很有可能產生大量的二次讀取,leader 可能會撐不住。
    • 依據業務的需要,主要業務一律讀 leader、次要業務才讀 follower:這個就是以不同角度在講 Monotonic Reads,不是以使用者為單位,而是以業務為單位。

    書上這個章節還有關於 Multi-Leader 和 Leaderless 的架構描述,就待有閒的時候再來整理紀錄了…。

    參考資料
    1. Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems
    2. 複製
    3. Replication
    4. 從零開始學架構:照著做,你也能成為架構師

    沒有留言: