本文是 IvorySQL 2025 生態大會暨 PostgreSQL 高峰論壇上的演講內容,作者:NKYoung。
6 月底濟南召開的 HOW2025 IvorySQL 生態大會上,我在內核論壇分享了“提升 vacuum 時間觀測能力”的主題,提出了新增統計信息的方法,增強 vacuum 執行時長的觀測能力。同時也給大家分享了一些 DBA 上手內核的感悟。
vacuum 在 PG 中是一個無法繞開的話題,甚至有些用戶因為 vacuum 的特性而放棄 PG。老楊認為只要做好 vacuum 的觀測以及優化,這都不是事。
監控 vacuum 的重要性
我們先來看vacuum 的主要工作:
- 移除死元組
- 移除每一頁中的死元組,并對每一頁內的元組進行碎片整理。
- 移除指向死元組的索引元組。
- 凍結舊的事務標識(txid)
- 如有必要,凍結舊元組的事務標識(txid)。
- 更新與凍結事務標識相關的系統視圖(pg_database 與 pg_class)。
- 如果可能,移除非必需的提交日志(clog)。
- 其他
- 更新已處理表的空閑空間映射(FSM)和可見性映射(VM)。
- 更新一些統計信息(pg_stat_all_tables 等)
vacuum 偽代碼:
// Phase 1: initializing(1) FOR each table
(2) Acquire a ShareUpdateExclusiveLock lock for the target table/* The first block */// Phase 2: Scan Heap
(3) Scan all pages to get all dead tuples, and freeze old tuples if necessary// Phase 3: Vacuuming Indexes
(4) Remove the index tuples that point to the respective dead tuples if exists/* The second block */// Phase 4: Vacuuming Heap
(5) FOR each page of the table
(6) Remove the dead tuples, and Reallocate the live tuples in the page
(7) Update FSM and VMEND FOR/* The third block */// Phase 5: Cleaning up indexes
(8) Clean up indexes// Phase 6: Truncating heap
(9) Truncate the last page ifpossible
(10) Update both the statistics and system catalogs of the target tableRelease the ShareUpdateExclusiveLock lockEND FOR/* Post-processing */// Phase 7: Final Cleaning
(11) Update statistics and system catalogs
(12) Remove both unnecessary files and pages of the clogif possible
如果 vacuum 不合理,可能會導致以下問題:
- 空間“膨脹”
- 統計信息偏差,SQL 性能劣化
- transaction ID wraparound or multixact ID wraparound
因此對 vacuum 的監控,是 PG 運維環節的重中之重。
監控執行時長的痛點
目前監控 vacuum 的手段:
- 查看統計信息視圖:pg_stat_all(user)_tables,獲取上次 vacuum 完成時間,vacuum 次數等
- 查看進度報告視圖:pg_stat_progress_vacuum(analyze)
- 手動執行 vacuum command 使用 verbose option
我們也可以根據現有的統計信息視圖,創建自定義視圖來觀測 vacuum,具體可以參考燦神的書中對應的章節。
如何監控(auto)vacuum 的時長?
- vacuum:
- 使用 verbose option,這樣命令行就會返回 vacuum 過程信息
- 配置 log_min_duration_statement 參數,超過閾值的 vacuum 會記錄到日志中
- Autovacuum:
- 配置 log_autovacuum_min_duration 參數,超過閾值的 autovacuum 會記錄到日志中。
因此要查看表(auto)vacuum 的時長,幾乎只能在 server log 中查看:
- log 閾值參數取值,需要在是否記錄大部分表 vacuum 和產生的日志條目占用的存儲空間中權衡
- 不是所有人員都有主機權限去訪問日志
- 即使將 server log 做成日志服務,檢索和分析起來也不是很方便
大多時候表的 vacuum 歷史執行時長對于我們精細化 vacuum 調優來說是很必要的。比如結合表的 SQL 性能表現,確定 vacuum 的執行頻率,是否調參加速。
所以,為什么不做到統計信息里呢?老楊一直認為對于數據庫而言,SQL 是提供給外界訪問的最好的 API。這一點 Oracle 就做的很不錯,提供了各種各樣的統計信息視圖,相對豐富全面的統計信息,直接查詢系統視圖就可以定制大部分監控指標,并不是吹噓,實事求是。
當然 PG 的可觀測性也是一直在發展的。如果搭建監控有痛點的話,認準 pigsty。
提升執行時長觀測能力
方案:
PgStatStatTabEntry 結構體新增幾個成員,直觀來說就是對 pg_stat_all_tables 進行改造,新增 last(auto)vacuumduration,last(auto)analyze_duration 字段,記錄上次 vacuum 和 analyze 的時長。
這里特別感謝我的好友神醫(邱文輝),感謝他的幫助,同時我能不能成為貢獻者就看他了。不報什么期望,主打自嗨圖片。
效果:
比如可以精細化統計關鍵表的 Autovacuum 時長,定制監控項來預警。
偽代碼: vacuum/analyze 開始時記錄一個時間戳,結束時記錄一個時間戳。求 duration,并賦值給新增的統計信息。
heap_vacuum_rel:begintime=GetCurrentTimestamp();...do vacuum:...endtime=GetCurrentTimestamp();duration = endtime - begintime;last_vacuum_duration = duration;
do_analyze_rel:begintime=GetCurrentTimestamp();...do analyze:...endtime=GetCurrentTimestamp();duration = endtime - begintime;last_analyze_duration = duration;
代碼:
From caeddbf1ead4078d3dd6af36c0bab27f747409a9 Mon Sep 17 00:00:002001
From: Nickyoung0 <yxbshare@163.com>
Date: Fri, 13 Jun 202516:29:55 +0800
Subject: [PATCH] add the duration for last_vacuum/analyze orlast_autovacuum/autoanalyze---.../src/backend/access/heap/vacuumlazy.c | 38 ++++++++++++++++++-.../src/backend/catalog/system_views.sql | 6 ++-.../src/backend/commands/analyze.c | 38 ++++++++++++++++++-.../src/backend/utils/adt/pgstatfuncs.c | 30 +++++++++++++++.../src/include/catalog/pg_proc.dat | 16 ++++++++postgresql-17.4/src/include/pgstat.h | 4 ++
6 files changed, 127 insertions(+), 5 deletions(-)diff --git a/postgresql-17.4/src/backend/access/heap/vacuumlazy.c b/postgresql-17.4/src/backend/access/heap/vacuumlazy.c
index f2d2598..9c78200 100644
--- a/postgresql-17.4/src/backend/access/heap/vacuumlazy.c
+++ b/postgresql-17.4/src/backend/access/heap/vacuumlazy.c
@@ -59,7 +59,7 @@
#include "utils/memutils.h"
#include "utils/pg_rusage.h"
#include "utils/timestamp.h"
-
+#include "utils/pgstat_internal.h"/** Space/time tradeoff parameters: do these need to be user-tunable?
@@ -305,7 +305,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,new_rel_pages,new_rel_allvisible;PGRUsage ru0;
- TimestampTz starttime = 0;
+ TimestampTz starttime = 0,
+ begintime = 0;
+ long secs;
+ int usecs;
+ int msecs;
+ PgStat_EntryRef *entry_ref;
+ PgStatShared_Relation *shtabentry;
+ PgStat_StatTabEntry *tabentry;PgStat_Counter startreadtime = 0,startwritetime = 0;WalUsage startwalusage = pgWalUsage;
@@ -327,6 +334,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,}}+ begintime = GetCurrentTimestamp();
+pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,RelationGetRelid(rel));@@ -591,6 +600,31 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,vacrel->missed_dead_tuples);pgstat_progress_end_command();+ TimestampDifference(begintime,
+ GetCurrentTimestamp(),
+ &secs, &usecs);
+ msecs = usecs / 1000;
+ /* Add commentMore actions
+ * Store the data in the table's hash table entry.
+ * block acquiring lock for the same reason as pgstat_report_autovac()
+ */
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+ MyDatabaseId, RelationGetRelid(rel), false);
+
+ shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+ tabentry = &shtabentry->stats;
+
+ if (AmAutoVacuumWorkerProcess())
+
+ {
+ snprintf(tabentry->last_autovacuum_duration, 32, "%ld.%03d",
+ secs * 1000 + msecs, usecs % 1000);
+ }
+ else
+ snprintf(tabentry->last_vacuum_duration, 32, "%ld.%03d",
+ secs * 1000 + msecs, usecs % 1000);
+ pgstat_unlock_entry(entry_ref);
+
if (instrument){TimestampTz endtime = GetCurrentTimestamp();
diff --git a/postgresql-17.4/src/backend/catalog/system_views.sql b/postgresql-17.4/src/backend/catalog/system_views.sql
index efb29ad..411609f 100644
--- a/postgresql-17.4/src/backend/catalog/system_views.sql
+++ b/postgresql-17.4/src/backend/catalog/system_views.sql
@@ -695,7 +695,11 @@ CREATE VIEW pg_stat_all_tables ASpg_stat_get_vacuum_count(C.oid) AS vacuum_count,pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,pg_stat_get_analyze_count(C.oid) AS analyze_count,
- pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count
+ pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+ pg_stat_get_last_vacuum_duration(c.oid) AS last_vacuum_duration,
+ pg_stat_get_last_autovacuum_duration(c.oid) AS last_autovacuum_duration,
+ pg_stat_get_last_analyze_duration(c.oid) AS last_analyze_duration,
+ pg_stat_get_last_autoanalyze_duration(c.oid) AS last_autoanalyze_durationFROM pg_class C LEFT JOINpg_index I ON C.oid = I.indrelidLEFT JOIN pg_namespace N ON(N.oid = C.relnamespace)
diff --git a/postgresql-17.4/src/backend/commands/analyze.c b/postgresql-17.4/src/backend/commands/analyze.c
index c590a2a..a98da59 100644
--- a/postgresql-17.4/src/backend/commands/analyze.c
+++ b/postgresql-17.4/src/backend/commands/analyze.c
@@ -57,7 +57,7 @@
#include "utils/spccache.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
-
+#include "utils/pgstat_internal.h"/* Per-index data for ANALYZE */
typedef struct AnlIndexData
@@ -298,7 +298,14 @@ do_analyze_rel(Relation onerel, VacuumParams *params,totaldeadrows;HeapTuple *rows;PGRUsage ru0;
- TimestampTz starttime = 0;
+ TimestampTz starttime = 0,
+ begintime = 0;
+ long secs;
+ int usecs;
+ int msecs;
+ PgStat_EntryRef *entry_ref;
+ PgStatShared_Relation *shtabentry;
+ PgStat_StatTabEntry *tabentry;MemoryContext caller_context;Oid save_userid;
int save_sec_context;
@@ -353,6 +360,8 @@ do_analyze_rel(Relation onerel, VacuumParams *params,starttime = GetCurrentTimestamp();}+ begintime = GetCurrentTimestamp();
+
/** Determine which columns to analyze*
@@ -722,6 +731,31 @@ do_analyze_rel(Relation onerel, VacuumParams *params,/* Done with indexes */vac_close_indexes(nindexes, Irel, NoLock);
+
+ TimestampDifference(begintime,
+ GetCurrentTimestamp(),
+ &secs, &usecs);
+ msecs = usecs / 1000;
+ /* Add commentMore actions
+ * Store the data in the table's hash table entry.
+ * block acquiring lock for the same reason as pgstat_report_autovac()
+ */
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+ MyDatabaseId, RelationGetRelid(onerel), false);
+
+ shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+ tabentry = &shtabentry->stats;
+
+ if (AmAutoVacuumWorkerProcess())
+
+ {
+ snprintf(tabentry->last_autoanalyze_duration, 32, "%ld.%03d",
+ secs * 1000 + msecs, usecs % 1000);
+ }
+ else
+ snprintf(tabentry->last_analyze_duration, 32, "%ld.%03d",
+ secs * 1000 + msecs, usecs % 1000);
+ pgstat_unlock_entry(entry_ref);/* Log the action if appropriate */
if (AmAutoVacuumWorkerProcess() && params->log_min_duration >= 0)
diff --git a/postgresql-17.4/src/backend/utils/adt/pgstatfuncs.c b/postgresql-17.4/src/backend/utils/adt/pgstatfuncs.c
index 2575dba..7aaab84 100644
--- a/postgresql-17.4/src/backend/utils/adt/pgstatfuncs.c
+++ b/postgresql-17.4/src/backend/utils/adt/pgstatfuncs.c
@@ -140,6 +140,36 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
/* pg_stat_get_lastscan */PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)+#define PG_STAT_GET_RELENTRY_STRING(stat) \
+Datum \
+CppConcat(pg_stat_get_,stat)(PG_FUNCTION_ARGS) \
+{ \
+ Oid relid = PG_GETARG_OID(0); \
+ char *result; \
+ PgStat_StatTabEntry *tabentry; \
+ \
+ if ((tabentry = pgstat_fetch_stat_tabentry(relid)) == NULL) \
+ PG_RETURN_NULL(); \
+ else \
+ result = tabentry->stat; \
+ \
+ if (result == NULL || result[0] == '\0') \
+ PG_RETURN_NULL(); \
+ else \
+ PG_RETURN_TEXT_P(cstring_to_text(result)); \
+}
+/* pg_stat_get_last_vacuum_duration */
+PG_STAT_GET_RELENTRY_STRING(last_vacuum_duration)
+
+/* pg_stat_get_last_autovacuum_duration */
+PG_STAT_GET_RELENTRY_STRING(last_autovacuum_duration)
+
+/* pg_stat_get_last_analyze_duration */
+PG_STAT_GET_RELENTRY_STRING(last_analyze_duration)
+
+/* pg_stat_get_last_autoanalyze_duration */
+PG_STAT_GET_RELENTRY_STRING(last_autoanalyze_duration)
+Datumpg_stat_get_function_calls(PG_FUNCTION_ARGS){
diff --git a/postgresql-17.4/src/include/catalog/pg_proc.dat b/postgresql-17.4/src/include/catalog/pg_proc.dat
index 6a5476d..2809c39 100644
--- a/postgresql-17.4/src/include/catalog/pg_proc.dat
+++ b/postgresql-17.4/src/include/catalog/pg_proc.dat
@@ -5445,6 +5445,22 @@proname => 'pg_stat_get_autoanalyze_count', provolatile => 's',proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',prosrc => 'pg_stat_get_autoanalyze_count' },
+{ oid => '6347', descr => 'statistics: last manual vacuum duration for a table',
+ proname => 'pg_stat_get_last_vacuum_duration', provolatile => 's',
+ proparallel => 'r', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_last_vacuum_duration' },
+{ oid => '6348', descr => 'statistics: last auto vacuum duration for a table',
+ proname => 'pg_stat_get_last_autovacuum_duration', provolatile => 's',
+ proparallel => 'r', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_last_autovacuum_duration' },
+{ oid => '6349', descr => 'statistics: last manual analyze duration for a table',
+ proname => 'pg_stat_get_last_analyze_duration', provolatile => 's',
+ proparallel => 'r', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_last_analyze_duration' },
+{ oid => '6350', descr => 'statistics: last auto analyze duration for a table',
+ proname => 'pg_stat_get_last_autoanalyze_duration', provolatile => 's',
+ proparallel => 'r', prorettype => 'text', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_last_autoanalyze_duration' },{ oid => '1936', descr => 'statistics: currently active backend IDs',proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/postgresql-17.4/src/include/pgstat.h b/postgresql-17.4/src/include/pgstat.h
index 2136239..f3f3423 100644
--- a/postgresql-17.4/src/include/pgstat.h
+++ b/postgresql-17.4/src/include/pgstat.h
@@ -426,6 +426,10 @@ typedef struct PgStat_StatTabEntryPgStat_Counter analyze_count;TimestampTz last_autoanalyze_time; /* autovacuum initiated */PgStat_Counter autoanalyze_count;
+ char last_vacuum_duration[32];
+ char last_autovacuum_duration[32];
+ char last_analyze_duration[32];
+ char last_autoanalyze_duration[32];} PgStat_StatTabEntry;typedef struct PgStat_WalStats
--
2.39.5 (Apple Git-154)
DBA 上手內核的感悟
雖然老楊一直提倡 DBA 向內核精進,但是否搞內核,還是因人而異。
先說說我自己為什么會堅持玩內核:
說直白點,其實就是自嗨,給自己提供情緒價值,自我取悅,自我肯定。
工作是存在隱形鄙視鏈的,運維被視為“打雜的”再正常不過了。
所有的臟活累活運維來扛,卻沒什么成果可以呈現。
再加目前重應用,輕數據庫的玩法,甚至有些項目中數據庫被當做存儲來使用,DBA 的價值就更得不到體現了。另外就是本身 DBA 的某些能力也不如前輩了,比如我自己,老實說我的 SQL 能力就不怎么樣。
在晉升匯報時,時不時被拷打,沒有量化的成果去展示。當你表達客戶滿意度提升,收入增長,成本縮減時可能被質疑,這是研發和產品所負責的事情。
所以,運維拿著最低的工資,沒有價值體現,還被 PUA,你不自嗨,這工作干著得多心累?
多年前在某家公司,全組就我一個人能通過看代碼分析報錯,這期間我看過 PG、MySQL、Redis、MongoDB 等。雖然都是簡單問題,但這種裝 X 的感覺是真的好,人需要裝 X。
不過,我現在的團隊挺 nice 的,老板鼓勵大家深入技術,多參加技術活動。
當然不是說 DBA 非得去搞內核,看個人的興趣點在哪吧,這個節點更適合卷 AI。 工作的同時能夠發展自己的興趣,也是一件美事。
假如你也是非專業人士,想搞內核,可以參考下老楊的幾點感悟:
-
前期積累:對 PG 代碼的熟悉,對每個模塊邏輯的理解。
-
從何入手:“臨摹”已有的邏輯,比如實現一個新的 GUC 產生不同的控制邏輯
-
借助 AI :可以使用 AI 去分析代碼邏輯,并實現功能,對比 AI 和自己思路的差異
-
勇于嘗試:只要有想法就可以嘗試實現,不要過于在乎“正確性”、“專業性”
-
過程>結果:能不能實現不重要,最有收益的是對于原理的深入理解