ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL 8.0内存占用过高排查:性能模式与缓存配置优化

MySQL 8.0内存占用过高排查:性能模式与缓存配置优化 1. 问题现场与三个容易误判的方向先交代一下背景。当时手上有一台云服务器配置不算高4核8G的样子上面跑着一个MySQL 8.0实例外加几个内部用的Java服务。某天下午监控群突然开始报警说内存使用率连续五分钟超过90%。我登录上去第一眼看到free -h的结果时心里确实咯噔了一下total used free shared buff/cache available Mem: 7.6G 7.2G 108M 1.2G 328M 236M Swap: 2.0G 1.9G 112MMySQL 一个进程就吃掉了将近 6.8G 的 RES这还没算其他服务。再往下查top排序后的结果更直观——mysqld 的 RSS 名列前茅而且 swap 也已经被吃掉了 1.9G。这种情况如果不处理下一步就是 OOMmysqld 直接被内核杀掉那损失就不是一顿排查能补回来的了。很多人包括当时的我遇到 MySQL 内存占用高第一反应通常是三个方向一是怀疑连接数暴涨。是不是有业务方写了慢查询或者连接池泄漏几百上千个连接挂在上面把内存吃光了。二是怀疑缓冲池配置问题。是不是innodb_buffer_pool_size被人调成了一个明显不合理的值比如小内存机器上直接给了 6G。三是怀疑系统层面内存泄漏。比如 MySQL 版本有 bug、glibc 的内存碎片问题、或者某些 SQL 触发了异常分配。这三个方向确实都合理我也都顺着查了一遍。但实际情况是连接数并不高峰值也不过 80 来个innodb_buffer_pool_size用的是默认的 128M根本没动过版本也不是什么冷门 bug 版本。也就是说这台 MySQL 的压力完全谈不上大但内存占用就是一路涨到了物理内存快扛不住的地步。我后来复盘觉得这恰恰是很多内存排查最容易栽跟头的地方你盯着几个常见嫌疑点查了一圈发现都没问题于是怀疑是“玄学”或“泄漏”但实际上问题一直躺在某个你默认忽略的模块里。2. 拆内存去向MySQL 进程里的内存到底被谁吃了要排查 MySQL 内存问题先得知道 MySQL 的内存都花在哪些地方。我整理过一份清单基本覆盖了绝大部分内存去向排查时对着这个表逐项对照比瞎猜有效率得多。内存去向说明默认配置参考InnoDB Buffer Pool缓存数据页和索引页MySQL 内存占用的大头8.0 默认 128M生产上常见 4G~128G各 Session 私有内存排序缓冲、连接缓冲、临时表内存等按连接数翻倍sort_buffer_size 默认 256Kjoin_buffer_size 默认 256Kread_buffer_size 默认 128Kread_rnd_buffer_size 默认 256Kbulk_insert_buffer_size 默认 8Mmax_connections 默认 151InnoDB 日志与内部结构redo log buffer、adaptive hash index、数据字典、锁信息等redo log buffer 默认 16M细节较多难以一一列出临时表内存临时表由 tmp_table_size 和 max_heap_table_size 控制取两者较小值俩默认都是 16MPerformance Schema / 监控采集内存保存性能监控数据的内部内存5.7 以上默认启用属于隐性大户performance_schema 默认 ON占用可达几百 M 到 1G 以上线程与连接管理内存每建一个连接都要分配线程栈和连接缓冲线程栈默认 1M 左右thread_cache_size 默认 88.0每连接额外开销百 K 到数 M各类全局缓存key_buffer_sizeMyISAM、table_open_cache、table_definition_cache、query cache8.0 已移除key_buffer_size 默认 8Mtable_open_cache 默认 4000这张表本身并不复杂但有个常见误区必须点破很多人以为 MySQL 内存占用主要就是innodb_buffer_pool_size查了一下发现 buffer pool 没调大就放心了。这完全不对。Buffer pool 只决定“InnoDB 缓存数据”的那部分内存但 MySQL 进程的总内存是上述所有项的总和有些模块的用量平时根本不会出现在官方默认配置里却能在特定场景下把内存顶爆。我这次碰到的场景就是一个典型在这台服务器上MySQL 用了默认 128M 的 buffer pool进程 RSS 却逼近 7G。这中间差的 6G 多全来自其他几项。我当时顺着排查发现真正的大头有两个一个是performance_schema在 8.0 上默认开启后吃掉了接近 1.2G 的内存另一个是热更了一个大版本后MySQL 在运行时把一批表的 table cache 和 metadata 锁信息全压进了内存加上连接私有缓冲在低配机器上的叠乘效应最终把 RSS 推到了恐怖的高度。如果你不希望靠猜可以借助一条组合查询直接从 information_schema 和 performance_schema 里把内存占用按模块聚合出来这个我在下一节详细展开。3. 用两套查询直接捅进内存内部定位大头的过程排查内存我的习惯是先看系统侧再看 MySQL 侧。系统侧主要看free -h、top -o %MEM、pidstat -r -p PID 1这些命令能确认 mysqld 确实在持续涨内存排除了其他进程干扰。但真正定位“内存被谁吃了”的关键是 MySQL 提供的性能视图。3.1 查 Current Allocationperformance_schema 内存聚合MySQL 5.7 之后performance_schema提供了内存事件统计能按账号、线程、事件类型把内存分配情况聚合出来。先确认当前是否开启SHOW GLOBAL VARIABLES LIKE performance_schemaON;如果返回 ON直接跑下面这条查询就能看到当前实例里内存分配排在前面的事件类型SELECT EVENT_NAME, COUNT_ALLOC / 1024 / 1024 AS alloc_mb, COUNT_FREE / 1024 / 1024 AS free_mb, CURRENT_NUMBER_OF_BYTES_USED / 1024 / 1024 AS current_mb FROM performance_schema.memory_summary_global_by_event_name ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC LIMIT 20;我在这台机器上跑出来的结果非常有意思排名靠前的几项是EVENT_NAMEcurrent_mbmemory/innodb/buf_buf_pool128.0memory/performance_schema/table_handles约 680Mmemory/performance_schema/events_statements_*约 320Mmemory/sql/user_early_init约 200Mmemory/sql/THD::main_mem_root约 180Mperformance_schema自己那一堆表加起来直接占据了 1G 多的内存完全超出了我一开始的预期。这也是我在实战中总结出的一个重点在 8.0 默认配置里performance_schema 不是“一个开关”而是一整套内存消耗体系尤其当 table_handles 和 events_statements_history 开得很大时它完全可能比 buffer pool 更占内存。3.2 查连接数与连接私有内存接着我用下面的查询确认当前连接数和每个连接的内存开销SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Max_used_connections;-------------------------- | Variable_name | Value | -------------------------- | Threads_connected | 76 | --------------------------结合前面看到的内存表每个连接的 THD 占用按 2M~5M 估算80 个连接也不过 400M。这说明连接数不是主要矛盾但也不能忽略——如果哪天连接数冲到 500这一项就会成为压垮内存的帮凶在低配服务器上尤其危险。3.3 查 table cache 的隐性占用MySQL 对打开的表会做缓存缓存对象包括表结构、表句柄、元数据锁等。你可以用这两条 SQL 看当前 cache 的情况SHOW GLOBAL STATUS LIKE Open_tables; SHOW GLOBAL STATUS LIKE Table_open_cache_overflows;当Open_tables长时间贴近table_open_cache上限且Table_open_cache_overflows一直在增长说明 MySQL 的 table cache 处于高水位运行表结构元数据和句柄占用的内存会在不知不觉中上涨。3.4 系统侧观测的少见但有效的角度除了 MySQL 自带视图我还会做一件事连续几次采样/proc/pid/status里的 VmRSS 和 VmSwap再顺手看一眼 slab 缓存。命令不复杂grep -E VmRSS|VmSwap /proc/$(pgrep mysqld)/status在压力不变的情况下如果 VmRSS 只增不减那基本是内存增长型问题而不是瞬时流量打出来的峰值。这一步的意义在于别急着改参数先把“涨”和“高”区分开。瞬时高和持续涨的处理策略完全不同。4. 重启大法为什么救不了这台 MySQL8.0 缓冲池预热机制很多人的第一反应是“重启一下就好了”。说实话在那个场景下重启确实能把内存瞬间降下来但这不是一个可复现的长期解法。原因有两个4.1 8.0 的缓冲池预热是“按需恢复”而非“崩溃恢复”MySQL 8.0 里innodb_buffer_pool_dump_at_shutdown和innodb_buffer_pool_load_at_startup默认都是 ON。这意味着正常关机时MySQL 会把 buffer pool 里的页地址记录到磁盘文件下次启动时后台线程会按这些记录逐步把数据页加载回内存而不是一次全部加载。重启后的内存下降更多是因为 buffer pool 从 0 开始慢慢升温、performance_schema 相关的缓存也还没摊开。但这只是“暂时的失忆”随着业务流量重新加载内存还是会回到原来的水位。如果根因没找到重启只是帮你争取到一段排查窗口期三五个小时后问题依旧。4.2 重启还会引入一个新的下跌风险缓存命中率下降这也算我踩过的坑。某次线上我为了临时释放内存选择重启 MySQL结果随后一小时主库的 IO 压力翻了将近两倍大量热点查询从走内存变成走磁盘业务响应时间直接上升。重启之前一定要意识到你释放掉的不只是“多余内存”还有 InnoDB 辛苦缓存的热数据页。所以我的建议是除非 OOM 在即、MySQL 已经处于被杀的边缘否则不要轻易重启。先把内存去向摸清楚把可回收的模块回收掉把不可回收的部分用参数限制住这才是成年人该干的事。如果已经到了非重启不可的地步也有一点小技巧先温柔地把某些内存大户临时降下去比如把performance_schema相关的参数往低调再重启同时观察重启后内存的爬升斜率对比复发速度这能帮你判断是哪块配置在持续吞噬内存。5. 落地参数调整三类问题三类改法定位到根因之后我开始动手调参。如果只是简单地把参数降下去很容易引发副作用所以我把内存问题按“可动态回收”“需重启生效”“需要长期控制”三类分开处理。5.1 先做临时回收kill 多余连接 清理表缓存排查过程中发现的连接私有内存和 table cache 都属于可动态回收类型。具体做法找到并 kill 掉长时间空闲的长连接。可用这条 SQL 找出空闲超过 600 秒的连接SELECT id, user, host, db, command, time, state FROM information_schema.processlist WHERE command Sleep AND time 600;逐个确认后KILL掉能立刻释放掉一批 THD 内存。我当时清理了十来个闲置连接RSS 降了几百 M。刷新表缓存释放 table cache 占用的旧句柄。执行FLUSH TABLES;这条命令会关闭当前所有打开的表。注意它不是FLUSH TABLES WITH READ LOCK不会锁库但在业务高峰执行仍会有短暂影响建议低峰操作。执行后Open_tables会大幅下降缓存相关内存会回落。如果连接私有缓冲积压严重可以等低峰时把sort_buffer_size、join_buffer_size这些“按连接分配”的参数调低后让新连接按新值分配。命令如下SET GLOBAL sort_buffer_size 2 * 1024 * 1024; SET GLOBAL join_buffer_size 2 * 1024 * 1024; SET GLOBAL read_buffer_size 1 * 1024 * 1024;这些值在 MySQL 8.0 默认基础上翻倍放宽已经足够应付大多数中小业务对于低配服务器是合理的平衡。注意这些参数是 per-session 分配不是全局池子所以每调大 1M就意味着每个连接额外多吃 1M不是总共多吃 1M。5.2 动刀 performance_schema关掉不必要的信息采集performance_schema在 MySQL 8.0 里默认开启但它收集的很多事件你平时根本用不到。在这些资源有限的环境里我建议把内存消耗最重的几个采集项收窄而不是全关。修改my.cnf后重启生效[mysqld] performance_schema ON performance_schema_max_table_handles 2000 performance_schema_max_cond_instances 2000 performance_schema_max_mutex_instances 4000 performance_schema_max_rwlock_instances 2000 performance_schema_events_statements_history_size 64 performance_schema_events_transactions_history_size 16重点说下performance_schema_max_table_handles。这个参数直接影响table_handles的数量上限8.0 里默认可能是按table_open_cache的倍数自动计算的在我的场景下它被自动扩得很大结果单这一块就占了几百 M。手动收紧到 2000 后配合最大的events_statements_history缩小整个 performance_schema 内存能从 1.2G 压到 300M 左右效果非常明显。要强调的是如果你线上在用 MySQL 慢查询日志、sys schema 或者监控系统依赖 performance_schema 的数据不要贸然全关。收窄到够用比一刀切更稳妥。5.3 给 buffer pool 和 table cache 设定明确预算另一个需要启动时生效的参数就是把关键缓存限制在大致可控的范围内[mysqld] innodb_buffer_pool_size 2G innodb_buffer_pool_instances 2 table_open_cache 1000 table_definition_cache 500这里2G是我根据这台 8G 机器算出来的。把这台机器的内存预算大致拆一下系统本身留 1G其他 Java 服务留 3GMySQL 占 4G 上下再留 1G 余量给突发流量和文件缓存最终把 InnoDB buffer pool 落到 2G~2.5G 比较合适。千万别按“默认 128M”继续裸奔也别大手一挥给到 6G——前者浪费内存后者直接 OOM。有一说一innodb_buffer_pool_instances设成 2 是因为 8.0 里一个实例建议至少 1G2G 的池子拆 2 个实例是合理组合既能缓解并发扫描时的内部锁竞争也不会因为实例过多带来额外管理开销。5.4 不要照搬参数先压测或观察一个周期所有参数改完后不要一把梭重启直接上生产。我的习惯是先在测试环境按同一套参数跑一遍业务自测至少覆盖高峰时段的查询类型观察 mysqld 的 RSS 在重启后 15 分钟、1 小时、24 小时的爬升曲线判断内存是否稳定收敛还是继续单向上涨确认没问题再在低峰期对生产实例分批重启。6. 把一次排查沉淀成长期手段监控、基线与复盘改完参数、内存稳住之后这件事并没有结束。真正有价值的成果是把这次排查变成一套可重复执行的流程。我后来做了三件事推荐你也照做。6.1 给内存加监控和告警在云监控或自建监控里除了看系统内存使用率还要把 mysqld 进程的 RSS、swap 用量单独拉出来监控。我加了两条规则mysqld 的 RSS 超过物理内存的 70% 时告警swap 使用率超过 50% 且持续 10 分钟以上告警。这两条能让我在 OOM 之前预留出排查窗口。顺带一提很多云厂商的内存告警基于整机使用率但 MySQL 作为单进程大户进程级监控比整机监控更早反映问题。6.2 建立参数基线把改完后的参数组合连同当前业务量级、连接数基线、内存水位一起固化下来写成一篇内部文档。这能省掉很多重复排查时间——下次再遇到内存问题先对比“当前参数 vs 基线参数”而不是从零开始逐项翻系统变量。6.3 复盘时区分“占内存”和“耗内存”这是整个排查中我最看重的心得。一个 MySQL 实例内存占用高不等于它在持续消耗内存。buffer pool 大、performance_schema 大、table cache 大这些是“占”着内存是缓存、是池子只要没有持续泄漏或无限增长就未必是问题“耗”则是内存随时间单向上升、无法收敛比如连接泄漏、binlog 缓存异常、存储过程递归等。如果一开始就区分清楚你就能少走不少弯路。我这台机器本质上是“占”得太不合理——performance_schema 默认参数把内存空间撑爆了而不是业务把内存“耗”光了。需要做的只是把这些模块的预算重新规划把不必要吃的内存吐出来。按这套思路操作完我这边 MySQL 的 RSS 最终稳定在 3.2G 左右系统可用内存恢复到了 3.5G连接数和业务表现一切正常。回头看这次排查并没有用到什么高深技巧无非是把 MySQL 内存去向拆细、逐个量化、再做预算分配。但就是这么一套“笨功夫”比很多玄学调参有用得多。
返回列表