零停機遷移 Postgres 的正確方式

作者 | RIGAS PAPATHANASOPOULOS

譯者 | 王強

策劃 | 萬佳

在這篇博文中,我們會介紹如何在零停機時間的前提下,使用 Bucardo 將 Postgres 數據庫遷移到一個新實例上。我們將介紹如何避免常見的陷阱,比如數據丟失、性能下降和數據完整性故障等。我們已成功使用這一流程將我們的 Postgres 數據庫從 9.5 版遷移到 Amazon RDS 上的 12.5 版,但該流程不只適用於 RDS,也不依賴 AWS 獨有的任何內容。這種遷移策略應該能適用於任何自託管或託管的 Postgres。

分   析

在本文中,我們將討論將多個 Web 應用程序(如微服務)從一個數據庫遷移到另一個的過程。現代軟件架構由多個應用程序(或微服務)組成,而每個應用程序都有多個運行實例以增強擴展性。爲了將你的應用程序移動到新的數據庫,你必須首先確保兩個數據庫中的數據是同步的,並在任何給定時間點保持同步,否則你的客戶端遲早會丟失數據,甚至陷入無效狀態。

一個簡單的解決方案是停止舊數據庫的寫入操作,獲取快照,將其恢復到新的數據庫,然後在新數據庫中恢復操作。這種方案需要的停機時間太久,不適合生產環境。我們提到這一點只是爲了做參考,因爲這是確保你不會丟失任何數據的最簡單方法,但用它的話,你可能會失去一些客戶。

更現實的方法是在兩個數據庫之間設置一個近乎實時的雙向複製,這樣在理想情況下,應用程序可以同時向兩者讀取和寫入,而不會注意到任何差異。你可以用這種方法一次一個實例地逐步移動你的應用程序,過程中不會停機,且不會影響用戶。

由於我們希望應用程序能寫入兩個數據庫,我們需要進行多主複製(multi-master replication)。在谷歌上搜索 “Postgres 中的多主複製” 可以找到大量解決方案,每種方案都有自己需要注意的優缺點。

我們決定繼續使用 Bucardo,因爲它開源、速度快,並且提供了簡單的監控和衝突解決機制。

Bucardo 的工作機制

Bucardo 充當兩個 Postgres 實例之間的中間人。你可以讓 Bucardo 在你喜歡的任何機器上運行,只要它可以訪問源數據庫和目標數據庫即可。安裝並設置多主複製後,Bucardo 將爲你選擇複製的所有表添加一些額外的觸發器。

你運行 Bucardo 的實例在本地使用一個單獨的 Postgresql 數據庫以保存同步狀態,這樣你就可以隨意暫停和重啓同步過程。當發生更改時,觸發器會將所有受影響的主鍵添加到 Bucardo 實例的 Postgres 中的 “delta” 表,另一個觸發器將 “啓動(kick)” 同步。每次同步被啓動時,Bucardo 將對比所有主表中每個表的受影響行並選擇一個獲勝者,然後將更改同步到其餘數據庫。選擇獲勝者並不簡單,此時可能會發生衝突。

小心漂移

一些在線指南建議,使用 Bucardo 的正確方法是獲取源數據庫的快照,將其恢復到新的數據庫,然後啓動一個多主 Bucardo 同步。不要那樣做!

如果這樣做,你將丟失與當前數據庫大小和寫入流量成正比的數據。這是因爲獲取快照並恢復它需要大量時間。在這段時間裏,源數據庫將因爲數據寫入而開始漂移,並且這種漂移也必須同步以確保兩個主數據庫包含相同的數據。這裏的問題是人們相信 Bucardo 會做某種回填,但事實證明它在這項任務上不可靠,並且可能無法同步大的漂移。你自然可以使用跨數據庫對比數據的工具,確保消除偏差;但如果數據集很大,這樣做會浪費大量時間,而恰恰我們追求的就是零停機時間。

此外,如果複製延遲足夠大,正在進行的同步可能會被誤報爲漂移。

如何同步漂移

你可以啓動 Bucardo 同步,並使用 autokick=0 標誌告訴它在本地數據庫中緩存所有漂移。不幸的是,雖然這個選項很關鍵,但它沒有文檔支持!這一步很關鍵,據我們所知唯一明確的參考資料出現在 David E. Wheeler 的這篇優秀博文中。

注意 autokick=0。這個標誌確保了在記錄增量時,它們不會被複制到任何地方,直到我們讓 Bucardo 這樣做爲止。

使用這個標誌,你就可以在本地緩存 Bucardo 實例中的增量,爲你騰出了足夠的時間來準備新數據庫。這是非常關鍵的,尤其是對於大漂移更是如此。

如何引導新數據庫

這裏有兩個選項。你可以從第一個數據庫中獲取全包快照並將其恢復到新實例,或者你可以從一個新的空數據庫開始,然後分別傳輸用戶、模式和數據(按這個順序)。我們推薦後一種方法。原因是在對兩個解決方案進行基準測試對比後,第二個的結果更乾淨。我們可以從頭開始關閉舊用戶帳戶和臨時表並細化用戶權限。

如果你使用的是 AWS RDS,推薦的這個方案也會更快。獲取快照可能需要幾分鐘時間,具體取決於你的數據庫大小。

此外,如果你像我們一樣從未加密的服務器遷移到使用靜態加密的服務器,你需要獲取快照、加密快照,然後將其還原到新的 RDS 實例。這樣做用的時間更久,而最小化遷移時間是我們的一個關鍵目標。

選擇性同步

在開始 Bucardo 同步前,你需要正確配置它。你需要指定兩個數據庫、它們的類型(主 / 副本),還有指定數據庫的哪些部分應包含在同步中。你可以從一個模式(schema)中批量添加所有表,數據庫有很多表的時候這個辦法非常有用。

Bucardo 無法在沒有主鍵(PK)的情況下同步表,這很正常,因爲那種情況下它無法區分唯一條目。我們不得不在流程中排除一些表,這些表充當各種表遷移的緩存並且不包含 PK。一些未使用的表也被排除在外,因此我們沒有將未使用的數據傳輸到新數據庫。在 Bucardo 中很容易完成上述操作:添加所有表後,你可以移除要排除的表。

遷移用戶

Bucardo 不會遷移 Postgres 用戶,你需要手動轉移你的用戶帳戶。我們爲此編寫了一個腳本。這個腳本會到新數據庫,使用從配置服務器檢索到的密碼創建新用戶,然後設置他們的權限。儘管你可能不會將數據存儲爲代碼,但將用戶保存爲代碼是一種很好的做法,這樣在發生災難時就能夠恢復它們了。

遷移模式和數據

你可以使用 Postgres 及其 pg_dump/pg_restore 工具來傳輸你的模式和數據。這個步驟很簡單,但有一個要點。請記住,此時我們已經啓動並運行了 Bucardo 來記錄漂移,因此在目標服務器上恢復數據將被解釋爲同步回源數據庫的更改。這就是爲什麼我們需要啓用 session_replication_role=replica 標誌,使用一個副本會話將數據恢復到目標 Postgres 數據庫。在我們啓動你的持續同步之前,我們需要禁用它。

衝   突

高可用性是零停機遷移的先決條件,它通常要求每個應用程序有多個正在運行的實例。一般來說,每個實例都應該在重新啓動之前排空,因此無法在完全相同的時間點將所有實例切換到新數據庫。所以總會有一個關鍵的——或短或長的——時間窗口,在這個窗口中同一個應用程序將同時寫入兩個數據庫,並且在這段時間內可能會發生衝突。

衝突很少見,因爲它們需要在兩個數據庫中進行兩次寫入,然後 Bucardo 才能複製這兩個記錄。複製時間接近於零,你可能根本不會遇到任何衝突,但這種遷移發生在關鍵的生產環境中,因此不能忽略它們。

想象一下,兩個客戶試圖在同一天預訂同一所房子。如果他們同時嘗試這樣做並且每個用戶都指向不同的數據庫,則可能會發生衝突。Bucardo 有一個 衝突解決機制,提供了兩個基本選項:要麼讓 Bucardo 自動處理衝突(默認選項),要麼中止同步並手動解決它們。這是遷移過程中最關鍵的部分,我們進一步分析一下。

如果你的表有一個自動遞增的 ID 作爲主鍵,Postgres 會自動從相應的序列中選擇下一個 ID。Bucardo 也會同步序列。假設在上面的示例中,你有一個帶有自動遞增 ID 作爲 PK 的 bookings 表,並且最新的記錄 ID 是 42。這裏會發生併發插入,並且在兩個數據庫中創建兩條不同的記錄,它們都以 43 作爲 PK,但數據不同。如果你讓 Bucardo 處理衝突,它會只保留最新的一個並刪除另一個。最後你會丟失一個對你的客戶來說似乎是成功的預訂。你的數據庫仍處於有效狀態,但你會丟失數據,還沒法恢復。這是一個死衚衕!

在討論解決方案之前,讓我們考慮另一種情況。假設你的表使用 UUID 作爲 PK。回放上面的場景,併發預訂將在兩個數據庫中創建兩個不同的記錄,並具有兩個不同的 PK。這次沒有發生衝突。Bucardo 將成功同步兩個數據庫中的兩條記錄,但從業務角度來看你的數據仍然無效,因爲你不能兩次預訂同一所房子。因此這裏很明顯,從業務角度來看數據庫有效性並不能保證你的數據有效。你需要小心對待衝突的處理方式,以免你的客戶遇到問題。

Bucardo 支持自定義解析策略。你可以根據業務需求制定自己的策略,但這很快就會變得過於複雜和耗時。另一種方法是創建你自己的工具來檢測和解決遷移期間的數據違規問題。這並非易事:它必須根據數據的複雜程度來做設計,並且可能需要大量開發工作。

我們的解決方案是在開始遷移之前滿足兩個條件,來徹底避免衝突。

首先,我們努力最小化數據庫之間的轉換時間,以最小化衝突概率。爲了做到這一點,我們會修改應用的重配置腳本以指向新的數據庫,一次一個實例,但所有的不同應用會並行操作。

第二步最關鍵,就在我們開始將應用切換到新數據庫之前,我們撤銷了舊數據庫中應用用戶的寫入權限。通過這種方式,我們可以徹底避免衝突,但代價是一定比例的數據庫寫入失敗時間。這當然需要你的應用程序能夠優雅地處理失敗的數據庫寫入。你的應用程序執行此操作時應該能獨立於任何數據庫遷移活動,因爲這對於生產環境來說至關重要。

下面就是最終的遷移計劃:

實   現

本節將展示我們遵循的步驟,以及每個步驟對應的腳本。我們已將代碼上傳到這個 GitHub 存儲庫,下文會對代碼做具體拆解分析。

 準備

  1. 啓動一個新實例(在我們的例子中是 EC2)。該指令假設你運行的是 Debian 操作系統。

  2. 運行 install.sh 來安裝 Bucardo

  3. 編輯 vars.sh 以設置你的數據庫和 postgres 角色密碼

  4. 在 shell 中導出上述變量:$sourcevars.sh

  5. (可選)如果你之前在源數據庫中使用過 Bucardo,你可能需要運行 uninstall_bucardo.sh 來清除舊觸發器。在運行之前,請查看我們根據我們的數據庫生成的 uninstall.template。你需要在那裏列出你所有的表。

  6. 你需要手動運行$ bucardo install才能完成本地 Bucardo 安裝。

 遷移

仔細看看 configure.sh 腳本。在這裏,你需要編輯腳本以匹配你的遷移方案。你需要爲 Bucardo 對象定義描述性名稱並指定排除的表或略過此選項。在你瞭解腳本的作用後可以繼續運行它。該腳本執行以下操作:

  1. 設置.pgpass文件和一條 Bucardo 別名命令,以避免在此過程中要求你輸入密碼的交互式提示中斷流程

  2. 配置 Bucardo 數據庫、herds、數據庫組和同步。如果你需要進一步瞭解 Bucardo 對象類型,他們的文檔頁面中有一個 列表。

  3. 在新的 Postgresql 主機中初始化一個空數據庫並運行此腳本創建用戶。你需要編輯這個腳本來指定你的角色。密碼由我們之前獲取的vars.sh文件檢索。

  4. 這一步只傳輸數據庫模式,使用pg_dump並將其傳輸到新主機

  5. 使用本地緩存啓動 Bucardo 同步

  6. 以壓縮格式傳輸數據庫數據。當數據傳輸和漂移開始堆積時,Bucardo 會將其保存在本地並在 autokick 標誌更改值後重播

  7. 重置 autokick 標誌的值以停止本地緩存,然後重新加載配置以讓同步遵守新值

  8. 啓動多主同步

現在持續同步已就位,是時候開始在新數據庫中移動應用了。對我們來說,我們是更改配置服務器中的應用程序參數然後一一重新部署來完成這一步的。在這一步中,我們需要將舊數據庫中的用戶權限設置爲只讀。一旦我們應用的第一個實例連接到新數據庫,我們就運行 revoke_write_access_from_old_db.sql 腳本更改舊數據庫中的權限。這一步的時機非常重要。

 遷移後檢查

總   結

將你的 postgresql 數據庫遷移到一個新實例會面臨巨大挑戰。無論你選擇哪種工具來實施,你要面對的挑戰都是一樣的:

在本文中,我們介紹了自己是如何解決這些問題的。我們遇到的一大困難是沒有這方面的在線教程,因此我們不得不隨機應變,並多次迭代我們的解決方案,直到我們正確地完成任務。我們也想聽聽你的反饋意見,這樣可以幫助我們改進流程,並幫助可能面臨相同問題的其他讀者。

PS:背景故事

2020 年初,我們發現我們使用了兩個 Postgres9.5 實例,我們從 Blueground 的早期就一直在使用它們。2020 年 1 月,我們不得不關閉舊實例並使用新實例,因爲亞馬遜即將遷移到新的 SSL/TLS 證書。這次遷移中,我們丟失了不少數據,花費了幾天的時間來恢復它們。問題出在我們信任 Bucardo 的自動同步機制,讓它處理我們的漂移;正如前面提到的那樣,它有問題並且失敗了。今年我們不得不再做一次,因爲 Postgres 9.5 即將 EOL 了,否則它們會被 AWS 強行升級。這次我們下定決心要注意每一個小細節。我們相信我們可以快速、可靠且無故障地達成目標,我們做到了。

爲什麼要升級到新實例

首先,我們需要解釋爲什麼我們不讓亞馬遜在沒有我們干預的情況下在線升級我們的數據庫。亞馬遜提供了升級流程,但與遷移到新數據庫實例的方案相比,它有一些嚴重的缺點:

我們選擇 Bucardo 是因爲我們想要一個在我們的 VPC 中沙盒化的解決方案,這樣生產數據永遠不會泄露到互聯網上。最後遷移很成功,也沒有丟失數據。遷移過程的總耗時不到 2 小時,算是比較成功的!

原文鏈接:

https://engineering.theblueground.com/blog/zero-downtime-postgres-migration-done-right/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SIyv66JFh780-WlifB92lw