在使用PG的过程中,我相信大家或多或少都见过这个报错:
ERROR: out of shared memory
HINT: You might need to increase "max_locks_per_transaction".
当出现这个报错时,通常是通过增加max_locks_per_transaction参数值来规避的。
本篇我们将深入理解其背后的原理。
从官方文档[1] 对参数解释来看:这是每个进程或事务平均可在共享表锁中分配对象锁的数量,默认值为64。
起初,我对这里的理解也不深刻,总以为一个事务加锁超过64个后,就会抛出这个报错。最近刚好有小伙伴碰到这个报错,跑来咨询,他的场景让老杨对这里的认知产生了怀疑,借此研究了下这里的逻辑,。
小伙伴的场景如下,登录数据库后执行元命令\l+,就报错了。
[postgres@Nick ~]$ psql
psql (18beta2)
Type "help" for help.
postgres=# \l+
ERROR: out of shared memory
HINT: You might need to increase "max_locks_per_transaction".
postgres=#
\l+元命令,主要是查询pg_database和pg_tablespace两个系统表,按照自己之前的认知,并没有超过64个对象,所以就自我怀疑了。
SELECT
d.datname as "Name",
pg_catalog.pg_get_userbyid(d.datdba) as "Owner",
pg_catalog.pg_encoding_to_char(d.encoding) as "Encoding",
CASE d.datlocprovider WHEN 'b' THEN 'builtin' WHEN 'c' THEN 'libc' WHEN 'i' THEN 'icu' END AS "Locale Provider",
d.datcollate as "Collate",
d.datctype as "Ctype",
d.datlocale as "Locale",
d.daticurules as "ICU Rules",
CASE WHEN pg_catalog.array_length(d.datacl, 1) = 0 THEN '(none)' ELSE pg_catalog.array_to_string(d.datacl, E'\n') END AS "Access privileges",
CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')
THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))
ELSE 'No Access'
END as "Size",
t.spcname as "Tablespace",
pg_catalog.shobj_description(d.oid, 'pg_database') as "Description"
FROM pg_catalog.pg_database d
JOIN pg_catalog.pg_tablespace t on d.dattablespace = t.oid
ORDER BY 1;
那还是需要仔细看下这里的逻辑,当走读完这里的代码后,刷新了我之前错误的认知。
这里主要和Regular Lock申请相关,在此我们不展开Regular Lock或者叫Heavyweight Lock的细节,简单来说就是来操作表等对象加的锁。锁相关细节可以参考postgres-internals.cn[2] 第12-15章。
以下用Lock来简化描述,就是指Heavyweight Lock。
Lock的相关信息是放在对应的shared hash table中的,这个也不难理解,比如我们去查看pg_locks就可以知道所有的进程对某个表加了什么锁。
在实例启动时会根据参数配置,申请对应大小的共享内存,然后 init Lock manager对应的hash table。
max_locks_per_transaction和hash table的size是强相关的。
1、先看Lock 相关hash table的共享内存创建过程:
PostmasterMain--> CreateSharedMemoryAndSemaphores --> CalculateShmemSize --> LockManagerShmemSize
Postmaster主进程启动时,在创建共享内存环节,首先在CalculateShmemSize函数中计算所有共享内存的大小,在这个函数里调用每种共享内存的size计算函数(包含shared_buffers、wal_buffers等等),对于Lock hash table的size就是调用LockManagerShmemSize进行计算的。
如下代码所示,当前LockManager有两个hash table:
Lock hash table -- LockMethodLockHash(锁资源hash表):存储所有被锁定的对象(如表、行、页等),维护Lock结构体
proclock hash table -- LockMethodProcLockHash(进程-锁关联hash表):用来维护进程-锁资源关关系,维护ProcLock结构体
可以看到这两个hash table申请的内存大小都取决于max_table_size。
/*
* Estimate shared-memory space used for lock tables
*/
Size
LockManagerShmemSize(void)
{
Size size = 0;
long max_table_size;
/* lock hash table */
max_table_size = NLOCKENTS();
size = add_size(size, hash_estimate_size(max_table_size, sizeof(LOCK)));
/* proclock hash table */
max_table_size *= 2;
size = add_size(size, hash_estimate_size(max_table_size, sizeof(PROCLOCK)));
/*
* Since NLOCKENTS is only an estimate, add 10% safety margin.
*/
size = add_size(size, size / 10);
return size;
}
而max_table_size是通过NLOCKENTS()赋值的,值为: max_locks_per_xact * (MaxBackends + max_prepared_xacts)
即:max_locks_per_transaction * ( (max_connections + autovacuum_worker_slots + max_worker_processes + max_wal_senders + 2 ) + max_prepared_transactions)
默认参数下值为:64 * ((100+16+8+10+2)+0) = 8704
所以hash table的大小是一定的,且和max_locks_per_transaction联动。
#define NLOCKENTS() \
mul_size(max_locks_per_xact, add_size(MaxBackends, max_prepared_xacts))
MaxBackends = MaxConnections + autovacuum_worker_slots +
max_worker_processes + max_wal_senders + NUM_SPECIAL_WORKER_PROCS;
并且LockManager hash table 并不占用shared_buffers提供的空间,而是根据以上公式申请的专用共享内存空间。
2、再看LockManager hash的初始化:
在LockManagerShmemInit函数中,根据max_table_size设定的大小,初始化共享LockMethodLockHash和LockMethodProcLockHash。
同时初始化fast-path对应的结构体(fast-path是一个优化机制,用于快速管理表级锁,减少全局hash table的访问)。
void
LockManagerShmemInit(void)
{
HASHCTL info;
long init_table_size,
max_table_size;
bool found;
/*
* Compute init/max size to request for lock hashtables. Note these
* calculations must agree with LockManagerShmemSize!
*/
max_table_size = NLOCKENTS();
init_table_size = max_table_size / 2;
/*
* Allocate hash table for LOCK structs. This stores per-locked-object
* information.
*/
info.keysize = sizeof(LOCKTAG);
info.entrysize = sizeof(LOCK);
info.num_partitions = NUM_LOCK_PARTITIONS;
LockMethodLockHash = ShmemInitHash("LOCK hash",
init_table_size,
max_table_size,
&info,
HASH_ELEM | HASH_BLOBS | HASH_PARTITION);
/* Assume an average of 2 holders per lock */
max_table_size *= 2;
init_table_size *= 2;
/*
* Allocate hash table for PROCLOCK structs. This stores
* per-lock-per-holder information.
*/
info.keysize = sizeof(PROCLOCKTAG);
info.entrysize = sizeof(PROCLOCK);
info.hash = proclock_hash;
info.num_partitions = NUM_LOCK_PARTITIONS;
LockMethodProcLockHash = ShmemInitHash("PROCLOCK hash",
init_table_size,
max_table_size,
&info,
HASH_ELEM | HASH_FUNCTION | HASH_PARTITION);
/*
* Allocate fast-path structures.
*/
FastPathStrongRelationLocks =
ShmemInitStruct("Fast Path Strong Relation Lock Data",
sizeof(FastPathStrongRelationLockData), &found);
if (!found)
SpinLockInit(&FastPathStrongRelationLocks->mutex);
}
到这里,我们已经明白了max_locks_per_transaction决定了LockManager hash的大小,并且使用的是专用共享内存LockManagerShmem。
那么当报错ERROR: out of shared memory HINT: You might need to increase "max_locks_per_transaction". 意思就是LockManagerShmem使用完了,也就是对应的hash table没有空闲的bucket了。因此提示我们需要增加max_locks_per_transaction来进行扩容。
3、申请锁时对LockManager hash table的操作:
这里摘录部分源代码,主要是分析报错。完整代码请参考 [3]
可以看到有两处out of shared memory的报错,都是LockManager hash table没有free bucket的情况,也就是LockManagerShmem耗尽了,无法更新hash table中Lock信息了。
LockAcquireResult
LockAcquireExtended(const LOCKTAG *locktag,
LOCKMODE lockmode,
bool sessionLock,
bool dontWait,
bool reportMemoryError,
LOCALLOCK **locallockp,
bool logLockFailure)
{
/* 省略 */
/*
* Find or create a LOCALLOCK entry for this lock and lockmode
*/
MemSet(&localtag, 0, sizeof(localtag)); /* must clear padding */
localtag.lock = *locktag;
localtag.mode = lockmode;
locallock = (LOCALLOCK *) hash_search(LockMethodLocalHash,
&localtag,
HASH_ENTER, &found);
/* 省略 */
/* 当past-path锁冲突或者耗尽,转向访问共享hash table,当不能create new entry时报错 out of shared memory */
if (ConflictsWithRelationFastPath(locktag, lockmode))
{
uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
BeginStrongLockAcquire(locallock, fasthashcode);
if (!FastPathTransferRelationLocks(lockMethodTable, locktag,
hashcode))
{
AbortStrongLockAcquire();
if (locallock->nLocks == 0)
RemoveLocalLock(locallock);
if (locallockp)
*locallockp = NULL;
if (reportMemoryError)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
errhint("You might need to increase \"%s\".", "max_locks_per_transaction")));
else
return LOCKACQUIRE_NOT_AVAIL;
}
}
/*
* We didn't find the lock in our LOCALLOCK table, and we didn't manage to
* take it via the fast-path, either, so we've got to mess with the shared
* lock table.
*/
partitionLock = LockHashPartitionLock(hashcode);
LWLockAcquire(partitionLock, LW_EXCLUSIVE);
/* 当记录进程-锁对应关系时,访问hash table,无法create new entry时,报错out of memory */
proclock = SetupLockInTable(lockMethodTable, MyProc, locktag,
hashcode, lockmode);
if (!proclock)
{
AbortStrongLockAcquire();
LWLockRelease(partitionLock);
if (locallock->nLocks == 0)
RemoveLocalLock(locallock);
if (locallockp)
*locallockp = NULL;
if (reportMemoryError)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
errhint("You might need to increase \"%s\".", "max_locks_per_transaction")));
else
return LOCKACQUIRE_NOT_AVAIL;
}
/* 省略 */
通过以上的分析,我们已经掌握了max_locks_per_transaction和报错之间的联系。
为了方便测试,我将参数做了如下调整:
[postgres@Nick ~]$ psql
psql (18beta2)
Type "help"forhelp.
postgres=# show max_connections ;
max_connections
-----------------
16
(1 row)
postgres=# show max_worker_processes ;
max_worker_processes
----------------------
2
(1 row)
postgres=# show autovacuum_worker_slots ;
autovacuum_worker_slots
-------------------------
2
(1 row)
postgres=# show max_wal_senders ;
max_wal_senders
-----------------
1
(1 row)
postgres=# show max_prepared_transactions ;
max_prepared_transactions
---------------------------
0
(1 row)
postgres=# show max_locks_per_transaction ;
max_locks_per_transaction
---------------------------
10
(1 row)
postgres=# select 10 * (16+2+2+1+2+0);
?column?
----------
230
(1 row)
postgres=#
复现报错,并debug报错过程。
[postgres@Nick ~]$ psql
psql (18beta2)
Type "help" for help.
postgres=# \l+
ERROR: out of shared memory
HINT: You might need to increase "max_locks_per_transaction".
postgres=# \errverbose
ERROR: 53200: out of shared memory
HINT: You might need to increase "max_locks_per_transaction".
LOCATION: LockAcquireExtended, lock.c:1077
postgres=# \q
[postgres@Nick ~]$
1、先来看LockManagerShmemSize,hash table的共享内存size计算


2、再看LockManagerShmemInit,初始化hash table

3、申请锁时,记录进程-锁关系信息
get_hash_entry中遍历hash表,发现freelist为0,bucket已用完,因此currBucket为NULL

hash_search_with_hash_value中currBucket为NULL,且action为HASH_RNTER_NULL,则返回NULL

SetupLockInTable中,proclock为NULL,则返回NULL。

LockAcquireExtended函数中,SetupLockInTable返回NULL,所以proclock为NULL,进入了报错逻辑。报错ERROR: out of shared memory HINT: You might need to increase "max_locks_per_transaction".

顺便提一下,当业务高并发时偶尔能看到大量的LwLock类LockManager等待事件,其实就是批量SQL申请Lock更新LockManager hash table,导致Lwlock等锁队列长。
max_locks_per_transaction和LockManager hash table大小相关,这些hash table使用的是专用共享内存空间。当报错out of shared memory时,说明hash table没有空闲bucket了,也就是这里专用的共享内存几乎耗尽了,增加max_locks_per_transaction参数值就可以扩容。
那么hash table被耗尽,说明可能是长事务过多,持有的锁没有及时释放;或者单次访问太多分区表;或者本身就是业务变化,hash table需要扩容了。
Reference:
[1] https://www.postgresql.org/docs/18/runtime-config-locks.html#GUC-MAX-LOCKS-PER-TRANSACTION
[2] https://postgres-internals.cn/docs/chapter12/
[3] https://github.com/postgres/postgres/blob/REL_18_BETA2/src/backend/storage/lmgr/lock.c#L835
本文分享自 PostgreSQL运维之道 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!