Watch at Ease看見世界,讀懂生活。

科技 · 實用指南

PostgreSQL 顯示「資料庫正被其他使用者存取」?強制刪除前先建立工作階段台帳

不要急著讓 SQLSTATE 55006 消失。先以唯讀方式記錄工作階段、預備交易、邏輯複寫物件與可見性限制,再建立第二份全新快照找出重新連線競態,最後才把證據交給真正的擁有者。

原創概念插畫:六步唯讀 PostgreSQL 擁有權臺帳涵蓋身分、可見性、工作階段、持久阻塞、全新快照與決策界線。
原創概念插畫:只有叢集身分、可見性與兩次快照都完整時,零筆結果才有意義。 Watch at Ease

「資料庫正被其他使用者存取」不是根因診斷,而是 PostgreSQL 在需要獨佔存取的操作前,發現目標資料庫仍有連線或叢集物件。第一個工作不是把計數歸零,而是保留足夠證據,解釋每一個看得到的擁有者,以及每一個看不到的邊界。

連線池可能在應用程式物件釋放後重新建立工作階段;管理工具也可能正是你誤關的連線。官方文件還明確列出 FORCE 無法清除的三類阻礙:預備交易、使用中的邏輯複寫插槽,以及訂閱。若使用託管服務,殘留後端甚至可能屬於你無權終止的供應商角色。

本指南停在任何變更之前。它以一次唯讀交易記錄伺服器與資料庫身分、工作階段歸屬線索、預備交易、邏輯插槽、訂閱及角色可見性,再用第二個全新交易揭露重新連線競態。產出是交給資料庫擁有者或供應商變更流程的證據包,不是取消使用者、變更權限、重新啟動、重新命名或刪除資料庫的腳本。

1. 先保留拒絕現場,再考慮清除

記錄完整 SQLSTATE、目標資料庫、嘗試執行的操作、UTC 時間、相關部署或還原步驟,以及 PostgreSQL 回報的連線數。不要立刻把終止查詢貼進同一份事故紀錄並宣稱證據完整;一旦連線池重新連線或後端消失,歸屬軌跡就會改變。

本次研究的 Stack Overflow 案例在 revoke 與 terminate 嘗試後仍回報 24 個工作階段;另一個 DBA.StackExchange 案例則顯示腳本終止了自己連向維護資料庫的工作階段。兩者屬於不同故障族群,卻同樣證明「全部終止再重試」會遺失解釋阻礙所需的脈絡。

若要求屬於還原或環境替換,請另外保留備份證據與變更單。本台帳不能證明備份可還原、資料庫可丟棄,或應用程式擁有者已批准停機。

2. 證明收集器連到哪一台伺服器與哪一個資料庫

建立新的維護工作階段,連到 postgres 或另一個不是目標的資料庫。PostgreSQL 的 DROP DATABASE 文件要求這種分離;相同習慣也能避免證據收集器成為它正在解釋的目標工作階段之一。

記錄 current_database()、current_user、session_user、pg_backend_pid()、inet_server_addr()、inet_server_port()、server_version、server_version_num 與 pg_is_in_recovery()。將端點與應用程式設定及變更單比對;若代理或託管端點使你無法證明確切叢集,請標記「叢集身分未決」,不要因主機名稱看起來熟悉就推定正確。

第一個查詢也應判斷 current_database() 是否等於目標資料庫。若為真,立即停止並改連另一個維護資料庫。若 pg_database 中沒有目標,亦應停止:拼字錯誤、連錯叢集或變更已經完成,都是不同事件。

3. 建立有邊界的唯讀快照

每一次收集使用 BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY。記錄目標資料庫 OID、擁有者、datallowconn、連線上限、收集角色是否具有 CONNECT,並記錄 pg_read_all_stats 成員資格與 track_activities 設定。

PostgreSQL 說明,交易中首次要求目前查詢資訊時會擷取快照,之後在該交易內保持一致。這有利於串接多個檢視,但也表示在同一交易內等待不會產生稍後需要的全新比較。提交第一輪唯讀交易,再開啟新交易建立第二份快照。

一般角色能看見其他工作階段存在及其通用屬性,但其他使用者工作階段的許多欄位可能是空值。收集器若沒有 pg_read_all_stats,台帳就必須標記「可見性不完整」。空值不是空白證據,也永遠不能授權變更型命令。

4. 盤點工作階段,但不要匯出 SQL 文字

對 datname 等於目標的資料列,收集 pid、leader_pid、usename、application_name、client_addr、client_port、backend_type、state、backend_start、xact_start、query_start、state_change、wait_event_type 與 wait_event。依後端類型、角色、應用程式、用戶端與 PID 排序,使重複的連線池成員容易辨識。

預設不要匯出 query 欄位。查詢文字可能包含客戶資料、常值或認證資訊,而判斷連線擁有者並不需要它。台帳也應標示目標資料列是否包含收集器 PID;若收集器正確連到維護資料庫,目標資料列不應包含它。

application_name 與 client_addr 只是線索,不是最終身分。空白應用程式名稱仍可能是真實用戶端;client_addr 為空可能表示 Unix socket 或內部程序。只有角色、端點、部署脈絡與時間一致時,才把資料列歸給某個應用程式擁有者。

5. 檢查工作階段清單會漏掉的阻礙類型

查詢目標資料庫的 pg_prepared_xacts,記錄交易 ID、GID、準備時間、擁有者與資料庫。預備交易是持久的兩階段狀態,不是閒置用戶端工作階段。本指南不提交或回復它;請把 GID 交給擁有它的應用程式或交易協調器。

查詢資料庫為目標的邏輯 pg_replication_slots,記錄插槽名稱、外掛、類型、temporary、active、active_pid、保留的交易識別碼與 LSN 位置。即使 active 為 false,插槽仍是有擁有者的複寫物件,不能只因未啟用就刪除。

透過目標資料庫 OID 查詢 pg_subscription,只記錄訂閱名稱、擁有者、啟用狀態、插槽名稱與 publication 名稱。不要選取 subconninfo;PostgreSQL 警告其中可能含純文字密碼,並限制一般使用者存取該欄位。訂閱屬於複寫設計,不屬於一般清除清單。

這些檢查不是裝飾。PostgreSQL 明確指出 FORCE 不會清除預備交易、使用中的邏輯複寫插槽或目標資料庫中的訂閱。只要任何一類出現資料列,就應停止並取得所屬系統的處理計畫。

6. 用第二份全新快照揭露重新連線競態

提交第一輪唯讀交易,並以 UTC 時間儲存輸出。依應用程式連線池週期選擇短暫間隔後,開啟新的唯讀交易並重跑相同查詢。不可把仍快取於第一個交易中的結果當成第二份快照。

比較穩定鍵,而非資料列順序:工作階段使用 PID 加 backend_start,預備交易使用 GID,複寫插槽使用插槽名稱,訂閱使用 OID 或名稱。相同角色、應用程式與用戶端家族出現新 PID,表示連線池或背景工作程式可能正在重新連線。若供應商擁有的角色間歇出現,這是託管服務升級處理事件,不是等待空檔搶著刪除資料庫的理由。

若兩份快照不同,正確結果是「重新連線競態」,並應保留兩份證據。當擁有者持續補回工作階段時,某一瞬間計數為零不能證明已經靜默。

7. 分類擁有者分支,並在邊界停止

應用程式連線池:多個用戶端後端共用角色、應用程式與端點,或消失後以新 PID 返回;把台帳交給服務擁有者。管理工具:用戶端名稱或本機 socket 指向 pgAdmin、psql、IDE 或移轉工具;透過擁有者的正常流程關閉。背景工作程式:由 backend_type 或應用程式脈絡辨識;升級交由元件擁有者,而不是當成匿名使用者。

預備交易:pg_prepared_xacts 有目標資料列,決策屬於交易協調器。複寫物件:邏輯插槽或訂閱指向目標,生命週期由複寫擁有者決定。託管服務:可見供應商或超級使用者角色,但客戶角色無法處理;請帶著兩份快照向供應商開立支援案件。

可見性不完整:收集器缺少 pg_read_all_stats、相關欄位遭遮蔽、伺服器端點不明,或必要目錄查詢被拒絕。此分支一律停止。請授權的資料庫管理員或供應商重跑同一份台帳;不得從權限失敗推論物件不存在。

8. 儲存已去識別的證據包,而非清除腳本

保留事故時間、目標名稱與 OID、維護資料庫、端點與版本、收集角色與 PID、pg_read_all_stats 結果、track_activities 結果、兩份快照、分類、未決欄位,以及負責下一個決策的擁有者或團隊。支援流程不需要用戶端位址時,請將其去識別。

不要包含完整 SQL 文字、密碼、連線字串或 subconninfo。證據包若要在團隊間移動,請計算雜湊並記錄收集者。產物必須讓另一位授權操作員分辨「沒有可見工作階段」、「沒有阻礙」與「收集器看不到阻礙」。

離開條件刻意嚴格:每個可見工作階段與叢集物件都有擁有者;兩份快照穩定;收集器具備足夠可見性;目標、端點與維護資料庫已證實;備份與停機決策則存在另一份核准變更。在此之前,結果都是「仍需證據」,不是終止、撤權、重新啟動、重新命名或刪除的許可。

9. 將內嵌唯讀工作表完整執行兩次

把 target_db 設定成正確資料庫名稱,並從 postgres 或另一個維護資料庫執行以下工作表。腳本以 UTF-8 計為 2,365 位元組,SHA-256 為 e787aced94fd049e0e6335d61a51bca6ef0d659f88264e0e2153276f273fc196。它啟動 repeatable-read 唯讀交易,取得身分、資料庫、工作階段、預備交易、邏輯複寫插槽及訂閱證據,不選取原始查詢本文或訂閱連線字串。

```sql \set ON_ERROR_STOP on \pset pager off \if :{?target_db} \else \echo 'Set target_db before running, for example: \\set target_db my_database' \quit 2 \endif BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY; SELECT clock_timestamp() AT TIME ZONE 'UTC' AS captured_at_utc, current_database() AS maintenance_database, :'target_db' AS target_database, current_database() = :'target_db' AS collector_is_on_target, current_user, session_user, pg_backend_pid() AS collector_pid, inet_server_addr()::text AS server_address, inet_server_port() AS server_port, current_setting('server_version') AS server_version, current_setting('server_version_num') AS server_version_num, pg_is_in_recovery() AS server_in_recovery, current_setting('track_activities') AS track_activities, pg_has_role(current_user, 'pg_read_all_stats', 'member') AS has_pg_read_all_stats; SELECT d.oid AS target_oid, d.datname AS target_database, pg_get_userbyid(d.datdba) AS database_owner, d.datallowconn AS connections_allowed, d.datconnlimit AS connection_limit, has_database_privilege(current_user, d.oid, 'CONNECT') AS collector_has_connect FROM pg_database AS d WHERE d.datname = :'target_db'; SELECT a.pid, a.leader_pid, a.usename, a.application_name, a.client_addr::text AS client_address, a.client_port, a.backend_type, a.state, a.backend_start, a.xact_start, a.query_start, a.state_change, a.wait_event_type, a.wait_event FROM pg_stat_activity AS a WHERE a.datname = :'target_db' ORDER BY a.backend_type, a.usename, a.application_name, a.client_addr, a.pid; SELECT x.transaction, x.gid, x.prepared, x.owner, x.database FROM pg_prepared_xacts AS x WHERE x.database = :'target_db' ORDER BY x.prepared, x.gid; SELECT s.slot_name, s.plugin, s.slot_type, s.database, s.temporary, s.active, s.active_pid, s.xmin, s.catalog_xmin, s.restart_lsn, s.confirmed_flush_lsn FROM pg_replication_slots AS s WHERE s.database = :'target_db' ORDER BY s.slot_name; SELECT s.oid AS subscription_oid, s.subname AS subscription_name, pg_get_userbyid(s.subowner) AS subscription_owner, s.subenabled AS enabled, s.subslotname AS slot_name, s.subpublications AS publications FROM pg_subscription AS s JOIN pg_database AS d ON d.oid = s.subdbid WHERE d.datname = :'target_db' ORDER BY s.subname; COMMIT; ```

相同位元組已通過靜態稽核:唯讀交易界線、統一目標參數、五類系統目錄涵蓋、沒有原始 query 本文與 subconninfo,也沒有改變狀態的 SQL 敘述。靜態稽核只證明文字包含哪些內容,不等同於 PostgreSQL 實機整合測試。

投入營運前,應由獲授權的管理員在相同主要版本與託管限制的非生產叢集執行工作表、檢查每個結果集,並把腳本雜湊與第一次快照一併保存。提交交易、等待一個連線池週期後,再以完全相同位元組執行第二次快照。若系統目錄遭拒、欄位為空或版本不符,仍是未決界線;不可移除失敗檢查來讓臺帳看似完整。

原創概念插畫:把 PostgreSQL 工作階段與阻塞觀察映射到最小安全交接與停止條件。
原創概念插畫:每個阻塞都應指向擁有者或停止條件,而非自動清理命令。 Watch at Ease