1. 问题初现:openGauss 突然报“failed: Too many open files”
如果你手里正好维护着一套 openGauss 数据库,某个平平无奇的下午突然发现应用日志、数据库日志里接连出现“failed: Too many open files”这行报错,连接池里的连接成片失效,新的连接也建立不起来,不用慌,这个问题不一定是数据库本身坏了,绝大多数情况下是操作系统或者 openGauss 进程的文件描述符限制被触顶了。我最早遇到这个报错是在一个跑着业务报表的集群上,现象非常典型:客户端连不上,gsql 偶尔能进,但一执行复杂查询就断,日志里反复刷“Too many open files”,当时第一反应是磁盘满了或者内存不够,排查了一圈才发现是文件句柄数被打满。
这个报错从字面上看“打开的文件太多”,很多人会下意识去查磁盘、查日志文件数量,但实际在数据库场景里,触发点往往是连接数、线程数、临时文件、动态库加载、审计日志文件句柄这些更隐蔽的资源。openGauss 作为企业级关系型数据库,进程模型复杂,一个主进程下会拉起大量工作线程、辅助线程,每个连接、每个排序临时文件、每张外表扫描都可能占用一个文件描述符。所以这个报错本质上不是一个“数据库逻辑错误”,而是一个“操作系统资源限制”错误。
这篇内容我会从问题现象、原理分析、排查命令、解决方案、openGauss 特有参数调整几个层面完整拆解,也会把我踩过的坑一并写出来。适合正在维护 openGauss、或者刚接手 openGauss 生产环境的人参考,如果你是初学者,照着命令一步步来也能定位问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么是“文件描述符”被耗尽
2.1 文件描述符到底是什么
文件描述符(File Descriptor,FD)是操作系统为了管理进程打开的文件而分配的一个非负整数。在 Linux 里,一切皆文件:普通文件、目录、socket 套接字、管道、设备文件,甚至连共享内存都被抽象成文件描述符。数据库进程要接受客户端连接,就要创建 socket;要写 redo 日志,就要打开日志文件;要做排序,就要创建临时文件;要加载动态库,也要打开 .so 文件。每做一次这类操作,内核就会给进程分配一个 FD,用完再释放。
可以把 FD 理解成餐厅的座位号:进程是餐厅,文件资源是顾客,座位号就是临时分配给每个顾客的编号。餐厅座位有限,顾客多了就坐不下,后来的顾客只能排队或离开。数据库进程的“座位数”是由操作系统的 ulimit 设置决定的,默认往往只有 1024 或 4096,这对一个生产数据库来说太少了。
2.2 openGauss 进程为什么特别容易吃满 FD
openGauss 预设的最大连接数 max_connections 一般会配置成几百甚至几千。每个客户端连接至少占用 2 个 FD:一个是 socket 通信描述符,另一个是连接对应的线程/会话上下文可能涉及的管道或事件描述符。如果开启了审计日志、慢查询日志、WAL 归档,后台线程还要持续持有日志文件 FD。再加上每个会话执行 SQL 时可能创建排序临时文件、hash 临时文件,这些都是在高并发时段瞬时申请的。
所以经常出现这种情况:数据库当前连接数没有满,但文件描述符先满了。因为连接数是“数据库内部的资源限制”,FD 是“进程级别的系统资源限制”,两者不是一回事。连接数限制在数据库层,FD 限制在操作系统层,中间还隔着 openGauss 自身对文件句柄的封装。
2.3 这个报错带来的连锁反应
一旦 FD 耗尽,影响是灾难性的:
- 新的客户端连接无法建立,报“Too many open files”或连接超时。
- 已存在的连接虽然还在,但一旦执行需要打开临时文件的 SQL,比如 ORDER BY、GROUP BY、Hash Join,就会直接失败。
- WAL 日志写入可能失败,严重时会导致实例 xlog 无法推进,进而触发备机断连或主备切换异常。
- 监控采集、备份工具连接数据库时也会失败,造成监控数据缺失。
所以这个报错不能拖,出现了就要尽快处理,否则故障面会持续扩大。
3. 一步步确认:到底是哪里把 FD 吃光了
3.1 先看进程级 FD 使用情况
登录 openGauss 所在服务器,找到数据库主进程 PID。openGauss 的进程名通常是 gaussdb,用下面命令查看:
bash复制ps -ef | grep gaussdb | grep -v grep
输出里会有一个类似 gaussdb --single_node -D /data/opengauss/data 的进程,记住它的 PID,比如 12345。然后用:
bash复制ls /proc/12345/fd | wc -l
这个命令统计该进程当前打开的 FD 总数。如果数字已经非常接近上限,比如达到几万,那就坐实了 FD 耗尽。
再看进程的 FD 上限:
bash复制cat /proc/12345/limits | grep "open files"
输出会显示类似:
text复制Max open files 65535 65535 files
软限制和硬限制都在这里。如果软限制是 1024,那问题就非常明显了——openGauss 根本没拿到足够多的文件描述符配额。
3.2 找出 FD 被什么占用了
光知道总量不够,得知道被什么吃掉了。遍历一下 /proc/PID/fd 目录,看每个 FD 指向什么:
bash复制ls -l /proc/12345/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
这条命令统计了数量最多的前 20 类 FD 目标。正常情况下你会看到大量的 socket:[...] 和 anon_inode:[eventpoll],还有指向数据目录下文件的各种路径。
我自己排查时发现,两类最容易异常增长:
socket:[...]数量巨大:说明连接数很多,或者存在连接泄漏——客户端不断建立连接但不释放。- 指向
/data/opengauss/data/base/.../pgsql_tmp的临时文件 FD 数量多:说明有大查询在同时执行,产生了大量排序临时文件,并且没有及时清理。
还有一种情况:如果配置了 enable_audit = on,审计日志相关的 FD 也是常驻的,但一般增长缓慢,不会突然打满。真正突然打满大概率是连接风暴或大查询并发。
3.3 顺便看系统全局的 FD 使用情况
进程级排查完,再看系统整体:
bash复制cat /proc/sys/fs/file-nr
这个文件输出三个数字:已分配 FD 总数、未使用 FD 数、系统最大 FD 数。比如:
text复制58240 0 1000000
如果第一项已经接近第三项,说明系统级限制也不够了,需要调高 fs.file-max。如果系统级充足,只是进程级打满,那重点就放在进程的 ulimit 和 openGauss 连接数上。
4. 应急处理:先让业务恢复,再慢慢优化
4.1 最快的止血方法:临时调高进程限制
生产环境出了这个问题,首要目标是恢复业务,不是做根因分析。如果你用的是 systemd 管理 openGauss 服务,可以直接修改 service 文件的 LimitNOFILE,然后 reload 并重启数据库。但重启数据库在一堆业务连接池挂着的情况下是有风险的,所以还有更轻量的做法。
如果 openGauss 进程是手工启动的,或者通过 shell 脚本启动的,可以用 gdb 临时修改进程的 rlimit。不过这个操作太 hacky,很多环境不允许,而且 openGauss 是分布式/共享进程模型,乱动进程属性有未知风险,不建议,除非你真的很清楚自己在干什么。
更稳妥的应急办法是:先杀掉部分空闲连接,释放 FD,让业务喘口气。比如用 pg_terminate_backend(openGauss 兼容 PostgreSQL 的部分管理函数)结束掉空闲会话:
sql复制SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND pid <> pg_backend_pid();
注意,openGauss 部分版本可能叫 pg_stat_activity,也可能叫 dbe_perf.session_stat,需要根据版本确认。不过大多数场景下兼容性没问题。
如果是因为某个应用连接池配置了过大的最大连接数,导致大量空闲连接占着 FD,可以临时把连接池的最大连接数调小,然后强制回收空闲连接。这一步通常能立刻释放几千个 FD。
4.2 修改 ulimit 和 systemd 配置
应急后必须做永久性调整。先修改操作系统用户的 ulimit。openGauss 通常以 omm 用户运行,编辑 /etc/security/limits.conf:
text复制omm soft nofile 65535
omm hard nofile 65535
如果 openGauss 是通过 systemd 服务托管的,还需要在 service 文件里加上:
ini复制LimitNOFILE=65535
然后 systemctl daemon-reload,再重启数据库服务。
这里有个很容易踩的坑:修改了 limits.conf 后,不重启服务不生效。而重启 openGauss 意味着业务会中断,所以如果有可能,先用上面的空闲会话清理恢复业务,再找维护窗口重启。但现实往往是,不重启根本没法完整生效,因为 openGauss 进程已经以旧的软限制运行了。有些场景下可以通过给进程发信号让它重新读取配置吗?答案是不能,rlimit 是进程启动时就固定下来的。所以维护窗口必不可少。
4.3 千万不要只靠重启解决问题
有些人看到这个报错,第一反应是重启数据库。重启后 FD 确实会回到初始状态,但如果根因是连接数过大或连接泄漏,过几个小时又会打满。我见过有现场一天重启三次的,最后发现问题出在应用层连接池的 maxTotal 配置太大,加上 SQL 执行时间过长,连接全部被占满后应用还在不断创建新连接,FD 变成了一个无底洞。
所以重启只是暂时的,必须配合后面的参数调整和业务侧优化。
5. 根治:openGauss 侧参数与系统侧参数协同调整
5.1 系统侧参数调整
系统最大 FD 数一般不用动,除非你的机器上跑了很多数据库实例。但为了保险,可以看下:
bash复制sysctl -w fs.file-max=2000000
如果要持久化,写入 /etc/sysctl.conf:
text复制fs.file-max = 2000000
然后是 sysctl -p 生效。
另外还需要确认 fs.inotify.max_user_instances 和 fs.inotify.max_user_watches,这两个参数影响文件系统事件监听,如果 openGauss 使用了一些依赖 inotify 的组件,也可能间接报错,不过这个概率比较低,一般不用优先处理。
5.2 openGauss 的 max_connections 与 max_files_per_process
openGauss 里有一个和 FD 直接相关的参数:max_files_per_process。这个参数控制每个数据库进程允许打开的最大文件数,默认值通常是 1024 或 1000。可以把它调大,比如 65535:
sql复制ALTER SYSTEM SET max_files_per_process TO 65535;
注意,这个参数不是立即生效的,需要重启数据库才能加载。而且它本身是“每个进程”的限制,不是“每个连接”的限制。openGauss 是共享进程池架构,所有会话都在同一个 gaussdb 主进程下,所以这个参数实际上是整个实例所有线程共享的 FD 配额上限。如果实例连接数多、临时文件使用频繁,建议至少配置为 65535,机器内存足够甚至可以配到 131072。
还有一个相关参数是 max_connections。连接数越多,FD 占用越多。如果业务并行度不需要那么高,可以适当调低。比如原来配置 5000,但实际平均并发只有 800,那就改成 2000,释放大量冗余 FD 配额。这里需要强调:连接数不等于并发数,连接池里大多数连接可能是空闲的。不要盲目把 max_connections 调大,它会直接拉高内存和 FD 占用。
5.3 临时文件相关的参数
如果 FD 占用大头是临时文件,可以考虑限制单个会话临时文件大小:
sql复制ALTER SYSTEM SET temp_file_limit TO '10GB';
这可以防止某个失控 SQL 创建海量临时文件,把 FD 瞬间打满。但要注意,临时文件在磁盘上被删除后,FD 并不一定立刻释放,有些场景下句柄要等文件真正被清理才会关闭。所以调这个参数是“预防”,不是“事后清理”。
另外 openGauss 的高性能排序、Hash Join 都可能使用 work_mem。如果 work_mem 太小,大量排序会落到磁盘,生成很多临时文件。适当调大 work_mem 可以减少落盘频率,从而减少临时文件 FD 的创建。work_mem 是会话级参数,可以用 SET work_mem = '64MB' 临时调整,也可以全局设置:
sql复制ALTER SYSTEM SET work_mem TO '64MB';
不过这要考虑内存压力,work_mem 是每会话分配的,如果 1000 个并发连接都使用 64MB,那就是 64GB 内存,很容易把机器搞挂。所以调这个参数要结合活跃并发量和总内存,不是越大越好。
5.4 连接池侧配置
如果是 Java 应用,经常用 HikariCP、Druid 这类连接池。建议把最大连接数控制在 openGauss max_connections 的 60% 左右,并设置合理的 connectionTimeout 和 idleTimeout,避免空闲连接长期占用 FD。比如 HikariCP 配置:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 200
minimum-idle: 20
idle-timeout: 300000
connection-timeout: 5000
maximum-pool-size 设置成 200,如果一个 openGauss 实例 max_connections 是 1000,那么 5 个应用实例各 200 就是 1000,刚好顶满,实际上还要留出运维连接和后台任务的余量,所以建议所有应用连接池总和不要超过 max_connections 的 70%。
5.5 参数调整后的验证
改完所有参数,重启 openGauss 后,再次检查:
bash复制cat /proc/$(pidof gaussdb)/limits | grep "open files"
确保软限制和硬限制都是 65535 以上。
然后看当前 FD 使用:
bash复制ls /proc/$(pidof gaussdb)/fd | wc -l
重启初期 FD 使用量会比较低,随着连接创建和业务运行会逐渐上升,最终稳定在某一个水位。建议在稳定运行一段时间后,比如业务高峰过后,再统计一次,确认没有异常增长。
6. 我踩过的几个坑和排查技巧
6.1 坑一:只看 nofile 忽略 systemd 覆盖
有一次我改完 limits.conf,用 ulimit -n 检查 omm 用户,显示 65535,以为万事大吉。结果业务高峰又报“Too many open files”。最后发现 openGauss 是 systemd 启动的,systemd 启动的进程不受 limits.conf 控制,必须在 service 文件里加 LimitNOFILE。从那之后我每次排查这个报错,第一步就是先确认这个进程到底是谁拉起来的,是 systemd、crontab 还是手工脚本。不同启动方式的配置入口完全不同。
6.2 坑二:lsof 卡死,改用 /proc
生产环境 FD 到几万级别时,lsof 会非常慢,因为要遍历所有进程的所有 FD。有一次我执行 lsof -p 12345,卡了半分钟没出结果,那半分钟业务还在持续报错。后来我学乖了,直接看 /proc/PID/fd,又快又稳。如果是想统计某个路径下的 FD 数量,用:
bash复制ls -l /proc/12345/fd | grep '/data/opengauss' | wc -l
比 lsof 高效得多。
6.3 坑三:临时文件 FD 一直不释放
之前遇到过一个场景:FD 总量很高,但连接数没有异常,日志文件也不多。排查发现 /proc/PID/fd 大量指向已被删除的临时文件(路径后面有“ (deleted)”标记)。这是典型的 SQL 排序或 Hash Join 落盘后,文件虽然从目录里删了,但进程持有的 FD 还没关闭。这种情况下的根因往往是并发大查询太多,每个查询都创建了很大的临时文件。光调 ulimit 只是拖延了爆雷时间,必须优化 SQL 或调大 work_mem 减少落盘。
有一个技巧是查看数据库当前正在执行的查询:
sql复制SELECT pid, state, query, now() - backend_start AS duration
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC;
找出长时间运行的查询,分析它的执行计划,看看是不是走了全表排序或者 Hash Join 落盘。有些查询优化器可能因为统计信息不准选错了执行计划,重新 analyze 一下表往往有奇效:
sql复制ANALYZE;
6.4 坑四:连接泄漏才是幕后黑手
应用侧如果使用完连接不归还,连接池的连接数会缓慢爬升,最终把 FD 吃光。这种问题在数据库日志里看不到明显特征,只能从连接数增长趋势判断。可以定时采集 pg_stat_activity 里的连接数,画个趋势图:
sql复制SELECT count(*) FROM pg_stat_activity;
如果连接数无业务高峰也持续增长,基本可以断定是连接泄漏。需要应用侧排查是不是每次数据库操作都从连接池获取了连接却没有 close。Druid 连接池有 removeAbandoned 参数可以强制回收泄漏连接,但生产环境要谨慎开启,避免误杀长事务。
7. 日常监控与预防
7.1 把 FD 使用率纳入监控
这个比什么都重要。不要等报错了才去处理,你要在 FD 使用率达到 70% 的时候就收到告警。最简单的做法是用脚本来监控:
bash复制#!/bin/bash
PID=$(pidof gaussdb)
total=$(ls /proc/$PID/fd | wc -l)
limit=$(cat /proc/$PID/limits | grep "open files" | awk '{print $4}')
usage=$((total * 100 / limit))
if [ $usage -gt 70 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') gaussdb FD usage: $usage%" >> /var/log/fd_monitor.log
fi
配合 crontab 每 5 分钟执行一次,基本能覆盖大多数风险。如果有 Zabbix、Prometheus 这类监控,可以直接采集 /proc/PID/fd 的目录项数量,做成图表。
7.2 定期评估 max_files_per_process
参数配好之后不是一劳永逸。业务增长、连接数增加、新的复杂分析查询上线,都可能让 FD 水位发生变化。建议每季度或每半年统计一次高峰期 FD 使用率,对比历史数据,如果发现持续上涨,尽早调整参数或优化应用。
7.3 养成看日志的好习惯
openGauss 的运行日志一般在数据目录下的 pg_log 或 log 目录里,文件名类似 gaussdb-2025-01-01_000000.log。报错“Too many open files”时,日志里往往还有更详细的上下文,比如是哪个模块报出来的。排查时先看报错时间点前后 5 分钟的日志,比盲目看系统日志更有效率。
8. 最后分享一个排查命令模板
如果你接手了一个陌生的 openGauss 环境,怀疑 FD 有问题,直接按下面这个顺序执行一遍,五分钟内就能定位:
bash复制# 1. 找进程
ps -ef | grep gaussdb | grep -v grep
# 2. 查看进程 FD 总量
PID=$(pidof gaussdb)
ls /proc/$PID/fd | wc -l
# 3. 查看进程 FD 限制
cat /proc/$PID/limits | grep -i "open files"
# 4. 查看 FD 使用分布 top5
ls -l /proc/$PID/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head -5
# 5. 查看系统级 FD 使用
cat /proc/sys/fs/file-nr
这五步下来,基本能确认是进程级限制问题、系统级限制问题,还是某个具体资源泄漏。然后按照前面说的应急和根治方案处理就行。
再提醒一点:openGauss 的版本很多,有些参数在不同小版本里默认值有差异,改参数之前先用 SHOW 命令确认下当前值。比如:
sql复制SHOW max_files_per_process;
SHOW max_connections;
SHOW temp_file_limit;
只有先摸清默认值,才知道改动的幅度和意义。不要拿着网上搜到的命令不管三七二十一就往生产环境上怼,尤其 ALTER SYSTEM 这种是持久化参数修改,改错了再改回来也要重启,代价不小。
就我自己的经验而言,openGauss 的“Too many open files”报错,90% 以上都是配置和架构层面的问题,真正是代码 bug 导致的非常少。把这个报错的原理弄懂,把 ulimit、max_files_per_process、连接池这三个点管好,基本就不会再被这个问题折磨。剩下的 10%,就要靠监控和日常巡检去兜底了。
