本文整理自 IvorySQL 2025 生態大會暨 PostgreSQL 高峯論壇的演講分享,演講嘉賓:劉智龍。
引言
在數據庫運維過程中,停庫與起庫是繞不開的核心環節。然而,在複雜的生產環境中,這些操作並非總能順利完成。以下結合實際案例,對 PostgreSQL 在停庫和起庫過程中可能遇到的典型問題進行技術剖析。
WALsender、archiver 如何優雅阻止停庫
WALsender 阻止停庫
在邏輯複製場景下,WALsender 進程會阻止數據庫停庫,此時僅保留 checkpointer 和 WALsender 等關鍵進程,控制文件狀態顯示為 in production,表明數據庫仍處於運行中。
WALsender 阻止停庫時,數據庫的停庫狀態:
此時的控制文件:
在停庫過程中,WALsender 進程卡在WalSndWaitStopping函數,導致 checkpointer 也被阻塞在該函數中,數據庫因此停在“半停庫”狀態。若此時強行執行 kill-9,將造成數據庫以非一致性方式停庫。
怎麼優雅的停庫
在邏輯複製場景下,WALsender 進程可能阻止數據庫停庫。常見處理方案有兩種:
方案一:關閉下游進程
- 執行
ALTER SUBSCRIPTION sub_lzl DISABLE;---需提前找到所有關聯的下游 PG 庫。 - 停止同步工具---同步工具可能無法及時維護。
方案二:發送 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 讓其最後一次歸檔並退出:
PM 進程的退出依賴於歸檔進程:
在模擬歸檔場景時可以看到,停庫狀態與 WALsender 阻止停庫的情況並不相同:此時存在 archiver 進程,但 checkpointer 進程並未出現,表現出不同的停庫特徵:
可能的原因:歸檔延遲較多,歸檔盤寫入較慢。
不可能的原因:歸檔失敗,NUM_ARCHIVE_RETRIES限制。
此時暴力停庫是否有問題?
當只有 archiver 進程阻止停庫時,checkpointer已完成停止操作,shutdown checkpoint 條目已寫入 WAL,controldata 狀態為 shut down,表明數據庫已實現一致性停庫。此時,即使 archiver 仍在運行,執行 kill-9 也不會影響數據庫本身的完整性。
怎麼優雅的停庫
在數據庫停庫時,可以通過以下操作實現更安全可控的關閉流程:
- 鎖定邏輯同步用户。
- 執行
pg_terminate_backend($logical_WALsender);。 - 臨時關閉歸檔(可選,將 archive_command 置空)。
- 手動執行 checkpoint。
- 執行 stop fast。
- 如果僅有 archiver 阻止停庫,可以考慮暴力停庫。
PostgreSQL 停庫邏輯
停庫的信號機制
PostgreSQL 的停庫依賴操作系統的信號機制。在 Linux 中,進程間可以通過信號進行通信,系統也定義了多種信號來控制進程的行為。
PG 常用的信號包括:
pg_ctl 通過信號管理停庫方式
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,分別用於處理關閉邏輯與子進程回收。
子進程註冊的信號(以 checkpointer 為例)
每個子進程都會註冊信號,整體邏輯類似,僅根據職責略有差異。以 checkpointer 為例,它不屏蔽 SIGTERM,實際停止時使用 SIGUSR2 發出請求後再退出。
reaper 函數
reaper 是進程回收函數,子進程退出後會發送 pm SIGCHLD 信號,pm 通過 reaper 函數清理進程。 如 backend、startup、checkpointer 等進程都有自己的清理流程。
以 checkpointer 進程退出,reaper 回收為例:退出時會先判斷歸檔進程是否存在,並向歸檔進程和 WALsender 發送 SIGUSR2,最後調用 PostmasterStateMachine() 完成狀態轉換。
pmdie 函數
pmdie 函數用於處理不同的 postmaster signals,包括子進程給 pm 發送的 SIGCHILD 和 pg_ctl 發送的停庫信號。pm 信號處理主體邏輯是根據 signal 轉換 pmState 狀態機狀態,並進入狀態機 PostmasterStateMachine 處理。
PostmasterStateMachine 函數
PostmasterStateMachine 函數是處理 pm 退出的主體函數,主要是處理 pm 在不同停庫狀態下做什麼。比如一般進程退出、WALsender、歸檔進程退出、異常退出等等狀態。
正常停庫時,除 checkpointer、archiver、stats 和 syslogger 外的子進程會收到 SIGTERM 並退出,然後向 checkpointer 發送 SIGUSR2 ,進入 SHUTDOWN 狀態。
SHUTDOWN_2 狀態在 checkpointer 退出時設置,等待 WALsender 和 archiver 完全退出,保證數據庫一致性停庫。
checkpointer 和 WALsender 的退出
checkpointer 進程在 pm 退出時會被喚醒做最後一次 shutdown checkpoint 等工作,但是創建 shutdown checkpoint 需要等 WALsender 全部進入退出狀態。
WalSndWaitStopping會等待 WALsender 退出,如果不退出是死循環等待。
停庫邏輯
Spill 阻止起庫和加速起庫
Spill 阻止起庫
現象:數據庫啓動緩慢,startup 進程在讀取 Spill 文件,文件名在變化。查看 Spill 文件也很慢,ls-l 最後跑出來 1000 萬個文件 Spill 文件。
- 數據庫雖然在啓動,但整個過程非常耗時。
- 1000 萬個 Spill 文件嚴重影響 Linux 系統性能,任何操作都很費勁。
問題關注點:
- 1000 萬 Spill 文件是如何產生的?
- 當數據庫起庫受阻時,應採取什麼措施?
Spill 怎麼來的,怎麼定位到 WAL 文件
Spill 文件主要由 WALsender 在事務溢出時生成。當定位到 Spill 文件後,可以寫入 ReorderBuffer 存放在 replslot 目錄下。
這些文件名中包含事務的相關信息,例如 xid。通過讀取文件名中的 xid,就能追溯到對應事務的來源。
在定位某個 Spill 文件對應的 WAL 位置時,如果單純依賴 xid 去過濾,面對上千萬個事務顯然效率極低。更可行的辦法是結合 LSN 信息來定位。
Spill 怎麼來的
Spill 文件生成規則如下:
- 同一個事務 id,如果跨 WAL 就會產生多個 Spill。如:一個不含子事務的大事務跨越 3 個 WAL,就會對應 3 個 Spill 文件。
- 不同的事務 id 對應不同的 Spill。如:1000w 個子事務對應 1000w 個 Spill。
例如:Spill 文件名結構xid-407989064-lsn-42D1E-20000000.Spill。
Spill 溢出邏輯的版本差異:
- PG12 及以前是固定 4096 條 changes 後觸發 Spill。
- PG13 新增
logical_decoding_work_mem參數,可通過調整內存大小來降低 Spill 發生概率。 - PG14 及以後支持流式複製(Streaming),但仍需滿足特定條件才能觸發,因此並非完全避免 Spill。
- PG17 新增
debug_logical_replication_streaming參數以強制觸發流式傳輸。
如何加速起庫
庫起不來怎麼辦?
數據庫啓動時的核心進程是 startup 進程,無論如何都會在起庫時拉起。
當遇到非一致性停庫時,startup 需要執行更多邏輯,其一便是 sync data 目錄:根據控制文件狀態觸發對整個 data 目錄的持久化操作,以確保在數據庫重新運行前,所有數據文件處於一致狀態。
因為控制文件狀態顯示為非正常停庫,系統會進入 if 分支並調用 SyncDataDirectory() 執行 fsync 持久化操作,以確保在數據庫重新運行前,data 目錄已被完整持久化。
startup 除了執行 fsync data 外,還會處理與 Spill 相關的邏輯,其中關鍵一步是啓動 ReorderBuffer。由於 Spill 文件對應的事務尚未完成,startup 會清理所有 replslot 目錄下的 Spill 文件,確保複製槽狀態恢復正常。
起庫邏輯小結:
- 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 測試起庫時間:
總結
數據庫的停庫與起庫是運維中的關鍵環節,也是最容易遇到挑戰的地方。通過本次分享,我們看到,無論是 WALsender、archiver 的阻止,還是大量 Spill 文件帶來的起庫延遲,都有對應的分析與解決思路。理解 PostgreSQL 的信號機制、啓動流程以及複製槽管理,不僅能夠幫助我們優雅停庫、快速起庫,也能在異常狀態下保持數據一致性和系統穩定性。在實際運維中,將理論與操作結合,才能真正做到既安全又高效。