一次近乎完美的PostgreSQL版本大升級
2020 年 5 月,我們與 OnGres 合作,對 GitLab 上的 Postgres 集群進行版本大更新,從 9.6 版本升級到 11 版本。升級全部在維護窗口內(nèi)運行,沒有絲毫差錯;更新中所有涉及的內(nèi)容、計劃、測試,以及全流程自動化,全部進行拆包,只為實現(xiàn)一次近乎完美的 PostgreSQL 升級。
本次版本更新,我們面臨的最大難題在于如何利用一個規(guī)劃完善的 pg_upgrade,方便且高效地對整體項目進行重要版本升級。為此,我們需要制定一個回滾計劃,以保證 12 節(jié)點集群的 6 TB 數(shù)據(jù)一致的同時,優(yōu)化恢復(fù)目標(biāo)時間(RTO)后的容量,為 600 萬用戶提供每秒 300000 次的聚合交易服務(wù)。
解決工程難題的最佳方案是按照藍圖和設(shè)計文檔行事。在創(chuàng)建藍圖的過程中,我們需要定義目標(biāo)問題,評估最合適的解決方案,并考慮每個解決方案的優(yōu)缺點。
在此,我們附上為這個項目準(zhǔn)備的藍圖鏈接。
一、我們?yōu)槭裁匆塒ostgreSQL
我們決定在 GitLab 13.0 中停止對 PostgreSQL 10.0 的支持,而 PostgreSQL 9.6 版本將在 2021 年 11 月 EOL(項目終止),因此,我們需要采取相應(yīng)的行動。
下面是 PostgreSQL 9.6 和 11 版本之間的主要區(qū)別:
- 表分區(qū)支持 LIST、RANGE,以及 HASH;
- 存儲過程支持事務(wù);
- 即時編譯(JIT)加快查詢表達式的運行速度;
- 并行查詢,增加并行化數(shù)據(jù)定義功能;
- 新版本的 PostgreSQL 繼承了版本 10 中的“邏輯復(fù)制——分發(fā)數(shù)據(jù)的發(fā)布 / 訂閱框架”,該功能可以使日后的升級更加順滑,簡化了其他相關(guān)流程;
- 基于 Quorum 的提交(commit),確保事務(wù)能在集群中指定節(jié)點進行提交;
- 提升了通過分區(qū)表進行查詢的性能。
二、環(huán)境與架構(gòu)
PostgreSQL 集群,其基礎(chǔ)架構(gòu)容量由 12 個服務(wù)于 OLTP 以及異步管道的 n1-highmem-96 GCP 示例組成。同時,它還有兩個不同規(guī)格的 BI 節(jié)點,每個節(jié)點都有 96 個 CPU 內(nèi)核以及 614GB 的 RAM。HA 集群通過 Patroni 進行管理和配置,以保證 Consul 集群及其所有復(fù)制體在異步流復(fù)制中,使用復(fù)制槽和 WAL 對 GCS 存儲桶進行復(fù)制工作時的 leader 選舉一致性。
https://github.com/zalando/patroni
我們的配置目前使用的是 Patroni HA 解決方案,它會不斷收集集群、leader 檢測,以及節(jié)點可用性的關(guān)鍵信息。該解決方案采用 Consul 的 DNS 服務(wù)等關(guān)鍵功能來實現(xiàn),進而更新 PgBouncer 端點,確保讀寫和只讀流量使用不同架構(gòu)。
GitLab.com 架構(gòu)
因為 HA 的緣故,其中兩個復(fù)制體不在只讀服務(wù)器列表池中,而是由 Consul DNS 支持,服務(wù)于 API。對 GitLab 架構(gòu)幾次改進后,我們得以將項目整體降到 7 個節(jié)點。
此外,我們的整個集群平均每周要處理大約 181000 個交易每秒,如下圖所示,流量會在周一有明顯增加,并在周一至周五 / 六內(nèi)保持該吞吐量。我們需要讓維護影響到盡量少的用戶,因此流量數(shù)據(jù)的統(tǒng)計對于設(shè)置合適的維護窗口至關(guān)重要。
GitLab.com 上連接數(shù)量統(tǒng)計
項目整體在全天中最忙碌的時刻可以到達 25000 交易每秒。
GitLab.com 上 commit 數(shù)量統(tǒng)計
與此同時,項目處理的交易峰值可以到達每秒 30 萬次交易,GitLab.com 能到達每秒 6 萬次連接。
三、我們的升級需求
在生產(chǎn)環(huán)境進行升級前,我們首先確定了一些需求:
- PostgreSQL 11 上不能有回歸。我們開發(fā)了一個自定義基準(zhǔn)測試來運行更廣泛的回歸測試,目標(biāo)是識別 PostgreSQL 11 中潛在的查詢性能下降;
- 升級應(yīng)當(dāng)針對整體項目,并在維護窗口內(nèi)完成;
- 使用 pg_upgrade 升級,其依賴于物理層面,而非邏輯或者復(fù)制。
- 保留一個 9.6 版本的集群樣本。并非所有節(jié)點都需要升級,我們應(yīng)保留一些 9.6 版本的節(jié)點以備回滾;
- 升級應(yīng)全自動化,以降低人類失誤的可能性;
- 全部數(shù)據(jù)庫升級的維護窗口只有 30 分鐘;
- 升級應(yīng)留有記錄并將其發(fā)布。
四、項目
為使生產(chǎn)升級能順利運行,我們將項目劃分為以下四個階段:
第一階段:在封閉環(huán)境中開發(fā)自動化
1)開發(fā) ansible-playbook,并在 staging 上備份的 PostgreSQL 環(huán)境中進行測試
https://gitlab.com/gitlab-com/gl-infra/db-migration/-/tree/master/pg-upgrade
2)獨立環(huán)境的使用讓我們可以隨時停止、啟動,或者恢復(fù)備份,也讓我們專注開發(fā),并得以將環(huán)境隨時回滾到升級前。
3)我們使用 staging 上的備份在環(huán)境中進行項目升級。在這個過程中,我們也遇到一些諸如在遷移數(shù)據(jù)庫的過程中如何監(jiān)視不同程序之類的挑戰(zhàn)。
第二階段:在staging中將升級開發(fā)與配置管理進行分段式融合
1)在 Chef 中集成配置管理,并運行數(shù)據(jù)庫磁盤中的一個快照(可用于還原更新前狀態(tài))。
2)通知用戶,本次維護窗口將力爭對他們工作的影響降到最低,并在沒有數(shù)據(jù)損失風(fēng)險的情況下進行安全升級。
3)在對配置管理進行迭代和集成測試后,我們開始在 staging 上運行端到端測試。這些測試內(nèi)容是在內(nèi)部公開的,所以其他共享這個環(huán)境的團隊會知道 staging 在這段時間暫時不可用。
第三階段:在staging上測試端到端升級
- 正式運行前對環(huán)境的檢查。我們有時候會在這一步發(fā)現(xiàn)認證的問題,有時候也會做一些能提升測試效率的小調(diào)整;
- 停止 GitLab 上所有應(yīng)用和流量,在 CloudFlare 和 HA-proxy 上添加維護模式,停止包括數(shù)據(jù)庫、sidekiq、workhorse、WEB-API 等一切能訪問數(shù)據(jù)庫的應(yīng)用;
- 升級集群中六個節(jié)點中的三個。與生產(chǎn)中部分場景的策略類似,我們同樣準(zhǔn)備了回滾方案;
- 為 PostgreSQL 的更新運行 ansible-playbook。首先是數(shù)據(jù)庫 leader 節(jié)點,之后是一些二級節(jié)點;
- 升級之后:我們在 ansible-playbook 中運行了一些自動化測試,用以檢測復(fù)制數(shù)據(jù)與原數(shù)據(jù)是否相符;
- 接下來,啟動應(yīng)用程序,讓我們的 QA 團隊能運行一些測試。他們在升級后的數(shù)據(jù)庫上運行了本地單元測試,我們對負面結(jié)果進行了調(diào)查。
測試結(jié)束后,我們再次停止程序運行,并將 staging 集群還原到 9.6 版本,將升級過后的節(jié)點關(guān)閉到版本 11,最后啟動舊版集群。Patroni 會 promote 其中一個節(jié)點,啟動應(yīng)用后集群就可以收到流量反饋。我們將 Chef 的配置恢復(fù)到集群 9.6 版本后重建數(shù)據(jù)庫,留出六個節(jié)點為下次測試做準(zhǔn)備。
我們總共在 staging 中運行過 7 次測試,并通過反饋不斷完善程序。
第四階段:升級進入生產(chǎn)環(huán)境
生產(chǎn)環(huán)境的步驟與 staging 中類似,我們計劃遷移八個節(jié)點,留下四個作為備份。
- 執(zhí)行項目前期檢查;
- 宣布維護開始;
- 運行 ansible-playbook 以停止流量和應(yīng)用;
- 運行 ansible-playbook 以進行 PostgreSQL 升級;
- 開始驗證測試并恢復(fù)流量。我們只運行了必需的測試,才能在短暫的維護窗口內(nèi)完成所有內(nèi)容。
回滾計劃只會在數(shù)據(jù)庫不一致或者 QA 測試出錯時才調(diào)用,以下是具體步驟:
- 停止 PostgreSQL 11 集群;
- 還原 Chef 中配置到 PostgreSQL 9.6;
- 用 9.6 版本中的四個節(jié)點初始化集群。通過這四個節(jié)點,我們可以在流量較低的時候恢復(fù) GitLab 上的活動;
- 開始接收流量,借此可以盡量減少停機時間;
- 使用在維護期間和升級前的磁盤快照恢復(fù)其他節(jié)點;
- 升級中的所有步驟都在用于運行項目的模板中有詳細說明。
五、pg_upgrade運行原理
pg_upgrade 讓我們可以在不用 dump/reload 策略,不用更多停機時間的情況下,將 PostgreSQL 數(shù)據(jù)文件升級到日后的主要版本。
https://www.postgresql.org/docs/11/pgupgrade.html
正如在 PostgreSQL 官方文檔中所寫,pg_upgrade 工具通過避免執(zhí)行 dump/restore 的方法來升級 PostgreSQL 版本。這里有幾點細節(jié)需要注意:PostgreSQL 的主要版本會添加新功能,這些新功能經(jīng)常會改變系統(tǒng)表的布局,但內(nèi)部數(shù)據(jù)存儲格式基本會保持不變。如果某次主要版本升級改變了數(shù)據(jù)格式,那么就不能繼續(xù)用 pg_upgrade 了。因此,我們必須要先驗證這些版本之間都有什么變化。
還有一點很重要,任何外部模塊都必須兼容二進制,雖然你并不能通過 pg_upgrade 來檢查這點。對 GitLab 的更新來說,我們在升級前先卸載了 postgres_exporter 等視圖及拓展,以便在升級后重新創(chuàng)建,出于兼容性考慮,還要稍作修改。
https://github.com/wrouesnel/postgres_exporter
在更新前,必須先安裝新版本的二進制文件。新的 PostgreSQL 二進制文件及拓展文件都裝在需要升級的主機中。
pg_upgrade 在使用時有很多選項。我們選擇在 Leader 節(jié)點上使用 pg_upgrade 的鏈接模式,因為維護窗口很短暫,只有兩個小時。這種模式可以通過 inode 硬鏈接文件,避免了復(fù)制 6TB 文件的麻煩。缺點則是舊數(shù)據(jù)集群無法回滾到 9.6 版本。我們保存了 9.6 版本的副本和 GCP 快照作為后備計劃的回滾路徑。因為從頭開始重建副本是不可能的,所以我們選擇使用 rsync 增量功能來進行升級。pg_upgrade 的官方文檔也有寫:“從主服務(wù)器上位于舊數(shù)據(jù)庫集群目錄和新數(shù)據(jù)庫集群目錄上方的目錄中,在每個備用服務(wù)器的 primary 上運行此命令。”
ansible-playbook 對于這一步的實現(xiàn),是通過從 leader 節(jié)點到每一個副本都有一個任務(wù),在新舊數(shù)據(jù)目錄中的父目錄中觸發(fā) rsync 命令。
六、回歸測試的基準(zhǔn)
任何的遷移或數(shù)據(jù)庫升級都需要在最終的生產(chǎn)升級前進行回歸測試。
對團隊來說,數(shù)據(jù)庫測試在升級過程中是至關(guān)重要的一步,根據(jù)生產(chǎn)過程中的查詢數(shù)額來進行性能測試,將結(jié)果存到 pg_stat_statement 表中。這些都是在同一個數(shù)據(jù)集中運行的,一次是在 9.6 版本,一次是在 11 版本的迭代。
這一步過程可以在下面這個公共的 issue 中找到:
https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/
- 工具的準(zhǔn)備;
- 創(chuàng)建測試環(huán)境;
- 計算容量;
- 使用 JMeter 工具運行基準(zhǔn)測試。
最后,根據(jù) OnGres 在這一基準(zhǔn)測試上的工作,GitLab 將在未來跟進新的基準(zhǔn)測試。
- 主要生產(chǎn)數(shù)據(jù)庫集群的能力評估;
- 數(shù)據(jù)庫容量及飽和度分析。
七、升級過程:全自動就完事了
在升級項目中,升級團隊堅持使用自動化和基礎(chǔ)架構(gòu)及代碼工具(IaC)。所有流程必須全部自動化,以減少在維護窗口的人為失誤。pg_upgrade 所有的運行步驟都可以在這個 GitLab 的 pg_upgrade 的模板 issue 上找到詳細說明。
GitLab.com 的環(huán)境由 Terraform 和 Chef 共同管理,所有的升級自動化都是用 Ansible 2.9 的 playbook 和 roles 編寫的,我們用了兩個 ansible-playbook 來完成升級自動化:
一個 ansible-playbook 控制流量和應(yīng)用:
- 將 Cloudflare 設(shè)置為維護狀態(tài),不接受流量;
- 停止 HA-proxy;
- 停止訪問數(shù)據(jù)庫的中間件:Sidekiq、Workhorse、WEB-API。
另一個 ansible-playbook 運行升級過程:
- 協(xié)調(diào)所有數(shù)據(jù)庫和連接池的流量;
- 控制 Patroni 集群和 Consul 實例;
- 在主節(jié)點和次級節(jié)點上執(zhí)行升級;
- 收集升級后的統(tǒng)計數(shù)據(jù);
- 使用 Chef 同步更改,以保持配置管理的完整性;
- 驗證集群的完整性和狀態(tài);
- 執(zhí)行 GCP 快照;
- (可能的)回滾過程。
playbook 以交互方式逐個運行所有任務(wù),讓程序員得以在任意給定執(zhí)行點跳過或暫停程序。參與 staging 測試和迭代的所有團隊成員都要過目升級過程中的所有步驟,staging 環(huán)境讓我們通過演習(xí)提前找到升級過程中潛在的漏洞。而執(zhí)行和迭代 staging 中自動化過程則讓我們實現(xiàn)了 PostgreSQL 9.6 版本至 11 版本的基本無缺陷升級。
為完成本次的版本升級,GitLab 的 QA 團隊將部分測試中發(fā)現(xiàn)的問題反饋給我們,這一部分的工作可以在這條 issue 中找到。
https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/106#note_332170837
八、PostgreSQL預(yù)升級的步驟
升級工作的第一步是“預(yù)升級”,這里涉及到預(yù)留給回滾的示例。我們做了相應(yīng)分析,以確保新的集群可以不丟失吞吐量的情況下,以 8 個示例為起點,保留 4 個通過標(biāo)準(zhǔn) Patroni 集群同步的 9.6 版本示例,為后續(xù)可能需要的回滾情況準(zhǔn)備(共計 12 個實例)。
在這個階段,我們還需要停止依賴 PostgreSQL 的服務(wù),諸如 PgBouncer、Chef 客戶端,以及 Patroni 服務(wù)。
在正式開始更新前,必須要告知 Patroni,避免任何虛假 leader 選舉,通過 GCP 快照(通過對應(yīng)低級備份 API 獲得)進行一致的備份,并通過運行 Chef 應(yīng)用新的設(shè)置。
九、PostgreSQL升級階段
首先,停止所有節(jié)點。
然后,運行以下檢查:
- pg_upgrade 版本檢查;
- 驗證所有節(jié)點都已同步,并且不再接受任何流量。
一旦主節(jié)點數(shù)據(jù)升級完畢,就會觸發(fā) rsync 進程以同步所有副本數(shù)據(jù)。在升級完成后,啟動 Patroni 服務(wù),這樣所有副本都能輕松更新至新集群的配置。
通過 Chef 安裝二進制文件,新集群在版本方面的設(shè)置是在同一個 MR 中定義的,MR 源自 GitLab.com,可以安裝用于數(shù)據(jù)庫中的拓展項。
最后一個階段則包括恢復(fù)流量、運行初始的真空期,以及最后的啟動 PgBouncer 和 Chef 客戶端服務(wù)。
十、遷移日
到了最后,我們?yōu)檫\行生產(chǎn)線上升級做好了萬全準(zhǔn)備,團隊在周日一早 8:45 UTC 開始會議(對有的人來說是晚上)。服務(wù)將最多下線兩小時,當(dāng)最終的通知下達后,工程團隊終于可以開始進行。
升級過程由停止所有流量及相關(guān)服務(wù)開始,這是為了避免用戶在更新中途訪問網(wǎng)站。
下面圖表顯示在服務(wù)更新之前,維護期間(圖標(biāo)中的空白部分)、以及維護結(jié)束、流量恢復(fù)后的流量和 HTTP 數(shù)據(jù)統(tǒng)計。
GitLab.com 上的數(shù)據(jù)統(tǒng)計圖,從維護開始到結(jié)束
整個流程共花費四個小時,其中僅包括兩小時斷線時間。
此外,我們錄下了 PostgreSQL 更新的全過程并發(fā)布在 GitLab Unfiltered 上。