Skip to content

Commit f19ca6e

Browse files
Naga AppaniCommitfest Bot
authored andcommitted
Add pg_get_multixact_stats() function for monitoring MultiXact usage
Expose multixact state via a new SQL-callable function pg_get_multixact_stats(), returning: - num_mxids : number of MultiXact IDs in use - num_members : number of member entries in use - members_size : bytes used by num_members in pg_multixact/members directory - oldest_multixact : oldest MultiXact ID still needed This patch adds pg_get_multixact_stats() function - SQL-callable interface to GetMultiXactInfo() - Returns NULLs if MultiXact system not initialized - Includes isolation tests for monitoring invariants Documentation updates: - func-info.sgml: add function entry - maintenance.sgml: mention monitoring multixact usage Build and catalog: - Add function to existing multixactfuncs.c - pg_proc.dat entry Author: Naga Appani <[email protected]> Reviewed-by: Ashutosh Bapat <[email protected]> Reviewed-by: Michael Paquier <[email protected]> Discussion: https://www.postgresql.org/message-id/flat/CA%2BQeY%2BAAsYK6WvBW4qYzHz4bahHycDAY_q5ECmHkEV_eB9ckzg%40mail.gmail.com
1 parent e6c9186 commit f19ca6e

File tree

7 files changed

+344
-6
lines changed

7 files changed

+344
-6
lines changed

doc/src/sgml/func/func-info.sgml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2975,6 +2975,39 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
29752975
modify key columns.
29762976
</para></entry>
29772977
</row>
2978+
2979+
<row>
2980+
<entry role="func_table_entry"><para role="func_signature">
2981+
<indexterm>
2982+
<primary>pg_get_multixact_stats</primary>
2983+
</indexterm>
2984+
<function>pg_get_multixact_stats</function> ()
2985+
<returnvalue>record</returnvalue>
2986+
( <parameter>num_mxids</parameter> <type>integer</type>,
2987+
<parameter>num_members</parameter> <type>bigint</type>,
2988+
<parameter>members_size</parameter> <type>bigint</type>,
2989+
<parameter>oldest_multixact</parameter> <type>xid</type> )
2990+
</para>
2991+
<para>
2992+
Returns statistics about current multixact usage:
2993+
<literal>num_mxids</literal> is the total number of multixact IDs currently present in the system,
2994+
<literal>num_members</literal> is the total number of multixact member entries currently
2995+
present in the system,
2996+
<literal>members_size</literal> is the storage occupied by <literal>num_members</literal>
2997+
in the <literal>pg_multixact/members</literal> directory,
2998+
<literal>oldest_multixact</literal> is the oldest multixact ID still in use.
2999+
</para>
3000+
<para>
3001+
The function reports statistics at the time it is invoked. Values may vary between calls,
3002+
even within a single transaction.
3003+
</para>
3004+
<para>
3005+
Returns <literal>NULL</literal> when multixact statistics are unavailable,
3006+
such as during startup before multixact initialization completes.
3007+
Specifically, this occurs when the oldest multixact offset
3008+
corresponding to a multixact referenced by a relation is not known.
3009+
</para></entry>
3010+
</row>
29783011
</tbody>
29793012
</tgroup>
29803013
</table>

doc/src/sgml/maintenance.sgml

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -813,14 +813,60 @@ HINT: Execute a database-wide VACUUM in that database.
813813
<para>
814814
As a safety device, an aggressive vacuum scan will
815815
occur for any table whose multixact-age is greater than <xref
816-
linkend="guc-autovacuum-multixact-freeze-max-age"/>. Also, if the
817-
storage occupied by multixacts members exceeds about 10GB, aggressive vacuum
818-
scans will occur more often for all tables, starting with those that
819-
have the oldest multixact-age. Both of these kinds of aggressive
820-
scans will occur even if autovacuum is nominally disabled. The members storage
821-
area can grow up to about 20GB before reaching wraparound.
816+
linkend="guc-autovacuum-multixact-freeze-max-age"/>. Also, if the number
817+
of multixact member entries created exceeds approximately 2^31 entries
818+
(occupying roughly 10GB in the <literal>pg_multixact/members</literal> directory),
819+
aggressive vacuum scans will occur more often for all tables, starting with those that
820+
have the oldest multixact-age. Both of these kinds of aggressive
821+
scans will occur even if autovacuum is nominally disabled. The members can grow
822+
up to approximately 2^32 entries (occupying roughly 20GB in the
823+
<literal>pg_multixact/members</literal> directory) before reaching wraparound.
822824
</para>
823825

826+
<para>
827+
The <function>pg_get_multixact_stats()</function> function described in
828+
<xref linkend="functions-pg-snapshot"/> provides a way to monitor
829+
multixact allocation and usage patterns in real time, for example:
830+
<programlisting>
831+
postgres=# SELECT *,pg_size_pretty(members_size) members_size_pretty FROM pg_catalog.pg_get_multixact_stats();
832+
num_mxids | num_members | members_size | oldest_multixact | members_size_pretty
833+
-----------+-------------+--------------+------------------+---------------------
834+
311740299 | 2785241176 | 13926205880 | 2 | 13 GB
835+
(1 row)
836+
</programlisting>
837+
This output shows a system with significant multixact activity: about ~312 million
838+
multixact IDs and ~2.8 billion member entries consuming 13 GB of storage space.
839+
By leveraging this information, the function helps:
840+
<orderedlist>
841+
<listitem>
842+
<simpara>
843+
Identify unusual multixact activity from concurrent row-level locks
844+
or foreign key operations. For example, a spike in <literal>num_mxids</literal> might indicate
845+
multiple sessions running <literal>UPDATE</literal> statements with foreign key checks,
846+
concurrent <literal>SELECT FOR SHARE</literal> operations, or frequent use of savepoints
847+
causing lock contention.
848+
</simpara>
849+
</listitem>
850+
<listitem>
851+
<simpara>
852+
Track multixact cleanup efficiency by monitoring oldest_multixact.
853+
If this value remains unchanged while <literal>num_members</literal> grows, it could indicate
854+
that long-running transactions are preventing cleanup, or autovacuum is
855+
not keeping up with the workload.
856+
</simpara>
857+
</listitem>
858+
<listitem>
859+
<simpara>
860+
Detect potential performance impacts before they become critical.
861+
For instance, high multixact usage from frequent row-level locking or
862+
foreign key operations can lead to increased I/O and CPU overhead during
863+
vacuum operations. Monitoring these stats helps tune autovacuum frequency
864+
and transaction patterns.
865+
</simpara>
866+
</listitem>
867+
</orderedlist>
868+
</para>
869+
824870
<para>
825871
Similar to the XID case, if autovacuum fails to clear old MXIDs from a table, the
826872
system will begin to emit warning messages when the database's oldest MXIDs reach forty

src/backend/utils/adt/multixactfuncs.c

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "postgres.h"
1616

1717
#include "access/multixact.h"
18+
#include "access/htup_details.h"
1819
#include "funcapi.h"
1920
#include "utils/builtins.h"
2021

@@ -85,3 +86,48 @@ pg_get_multixact_members(PG_FUNCTION_ARGS)
8586

8687
SRF_RETURN_DONE(funccxt);
8788
}
89+
90+
/*
91+
* pg_get_multixact_stats
92+
*
93+
* Returns statistics about current multixact usage.
94+
*
95+
* Returns NULL if the oldest referenced offset is unknown.
96+
*/
97+
Datum
98+
pg_get_multixact_stats(PG_FUNCTION_ARGS)
99+
{
100+
TupleDesc tupdesc;
101+
Datum values[4];
102+
bool nulls[4];
103+
MultiXactOffset members;
104+
MultiXactId oldestMultiXactId;
105+
uint32 multixacts;
106+
MultiXactOffset oldestOffset;
107+
int64 membersBytes;
108+
109+
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
110+
ereport(ERROR,
111+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
112+
errmsg("return type must be a row type")));
113+
114+
if (GetMultiXactInfo(&multixacts, &members, &oldestMultiXactId, &oldestOffset))
115+
{
116+
/*
117+
* Calculate storage space for members. Members are stored in groups of 4,
118+
* with each group taking 20 bytes, resulting in 5 bytes per member.
119+
* Note: This ignores small page overhead (12 bytes per 8KB)
120+
*/
121+
membersBytes = (int64) members * 5;
122+
123+
values[0] = UInt32GetDatum(multixacts);
124+
values[1] = UInt32GetDatum(members);
125+
values[2] = Int64GetDatum(membersBytes);
126+
values[3] = UInt32GetDatum(oldestMultiXactId);
127+
memset(nulls, false, sizeof(nulls));
128+
129+
return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls));
130+
}
131+
132+
PG_RETURN_NULL();
133+
}

src/include/catalog/pg_proc.dat

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12604,4 +12604,14 @@
1260412604
proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
1260512605
prosrc => 'pg_get_aios' },
1260612606

12607+
# Get multixact usage
12608+
{ oid => '9001', descr => 'get current multixact usage statistics',
12609+
proname => 'pg_get_multixact_stats',
12610+
provolatile => 'v', proparallel => 's', prorettype => 'record',
12611+
proargtypes => '',
12612+
proallargtypes => '{int8,int8,int8,xid}',
12613+
proargmodes => '{o,o,o,o}',
12614+
proargnames => '{num_mxids,num_members,members_size,oldest_multixact}',
12615+
prosrc => 'pg_get_multixact_stats'},
12616+
1260712617
]
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
Parsed test spec with 2 sessions
2+
3+
starting permutation: snap0 s1_begin s1_lock snap1 s2_begin s2_lock snap2 check_while_pinned s1_commit s2_commit
4+
step snap0:
5+
CREATE TEMP TABLE snap0 AS
6+
SELECT num_mxids, num_members, oldest_multixact
7+
FROM pg_get_multixact_stats();
8+
9+
step s1_begin: BEGIN;
10+
step s1_lock: SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE;
11+
?column?
12+
--------
13+
1
14+
(1 row)
15+
16+
step snap1:
17+
CREATE TEMP TABLE snap1 AS
18+
SELECT num_mxids, num_members, oldest_multixact
19+
FROM pg_get_multixact_stats();
20+
21+
step s2_begin: BEGIN;
22+
step s2_lock: SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE;
23+
?column?
24+
--------
25+
1
26+
(1 row)
27+
28+
step snap2:
29+
CREATE TEMP TABLE snap2 AS
30+
SELECT num_mxids, num_members, oldest_multixact
31+
FROM pg_get_multixact_stats();
32+
33+
step check_while_pinned:
34+
SELECT r.assertion, r.ok
35+
FROM snap0 s0
36+
JOIN snap1 s1 ON TRUE
37+
JOIN snap2 s2 ON TRUE,
38+
LATERAL unnest(
39+
ARRAY[
40+
'is_init_mxids',
41+
'is_init_members',
42+
'is_init_oldest_mxid',
43+
'is_init_oldest_off',
44+
'is_oldest_mxid_nondec_01',
45+
'is_oldest_mxid_nondec_12',
46+
'is_oldest_off_nondec_01',
47+
'is_oldest_off_nondec_12',
48+
'is_members_increased_ge1',
49+
'is_mxids_nondec_01',
50+
'is_mxids_nondec_12',
51+
'is_members_nondec_01',
52+
'is_members_nondec_12'
53+
],
54+
ARRAY[
55+
(s2.num_mxids IS NOT NULL),
56+
(s2.num_members IS NOT NULL),
57+
(s2.oldest_multixact IS NOT NULL),
58+
59+
(s1.oldest_multixact::text::bigint >= COALESCE(s0.oldest_multixact::text::bigint, 0)),
60+
(s2.oldest_multixact::text::bigint >= COALESCE(s1.oldest_multixact::text::bigint, 0)),
61+
62+
(s2.num_members >= COALESCE(s1.num_members, 0) + 1),
63+
64+
(s1.num_mxids >= COALESCE(s0.num_mxids, 0)),
65+
(s2.num_mxids >= COALESCE(s1.num_mxids, 0)),
66+
(s1.num_members >= COALESCE(s0.num_members, 0)),
67+
(s2.num_members >= COALESCE(s1.num_members, 0))
68+
]
69+
) AS r(assertion, ok);
70+
71+
assertion |ok
72+
------------------------+--
73+
is_init_mxids |t
74+
is_init_members |t
75+
is_init_oldest_mxid |t
76+
is_init_oldest_off |t
77+
is_oldest_mxid_nondec_01|t
78+
is_oldest_mxid_nondec_12|t
79+
is_oldest_off_nondec_01 |t
80+
is_oldest_off_nondec_12 |t
81+
is_members_increased_ge1|t
82+
is_mxids_nondec_01 |t
83+
is_mxids_nondec_12 |
84+
is_members_nondec_01 |
85+
is_members_nondec_12 |
86+
(13 rows)
87+
88+
step s1_commit: COMMIT;
89+
step s2_commit: COMMIT;

src/test/isolation/isolation_schedule

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,4 @@ test: serializable-parallel-2
120120
test: serializable-parallel-3
121121
test: matview-write-skew
122122
test: lock-nowait
123+
test: multixact_stats
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Test invariants for pg_get_multixact_stats()
2+
# We create exactly one fresh MultiXact on a brand-new table. While it is pinned
3+
# by two open transactions, we assert only invariants that background VACUUM/FREEZE
4+
# cannot violate:
5+
# • members increased by ≥ 1 when the second session locked the row,
6+
# • num_mxids / num_members did not decrease vs earlier snapshots,
7+
# • oldest_* never decreases.
8+
# We make NO assertions after releasing locks (freezing/truncation may shrink deltas).
9+
#
10+
# Terminology (global counters):
11+
# num_mxids, num_members : "in-use" deltas derived from global horizons
12+
# oldest_multixact, offset : oldest horizons; they move forward, never backward
13+
#
14+
# All assertions execute while our multixact is pinned by open txns, which protects
15+
# the truncation horizon (VACUUM can't advance past our pinned multi).
16+
17+
setup
18+
{
19+
CREATE TABLE mxq(id int PRIMARY KEY, v int);
20+
INSERT INTO mxq VALUES (1, 42);
21+
}
22+
23+
teardown
24+
{
25+
DROP TABLE mxq;
26+
}
27+
28+
# Two sessions that lock on the same tuple -> one MultiXact with >= 2 members.
29+
session "s1"
30+
setup { SET client_min_messages = warning; SET lock_timeout = '5s'; }
31+
step s1_begin { BEGIN; }
32+
step s1_lock { SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE; }
33+
step s1_commit { COMMIT; }
34+
35+
session "s2"
36+
setup { SET client_min_messages = warning; SET lock_timeout = '5s'; }
37+
step s2_begin { BEGIN; }
38+
step s2_lock { SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE; }
39+
step s2_commit { COMMIT; }
40+
41+
# Baseline BEFORE any locking; may be NULLs if multixact isn't initialized yet.
42+
step snap0 {
43+
CREATE TEMP TABLE snap0 AS
44+
SELECT num_mxids, num_members, oldest_multixact
45+
FROM pg_get_multixact_stats();
46+
}
47+
48+
# After s1 has locked the row.
49+
step snap1 {
50+
CREATE TEMP TABLE snap1 AS
51+
SELECT num_mxids, num_members, oldest_multixact
52+
FROM pg_get_multixact_stats();
53+
}
54+
55+
# After s2 joins on the SAME tuple -> multixact with >= 2 members.
56+
step snap2 {
57+
CREATE TEMP TABLE snap2 AS
58+
SELECT num_mxids, num_members, oldest_multixact
59+
FROM pg_get_multixact_stats();
60+
}
61+
62+
# Pretty, deterministic key/value output of boolean checks.
63+
# Keys:
64+
# is_init_mxids : num_mxids is non-NULL
65+
# is_init_members : num_members is non-NULL
66+
# is_init_oldest_mxid : oldest_multixact is non-NULL
67+
# is_oldest_mxid_nondec_01 : oldest_multixact did not decrease (snap0→snap1)
68+
# is_oldest_mxid_nondec_12 : oldest_multixact did not decrease (snap1→snap2)
69+
# is_members_increased_ge1 : members increased by at least 1 when s2 joined
70+
# is_mxids_nondec_01 : num_mxids did not decrease (snap0→snap1)
71+
# is_mxids_nondec_12 : num_mxids did not decrease (snap1→snap2)
72+
# is_members_nondec_01 : num_members did not decrease (snap0→snap1)
73+
# is_members_nondec_12 : num_members did not decrease (snap1→snap2)
74+
step check_while_pinned {
75+
SELECT r.assertion, r.ok
76+
FROM snap0 s0
77+
JOIN snap1 s1 ON TRUE
78+
JOIN snap2 s2 ON TRUE,
79+
LATERAL unnest(
80+
ARRAY[
81+
'is_init_mxids',
82+
'is_init_members',
83+
'is_init_oldest_mxid',
84+
'is_init_oldest_off',
85+
'is_oldest_mxid_nondec_01',
86+
'is_oldest_mxid_nondec_12',
87+
'is_oldest_off_nondec_01',
88+
'is_oldest_off_nondec_12',
89+
'is_members_increased_ge1',
90+
'is_mxids_nondec_01',
91+
'is_mxids_nondec_12',
92+
'is_members_nondec_01',
93+
'is_members_nondec_12'
94+
],
95+
ARRAY[
96+
(s2.num_mxids IS NOT NULL),
97+
(s2.num_members IS NOT NULL),
98+
(s2.oldest_multixact IS NOT NULL),
99+
100+
(s1.oldest_multixact::text::bigint >= COALESCE(s0.oldest_multixact::text::bigint, 0)),
101+
(s2.oldest_multixact::text::bigint >= COALESCE(s1.oldest_multixact::text::bigint, 0)),
102+
103+
(s2.num_members >= COALESCE(s1.num_members, 0) + 1),
104+
105+
(s1.num_mxids >= COALESCE(s0.num_mxids, 0)),
106+
(s2.num_mxids >= COALESCE(s1.num_mxids, 0)),
107+
(s1.num_members >= COALESCE(s0.num_members, 0)),
108+
(s2.num_members >= COALESCE(s1.num_members, 0))
109+
]
110+
) AS r(assertion, ok);
111+
}
112+
113+
permutation snap0 s1_begin s1_lock snap1 s2_begin s2_lock snap2 check_while_pinned s1_commit s2_commit

0 commit comments

Comments
 (0)