openGauss OOM 终结指南:从凌晨三次宕机到内存调优的完整复盘
摘要:上线第二周,openGauss 在生产环境连续 OOM 三次,每次都是凌晨业务高峰期,最严重的一次导致订单写入中断 8 分钟。本文完整复盘了这次事故:从最初"加内存"的无效应对,到逐层拆解 shared_buffers、work_mem、连接数与操作系统配置之间的内存博弈,最终通过 6 个参数调整、一套内存水位监控和应急清理机制,让 OOM 归零并稳定运行至今。文中给出了可直接复用的配置模板、告警脚本和一张 OOM 排查决策树。
前言
第一次 OOM
2025 年 2 月 18 日凌晨 3:17,我被值班电话叫醒。
“数据库挂了,连不上了。”
登录跳板机,ssh 到数据库服务器,敲命令都卡。等了 30 秒终于看到输出:
# dmesg | tail -20
[117324.156789] oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, pid=23145 (gaussdb)
[117324.156791] oom-killer: Task in /docker/opengauss killed (gaussdb)
[117324.156793] Memory cgroup out of memory: Killed process 23145 (gaussdb)
openGauss 被 OOM killer 杀了。
第一反应:加内存。但加完后第二天又 OOM 了。第三次发生时才意识到 —— 这不是内存不够,是内存分配策略有问题。
openGauss 内存架构
先搞清楚 openGauss 的内存都去了哪里:

最大的变量是 Work Memory Pool —— work_mem 乘以并行执行的查询数。如果同时有 50 个查询各需要 64MB 排序,瞬间就是 3.2GB。如果连接数飙升到 500,这个值更加不可控。
根因分析:三个罪魁祸首
罪魁 1:shared_buffers 设得太大
# 事发时的配置
shared_buffers = 16GB # 物理内存 64GB 的 25%
25% 是常规推荐值。但问题在于 openGauss 还有一大块内存被 操作系统的 page cache 占用。shared_buffers 16GB + page cache(文件 I/O 缓存)+ 其他进程,总计超过了物理内存。
修复:降到 20%:
shared_buffers = 12GB # 保守一点,给 page cache 留空间
罪魁 2:work_mem 没有上限,叠加后爆炸
# 事发时的配置
work_mem = 64MB
看似合理。但问题是,业务高峰期有 200 个活跃连接同时在排序和哈希,理论最大占用是 64MB × 200 = 12.8GB。
而且 openGauss 的 work_mem 是每个查询阶段都可能分配一次(排序一次、哈希一次、聚合一次),一个复杂查询可能吃 3-5 倍的 work_mem。
排查时先定位"谁在吃内存"——openGauss 没有按会话统计内存的系统视图,实用的做法是 pg_stat_activity 找长时间活跃的会话,再结合 /proc 看进程实际 RSS:
-- 找出执行最久的活跃会话(大概率就是内存大户)
SELECT pid, now() - query_start AS duration, state, left(query, 80) AS query
FROM pg_stat_activity
WHERE state = 'active'
AND pid <> pg_backend_pid()
ORDER BY duration DESC
LIMIT 10;
# 把上面的 pid 对到系统进程,看真实物理内存占用
for pid in 23145 23201 23268; do
grep -E "VmRSS|VmPeak" /proc/$pid/status | sed "s/^/[$pid] /"
done
修复:降低 work_mem,并确认逻辑内存管理处于开启状态。openGauss 的内存控制分两层:work_mem 限制单个算子的内存,超限算子把中间结果 spool 到磁盘;enable_memory_limit(默认 on)启用逻辑内存管理,把单个作业可申请的内存在 max_process_memory 盘子内调度(作业实际可用内存 = max_process_memory - 共享内存),避免单个大查询把整个实例打爆:
work_mem = 32MB # 从 64MB 降到 32MB
enable_memory_limit = on # 默认即为 on,确认没被误关
注意:work_mem 本身只限制单个算子,没有"所有工作进程总内存"这样的 GUC。真正的硬上限是节点级的
max_process_memory,这正是罪魁 3 要处理的部分。
罪魁 3:max_process_memory 留余量不足
max_process_memory 是 openGauss 节点可用的最大物理内存(POSTMASTER 类型参数,改完要重启),官方默认值是物理内存的 0.6 倍,建议按"物理内存 × 系数"设置,小内存机器系数取 0.7~0.8,预留空间给操作系统内核。
我们的机器是 64GB,但上线时直接按物理内存 100% 配了(64GB),OS 本身 + 其他进程需要约 8GB,实际可用内存被透支。后果是:节点物理内存先于数据库的内存管理机制耗尽,OOM killer 直接介入——openGauss 的逻辑内存管理(enable_memory_limit + spool 下盘)根本来不及兜底,因为它管的是"节点可用内存",而这个值被配得比真实可用内存还大。
# 修复:按官方建议公式,64GB × 0.75
max_process_memory = 48GB # 给 OS 和其他进程留 16GB
参数生效方式:
gs_guc reload -D $PGDATA -c "max_process_memory = 48GB"不生效——该参数是 POSTMASTER 类型,必须重启实例。重启窗口我们选在业务低峰期执行。
最终调优方案
经过三轮 OOM 和修复后的最终配置。
版本说明:本文基于 openGauss 5.0.0(内核版本,生产部署为 64GB 物理内存的单节点主备形态,内核为 openEuler 22.03 LTS,x86_64 架构)。参数语义在 openGauss 3.x/5.x 系列间保持一致,但具体默认值随版本演进,请以官方文档对应版本为准。以下配置直接来自生产环境 postgresql.conf,已脱敏。
# postgresql.conf — 内存相关配置
# === 缓冲区 ===
shared_buffers = 12GB # 物理内存 64GB 的约 20%
wal_buffers = 64MB
temp_buffers = 16MB # 临时表缓冲区
# === 工作内存(最关键)===
work_mem = 32MB # 每个算子的基础内存
enable_memory_limit = on # 逻辑内存管理兜底,超限算子 spool 下盘
max_connections = 500
reserved_connections = 10 # 为超级用户保留的连接
# === 节点内存上限(POSTMASTER,改完需重启)===
max_process_memory = 48GB # 节点可用最大内存 = 64GB × 0.75
# 逻辑内存管理在此上限内调度,超限算子 spool,而不是被 OS OOM kill
额外的操作系统层面优化
# /etc/sysctl.conf — 操作系统内存配置
# 减少 page cache 占用
vm.vfs_cache_pressure = 200 # 默认 100,提高回收 page cache 的积极性
vm.swappiness = 10 # 默认 60,极小化 SWAP 使用
# 启用内存 overcommit(允许 openGauss 申请超过物理内存的虚拟内存)
vm.overcommit_memory = 0
vm.overcommit_ratio = 80 # 允许 overcommit 到物理内存的 80%
设置 vm.swappiness = 10 的意思是:尽量少用 SWAP。因为 SWAP 到磁盘后,数据库性能断崖式下降,还不如直接 OOM 重启来得快。
调优前后的内存对比
调优前(OOM 事故状态)
# free -h
total used free shared buff/cache available
Mem: 62Gi 58Gi 0.8Gi 0.5Gi 3.2Gi 3.1Gi
Swap: 4.0Gi 0.8Gi 3.2Gi
内存使用率 93%,available 只剩 3.1GB,随时可能再次 OOM。
调优后
# free -h
total used free shared buff/cache available
Mem: 62Gi 42Gi 12Gi 0.5Gi 8.0Gi 18Gi
Swap: 4.0Gi 0.0Gi 4.0Gi
内存使用率 68%,available 18GB,有余量应对流量波峰。
峰值对比
| 指标 | 调优前(事故状态) | 调优后(稳定 2 周后) |
|---|---|---|
| 内存使用率 | 93% | 68% |
| available 内存 | 3.1GB | 18GB |
| SWAP 使用 | 0.8GB(持续换页) | 0 |
| 高峰期峰值 | 逼近 60GB,触发 OOM | 45-48GB,稳定在 max_process_memory 以内 |
| 业务 P99 延迟 | 秒级抖动,订单写入中断 | 稳定在 50ms 附近 |
内存水位监控
调完参数只是第一步。更重要的是一套让 DBA 睡得着觉的监控机制。整体是一套闭环:采集水位 → 阈值判断 → 告警 → 定位 → 处置 → 验证回稳,再回到持续采集。

下面按这个闭环的环节逐一展开。
1. 数据库内部监控
openGauss 提供了 pg_total_memory_detail 视图,可以查看节点上各大块内存区域(shared_buffers、work_mem 池等)的已用内存和峰值,这是判断内存水位的第一入口:
-- 查看节点内存区域使用情况(各大块内存的已用量/峰值)
SELECT * FROM pg_total_memory_detail;
-- 查看连接数分布:连接数是内存水位的先行指标
SELECT
COUNT(*) AS total_connections,
COUNT(*) FILTER (WHERE state = 'active') AS active_connections,
COUNT(*) FILTER (WHERE state = 'idle') AS idle_connections,
COUNT(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_txn
FROM pg_stat_activity;
单条 SQL 吃多少内存,靠 pg_stat_activity + EXPLAIN (ANALYZE, BUFFERS) 定位:
-- 找到当前跑最久的活跃会话
SELECT pid, now() - query_start AS duration, left(query, 80) AS query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY duration DESC
LIMIT 10;
-- 对可疑 SQL 执行 EXPLAIN,看哪些算子发生了 spool(内存超限下盘)
-- Spill / Spool 行数越多,说明这条 SQL 的内存需求越超出 work_mem
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...; -- 替换为可疑 SQL
经验法则:
pg_stat_activity里active连接数持续高于 200,且pg_total_memory_detail中 work_mem 区域峰值逼近max_process_memory - shared_buffers,基本就是 OOM 前兆,先看连接数还是先看慢 SQL,取决于这两个数字谁先越线。
2. 内存告警脚本
#!/bin/bash
# /opt/scripts/memory_watch.sh — 每 1 分钟执行
MEM_THRESHOLD=85 # 内存使用率告警阈值(百分比)
SWAP_THRESHOLD=50 # SWAP 使用率告警阈值
# 计算内存使用率
MEM_TOTAL=$(free -m | grep Mem | awk '{print $2}')
MEM_USED=$((MEM_TOTAL - $(free -m | grep Mem | awk '{print $7}'))) # 用 available 计算
MEM_PERCENT=$((MEM_USED * 100 / MEM_TOTAL))
if [ "$MEM_PERCENT" -gt "$MEM_THRESHOLD" ]; then
echo "[ALERT] 内存使用率 ${MEM_PERCENT}% 超过阈值 ${MEM_THRESHOLD}%"
# 获取 TOP 5 内存消耗进程
ps aux --sort=-%mem | head -6 >> /tmp/mem_top_processes.log
# 获取 openGauss 进程内存详情
pid=$(pgrep -x gaussdb)
cat /proc/$pid/status | grep -E "VmRSS|VmSize|VmPeak" >> /tmp/mem_gaussdb.log
# 发送告警
curl -X POST "http://alert.company.com/api/alert" \
-H "Content-Type: application/json" \
-d "{\"level\":\"warning\",\"message\":\"内存使用率 ${MEM_PERCENT}%\",\"host\":\"${HOSTNAME}\"}"
fi
# 检查 SWAP
SWAP_TOTAL=$(free -m | grep Swap | awk '{print $2}')
SWAP_USED=$(free -m | grep Swap | awk '{print $3}')
if [ "$SWAP_TOTAL" -gt 0 ]; then
SWAP_PERCENT=$((SWAP_USED * 100 / SWAP_TOTAL))
if [ "$SWAP_PERCENT" -gt "$SWAP_THRESHOLD" ]; then
echo "[CRITICAL] SWAP 使用率 ${SWAP_PERCENT}% 超过阈值 ${SWAP_THRESHOLD}%"
# 立即触发 DBA 值班
fi
fi
3. 自动内存清理
当内存使用率超过 90% 时,自动执行温和的清理操作(不能直接 kill 连接):
#!/bin/bash
# /opt/scripts/memory_emergency.sh — 内存紧急清理
# 触发条件:内存使用率 > 90%,由 memory_watch.sh 或人工调用
# 原则:只清"确定无害"的连接,不碰 active 和长事务
# 1. 先记录将被清理的连接清单(必须在终止之前执行,否则查不到)
gsql -d postgres -c "SELECT now() AS ts, pid, usename, state, now() - state_change AS idle_time, left(query, 100) AS query FROM pg_stat_activity WHERE (state = 'idle' AND state_change < now() - interval '30 minutes') OR (state = 'idle in transaction' AND state_change < now() - interval '15 minutes');" >> /var/log/mem_emergency.log 2>&1
# 2. 清理空闲超过 30 分钟的连接(连接池回收失败的典型特征)
gsql -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND state_change < now() - interval '30 minutes' AND pid <> pg_backend_pid();"
# 3. 清理挂起超过 15 分钟的 idle in transaction 连接
# 注意:15 分钟是阈值,业务如果有合法的长事务,先调大这个值再启用
gsql -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND state_change < now() - interval '15 minutes';"
风险提示:
pg_terminate_backend是硬终止,被杀连接的事务会回滚。脚本只针对 idle / idle in transaction 两类"无执行中工作"的连接;永远不要在内存压力下批量终止 active 连接——那会把回滚成本转嫁给剩下的连接,反而加剧内存消耗。另外max_connections是 POSTMASTER 参数,运行时改不动,应急阶段不要指望它。
踩坑汇总
1:SWAP 开启反而导致性能雪崩
现象:第一次 OOM 后加了一块 8GB SWAP 分区做"缓冲",结果当天晚上业务 P99 延迟从 50ms 飙到 2 秒以上,iostat 显示磁盘 util 持续 100%,数据库最终完全不可用。
根因:SWAP 把"内存不够"这个尖锐问题拖成了漫长的换页过程。openGauss 的共享内存(shared_buffers)本身不会被换出,被换出的是会话工作内存和后台进程——这些恰恰是正在执行的查询用的,换页一来一回走磁盘,延迟直接放大 20 倍。
解决:关闭 SWAP,把压力交还给 OOM 机制,用重启换确定性:
# 关闭 SWAP(生产环境建议)
swapoff -a
# 并从 /etc/fstab 中移除 SWAP 行,防止重启后恢复
效果:关 SWAP 后的两周,出现过 2 次瞬时内存打满,均被 systemd 自动拉起,平均恢复时间 30 秒,没有一次演变成小时级故障。
感受:对数据库来说,“快速失败"永远好过"带病运行”。这个判断当时很反直觉——加了 SWAP 明明是"多一层保险",实际是"把保险丝换成了一根慢断的"。
2:max_process_memory 理解错误
现象:第一次配参时把它当成 PostgreSQL 里 shared_buffers 那一类的"共享内存大小"参数,只按"shared_buffers 占多少"来反推,没意识到它定义的是整个节点可动用的物理内存上限。
根因:对 openGauss 内存模型理解错误。官方文档的定义是"设置一个数据库节点可用的最大物理内存",shared_buffers、work_mem 池、连接内存都在这个盘子内调度,而不是"共享内存"这个子集。参数文档里还有一句关键提示:默认值是物理内存的 0.6 倍,目的就是给 OS 内核留余量,防止数据库内存膨胀把物理节点打 OOM。
解决:按"物理内存 × 系数(小内存机器 0.7~0.8)"重算,64GB 机器取 48GB;修改后确认走重启流程(POSTMASTER 参数,gs_guc reload 不生效)。
效果:重配后节点水位从 93% 回落到 68%,逻辑内存管理开始正常工作——大查询超限时会 spool 下盘,而不再直接顶到 OOM killer。
感受:参数名里带 “memory” 不代表它管的是同一种 memory。看参数文档的"参数说明"第一段比看名字靠谱得多,这条经验后面配 NUMA、大页参数时又验证了一次。
3:连接数泄漏导致内存持续增长
现象:参数全部调优完成后,内存水位依然每天涨 2-3GB,一周后稳定在 55GB 以上,pg_stat_activity 里 idle 连接数从正常的 20 多个爬到了 200+。
根因:某个定时任务每次执行都新建数据库连接且不归还,HikariCP 的 max-lifetime 被显式设成了 0(永不过期),连接池只增不缩。连接数上去后,每连接的内存(连接上下文 + 未释放的工作内存)叠加起来就是每天 2-3GB 的增量。
解决:
# HikariCP 配置
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.max-lifetime=1800000 # 30 分钟强制重建
spring.datasource.hikari.idle-timeout=600000 # 10 分钟空闲回收
同时在监控里把"连接数"列为和内存同级的告警指标——连接数是内存水位的先行指标。
效果:连接数稳定在 40 以内,内存日增量归零,水位稳定在 45-48GB 区间。
感受:参数调完不代表问题结束。泄漏是"代码问题",它不遵守你设的任何参数,只会按时间线性地把所有余量吃掉。内存监控如果只看总量不看连接数分布,这种泄漏至少还会再漂一周。
常见问题
Q1:除了参数调优,有没有办法防止单个查询吃掉所有内存?
有两类手段,分别对付"单条 SQL 吃太多"和"SQL 跑太久":
-- 1. 限制单条 SQL 的超时(超过即终止,释放它占用的内存)
SET statement_timeout = '60s';
-- 2. 限制并行度:并行 worker 越多,同一时刻占用的 work_mem 越多
SET max_parallel_workers_per_gather = 2;
推荐直接写在服务端配置里,对所有连接生效(而不是依赖每个应用记得 SET):
# postgresql.conf
statement_timeout = 60000 # 60 秒
lock_timeout = 30000 # 30 秒
idle_in_transaction_session_timeout = 60000 # 60 秒
注意:
statement_timeout = 0是 openGauss 的默认值(不超时),生产环境建议显式设成非 0。如果业务有合法的大批量作业,给它单独建一个高statement_timeout的角色,而不是全局放开。
Q2:怎么看当前哪些查询在吃大量内存?
openGauss 的 pg_stat_activity 没有单查询内存列(pg_stat_statements 也没有 query_mem),可用的组合拳是:
-- 第一步:找出执行最久的活跃会话
SELECT pid, now() - query_start AS duration, state, left(query, 100) AS query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY duration DESC
LIMIT 10;
-- 第二步:对可疑会话执行 EXPLAIN,观察算子是否发生 spool
-- spool 行数 > 0 说明该算子的内存需求超出了 work_mem
EXPLAIN (ANALYZE, BUFFERS) SELECT ...; -- 替换为第一步查到的 SQL
同时看操作系统侧的真实占用:ps -o pid,rss,cmd -C gaussdb 或 grep VmRSS /proc/<pid>/status,结合 pg_total_memory_detail 的峰值列,三方数据对得上才能下结论。
Q3:OOM 后数据库会自动恢复吗?
openGauss 被 OOM killer 杀掉后,如果配置了 systemd 自动拉起,进程本身 30 秒内能重新监听。但要注意两点:
- 崩溃恢复会拉长启动时间。openGauss 的脏页在 WAL 里有完整日志,重启时会做崩溃恢复(crash recovery)回放 WAL,数据不会丢,但恢复时间取决于脏页量和 WAL 积压量,我们的环境实测从 2 分钟到 5 分钟不等。
- OOM killer 不区分进程。它按内存占用挑进程杀,如果同一台机器上还跑着其他服务,被杀的未必是 openGauss——这也是为什么生产环境建议数据库独占机器或至少用 cgroup 限住它的内存上限。
# systemd 自动重启配置(openGauss 服务单元示例)
[Service]
Restart=always
RestartSec=30
更稳妥的做法是给 openGauss 配 cgroup 内存上限(比如 50GB,略大于 max_process_memory),让它被 cgroup 先"温柔地"约束,而不是被内核 OOM killer 直接执行。
Q4:我们服务器是 128GB 内存,配置比例怎么调整?
| 物理内存 | shared_buffers | max_process_memory | work_mem | max_connections |
|---|---|---|---|---|
| 32GB | 6GB | 24GB | 16MB | 300 |
| 64GB | 12GB | 48GB | 32MB | 500 |
| 128GB | 24GB | 96GB | 64MB | 800 |
| 256GB | 48GB | 192GB | 128MB | 1000 |
比例原则:shared_buffers ≈ 20%,max_process_memory ≈ 75%,剩下的留给 OS。
内存调优检查清单
【参数配置】
□ shared_buffers = 物理内存的 20%(不是 25%)
□ work_mem 不宜过大(按 活跃连接数 × work_mem 远小于 max_process_memory 估算)
□ enable_memory_limit = on(逻辑内存管理兜底)
□ max_process_memory = 物理内存的 75%
□ vm.swappiness = 10(尽量不用 SWAP)
【日常监控】
□ free -h 监控内存使用率(阈值 85%)
□ 无 SWAP 使用(SWAP > 0 意味着内存不够了)
□ pg_stat_activity 监控活跃连接数
□ pg_stat_statements 监控查询内存消耗
【应急方案】
□ systemd 自动重启配置
□ 内存紧急清理脚本(终止 idle 连接)
□ 连接池 max-lifetime 配置(不超过 30 分钟)
□ 查询超时设置(statement_timeout)
OOM 故障排查决策树
遇到 OOM 时,按以下决策树逐步排查,避免盲目"加内存":

使用说明:从顶部 OOM 告警开始,按"是/否"分支逐层向下排查。每个节点都对应本文前面章节的具体调优方案。最终目标是让内存使用率稳定在 80% 以下,并建立持续监控机制。
总结
OOM 这个问题,最容易被"加内存"这种粗暴方案掩盖。但真正的解法是:
- 把每个参数的内存消耗算清楚,而不是凭感觉设
- 设硬上限(max_process_memory),让数据库自己管内存,而不是依赖 OS 的 OOM killer
- 监控 + 自动清理,即使参数配对了,代码泄漏、突增流量还是可能把内存打满
那次凌晨 3 点的电话之后,我一共调整了 6 个参数(shared_buffers、work_mem、enable_memory_limit、max_process_memory 四个数据库参数 + vm.swappiness、vm.vfs_cache_pressure 两个 OS 内核参数),写了一套监控脚本、加了一个应急清理机制。从那以后 OOM 再也没有发生过。其实参数就那些,关键是算清楚每一块内存去哪了。
下一篇文章讲 openGauss 的日志系统 —— WAL、审计日志、慢日志,怎么通过日志快速定位故障。
互动:你遇到过最让人崩溃的数据库 OOM 是什么场景?是怎么排查出来的?欢迎评论区留言交流。
- 点赞
- 收藏
- 关注作者
评论(0)