openGauss OOM 终结指南:从凌晨三次宕机到内存调优的完整复盘

举报
行者·全栈架构师 发表于 2026/08/30 18:07:22 2026/08/30
【摘要】 上线第二周,openGauss 在生产环境连续 OOM 三次,每次都是凌晨业务高峰期,最严重的一次导致订单写入中断 8 分钟。本文完整复盘了这次事故:从最初"加内存"的无效应对,到逐层拆解 shared_buffers、work_mem、连接数与操作系统配置之间的内存博弈,最终通过 6 个参数调整、一套内存水位监控和应急清理机制,让 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 的内存都去了哪里:

010-oom-memory-tuning_diagram_1.png

最大的变量是 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 睡得着觉的监控机制。整体是一套闭环:采集水位 → 阈值判断 → 告警 → 定位 → 处置 → 验证回稳,再回到持续采集。

010-oom-memory-tuning_diagram_2.png

下面按这个闭环的环节逐一展开。

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_activityactive 连接数持续高于 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 gaussdbgrep VmRSS /proc/<pid>/status,结合 pg_total_memory_detail 的峰值列,三方数据对得上才能下结论。

Q3:OOM 后数据库会自动恢复吗?

openGauss 被 OOM killer 杀掉后,如果配置了 systemd 自动拉起,进程本身 30 秒内能重新监听。但要注意两点:

  1. 崩溃恢复会拉长启动时间。openGauss 的脏页在 WAL 里有完整日志,重启时会做崩溃恢复(crash recovery)回放 WAL,数据不会丢,但恢复时间取决于脏页量和 WAL 积压量,我们的环境实测从 2 分钟到 5 分钟不等。
  2. 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 时,按以下决策树逐步排查,避免盲目"加内存":

010-oom-memory-tuning_diagram_3.png

使用说明:从顶部 OOM 告警开始,按"是/否"分支逐层向下排查。每个节点都对应本文前面章节的具体调优方案。最终目标是让内存使用率稳定在 80% 以下,并建立持续监控机制。

总结

OOM 这个问题,最容易被"加内存"这种粗暴方案掩盖。但真正的解法是:

  1. 把每个参数的内存消耗算清楚,而不是凭感觉设
  2. 设硬上限(max_process_memory),让数据库自己管内存,而不是依赖 OS 的 OOM killer
  3. 监控 + 自动清理,即使参数配对了,代码泄漏、突增流量还是可能把内存打满

那次凌晨 3 点的电话之后,我一共调整了 6 个参数(shared_buffers、work_mem、enable_memory_limit、max_process_memory 四个数据库参数 + vm.swappiness、vm.vfs_cache_pressure 两个 OS 内核参数),写了一套监控脚本、加了一个应急清理机制。从那以后 OOM 再也没有发生过。其实参数就那些,关键是算清楚每一块内存去哪了。

下一篇文章讲 openGauss 的日志系统 —— WAL、审计日志、慢日志,怎么通过日志快速定位故障。

互动:你遇到过最让人崩溃的数据库 OOM 是什么场景?是怎么排查出来的?欢迎评论区留言交流。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。