mirror of https://github.com/postgres/postgres
This commit adds a new stats kind, called PGSTAT_KIND_LOCK, implementing statistics for lock tags, as reported by pg_locks. The implementation is fixed-sized, as the data is caped based on the number of lock tags in LockTagType. The new statistics kind records the following fields, providing insight regarding lock behavior, while avoiding impact on performance-critical code paths (such as fast-path lock acquisition): - waits and wait_time: respectively track the number of times a lock required waiting and the total time spent acquiring it. These metrics are only collected once a lock is successfully acquired and after deadlock_timeout has been exceeded. fastpath_exceeded: counts how often a lock could not be acquired via the fast path due to the max_locks_per_transaction slot limits. A new view called pg_stat_lock can be used to access this data, coupled with a SQL function called pg_stat_get_lock(). Bump stat file format PGSTAT_FILE_FORMAT_ID. Bump catalog version. Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com> Reviewed-by: Andres Freund <andres@anarazel.de> Reviewed-by: Michael Paquier <michael@paquier.xyz> Discussion: https://postgr.es/m/aIyNxBWFCybgBZBS%40ip-10-97-1-34.eu-west-3.compute.internalmaster
parent
a90d865182
commit
4019f725f5
@ -0,0 +1,178 @@ |
||||
/* -------------------------------------------------------------------------
|
||||
* |
||||
* pgstat_lock.c |
||||
* Implementation of lock statistics. |
||||
* |
||||
* This file contains the implementation of lock statistics. It is kept |
||||
* separate from pgstat.c to enforce the line between the statistics |
||||
* access / storage implementation and the details about individual types |
||||
* of statistics. |
||||
* |
||||
* Copyright (c) 2021-2026, PostgreSQL Global Development Group |
||||
* |
||||
* IDENTIFICATION |
||||
* src/backend/utils/activity/pgstat_lock.c |
||||
* ------------------------------------------------------------------------- |
||||
*/ |
||||
|
||||
#include "postgres.h" |
||||
|
||||
#include "utils/pgstat_internal.h" |
||||
|
||||
static PgStat_PendingLock PendingLockStats; |
||||
static bool have_lockstats = false; |
||||
|
||||
PgStat_Lock * |
||||
pgstat_fetch_stat_lock(void) |
||||
{ |
||||
pgstat_snapshot_fixed(PGSTAT_KIND_LOCK); |
||||
|
||||
return &pgStatLocal.snapshot.lock; |
||||
} |
||||
|
||||
/*
|
||||
* Simpler wrapper of pgstat_lock_flush_cb() |
||||
*/ |
||||
void |
||||
pgstat_lock_flush(bool nowait) |
||||
{ |
||||
(void) pgstat_lock_flush_cb(nowait); |
||||
} |
||||
|
||||
/*
|
||||
* Flush out locally pending lock statistics |
||||
* |
||||
* If no stats have been recorded, this function returns false. |
||||
* |
||||
* If nowait is true, this function returns true if the lock could not be |
||||
* acquired. Otherwise, return false. |
||||
*/ |
||||
bool |
||||
pgstat_lock_flush_cb(bool nowait) |
||||
{ |
||||
LWLock *lcktype_lock; |
||||
PgStat_LockEntry *lck_shstats; |
||||
bool lock_not_acquired = false; |
||||
|
||||
if (!have_lockstats) |
||||
return false; |
||||
|
||||
for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++) |
||||
{ |
||||
lcktype_lock = &pgStatLocal.shmem->lock.locks[i]; |
||||
lck_shstats = |
||||
&pgStatLocal.shmem->lock.stats.stats[i]; |
||||
|
||||
if (!nowait) |
||||
LWLockAcquire(lcktype_lock, LW_EXCLUSIVE); |
||||
else if (!LWLockConditionalAcquire(lcktype_lock, LW_EXCLUSIVE)) |
||||
{ |
||||
lock_not_acquired = true; |
||||
continue; |
||||
} |
||||
|
||||
#define LOCKSTAT_ACC(fld) \ |
||||
(lck_shstats->fld += PendingLockStats.stats[i].fld) |
||||
LOCKSTAT_ACC(waits); |
||||
LOCKSTAT_ACC(wait_time); |
||||
LOCKSTAT_ACC(fastpath_exceeded); |
||||
#undef LOCKSTAT_ACC |
||||
|
||||
LWLockRelease(lcktype_lock); |
||||
} |
||||
|
||||
memset(&PendingLockStats, 0, sizeof(PendingLockStats)); |
||||
|
||||
have_lockstats = false; |
||||
|
||||
return lock_not_acquired; |
||||
} |
||||
|
||||
|
||||
void |
||||
pgstat_lock_init_shmem_cb(void *stats) |
||||
{ |
||||
PgStatShared_Lock *stat_shmem = (PgStatShared_Lock *) stats; |
||||
|
||||
for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++) |
||||
LWLockInitialize(&stat_shmem->locks[i], LWTRANCHE_PGSTATS_DATA); |
||||
} |
||||
|
||||
void |
||||
pgstat_lock_reset_all_cb(TimestampTz ts) |
||||
{ |
||||
for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++) |
||||
{ |
||||
LWLock *lcktype_lock = &pgStatLocal.shmem->lock.locks[i]; |
||||
PgStat_LockEntry *lck_shstats = &pgStatLocal.shmem->lock.stats.stats[i]; |
||||
|
||||
LWLockAcquire(lcktype_lock, LW_EXCLUSIVE); |
||||
|
||||
/*
|
||||
* Use the lock in the first lock type PgStat_LockEntry to protect the |
||||
* reset timestamp as well. |
||||
*/ |
||||
if (i == 0) |
||||
pgStatLocal.shmem->lock.stats.stat_reset_timestamp = ts; |
||||
|
||||
memset(lck_shstats, 0, sizeof(*lck_shstats)); |
||||
LWLockRelease(lcktype_lock); |
||||
} |
||||
} |
||||
|
||||
void |
||||
pgstat_lock_snapshot_cb(void) |
||||
{ |
||||
for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++) |
||||
{ |
||||
LWLock *lcktype_lock = &pgStatLocal.shmem->lock.locks[i]; |
||||
PgStat_LockEntry *lck_shstats = &pgStatLocal.shmem->lock.stats.stats[i]; |
||||
PgStat_LockEntry *lck_snap = &pgStatLocal.snapshot.lock.stats[i]; |
||||
|
||||
LWLockAcquire(lcktype_lock, LW_SHARED); |
||||
|
||||
/*
|
||||
* Use the lock in the first lock type PgStat_LockEntry to protect the |
||||
* reset timestamp as well. |
||||
*/ |
||||
if (i == 0) |
||||
pgStatLocal.snapshot.lock.stat_reset_timestamp = |
||||
pgStatLocal.shmem->lock.stats.stat_reset_timestamp; |
||||
|
||||
/* using struct assignment due to better type safety */ |
||||
*lck_snap = *lck_shstats; |
||||
LWLockRelease(lcktype_lock); |
||||
} |
||||
} |
||||
|
||||
/*
|
||||
* Increment counter for lock not acquired with the fast-path, per lock |
||||
* type, due to the fast-path slot limit reached. |
||||
* |
||||
* Note: This function should not be called in performance-sensitive paths, |
||||
* like lock acquisitions. |
||||
*/ |
||||
void |
||||
pgstat_count_lock_fastpath_exceeded(uint8 locktag_type) |
||||
{ |
||||
Assert(locktag_type <= LOCKTAG_LAST_TYPE); |
||||
PendingLockStats.stats[locktag_type].fastpath_exceeded++; |
||||
have_lockstats = true; |
||||
pgstat_report_fixed = true; |
||||
} |
||||
|
||||
/*
|
||||
* Increment the number of waits and wait time, per lock type. |
||||
* |
||||
* Note: This function should not be called in performance-sensitive paths, |
||||
* like lock acquisitions. |
||||
*/ |
||||
void |
||||
pgstat_count_lock_waits(uint8 locktag_type, long msecs) |
||||
{ |
||||
Assert(locktag_type <= LOCKTAG_LAST_TYPE); |
||||
PendingLockStats.stats[locktag_type].waits++; |
||||
PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs; |
||||
have_lockstats = true; |
||||
pgstat_report_fixed = true; |
||||
} |
||||
@ -0,0 +1,197 @@ |
||||
Parsed test spec with 2 sessions |
||||
|
||||
starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s1_begin s1_lock_relation s2_begin s2_ff s2_lock_relation s1_sleep s1_commit s2_commit s2_report_stat_lock_relation |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock'); |
||||
pg_stat_reset_shared |
||||
-------------------- |
||||
|
||||
(1 row) |
||||
|
||||
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s1_begin: BEGIN; |
||||
step s1_lock_relation: LOCK TABLE test_stat_tab; |
||||
step s2_begin: BEGIN; |
||||
step s2_ff: SELECT pg_stat_force_next_flush(); |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s2_lock_relation: LOCK TABLE test_stat_tab; <waiting ...> |
||||
step s1_sleep: SELECT pg_sleep(0.05); |
||||
pg_sleep |
||||
-------- |
||||
|
||||
(1 row) |
||||
|
||||
step s1_commit: COMMIT; |
||||
step s2_lock_relation: <... completed> |
||||
step s2_commit: COMMIT; |
||||
step s2_report_stat_lock_relation: |
||||
SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time |
||||
FROM pg_stat_lock WHERE locktype = 'relation'; |
||||
|
||||
has_waits|has_wait_time |
||||
---------+------------- |
||||
t |t |
||||
(1 row) |
||||
|
||||
|
||||
starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s2_set_log_lock_waits s1_table_insert s1_begin s1_table_update_k1 s2_begin s2_ff s2_table_update_k1 s1_sleep s1_commit s2_commit s2_report_stat_lock_transactionid |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock'); |
||||
pg_stat_reset_shared |
||||
-------------------- |
||||
|
||||
(1 row) |
||||
|
||||
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s2_set_log_lock_waits: SET log_lock_waits = on; |
||||
step s1_table_insert: INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1); |
||||
step s1_begin: BEGIN; |
||||
step s1_table_update_k1: UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1'; |
||||
step s2_begin: BEGIN; |
||||
step s2_ff: SELECT pg_stat_force_next_flush(); |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s2_table_update_k1: UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1'; <waiting ...> |
||||
step s1_sleep: SELECT pg_sleep(0.05); |
||||
pg_sleep |
||||
-------- |
||||
|
||||
(1 row) |
||||
|
||||
step s1_commit: COMMIT; |
||||
step s2_table_update_k1: <... completed> |
||||
step s2_commit: COMMIT; |
||||
step s2_report_stat_lock_transactionid: |
||||
SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time |
||||
FROM pg_stat_lock WHERE locktype = 'transactionid'; |
||||
|
||||
has_waits|has_wait_time |
||||
---------+------------- |
||||
t |t |
||||
(1 row) |
||||
|
||||
|
||||
starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s2_set_log_lock_waits s1_lock_advisory_lock s2_begin s2_ff s2_lock_advisory_lock s1_sleep s1_lock_advisory_unlock s2_lock_advisory_unlock s2_commit s2_report_stat_lock_advisory |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock'); |
||||
pg_stat_reset_shared |
||||
-------------------- |
||||
|
||||
(1 row) |
||||
|
||||
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s2_set_log_lock_waits: SET log_lock_waits = on; |
||||
step s1_lock_advisory_lock: SELECT pg_advisory_lock(1); |
||||
pg_advisory_lock |
||||
---------------- |
||||
|
||||
(1 row) |
||||
|
||||
step s2_begin: BEGIN; |
||||
step s2_ff: SELECT pg_stat_force_next_flush(); |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s2_lock_advisory_lock: SELECT pg_advisory_lock(1); <waiting ...> |
||||
step s1_sleep: SELECT pg_sleep(0.05); |
||||
pg_sleep |
||||
-------- |
||||
|
||||
(1 row) |
||||
|
||||
step s1_lock_advisory_unlock: SELECT pg_advisory_unlock(1); |
||||
pg_advisory_unlock |
||||
------------------ |
||||
t |
||||
(1 row) |
||||
|
||||
step s2_lock_advisory_lock: <... completed> |
||||
pg_advisory_lock |
||||
---------------- |
||||
|
||||
(1 row) |
||||
|
||||
step s2_lock_advisory_unlock: SELECT pg_advisory_unlock(1); |
||||
pg_advisory_unlock |
||||
------------------ |
||||
t |
||||
(1 row) |
||||
|
||||
step s2_commit: COMMIT; |
||||
step s2_report_stat_lock_advisory: |
||||
SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time |
||||
FROM pg_stat_lock WHERE locktype = 'advisory'; |
||||
|
||||
has_waits|has_wait_time |
||||
---------+------------- |
||||
t |t |
||||
(1 row) |
||||
|
||||
|
||||
starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s2_unset_log_lock_waits s1_begin s1_lock_relation s2_begin s2_ff s2_lock_relation s1_sleep s1_commit s2_commit s2_report_stat_lock_relation |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock'); |
||||
pg_stat_reset_shared |
||||
-------------------- |
||||
|
||||
(1 row) |
||||
|
||||
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms'; |
||||
step s2_unset_log_lock_waits: SET log_lock_waits = off; |
||||
step s1_begin: BEGIN; |
||||
step s1_lock_relation: LOCK TABLE test_stat_tab; |
||||
step s2_begin: BEGIN; |
||||
step s2_ff: SELECT pg_stat_force_next_flush(); |
||||
pg_stat_force_next_flush |
||||
------------------------ |
||||
|
||||
(1 row) |
||||
|
||||
step s2_lock_relation: LOCK TABLE test_stat_tab; <waiting ...> |
||||
step s1_sleep: SELECT pg_sleep(0.05); |
||||
pg_sleep |
||||
-------- |
||||
|
||||
(1 row) |
||||
|
||||
step s1_commit: COMMIT; |
||||
step s2_lock_relation: <... completed> |
||||
step s2_commit: COMMIT; |
||||
step s2_report_stat_lock_relation: |
||||
SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time |
||||
FROM pg_stat_lock WHERE locktype = 'relation'; |
||||
|
||||
has_waits|has_wait_time |
||||
---------+------------- |
||||
t |t |
||||
(1 row) |
||||
|
||||
@ -0,0 +1,128 @@ |
||||
# Test for the lock statistics |
||||
# |
||||
# This test creates multiple locking situations when a session (s2) has to |
||||
# wait on a lock for longer than deadlock_timeout. The first permutations |
||||
# test various lock tags. The last permutation checks that log_lock_waits |
||||
# has no impact on the statistics counters. |
||||
|
||||
setup |
||||
{ |
||||
CREATE TABLE test_stat_tab(key text not null, value int); |
||||
INSERT INTO test_stat_tab(key, value) VALUES('k0', 1); |
||||
SELECT pg_stat_force_next_flush(); |
||||
} |
||||
|
||||
teardown |
||||
{ |
||||
DROP TABLE IF EXISTS test_stat_tab; |
||||
} |
||||
|
||||
session s1 |
||||
setup { SET stats_fetch_consistency = 'none'; } |
||||
step s1_begin { BEGIN; } |
||||
step s1_commit { COMMIT; } |
||||
step s1_table_insert { INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);} |
||||
step s1_table_update_k1 { UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';} |
||||
step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; } |
||||
step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); } |
||||
step s1_sleep { SELECT pg_sleep(0.05); } |
||||
step s1_lock_relation { LOCK TABLE test_stat_tab; } |
||||
step s1_lock_advisory_lock { SELECT pg_advisory_lock(1); } |
||||
step s1_lock_advisory_unlock { SELECT pg_advisory_unlock(1); } |
||||
|
||||
session s2 |
||||
setup { SET stats_fetch_consistency = 'none'; } |
||||
step s2_begin { BEGIN; } |
||||
step s2_commit { COMMIT; } |
||||
step s2_ff { SELECT pg_stat_force_next_flush(); } |
||||
step s2_table_update_k1 { UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';} |
||||
step s2_set_deadlock_timeout { SET deadlock_timeout = '10ms'; } |
||||
step s2_set_log_lock_waits { SET log_lock_waits = on; } |
||||
step s2_unset_log_lock_waits { SET log_lock_waits = off; } |
||||
step s2_report_stat_lock_relation { |
||||
SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time |
||||
FROM pg_stat_lock WHERE locktype = 'relation'; |
||||
} |
||||
step s2_report_stat_lock_transactionid { |
||||
SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time |
||||
FROM pg_stat_lock WHERE locktype = 'transactionid'; |
||||
} |
||||
step s2_report_stat_lock_advisory { |
||||
SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time |
||||
FROM pg_stat_lock WHERE locktype = 'advisory'; |
||||
} |
||||
step s2_lock_relation { LOCK TABLE test_stat_tab; } |
||||
step s2_lock_advisory_lock { SELECT pg_advisory_lock(1); } |
||||
step s2_lock_advisory_unlock { SELECT pg_advisory_unlock(1); } |
||||
|
||||
###################### |
||||
# Lock stats tests |
||||
###################### |
||||
|
||||
# relation lock |
||||
|
||||
permutation |
||||
s1_set_deadlock_timeout |
||||
s1_reset_stat_lock |
||||
s2_set_deadlock_timeout |
||||
s1_begin |
||||
s1_lock_relation |
||||
s2_begin |
||||
s2_ff |
||||
s2_lock_relation |
||||
s1_sleep |
||||
s1_commit |
||||
s2_commit |
||||
s2_report_stat_lock_relation |
||||
|
||||
# transaction lock |
||||
|
||||
permutation |
||||
s1_set_deadlock_timeout |
||||
s1_reset_stat_lock |
||||
s2_set_deadlock_timeout |
||||
s2_set_log_lock_waits |
||||
s1_table_insert |
||||
s1_begin |
||||
s1_table_update_k1 |
||||
s2_begin |
||||
s2_ff |
||||
s2_table_update_k1 |
||||
s1_sleep |
||||
s1_commit |
||||
s2_commit |
||||
s2_report_stat_lock_transactionid |
||||
|
||||
# advisory lock |
||||
|
||||
permutation |
||||
s1_set_deadlock_timeout |
||||
s1_reset_stat_lock |
||||
s2_set_deadlock_timeout |
||||
s2_set_log_lock_waits |
||||
s1_lock_advisory_lock |
||||
s2_begin |
||||
s2_ff |
||||
s2_lock_advisory_lock |
||||
s1_sleep |
||||
s1_lock_advisory_unlock |
||||
s2_lock_advisory_unlock |
||||
s2_commit |
||||
s2_report_stat_lock_advisory |
||||
|
||||
# Ensure log_lock_waits has no impact |
||||
|
||||
permutation |
||||
s1_set_deadlock_timeout |
||||
s1_reset_stat_lock |
||||
s2_set_deadlock_timeout |
||||
s2_unset_log_lock_waits |
||||
s1_begin |
||||
s1_lock_relation |
||||
s2_begin |
||||
s2_ff |
||||
s2_lock_relation |
||||
s1_sleep |
||||
s1_commit |
||||
s2_commit |
||||
s2_report_stat_lock_relation |
||||
Loading…
Reference in new issue