本文為 Design Data Intensive Applications 的書摘 + 個人心得。
Replication 就是保持資料有多份副本在多個節點上,它提供了 redundancy 能力,意思為一個節點掛了也能用其他節點提供服務,Replication 也能提高執行效率,
如果你的資料不會變動,做到 Replication 很簡單,只要把資料複製到別的節點就好了,搞定!
但資料可是會時時刻刻改變,所以這就是 Replication 的難處,處理改變後的副本資料,我們之後會詳細討論以下 3 種常見處理資料改變的方法:leader-base, multi-leader 和 leaderless 。
Leaders and Followers
每一個 node (節點) 中儲存的資料庫複製資料稱為 replica ,在多台機器都有 replica 的情況下,我們要如何確保每一份 replica 都是最新的?每一次的資料庫寫入都應該在每份 replica 中也處理一次,否則 replica 的資料會不一樣;首先先來介紹一個很常見且又簡單的方法:leader-base replication (也可稱 master-slave replication),如下圖:
處理步驟為:
- 指定某一台 replica 為 Leader (也可稱 master 或 primary,但 master 一詞現在應該要少用了XD),當 user 想要寫入資料時就寫到 Leader 中。
- 其他的 replica 為 Follower (也可稱 slave, replicas, secondaries),當 Leader 寫入資料時,Leader 同時也會通知 所有 Followers 它做了哪些變更,這些變更稱為 replication log 或 change stream,每一個 Follower 取得這些 log 後就更新自己的資料。
- 當 user 想要查詢資料時,他可以選擇查詢 Leader 或任一 Follower,Leader 為 read-write,Follower 為 read-only 。
這個 Replication 模式已內建在許多 relational 資料庫中,像 PostgreSQL、MySQL、Oracle Data Guard 等等。
Synchronous Versus Asynchronous Replication (同步 v.s. 非同步 replication)
同步和非同步主要的差別如下圖,Follower 1 是同步,Follower 2 是非同步:
可以看到最大的差別就是 response time 的高低,同步就是需要每個 Follower 都回 Ok 後才會回覆 request 端,非同步就是通知 Follower 後就不等回覆了。
同步的好處是能確保所有 Followers 的資料都是最新的,壞處就是當 Follower 未回覆時 (可能是網路、Follower 掛掉或其他原因),Leader 的寫入會等待,然後卡到後面其他的寫入。
非同步的好處就是不怕 Follower 爛掉,但就不能確保資料是最新。
還有一種是 semi-synchronous ,也就是只有一台 Follower 是同步,其他 Followers 為非同步,如此起碼會有 2 台的資料都是最新,然後也不怕對 Leader 影響太大。
Setting Up New Followers
這裡來看看 leader-base 要怎麼加入新的 Follower 後,又能確保新 Follower 的資料是一致的,因為我們是 high availability (高可用性) 的系統,所以沒有 downtime 才是我們的重點!做法如下:
- 取得 Leader 資料庫 某個時間點 的 snapshot (快照)。
- 複製該 snapshot 到新的 Follower。
- 將這個 Follower 連接到 Leader 上,然後開始處理所有在 某個時間點 之後的資料變更。
- 當 Follower 積累的資料都變更完成後,稱為 caught up ,此時就可以對外服務了。
Handling Node Outages
節點掛掉時怎辦?如何在 leader-based replication 方法維持 high availability (高可用性)?我們來分 2 個面向看。
Follower failure: Catch-up recovery
Follower 掛掉時很單純,在 log 裡,每筆資料都有時間戳記,Follower 只要向 Leader 請求某個時間戳記後的所有資料變更就好了,直到該 Follower caught up 。
Leader failure: Failover
Leader 掛掉就比較有趣了,首先要從所有的 Followers 裡選出一個 Follower 當新的 Leader,然後 user 需重新設定新的 Leader,其他的 Follower 也改為向新 Leader 做資料變更,通常這個 process 稱為 Failover ,整個 Failover 的處理步驟如下:
- 檢查 Leader 是否掛掉: 當節點超過一定秒數後還未回覆,可視為死去。
- 選一個新的 Leader: 這裡有許多種選新 Leader 的方法,最好的方法為挑一個資料最新的 replica 當 Leader,讓資料遺失降到最小。
- 重設系統設定檔: 主要是讓相關系統知道新 Leader 誕生了,如果舊的 Leader 恢復,要能確保舊 Leader 能變成 Follower。
然而,Failover 也可能會遇到下述幾個問題:
- 如果是非同步做 Replication,新 Leader replica 中的資料可能不是最新,可能會造成資料遺失。
- 在某些情形下可能會有 2 個節點都說自己是 Leader,因為是 leader-base 架構,所以不會有 process 解決資料衝突的問題。
- 什麼樣的 timeout 才是合理的?若設太短可能會造成無謂的 failover (假設流量忽然提高,然後各 service 會多花一些時間處理),設太長又會影響該 Leader 恢復正常的時間。
Problems with Replication Lag
把 Follower 的 replica 全設為 synchronous (同步) 是不實際的;倘若將 Follower replica 設為 Asynchronous (非同步),雖然短時間內 Follower 資料可能與 Leader 不一致,但 Follower 最終會 catch up 並保持資料跟 Leader 一致,這個結果稱為 eventual consistency (最終一致性) 。
雖然理論上會 eventual consistency,但當這個 Lag (延遲) 慢慢變大時,這就從理論升級成實在的問題了,接下來會介紹 3 個 Lag 的範例,並看看要如何解這問題。
Reading Your Own Writes
讀你所寫 (很像在駡人齁 XD);有些系統是在 user 送出後馬上就看到他送出後的內容,例如對一篇文章寫評論,在非同步 Replication 下,若沒做特別處理,user 有可能會看不到自己剛剛寫的評論,如下圖說明:
在這個情況底下,我們需要 read-after-write 一致性 (或稱為 read-your-writes 一致性);那我們如何在 leader-based replication 實做 read-your-writes 呢?
- 當 user 需要讀取自己剛剛變更過的資料時,就從 Leader 讀取,否則從 Follower 讀取;例如社群網站的個人資料會時常被自己編輯,所以 user 在讀自己的個人資料時統一從 Leader 讀,讀其他人的個人資料時就從 Follower 讀。
- 如果你的應用軟體的絕大多數情形會一直被改資料,則上面那個方法沒太大效益,因為 Leader 會很忙碌,此時就需要加入一些標準來決定是否要從 Leader 讀取資料;例如在最後一筆更新時間後的一分鐘內只讀取 Leader,一分鐘後就去 Follower,你可以監控各 Follower Replica 的時間來決定這些標準。
- 上面那個 case 是把最後更新時間存在 server 端,我們也可以把這個時間存在 client 端,所以 server 可以確保所有的 replica 在被讀取前是否 catch up。
- 如果你的 replica 是分散至多個資料中心時,要確保 request 能被導至原資料中心的 Leader (該 Leader 符合上面案例有變更過資料)。
Monotonic Reads
第二個 replication lag 的範例為不規則的讀取,如下圖,user 2345 在第二次讀取時向 Follower 2 讀取資料,但那台 Lag 比較久所以就沒讀取到資料,這種情形挺常發生,例如重新整理網頁,這只會讓 user 對你的系統失去信心。
Monotonic reads 能保證這種情形不會發生,第一個做法舊的資料不做變動,只讀取比較新的資料;第二個做法比較直覺,讓 user 只讀取特定的 replica 資料,不同 user 可透過 user id 或某個方式 hash 後,他就只會讀取某個 replica,但要注意的是當 Follower 掛掉時,要有機制轉移 user 到不同的 replica 上。
Consistent Prefix Reads
第三個 replication lag 的範例也是不規則的讀取,且違法因果關係,想像一下你在聊天室觀察 2 人對話,正常的順序應是這樣:
Mr. Poons:
How far into the futre can you see, Mrs. Cake? (妳能看到多久的未來?)
Mrs. Cake:
About ten seconds usually, Mr. Poons. (大約 10 秒)
然後發生了 Lag,結果你看到的對話變成了:
Mrs. Cake:
About ten seconds usually, Mr. Poons. (大約 10 秒)
Mr. Poons:
How far into the futre can you see, Mrs. Cake? (妳能看到多久的未來?)
這令人困惑的對話發生原因如下圖:
上面這個範例我們就需要另一個保證:consistent prefix reads ,這能保證任何人讀取的順序跟寫入的順序一致,這是在有 partitioned (sharded) 功能的資料庫中會發生的問題,我們之後談到 Partition 時會在詳細說明!
Solutions for Replication Lag
如前面所討論,工程師們能設計這些方法在應用軟體端保證資料的正確性,可能你會想說「啊我用 single-node 且有 transactions 保證的資料庫就好啦」,但系統終究會越來越大,數據更密集,分散式資料是必不可免的,在有 scalable (可擴充性) 性質的系統下,transactions 是昂貴的操作,許多系統會捨棄這個特性,所以只能斷言 eventual consistency 會發生,然後再加上前面提到的方法來保證資料正確。