How We Achieved Minimal Downtime During Our PostgreSQL Database Upgrade (Chinese Version)

前陣子,我們將 Dcard 服務主要的 PG Cluster 進行主版本的升級,並且控制寫入 downtime 在十分鐘內,在一小時內重建完成 OfflineDB 與 CDC 服務。因此透過這篇文章來分享我們是如何完成這項艱困的任務。

以下文章內容跟公司發佈在 medium 內容一樣,也算是紀錄自己做這大專案的嘔心歷程 XD

然後之後會再發一篇英文版的文章,也算是想分享給外國朋友,因為做這升級的規劃時,也上網看了不少其他國外公司的升級文章,所以我也想把我的升級過程翻譯成英文分享出去。

為什麼要升級

為什麼決定要從 9.6 升級到 11 有以下幾點理由:

  1. 官方不再維護 9.6,當前維護的最小版本為 11。

  2. 9.6 到 11 之間有新增不少新功能或是加強的部分:

    1. 在 10 支援 native logical replication。
    2. 在 10 加強 Query Parallelism 功能,例如支援 parallel b-tree index scans 跟 parallel bitmap heap scans。
    3. 在 10 新增 CREATE STATISTICS 功能來建立額外的統計資訊幫助 planner 使用更好的 plan 並提升查詢效率。
    4. 在 11 新增 Parallel CREATE INDEX,提升效率。
    5. 在 11 中 default value 不需要 rewrite table。
    6. 在 11 中提升 vacuum 的效率。
    7. 在 11 中新增 CREATE INDEX … INCLUDE 使得可以 index only scan。

另外,Dcard 目前的 PG Cluster 其 disk 是沒有做拆分的,其 OS 及 PG data 都是集中在一顆 disk,這樣不好處理 backup 跟 recovery。因此想藉由升級來提升 PG 性能帶給使用者更好的體驗,也重新拆分 disk 對後續的維運更加方便。

常見升級策略

以下分析 PG 主版本升級常見策略的優缺點。

dump / restore

使用 pg_dump 將舊版本的 PG 資料進行 dump 並在新版本的 PG 來進行 restore。

缺點:

  1. 考量到我們資料庫的大小,需要花費大量的時間來進行 dump 跟 restore。
  2. dump 的過程中會使用大量 DB 的 CPU 及 memory,因此會影響在線使用者的體驗。
  3. dump 只能建立當下的 snapshot,後續 db 新的寫入無法捕捉到。

pg_upgrade

pg_upgrade 是官方提供 in place 的將舊版本升級為新版本的工具,因為 PG 主版本之間的內部資料結構不太會變更,因此透過 pg_upgrade 工具可以重用舊資料並且使用 --link 功能來建立 hard link 而不用 copy 既有的 data 到新的 PG,整個升級過程可以在幾秒鐘就可以完成。

缺點:

  1. 舊的 PG 必須停機。
  2. 要在 OnlineDB 上做 in place 升級,容易搞髒裡面的資料且不好做 rollback,就算是用 disk snapshot 處理,從建立 snapshot 到開出 VM,整體的 downtime 會被拉長很多。

logical replication

logical replication 因為是 statement level 的 replication 所以可以支援不同主版本的 PG 彼此之間做 sync,因此可以將當前舊版的 PG cluster sync 到新版本的 PG cluster,並當兩者 sync 一致並在離峰階段時將 client 端改成連新的 PG cluster 即可。

缺點:

  1. native logical replication 在 PG 10 才支援,並且無法複製 DDL。

我們選擇的升級策略

在講我們選擇的升級策略前,先介紹目前 Dcard 簡略架構圖如下:

  1. Dcard 主要 DB 是 PG 9.6 Cluster,並採用 streaming replication 的方式,因此 cluster 內的 PG 主版本都必須是相同的。
  2. Dcard-API Server 主要負責 Dcard 核心功能,且資料來自於背後的 PG 9.6 Cluster。
  3. PG 9.6 Primary 會將 logical decoding change 送至 Pulsar,給下游的 OfflineDB 跟 CDC consumer 做使用。
  4. 因為 Dcard 架構是採取 microservice,因此也會有其他服務會直連 PG 9.6 Cluster。

關於 OfflineDB 跟 CDC 的細節請參考:PostgreSQL 技術筆記:跟疾管署沒有關係的 CDC,這邊就略過不介紹。

根據上面的架構圖,可以知道 PG 9.6 Cluster 牽涉到許多服務,但我們不希望為了升級而要特地改現有服務架構,搭配前面說的常見升級策略,最後決定採用 pg_upgrade + logical replication 的組合,並接受寫入 downtime 而不是完全的 zero downtime,原因如下:

  1. pg_upgrade 對於單台升級速度很快。
  2. Dcard 已經有很成熟的 CDC 的架構且能克服 native logical replication 的限制。
  3. 如果要做到 zero downtime,勢必會有 PG 9.6 Cluster 跟 PG 11 Cluster 雙寫入的情況,考量到我們資料使用情境,很容易會有 conflict,解決方式可能會需要在 application layer 做特別處理或是要人工干預,處理不好可能會有大量的髒資料,這對於我們後續處理成本會變很高,因此經過衡量後決定不採用 zero downtime。

鑑於以上原因,我們最終升級目標如下:

  1. 接受寫入 downtime (控制在 10 分鐘內),但可以讀取,盡可能地維持使用者體驗。
  2. 背後的 OfflineDB 及 CDC 服務在 1 小時內可以重建完成。
  3. 在升級的過程不影響現有服務架構,例如在 application layer 做 migration 處理。

而整個升級步驟如下:

  1. 在白天架設好新的 PG 11 Cluster 並且利用 CDC 與 PG 9.6 Cluster 進行 sync。
  2. 在晚上離峰階段將 client 端切換至連線 PG 11 Cluster,並控制寫入 downtime 在十分鐘內,使用者在這切換期間仍然可以進行任何讀取。
  3. 切換成功後,開始重建背後的 OfflineDB,確保 OLAP Application 在之後能夠從新的 offlineDB 處理新的資料。
  4. 確保當前所有的 CDC Consumers 都已經消耗完舊的 topic 的 messages,接著將當前所有的 CDC Consumers 改聽新的 topic。

實驗演練

規劃好升級的策略之後,接著針對重要步驟進行實驗,確認所需要花費的時間及資源,並且要確保之後的整個升級演練都能順利完成才能排定正式升級時間。

實驗 pg_upgrade

在架設 PG 11 Cluster 的前置作業需要先 clone 當前的 PG 9.6 Primary,並透過 pg_upgrade 來進行 in place 的升級。此外,需要實驗 pg_upgrade 將 PG 9.6 升級到 PG 11 是否有不相容的問題,還有確認升級的所需要花費的時間。實驗結果:

  1. 原本的 PG 9.6 機器的 OS 版本太舊,其 apt repository 已經無法安裝新版的 PG,因此透過 source code 的方式來安裝 PG 11。
  2. 需要先將舊版本的 PG 所安裝的第三方 extension 都先安裝在新版本的 PG 才能使用 pg_upgrade 進行升級。
  3. pg_upgrade 要使用 --link 選項才能快速升級,根據結果只需要花費 10 秒鐘。

實驗 VACUUM FULL

藉由這次的升級,我們檢查了當前 DB 的 dead tuple 與 free space 的狀況,並實驗 VACUUM FULL 能夠釋放多少的 disk 空間出來,根據結果可以釋放 200 GB 的空間,因此我們決定在這次升級使用 VACUUM FULL 進行清理。

開發環境實驗 PG 11

為了確保當前服務所用的 ORM Libraray 行為對於 PG 11 可以相容,因此我們先在開發環境上對重要的 API 進行測試。

線上環境實驗 PG 11

當開發環境確認沒問題,接著實驗線上環境 PG 11 可以運作正常,我們在當前的 PG 9.6 Cluster 新增一個 PG 11 Replica,該 Replica 透過 CDC 與 PG 9.6 Primary 進行 sync。但這個實驗的缺點在於只能測試服務對 PG 11 的讀取而無法測試寫入,但如果需要在正式環境做寫入的演練勢必需要在 application layer 做特別處理或是使用自製的 proxy,因此經過衡量後決定只測試重要的 API 寫入行為,就不額外增加升級的成本。

實驗 PG 9.6 與 PG 11 的 CDC Sync

當建立好 PG 11 Cluster 這個過程可能會需要數小時的準備時間,此時 PG 11 會與 PG 9.6 有一段資料落差,我們需要實驗 PG 11 Cluster catch up PG 9.6 Cluster 的時間是否會太長,且也需要監控之後的 replication lag 是否短而穩定。經過實驗結果,即使有數小時的資料落差,但仰賴我們成熟的 CDC 架構,使得 catch up 只需要一小時就可以完成。

實驗重建 OfflineDB

當升級完成後需要選定一台 PG 11 機器的 data disk 產生 snapshot,並重新產生一台 OfflineDB 出來,讓 OLAP application 可以使用新的 OfflineDB。因此需要實驗重建 OfflineDB 會花費多久的時間,另外也確認新的 OfflineDB 的 CDC sync 機制是否運作正常。

實驗 CDC Consumer 切換新 topic

既有的 PG 9.6 Primary 透過 pg2pulsar 會將 logical decoding message 送至 pulsar topic:dcard_api。
這邊要注意:如果切換到新的 PG 11 Cluster 後如果將新的 logical decoding message 透過 pg2pulsar 將 logical decoding message 也送至相同的 topic,兩邊 PG 的 LSN 會混雜在同一個 topic,不好做後續的 rollback 且我們目標是不影響現有的架構,因此決定新的 PG 11 Primary 會將 logical decoding message 送至新的 pulsar topic:dcard_api_11

此外,我們透過監控既有的 CDC consumer 發現在過去的凌晨時段都能消耗完 topic 內的 message,這樣我們就能在升級 PG 1 後,迅速將 CDC consumer 改聽新的 topic。
這邊要注意:當 consumer 建立 pulsar subscription 時預設會從 topic 拿最新的 message 而不會回放前面的 message,但是當我們升級 PG 11 之後就會馬上開放 client 端可以對其寫入,因此我們需要手動為現在所有的 CDC Consumer 對新 topic 建立 subscription 並設定其 cursor 從最早的 message 開始拿,這樣我們能確保 CDC Consumer 都能 consume 新 topic 的所有 message。

演練完整升級流程

當前面的實驗演練確認都沒問題,我們透過在正式環境建立一套仿 Dcard 當前架構來進行演練升級的過程,每一次的演練過程都有錄影,並且在錄影過後檢討有問題的地方並改善流程,例如完善升級腳本或是控制 downtime 都能夠在 10 分鐘內。

我們在這些的升級演練中,發現最嚴重的問題:當前我們的 CDC 架構是無法處理 sequence,因為 logical decoding 的先天限制,只能拿到每一個 column 的 new value,而 CDC sync 會將這個 new value 直接 update 或是 insert 對應的 Table,因此 sequence 的 last value 無法被更新到,如果開放 client 對 PG 11 進行寫入就會因為 auto increment 的特性出現 unique constraint 之類的錯誤,因此我們需要透過 script 查詢出所有用到 sequence 特性的欄位並更正 last value 解決這個問題。

升級步驟

以下介紹整個升級流程的每一項步驟。

使用 Disk Snapshot 建立 PG 9.6 Primary

為了方便之後做 in place 升級與規劃 disk 拆分,這邊先透過 clone 一台 PG 9.6 Primary 。

詳細步驟如下:

  1. 將正式環境的 PG 9.6 Primary 建立 Disk Snapshot 產生一台 PG 9.6 Primary。
  2. 記下當前 PG 9.6 recovery 之後的最新的 Checkpoint 的 LSN,方便之後從這個斷點做與 PG 11 Cluster 的 CDC Sync。
  3. 安裝 PG 11 在這台機器上。
  4. 使用 pg_upgrade 進行 in place 升級。
  5. 使用 VACUUM FULL 清理 dead tuple 並且釋放硬碟空間。

這邊因為有 VACUUM FULL 所以會花數小時的時間。

建立新的 PG 11 Cluster

由於前面處理好的 PG 11 機器還是單一 disk,因此我們需要先開新的 PG 11 Primary 機器,並設定好 boot disk、data disk、archive disk。接著只需要將前面處理好的 PG 11 機器的資料部分透過 pg_basebackup 複製到新的 PG 11 Primary 即可。

詳細步驟如下:

  1. 使用 pg_basebackup 將 temp PG 11 Primary 資料轉移過來新的 PG 11 機器。
  2. 根據 PG 11 機器的規格設定 postgresql.conf 。
  3. 建立每一台 replica 所需要的 physical replication slot。
  4. 手動建立 checkpoint,將當前 WAL buffer flush 到 disk。
  5. 利用 PG 11 Primary 的 disk snapshot 快速建立多台 Replica 出來,並設定好 streaming replication。
  6. 在 PG 11 Primary 上設定好 pulsar2pg 並填上之前紀錄的 PG 9.6 的 checkpoint,使得可以與 PG 9.6 Cluster 從正確的 LSN 斷點開始進行 CDC sync。

設定好 CDC sync 之後,透過 grafana 拉好的 dashboard 去監控當前 pulsar topic 對應 subscription 的 backlog 消耗的進度,就能清楚知道 CDC sync 的狀況。

切換 PG 9.6 Cluster to 11 Cluster

在離峰階段時確認兩邊的 sync 進度一致時就可以進行切換。

詳細步驟如下:

  1. 設定 firewall 阻擋 PG 9.6 Primary 的任何外部連線。
  2. 將所有目前 PG 9.6 Primary 的所有連線都 terminate 掉,並等待幾秒鐘。
  3. 在 PG 9.6 Primary 建立 marker table 當作是最後一筆寫入,並確保 PG 11 Primary 有收到這最後一筆寫入,代表兩邊是完全 sync 進度一致。此外,因為 logical decoding 的限制而無法更新 sequence 的 latest value,因此需要跑 script 在 PG 11 更新 sequence 相關欄位。
  4. 將 PG 9.6 Primary 的 pg2pulsar 停掉並且開啟 pulsar2pg,也在 PG 11 Primary 開啟 pg2pulsar,讓這兩座 Cluster 可以持續 sync,方便未來的 rollback。
  5. 在 PG 11 Primary 將前面建立的 marker table drop 掉,當作是 PG 11 當前最後一筆寫入,並確保說 PG 9.6 Primary 也有將 marker table drop 掉,代表兩邊可以正常 sync。
  6. 將所有有連 PG 9.6 Cluster 的服務都改連到 PG 11 Cluster,讓使用者可以開始正常寫入到新的 PG 11,並結束 downtime。
  7. 產生 PG 11 的 disk snapshot 並重建新的 OfflineDB,讓 OLAP 服務可以改連到新的 OfflineDB。
  8. 確認既有的 CDC consumer 都已經消耗完舊的 topic 的 message,並在新的 topic 為每一個 consumer 重建 subscription 並設定好 cursor 會從 topic 的第一筆 message 開始 consume,接著將既有的 CDC consumer 都改連到新的 topic。

此外,為了確保切換升級的過程是否順利,我們在 grafana 預先拉好會影響到服務的 dashboard 來進行監控,而相關的 metrics 主要有:

  1. 各個服務的 K8S pod 狀態及 replica 數量。
  2. 重要的 API 的 success 與 fail rate。
  3. 新舊的 OfflineDB 與 CDC consumer 的 pulsar backlog 數量及 ack rate。

最後,實際切換升級導致的寫入 downtime 約花 8 ~ 9 分鐘。

Rollback 方案

因為切換升級後這兩座 cluster 仍然會持續 sync,就算需要 rollback ,只要根據上面升級切換的步驟照做就可以了。
這邊要注意:如果要將 CDC consumer 切換到舊的 topic 的時候,必須要先在 PG 9.6 Primary 上手動建立 cursor table,這個 cursor table 是為了方便我們找到在舊 topic 的 cursor 位置,並透過重設 cursor 可以避免 CDC consumer 消耗重複 message 的問題。
其他詳細步驟這邊就略過不提。

結語

整個 migration 流程並不是一簇可及的,我們需要:

  1. 研究所有可行的升級與 rollback 方案,並分析其優缺點,搭配目前的 Infra 架構,選出最符合我們升級目標的策略。
  2. 實驗演練多次,每一次的演練都會錄影並確認哪邊可以改善的部分。
  3. 與營運團隊及開發團隊協調並定好正式升級的時間。
  4. 在切換升級前持續 monitor 各個元件,確保每個階段都沒問題才正式切換。

最後,migration 的過程還是有需要人工手動確認的部分,之後也會改善 migration 的流程且盡可能地降低 downtime 與重建 OfflineDB 與 CDC consumer 的時間。
另外,因為 PG 12 ~ PG 15 在之間也做了不少效能的提升,因此之後也會繼續做升級的計畫,期待之後有機會再跟各位分享相關的內容。

最後個人心得:如果沒有同事及朋友的指導,這次的升級過程是不會那麼順利的,在此感謝大家,讓我進步許多!