动态

详情 返回 返回

PostgreSQL的邏輯複製spill溢出案例和啓停庫邏輯 - 动态 详情

本文整理自 IvorySQL 2025 生態大會暨 PostgreSQL 高峯論壇的演講分享,演講嘉賓:劉智龍。

引言

在數據庫運維過程中,停庫與起庫是繞不開的核心環節。然而,在複雜的生產環境中,這些操作並非總能順利完成。以下結合實際案例,對 PostgreSQL 在停庫和起庫過程中可能遇到的典型問題進行技術剖析。

WALsender、archiver 如何優雅阻止停庫

WALsender 阻止停庫

在邏輯複製場景下,WALsender 進程會阻止數據庫停庫,此時僅保留 checkpointer 和 WALsender 等關鍵進程,控制文件狀態顯示為 in production,表明數據庫仍處於運行中。

WALsender 阻止停庫時,數據庫的停庫狀態:

圖片1.jpg

此時的控制文件:

圖片2.jpg

在停庫過程中,WALsender 進程卡在WalSndWaitStopping函數,導致 checkpointer 也被阻塞在該函數中,數據庫因此停在“半停庫”狀態。若此時強行執行 kill-9,將造成數據庫以非一致性方式停庫。

圖片3.jpg

怎麼優雅的停庫

在邏輯複製場景下,WALsender 進程可能阻止數據庫停庫。常見處理方案有兩種:

方案一:關閉下游進程

圖片4.jpg

  1. 執行 ALTER SUBSCRIPTION sub_lzl DISABLE;---需提前找到所有關聯的下游 PG 庫。
  2. 停止同步工具---同步工具可能無法及時維護。

方案二:發送 SIGTERM 給 WALsender

可直接向 WALsender 進程發送 SIGTERM 信號,使其正常退出。

running 狀態:

select pg_terminate_backend($WALsender_pid)

「pg_terminate_backend() 本質上就是在發送 SIGTERM 給子進程」

停一半的狀態:

kill -SIGTERM $WALsender_pid

kill -15 $WALsender_pid

kill $WALsender_pid

archiver 阻止停庫

除了 WALsender 外,常見的還有 archiver 進程也可能阻止停庫。reaper checkpointer 會發送 SIGUSR2 給 archiver 讓其最後一次歸檔並退出:

圖片5.jpg

PM 進程的退出依賴於歸檔進程:

圖片6.jpg

在模擬歸檔場景時可以看到,停庫狀態與 WALsender 阻止停庫的情況並不相同:此時存在 archiver 進程,但 checkpointer 進程並未出現,表現出不同的停庫特徵:

圖片7.jpg

可能的原因:歸檔延遲較多,歸檔盤寫入較慢。

不可能的原因:歸檔失敗,NUM_ARCHIVE_RETRIES限制。

此時暴力停庫是否有問題?

當只有 archiver 進程阻止停庫時,checkpointer已完成停止操作,shutdown checkpoint 條目已寫入 WAL,controldata 狀態為 shut down,表明數據庫已實現一致性停庫。此時,即使 archiver 仍在運行,執行 kill-9 也不會影響數據庫本身的完整性。

怎麼優雅的停庫

在數據庫停庫時,可以通過以下操作實現更安全可控的關閉流程:

  1. 鎖定邏輯同步用户。
  2. 執行pg_terminate_backend($logical_WALsender);
  3. 臨時關閉歸檔(可選,將 archive_command 置空)。
  4. 手動執行 checkpoint。
  5. 執行 stop fast。
  6. 如果僅有 archiver 阻止停庫,可以考慮暴力停庫。

PostgreSQL 停庫邏輯

停庫的信號機制

PostgreSQL 的停庫依賴操作系統的信號機制。在 Linux 中,進程間可以通過信號進行通信,系統也定義了多種信號來控制進程的行為。

PG 常用的信號包括:

圖片8.jpg

pg_ctl 通過信號管理停庫方式

圖片9.jpg

pg_ctl 通過發送不同的信號來控制PostgreSQL的停庫方式。其中,kill -9(SIGKILL)pg_ctl stop -m immediate是不同的:

  • pg_ctl 不支持直接發送 SIGKILL。雖然可以手動向 PM 進程發送 SIGKILL ,但這種方式不推薦,因為 PM 在收到 SIGKILL 時不會對子進程、共享內存或信號做任何清理工作。
  • SIGQUIT 停庫更安全。當 PM 收到 SIGQUIT 時,會觸發兜底邏輯,對子進程發送 SIGKILL,並做必要清理,從而基本保證數據庫能完整停下來。

PM 註冊的信號

接收到該信號後,會調用兩個關鍵函數:pmdie 和 reaper,分別用於處理關閉邏輯與子進程回收。

圖片10.jpg

子進程註冊的信號(以 checkpointer 為例)

每個子進程都會註冊信號,整體邏輯類似,僅根據職責略有差異。以 checkpointer 為例,它不屏蔽 SIGTERM,實際停止時使用 SIGUSR2 發出請求後再退出。

圖片11.jpg

reaper 函數

reaper 是進程回收函數,子進程退出後會發送 pm SIGCHLD 信號,pm 通過 reaper 函數清理進程。 如 backend、startup、checkpointer 等進程都有自己的清理流程。

以 checkpointer 進程退出,reaper 回收為例:退出時會先判斷歸檔進程是否存在,並向歸檔進程和 WALsender 發送 SIGUSR2,最後調用 PostmasterStateMachine() 完成狀態轉換。

圖片12.jpg

pmdie 函數

pmdie 函數用於處理不同的 postmaster signals,包括子進程給 pm 發送的 SIGCHILD 和 pg_ctl 發送的停庫信號。pm 信號處理主體邏輯是根據 signal 轉換 pmState 狀態機狀態,並進入狀態機 PostmasterStateMachine 處理。

圖片13.jpg

PostmasterStateMachine 函數

PostmasterStateMachine 函數是處理 pm 退出的主體函數,主要是處理 pm 在不同停庫狀態下做什麼。比如一般進程退出、WALsender、歸檔進程退出、異常退出等等狀態。

正常停庫時,除 checkpointer、archiver、stats 和 syslogger 外的子進程會收到 SIGTERM 並退出,然後向 checkpointer 發送 SIGUSR2 ,進入 SHUTDOWN 狀態。

SHUTDOWN_2 狀態在 checkpointer 退出時設置,等待 WALsender 和 archiver 完全退出,保證數據庫一致性停庫。

圖片14.jpg

checkpointer 和 WALsender 的退出

checkpointer 進程在 pm 退出時會被喚醒做最後一次 shutdown checkpoint 等工作,但是創建 shutdown checkpoint 需要等 WALsender 全部進入退出狀態。

圖片15.jpg

WalSndWaitStopping會等待 WALsender 退出,如果不退出是死循環等待。

圖片16.jpg

停庫邏輯

圖片17.jpg

Spill 阻止起庫和加速起庫

Spill 阻止起庫

現象:數據庫啓動緩慢,startup 進程在讀取 Spill 文件,文件名在變化。查看 Spill 文件也很慢,ls-l 最後跑出來 1000 萬個文件 Spill 文件。

  • 數據庫雖然在啓動,但整個過程非常耗時。
  • 1000 萬個 Spill 文件嚴重影響 Linux 系統性能,任何操作都很費勁。

問題關注點:

  1. 1000 萬 Spill 文件是如何產生的?
  2. 當數據庫起庫受阻時,應採取什麼措施?

Spill 怎麼來的,怎麼定位到 WAL 文件

Spill 文件主要由 WALsender 在事務溢出時生成。當定位到 Spill 文件後,可以寫入 ReorderBuffer 存放在 replslot 目錄下。

這些文件名中包含事務的相關信息,例如 xid。通過讀取文件名中的 xid,就能追溯到對應事務的來源。

圖片18.jpg

在定位某個 Spill 文件對應的 WAL 位置時,如果單純依賴 xid 去過濾,面對上千萬個事務顯然效率極低。更可行的辦法是結合 LSN 信息來定位。

圖片19.jpg

Spill 怎麼來的

Spill 文件生成規則如下:

  • 同一個事務 id,如果跨 WAL 就會產生多個 Spill。如:一個不含子事務的大事務跨越 3 個 WAL,就會對應 3 個 Spill 文件。
  • 不同的事務 id 對應不同的 Spill。如:1000w 個子事務對應 1000w 個 Spill。

例如:Spill 文件名結構xid-407989064-lsn-42D1E-20000000.Spill

圖片20.jpg

Spill 溢出邏輯的版本差異:

  • PG12 及以前是固定 4096 條 changes 後觸發 Spill。
  • PG13 新增logical_decoding_work_mem參數,可通過調整內存大小來降低 Spill 發生概率。
  • PG14 及以後支持流式複製(Streaming),但仍需滿足特定條件才能觸發,因此並非完全避免 Spill。
  • PG17 新增debug_logical_replication_streaming參數以強制觸發流式傳輸。

如何加速起庫

庫起不來怎麼辦?

數據庫啓動時的核心進程是 startup 進程,無論如何都會在起庫時拉起。

當遇到非一致性停庫時,startup 需要執行更多邏輯,其一便是 sync data 目錄:根據控制文件狀態觸發對整個 data 目錄的持久化操作,以確保在數據庫重新運行前,所有數據文件處於一致狀態。

圖片21.jpg

因為控制文件狀態顯示為非正常停庫,系統會進入 if 分支並調用 SyncDataDirectory() 執行 fsync 持久化操作,以確保在數據庫重新運行前,data 目錄已被完整持久化。

startup 除了執行 fsync data 外,還會處理與 Spill 相關的邏輯,其中關鍵一步是啓動 ReorderBuffer。由於 Spill 文件對應的事務尚未完成,startup 會清理所有 replslot 目錄下的 Spill 文件,確保複製槽狀態恢復正常。

圖片22.jpg

起庫邏輯小結:

  • PostgreSQL 在啓動時會拉起一個專門的輔助進程 startup,不同於常見的子進程(如 WALwriter、checkpointer 等),它是起庫過程中必定存在的核心進程,負責多項關鍵操作。
  • StartupXLOG 在起庫時必然會被調用,無論數據庫是否一致性停庫。
  • 只有非正常停庫狀態下,才會觸發 SyncDataDirectory
  • SyncDataDirectory 會 fsync 持久化所有 data 文件,並查看所有 data 文件的 stat 信息。
  • fsync 用於保證庫啓動前數據文件一致性,stat 則用於驗證文件的完整性和可讀性(在 startup 進程啓動前只驗證過 datadir 目錄可讀性)。
  • 無論停庫狀態如何,StartupReorderBuffer 都會被調用,用於清理所有複製槽中的 Spill 文件。

思考如何去加速起庫?

1000w 個 Spill 刪除起來肯定是很慢的,直接 mv 目錄的話就非常快。但是直接 mv 需要注意 mv 後的名稱和 state 文件,以及需要知道 mv 到底跳過了哪一個源碼步驟。

由於是異常停庫,startup 進程會執行SyncDataDirectoryfsync和 stat 所有 data 文件,這一點是比較難繞過的。SyncDataDirectory 做完以後,才開始處理複製槽。處理複製槽時會調用StartupReorderBuffer()->ReorderBufferCleanupSerializedTXNs全量清理 Spill 文件。

在進入清理前,會調用ReplicationSlotValidateName校驗複製槽名稱的有效性,我們可以在ReplicationSlotValidateName上做文章,以騙過 startup 進程跳過ReorderBufferCleanupSerializedTXNs的過程。

當 Spill 文件數量達到千萬級時,直接逐個刪除會極其緩慢,而通過 mv 整個目錄的方式則非常迅速。但此操作需注意:

  • 目錄改名後需保持與 state 文件 的一致性。
  • 必須清楚 mv 跳過了源碼中的哪些步驟,避免後續邏輯異常。

由於是異常停庫,startup 進程會強制執行 SyncDataDirectory,對所有 data 文件進行 fsync 和 stat,這一環節無法繞過。SyncDataDirectory完成後,startup 才會處理複製槽,並調用:StartupReorderBuffer()->ReorderBufferCleanupSerializedTXNs
以全量清理 Spill 文件。

在進入清理流程前,會先調用ReplicationSlotValidateName校驗複製槽名稱的有效性。

因此,可以在該校驗邏輯上“做文章”,讓 startup 進程跳過ReorderBufferCleanupSerializedTXNs,從而避免耗時的 Spill 文件全量清理。

複製槽校驗與清理

ReplicationSlotValidateName(const char *name, int elevel)
{...
        if (!((*cp >= 'a' && *cp <= 'z')
              || (*cp >= '0' && *cp <= '9')
              || (*cp == '_')))
...
}
  • 有效 slot name 只包含 a-z;0-9;\_。rename 時建議加個點.。
  • 建議slotname.bak,slotname.20241215等。
  • 不建議slotnamebackup,slotname20241215,slotname_bak等等。
  • 不建議.tmp 後綴,slotname 有.tmp 後綴有特殊含義。

最後 rename 後,要創建目錄和拷貝 state,不然啓動的 slot 會表現的很反常(比如重複的 slotname、自動生產一個 slotname、刪不到 slot、下游起不來鏈路等等)。

cd pg_replslot

mv slotname slotname.bak

mkdir slotname

cp slotname.bak/state slotname/

偽造 2000w 個 Spill 測試起庫時間:

圖片23.jpg

總結

數據庫的停庫與起庫是運維中的關鍵環節,也是最容易遇到挑戰的地方。通過本次分享,我們看到,無論是 WALsender、archiver 的阻止,還是大量 Spill 文件帶來的起庫延遲,都有對應的分析與解決思路。理解 PostgreSQL 的信號機制、啓動流程以及複製槽管理,不僅能夠幫助我們優雅停庫、快速起庫,也能在異常狀態下保持數據一致性和系統穩定性。在實際運維中,將理論與操作結合,才能真正做到既安全又高效。

user avatar wu_cat 头像 pannideniupai 头像 winnn 头像
点赞 3 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.