二月 26, 2025
摘要:在本教程中,您将学习如何在 PostgreSQL 中处理 “could not resize shared memory segment” 的错误。
目录
介绍
在使用 PostgreSQL 时,您可能会遇到一些非常常见的数据库错误,尤其是在应用程序中或使用 ORM 时。一个是 PG::DiskFull: ERROR: could not resize shared memory segment。它看起来会像这样:
PG::DiskFull: ERROR: could not resize shared memory segment "/PostgreSQL.938232807" to 55334241 bytes: No space left on device
如果您在日志中看到了该错误,不用太过担心,没有直接的理由对这些错误中的任何一个感到恐慌。如果您经常或一直看到这样的错误,或者您对它们是如何产生的感到好奇,让我们接下来进行故障排除。
不是真的磁盘满了
在这种情况下,当数据库报告 “no space left on device” 时,这里不是指整个磁盘,而是在那个确切时刻的共享内存设备。当一个 SQL 查询为哈希、排序等操作分配共享缓冲区时,会在此处创建内存段。并行工作进程也会分配共享缓冲区。当没有足够剩余的共享缓冲区时,该查询会终止并出现此类错误。
此错误消息中 “磁盘已满” 的部分有点掩人耳目。当您的 Postgres 实例无法分配更多内存以支持查询时,您将看到该错误。这不是真的磁盘满了的消息。有时,当有执行速度非常缓慢的消耗适度内存的查询,最终会耗尽可用的内存时,就会发生这种情况。其他时候,出现一个巨大的内存密集型查询,并占用大量内存时,也会产生这种问题。
为什么这些查询不像通常的大查询那样溢出到临时文件呢?好吧,您可能刚刚了解了总内存分配。工作内存(work_mem)是为每个需要它的查询计划节点分配的,而不是为每个查询或会话分配一次,这意味着一个会话可能会消耗许多份 work_mem。例如,如果 max_parallel_workers 为 8,work_mem 为 384MB,即使只使用了单个并行哈希连接,也可能使用高达 3072MB 的共享缓冲区。如果您的查询计划有 5 个会分配 work_mem 的计划节点(即排序/哈希操作),会使用到 4 个并行工作进程,则您可能使用到(384MB x 5 个计划节点 x 4 个工作进程)= 7.6GB 的共享缓冲区。如果您只有 7.7 GB 的可用空间,那是行不通的。
查看数据库日志
要查看这些错误是怎么回事,让我们来看下日志,看看我们会看到这些错误的频率。在日志中搜索 resize memory 的问题。
$ grep -iR "could not resize shared memory" * | sed 's/.log.*//' | uniq -c
1597 postgresql-Fri
587 postgresql-Mon
325 postgresql-Sat
1223 postgresql-Sun
1395 postgresql-Thu
您还可以查找 OOM 错误中提到的特定进程 ID。对于下面这条,它是 5883275
。
Aug 08 16:34:31 postgres[5883275]: [36-1] [5883275][client backend][17/20137143][0] [user=application,db=postgres,app=/rails/bin/rails] ERROR: could not resize shared memory segment "/PostgreSQL.2449246800" to 33554432 bytes: No space left on device
要想追溯错误的源头,请在日志中搜索该进程 ID。您可能会看到很长的查询被分解为较小的带序列号的部分,例如此示例中的 42-1、42-2 和 42-3:
Aug 08 16:34:31 postgres[5883275]: [42-1] [5883275][client backend][17/20137143][0] [user=application,db=postgres,app=/rails/bin/rails] ERROR: could not resize shared memory segment "/PostgreSQL.2551246800" to 5883275 bytes: No space left on device
Aug 08 16:34:31 postgres[5883275]: [42-2] [5883275][client backend][17/20137143][0] [user=application,db=postgres,app=/rails/bin/rails] STATEMENT: SELECT COUNT(*)
FROM trucks t
JOIN truck_locations tl ON t.truck_id = tl.truck_id
JOIN jobs j ON tl.location_id = j.location_id
JOIN job_hiring_locations_trucks_join jhltj ON j.job_id = jhltj.job_id AND t.truck_id = jhltj.truck_id
JOIN drivers d ON j.driver_id = d.driver_id
JOIN driver_certifications dc ON d.driver_id = dc.driver_id
JOIN certifications c ON dc.certification_id = c.certification_id
Aug 08 16:34:31 postgres[5883275]: [42-3] "JOIN maintenance_records mr ON t.truck_id = mr.truck_id
JOIN maintenance_types mt ON mr.maintenance_type_id = mt.maintenance_type_id
JOIN job_status js ON j.status_id = js.status_id
JOIN locations l ON tl.location_id = l.location_id
JOIN job_types jt ON j.job_type_id = jt.job_type_id
JOIN job_priorities jp ON j.priority_id = jp.priority_id
JOIN fuel_records fr ON t.truck_id = fr.truck_id
JOIN fuel_stations fs ON fr.fuel_station_id = fs.fuel_station_id
在日志中查找模式:开始查看错误中的各个示例,并查找 OOM 错误之前事件的模式。您是否看到了相同的查询?也许是大型的排序,也可能是大型的 JOIN
操作。您是否看到有大型分析类型的查询?
“could not resize shared memory segment” 的常见修复方法
减少对哈希表的依赖并添加索引
通常来说,哈希表似乎是导致这类错误的罪魁祸首,因此这是一个很好的起点。哈希连接用于跨表的非常大的连接,Postgres 会创建一个内存中的哈希表来存储一些数据。如果要连接的数据足够小,可以容纳在 work_mem
的内存中,但又足够大(或者已建立了索引),以至于嵌套循环效率低下,那么带有大量内存或者 work_mem
设置较大的系统可以优先使用哈希连接,而不是其他连接方法(如嵌套循环或合并连接)。
您可以通过查看查询的 EXPLAIN
计划,来了解查询规划器在使用哪种策略,即:
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM trucks t;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=238.12..238.13 rows=1 width=8) (actual time=5.276..5.276 rows=1 loops=1)
Buffers: shared hit=29
-> Gather (cost=238.01..238.12 rows=2 width=8) (actual time=5.236..5.272 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=29
-> Partial Aggregate (cost=238.01..238.02 rows=1 width=8) (actual time=5.226..5.227 rows=1 loops=3)
Buffers: shared hit=29
-> HashAggregate (cost=238.00..238.01 rows=1 width=4) (actual time=5.213..5.217 rows=3 loops=3)
Group Key: trucks.id
Buffers: shared hit=29
-> Hash Join (cost=37.75..236.75 rows=500 width=4) (actual time=0.605..4.879 rows=70 loops=3)
Hash Cond: (truck_locations.job_id = trucks.id)
Buffers: shared hit=29
-> Seq Scan on truck_locations (cost=0.00..18.20 rows=820 width=8) (actual time=0.010..0.054 rows=10 loops=3)
Buffers: shared hit=3
-> Hash (cost=27.25..27.25 rows=820 width=4) (actual time=0.575..0.576 rows=10 loops=3)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
Buffers: shared hit=26
Planning Time: 0.256 ms
Execution Time: 36.562 ms
由于要连接的数据集相当大,因此可以通过在两个表的连接键上添加索引,来推动规划器转向合并连接而不是哈希连接。连接键本身已经建立了索引,但由于查询中还有用于筛选条件和其他用途的其他条件,因此在索引中包含这些列可能是有益的。
一个好的经验法则是,如果查询在列 A 上有一个 WHERE 筛选条件,并通过列 B 连接到另一个表,那么一个 (A, B) 上的多列索引将会有助于减少连接的数据量。
减小 work_mem
您的 work_mem 可能设置得太大了,这样您允许每个工作进程使用的内存就过多了。
减小 max_parallel_workers
您可能要查看下并行工作进程的设置。如果您的 work_mem 设置较高、大量的并行工作进程和哈希连接,则可能会过度地分配资源。
深入研究查询
在许多情况下,通过特定的查询来提高其性能,可能是解决 OOM 问题的地方。
- 向
SELECT *
查询添加WHERE
子句或LIMIT
,可能是一个很好的起点。 - 创建视图或物化视图来存储表连接的数据,也可以帮助到您的数据库。
添加更多内存
在添加索引并对单个查询执行完所有调优后,如果还会看到这些错误,则可能需要给服务器添加更多内存。
总结
- ERROR: could not resize shared memory segment 可能只是一个查询或操作占用了您的所有内存。
- 如果你刚好碰到了一个这样的错误,那没什么大不了的。如果你有很多这样的错误,那么在将实例升级到更大的内存之前,您可以执行一些简单的动作,来优化查询和添加索引。
- 在日志中查找导致 OOM 错误的查询或进程。
- 第一个要查找的位置是连接大型表的查询,它正在处理的数据可以容纳在
work_mem
中。添加索引以针对性地限制正在处理的数据量,可能会有所帮助。