MySQL运维实践:文件句柄预算与表缓存配置

举报
数据库小学妹 发表于 2026/11/11 09:38:46 2026/11/11
【摘要】 文章讲清 max_connections 与 open_files_limit 两个额度、句柄被谁占走、分区表为什么最容易踩,给出从 error log 到 /proc/pid/limits 的定位顺序与句柄预算算法。

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

上个月一套报表库开始零星报错。业务同学截图过来,一行英文:Can't open file: './reportdb/t_fact_order.ibd' (errno: 24)。我第一反应是文件坏了,跑去看磁盘,好的;看连接数,没满;看 CPU,三成。重启一下,好了。第二天同一个时间点,又来了。这种"重启就好、隔天又来"的故障,八成是某样东西在被慢慢吃光。

一、先分清两个额度

MySQL 有两个看起来像"上限"的东西,很多人会混。max_connections 是 MySQL 自己管的上限,超了报 Too many connections。open_files_limit 是操作系统给的额度,超了报 errno 24,也就是 Too many open files。两个池子,各有各的闸门,一个满了不代表另一个满。

关键在第二个。句柄是操作系统发的许可证,MySQL 只能申请,不能凭空造。它启动时会读一次进程的额度,通常是 ulimit -n,然后把 open_files_limit 定在这个范围内。如果你用 systemd 或者容器拉起 mysqld,真正生效的是服务配置里的 LimitNOFILE,你在 shell 里敲 ulimit -n 65535 一点用都没有。我这套库就栽在这里,改了半天没生效,重启之后又回到 1024。

顺手记住一条经验关系。句柄的大头是两块:连接数,加上表缓存里打开的表。8.0 里 InnoDB 一张打开的表基本一个 .ibd 句柄,再留几十个给日志和后台文件。网上流传的"表缓存乘二"是 MyISAM 时代的算法,那时一张表要数据文件和索引文件两个句柄,5.7 还多一个 .frm,照抄到 8.0 会算大。所以 table_open_cache 调得越大,对 open_files_limit 的要求越高,这两个参数是一对,只调一个,另一个就会来收账。

二、句柄都被谁占走了

一张表被打开,就占一个文件。8.0 之前还要多一个 .frm,8.0 把表定义收进了数据字典,这一层省了,但 .ibd 还是每表一个。听着不多,可下面这几类会把数量翻上去。

占用方 说明 容易被忽略的点
表缓存中的 .ibd 被访问过的表会留在表缓存里 上限由 table_open_cache 控制,不是"用完就关"
分区文件的每一个分区 逻辑一张表,物理 N 个文件 分区数按天建、建了三年,就是一千多个
每个连接 一条 socket 一个 fd 连接池开得大,这部分跟着涨
临时表 磁盘临时表、内部临时表各有文件 大排序、大 GROUP BY 会临时开一堆
日志与后台文件 redo、binlog、relay log、slow log、error log 数量不多,但是常驻
其他 undo 表空间、doublewrite、socket 文件 常驻,几十个的量级

我那次就是分区表。t_fact_order 按天分区,建了四年,一千四百多个分区。业务查一个月的数据,跨三十个分区,这三十个分区文件全被打开、全部驻留。再加上库里另外几十张分区表,句柄很快见底。表面看是"一张表慢",实际是"一千个文件同时在开"。

三、定位:从报错走到额度

顺序很重要,先确认是句柄,再去找谁占的,别一上来就调参。

第一步看 error log。errno 24 就是 EMFILE,出现在打开文件失败的那一行。日志里通常带路径,能直接看出是哪张表、哪个分区。

第二步看进程实际拿到多少。SHOW VARIABLES LIKE 'open_files_limit' 是 MySQL 认为的额度,/proc/<pid>/limits 是操作系统真正给的。两个数不一致,说明配置没生效,问题在启动方式上。

# MySQL 认为的额度
mysql -e "SHOW VARIABLES LIKE 'open_files_limit';"

# 操作系统真正给的额度
cat /proc/$(pidof mysqld)/limits | grep -i 'open files'

# 当前用了多少(含库文件、日志、socket 等所有 fd)
lsof -p $(pidof mysqld) | wc -l

第三步看使用率和表缓存命中。Open_files 是当前打开的句柄数,除以 open_files_limit 就是使用率。Open_tables 贴着 table_open_cache 不动,说明缓存被打满。Opened_tables 一直涨、Table_open_cache_misses 也在涨,说明缓存偏小,表在反复开关。

SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Open_files','Open_tables','Opened_tables',
  'Table_open_cache_hits','Table_open_cache_misses',
  'Open_streams'
);

-- 用率算出来,直接当监控指标
SELECT
  (SELECT VARIABLE_VALUE FROM performance_schema.global_status
     WHERE VARIABLE_NAME='Open_files') AS open_files,
  @@open_files_limit AS fd_limit,
  ROUND((SELECT VARIABLE_VALUE FROM performance_schema.global_status
     WHERE VARIABLE_NAME='Open_files') / @@open_files_limit * 100, 1) AS used_pct;

lsof 的数字会比"表+连接"多一点,因为它把动态库、字符设备也算进去了。方向是对的,看趋势和量级够用,别拿它当精确的账本。

四、分区表为什么最容易踩

分区表在逻辑上是一张表,在文件层面是 N 个表空间。按天分区,一年就是三百六十五个文件;保留四年,一千四百多个。这个数字在设计阶段几乎没人算,因为建分区的时候只关心"查询能不能裁剪"。

更要紧的是,分区一旦被访问,就会进表缓存占住句柄。报表场景喜欢跨月、跨年扫,一次查询就能把几百个分区同时拉起来。这时候 table_open_cache 越大反而越危险,它会把句柄一直攥着不放。

所以分区设计要连句柄预算一起做。我的做法是先定住句柄总数,再倒推分区数:如果这台实例打算给表缓存留 8000 个句柄,所有分区表的分区数加起来最好控制在一半以内,也就是四千个上下。落成表就是 40 张表、每张 90 个分区这个量级。超过这个数,就按周分区,或者干脆把冷数据归档到独立实例。

五、调参:不是越大越好

table_open_cache 调大能减少反复开关表,但代价有两层。一层是内存,缓存的表对象要占内存;另一层就是句柄,调大等于向操作系统多要许可证。所以在句柄已经紧张的时候,调大它等于火上浇油。

table_open_cache_instances 是另一回事,它把表缓存分成多个实例,减少高并发下的锁竞争。这个参数不影响句柄总数,属于纯优化项。innodb_open_files 单独管 InnoDB 自己同时打开的 .ibd 数量,和 table_open_cache 不是一回事,调的时候别只改一个。

真正要动的,是 open_files_limit 和服务配置。两处都要改:服务单元里把 LimitNOFILE 提到 65535,配置里把 open_files_limit 显式写成一个数,比如 20000。不要留空让它自动算,自动算的结果经常不是你想要的。

# my.cnf
[mysqld]
open_files_limit   = 20000
table_open_cache   = 4000
table_open_cache_instances = 16
innodb_open_files  = 4000
# systemd 单元(mysqld.service)
[Service]
LimitNOFILE=65535

改完记得重启,并且重启后再对一次 /proc/<pid>/limits。这一步别省,我见过改完配置文件、以为生效了,实际服务被另一个单元拉起来的情况。

六、治本:少开文件

额度提到位只解决一半,另一半是"别一次开那么多"。三个方向,按性价比排。

分区数收敛。这是最大的一块,也是最容易被忽略的。按周或者按月分区,比按天省一个数量级。跨大范围的查询改成按分区扫、加汇总表,不要每次都让引擎去开几百个文件。

临时表和日志要管住。大排序、大 GROUP BY、SELECT * 都会催生临时表,把 tmp_table_size 和慢查询一起看。通用日志、审计日志这些"顺手打开"的功能,用完就关,它们也在吃句柄。

表太多就分实例。一个实例挂几千张表,无论怎么调参都在走钢丝。按业务域拆库,比继续加参数稳。

七、避坑清单

ulimit 和 systemd 的 LimitNOFILE 两处都要看,只改一处等于没改。我一开始就在 shell 里改,重启服务之后所有值回到 1024,白折腾一天。改完必须用 /proc/<pid>/limits 复核一次。

句柄监控要按使用率报警,不要按绝对值。Open_files 到 70% 就该看,到 85% 就该动手,别等 errno 24 出现,那时候业务已经在报错了。这条同样适用于连接数,只是阈值不同。

最后一条是我自己的教训。故障第二天我选择了重启,第三天又重启。第三次我才去看 error log 里的路径,一眼看到是分区表。重启能解决"当下",但会抹掉现场。现在我遇到"重启就好"的故障,第一件事是先把 error log、SHOW GLOBAL STATUS 和 lsof 的计数抓一份下来,再重启。留证据比抢时间重要。

写在最后

句柄是操作系统发的额度,MySQL 只能要,不能造。所以这类故障的解法顺序是固定的:先确认额度有没有给到位,再确认是谁在占,最后才是收敛占用方。

连接数、内存、磁盘这三样一般人都会盯。句柄不在日常看板上,所以它一旦出问题,就会伪装成"偶发的文件打不开"。把 Open_files 使用率加到一个监控盘里,成本很低。

你们库里的分区表建了多少个分区?有没有算过它要吃掉多少句柄?评论区说说你的数,我帮你核一下预算够不够。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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