MySQL 占用内存过高调优:buffer pool 与连接数参数实测

MySQL 内存调优封面:数据库服务器内存分配示意

凌晨收到服务器内存告警,登上去一看,mysqld 一个进程吃掉了整机 70% 以上的内存,网站响应也跟着变慢——这个场景很多站长都不陌生。大多数人的第一反应是把 MySQL 配置往小改,希望它少占点内存,结果缓存命中率掉下来,数据库反而更慢了。为什么会出现这种越调越糟的情况?因为 MySQL 占用内存过高在多数情况下不是”病”,而是它在按你给它的配额缓存数据。这篇文章会带你先搞清楚内存到底被谁拿走了,再通过一组实测数据,教你如何调整 buffer pool 和连接数相关参数,在不牺牲性能的前提下解决内存告警问题。

本文的实测环境是一台 4 核 8GB 内存的独立服务器,MySQL 8.0,数据量约 6GB。配置不同的机器同样适用文中的公式和验证方法,只是数值需按比例换算。

先搞清楚:MySQL 的内存都花在哪了

在动手改任何参数之前,先要弄明白内存构成,否则调了一个参数、实际吃内存的却是另一个。MySQL 8.0 的内存大头集中在四块:占比最高的 InnoDB Buffer Pool(数据页和索引页缓存,默认 128MB,很多”优化模板”会直接给到物理内存的 50%-75%);每个连接的线程内存(sort_buffer_size、join_buffer_size 等参数每连接分配,256 个连接乘以几 MB 就是好几个 GB);默认开启的 performance_schema 统计开销(8.0 下约 100-400MB);以及大查询临时表等波动项。

一个实际案例可以说明”每连接内存”的威力:测试机上曾有应用把 max_connections 设成 1000,同时 sort_buffer_size 配了 4MB。乍看只是两个普通数字,但按最坏情况算,光排序缓冲理论上就能膨胀到 4GB——这就是内存告警的真相。

怎么验证自己机器上的情况?用下面这条命令查看 Buffer Pool 的实际配置:

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'max_connections';

返回的字节数换算成 GB 后,和 free -h 里 mysqld 的实际占用对一下,就能大致判断内存主要被哪一块吃掉。如果 Buffer Pool 只配了 128MB 但进程占了 5GB,那问题几乎一定出在连接数或每连接缓冲区上。

Buffer Pool 实测:不同配置下的内存占用与命中率对比

Buffer Pool 实测:多大才合适

Buffer Pool 的经典公式是物理内存的 50%-75%,但这个比例只适用于专用数据库服务器。如果机器上还跑着 Web 服务、PHP-FPM、Redis,就要给它们留出空间。为验证不同配置的真实差异,我们在测试机上依次把 Buffer Pool 设为 1GB、4GB、6GB,每组跑 30 分钟混合读写压测(读为主,约 8:2):

Buffer Pool 空载内存 压测后内存 缓存命中率 平均延迟
1GB 0.9GB 1.6GB 87.2% 4.8ms
4GB 3.8GB 4.5GB 99.1% 1.2ms
6GB 5.7GB 6.4GB 99.3% 1.1ms

这组数据说明两个问题。第一,从 1GB 提到 4GB,命中率从 87% 跳到 99%,延迟降到约四分之一——数据量 6GB 的库,Buffer Pool 至少要能装下热点数据集才有意义。第二,从 4GB 提到 6GB 只换来 0.2 个百分点的命中率提升,多花 2GB 内存收益微乎其微。热点数据集的大小才是决定性因素,盲目堆到物理内存的 75% 并不划算。

修改 Buffer Pool 有两种方式。MySQL 8.0 支持在线调整,不用重启:SET GLOBAL innodb_buffer_pool_size = 4294967296;(即 4GB)。在线调整是分块渐进的,大内存实例上可能持续几分钟,建议在低峰期执行;要永久生效,再把 innodb_buffer_pool_size = 4G 写进配置文件的 [mysqld] 段即可。

调整完成后,用 SHOW STATUS LIKE 'Innodb_buffer_pool_read%' 观察命中率变化,确认新配置确实带来收益。对同时承载多个站点的机器,数据库往往和 Web 服务共享内存,我们建议按”物理内存减去其他服务峰值占用,再留 20% 余量”来定 Buffer Pool 上限,而不是套用固定比例。

连接数与每连接内存:被忽视的放大器

Buffer Pool 的大小是确定的,连接带来的内存却是弹性的,这也是很多内存告警的真正来源。MySQL 里真正决定线程数的参数有两个:max_connections 是允许的最大连接数,而 thread_cache_size 控制空闲线程的复用缓存。真正危险的是那组”每连接缓冲区”参数——它们平时只按需分配几 KB,一旦查询涉及大排序或大关联,就会涨到你配置的上限。

我们做了这样一组实测:同一个 8GB 实例上,保持 Buffer Pool 4GB 不变,分别用 100 和 400 个并发连接跑相同压力脚本,其中 10% 的查询带 ORDER BY 大结果集排序:

场景 连接数 sort_buffer_size 峰值内存 OOM 保护
A 100 256KB(默认) 4.9GB 否
B 400 256KB(默认) 5.6GB 否
C 400 4MB 7.8GB 险些触发

场景 C 的数据很直观:同样的 400 个连接,仅把 sort_buffer_size 从 256KB 放大到 4MB,峰值内存就从 5.6GB 涨到 7.8GB——排序类查询占比一成时,流量高峰足以让 8GB 的机器失去安全边际。

关于 max_connections 该设多少,与其拍脑袋,不如按内存倒推:每连接内存上限约等于 sort_buffer_size、join_buffer_size、read_buffer_size、binlog_cache_size 之和;总预算约为”物理内存减去 Buffer Pool 和系统预留”的 80%;max_connections 不应超过总预算除以每连接上限。

以本文测试机为例:8GB 减去 4GB Buffer Pool,再留 2.5GB 给系统和其它服务,剩余按 80% 算约 1.2GB 预算。每连接上限 1MB 时,max_connections 设 1000 以内都安全;配到 8MB 时安全值就只剩 150 左右。

这里要特别提醒使用共享主机的读者:虚拟主机环境下的数据库参数由服务商统一管控,单个用户无需也无法自行调整;而使用 VPS(虚拟专用服务器,由物理机虚拟化切分出的独立小服务器,详见 Hostease VPS 方案)或独立服务器的用户拥有完整控制权,需要自己完成这套计算和验证。另外,连接数长期打满往往不是 max_connections 太小,而是应用没有用连接池、每次请求都新建连接——这种情况先修应用层,比一味调大参数有效。确需限制连接时,可以配合 max_user_connections 给单个用户设独立上限,避免一个失控应用占满连接池。

一套可复用的排查流程

一套可复用的排查流程,遇到”MySQL 占用内存过高”时按下面的顺序处理:

  1. 确认内存分布:用 innodb_buffer_pool_size 配置值和 free -h 输出对比,判断大头是 Buffer Pool 还是连接内存
  2. 评估命中率:查 Innodb_buffer_pool_read_requests 与 Innodb_buffer_pool_reads 的比值,低于 95% 说明 Buffer Pool 偏小,高于 99.5% 时再加大意义有限
  3. 核对连接配置:检查 max_connections 与每连接缓冲区参数的乘积,确认最坏情况不会突破物理内存
  4. 一次只改一个参数:修改后在低峰期观察 24 小时,对比前后差异再决定下一步

这套流程的核心思路是”用数据定位问题,而不是凭感觉改配置”。参数之间存在联动,一次改多个会让你无法判断收益来自哪一项。

MySQL 内存调优排查清单:从监控到验证的完整流程

总结

回到开头的场景:mysqld 占用 70% 内存本身不一定是问题,关键是这部分内存有没有换来对应的缓存命中率。核心结论是:Buffer Pool 按”热点数据集”定大小,改完后用命中率验证收益,到 99% 以上就适可而止;每连接缓冲区保持默认值或更小,先算清最坏情况再决定要不要放大;一次只动一个参数,观察一个完整业务周期后再评估。

内存调优没有一劳永逸的数值,只有符合你自己数据量和流量特征的配置。建议把本文的排查流程保存下来,每次内存告警时按步骤走一遍,多数情况十分钟内就能定位到具体参数。如果你在托管环境部署数据库、希望有更省心的资源隔离,Hostease 的独立服务器提供独占内存和 root 权限,可以考虑配合文中的方法把 Buffer Pool 配到位;想进一步了解链路提速,也可以读一读我们的网站 TTFB 优化指南。

发表评论