正德厚生,臻于至善

PostgreSQL 打开大量 Cursor 的内存消耗解剖

单个 cursor 本身消耗很小,真正吃内存的是它拖住的共享缓冲、临时文件、以及解析 / 执行计划相关的进程私有内存。Cursor 是 “内存放大器”,而不是 “内存大胃王”。下面分层拆解。


一、总账:这些内存从哪来

内存层归属Cursor 相关消耗
共享内存(Shared Buffers + WAL + 锁)整个实例Cursor 打开期间钉住的缓冲页、tuple 快照 / 可见性
进程私有内存(backend process)每个连接进程解析树、计划树、执行状态(大头)
临时文件(temp_files)磁盘 / OS page cache排序、Hash Join、物化超过 work_mem 时溢出
OS 层每个 backend 进程进程堆(C 内存分配器 arena)、栈

Cursor 打开的瞬间几乎不分配共享内存,主要堆在 backend 进程的私有内存里。所以 “cursor 太多 → 内存涨”,涨的是每个连接进程的 RSS。


二、每个 Cursor 具体消耗什么(按占用从大到小)

1. 执行器状态(ExecutorState)—— 最大头

打开 cursor 会建立完整的执行状态:

  • Portal:cursor 的外壳(含查询字符串、计划、tuplestore)。
  • ExprContext:表达式计算上下文,包含内存上下文(MemoryContext)。
  • TupleTableSlot:每一行都要落地到 slot,绑定在内存上下文里。
  • 内存上下文(MemoryContext):PG 的核心。每个 portal 绑定一个 PortalContext,下面又挂 ExecutorState、ExprContext、PerTuple 等子上下文。这些上下文各自有内存池(malloc arena),即使内容释放了,已申请的堆块也可能不还给 OS(PG 内存上下文块按需增长、极少 shrink)。

关键点:内存上下文不会主动收缩。一个 cursor 曾处理过大量行,申请过的内存块会一直保留在进程堆里,直到该 portal 关闭并清理上下文。所以 “打开过” 比 “正在用” 更占内存。

2. 计划树 + 解析树

打开 cursor 会走完整的 parse → analyze → plan:

  • 语法树、查询树(Query)、计划树(Plan)全部驻留内存。
  • 含参数化查询时,Bind 阶段把参数值、计划参数拷进 ParamListInfo。
  • 一次 prepare + 多次 execute 的缓存计划(CachedPlanSource)也占内存,且是常驻的。

3. 排序 / Hash 的私有工作区

游标里若有 ORDER BY / GROUP BY / DISTINCT / JOIN:

  • 未超过 work_mem(默认 4MB):全在进程私有内存里做 tuplesort / hashjoin。
  • 超过 work_mem:溢出到临时文件,此时内存不涨但磁盘 IO 和 OS page cache 上涨 —— 这也是 “内存涨” 的另一种形态(OS 缓存被 tmp 文件吃)。

4. 事务快照(Snapshot)

每个 cursor 打开都会登记一个快照(取决于隔离级别 /WITH HOLD):

  • 快照本身小,但它让旧版本行(dead tuple)不能立即被 VACUUM 清理,导致表膨胀 → 共享缓冲被陈旧数据占满,间接推高共享内存占用。

5. Cursor 声明对象本体

  • cursor name、描述符等:极小,可忽略。

三、致命差异:两种 Cursor 完全不是一回事

类型生命周期内存去向
事务内游标(默认)随事务结束释放释放相对及时,事务提交后 portal 清理
WITH HOLD 游标跨事务存活,直到显式 CLOSE常驻进程内存,不关闭就不释放 → 这才是 “越开越多、内存只涨不跌” 的真凶

大量 WITH HOLD cursor 同时打开 = 每个都锁一份计划 + 执行状态 + 快照,且各自的内存上下文块持续累积,这是生产环境内存飙升最常见的病根。


四、怎么证实 / 怎么排查

-- 1. 看每个连接进程到底开多少游标
SELECT pid, count(*) 
FROM pg_cursors 
GROUP BY pid;

-- 2. 直接量:每个 backend 的私有内存(Linux)
SELECT pid, application_name, 
       pg_size_pretty(pg_stat_get_backend_memory(...))  -- 需扩展
FROM pg_stat_activity;

-- 或 OS 侧
SELECT pid, pg_size_pretty(rss) FROM pg_stat_activity;  -- 参考 pg_stat_activity 内存列

OS 验证(每个 backend 的 RSS 与游标数量正相关):ps -o pid,rss,cmd -p <pid>,对比关闭游标前后的 RSS 差。


五、结论一句话

Cursor 多 → 内存涨,涨的主要是每个 backend 进程私有的执行器状态 + 内存上下文堆块(且不主动归还)+ 被拖住的共享缓冲与临时文件缓存。其中 WITH HOLD 游标 + 事务内快照是内存 “只升不降” 的两个放大器。

赞(0) 打赏
未经允许不得转载:徐万新之路 » PostgreSQL 打开大量 Cursor 的内存消耗解剖

支持快讯、专题、百度收录推送、人机验证、多级分类筛选器,适用于垂直站点、科技博客、个人站,扁平化设计、简洁白色、超多功能配置、会员中心、直达链接、文章图片弹窗、自动缩略图等...

联系我们

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册