ARTICLE DETAIL

资讯详情

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

MySQL内存占用过高?从参数到底层原理的排查与优化指南

MySQL内存占用过高?从参数到底层原理的排查与优化指南 MySQL内存占用过高这活儿我前前后后接了几十次求助了。很多朋友装完MySQL顺手把配置一填也不管默认参数适不适合自己机器跑两天发现内存吃了好几个G第一反应就是“MySQL是不是有毛病”。其实大部分时候MySQL挺冤的它只是在认真执行你给它配的缓冲策略。要搞清楚它为什么占这么多内存先别急着杀进程或者重启你得先弄明白内存究竟花在哪儿了。这篇文章我就把整个排查思路、关键参数、实操命令和踩坑记录全部摊开讲适合刚接手MySQL运维的同行也适合那些被监控系统告警轰炸过的人参考。1. 先搞清楚MySQL的内存到底花在哪里1.1 全局缓冲一上来就吃掉大半内存MySQL里最占内存的头号大户必须是innodb_buffer_pool_size。这个参数管的是InnoDB用来缓存数据页和索引页的内存池它的设计初衷就是“尽量把热点数据留在内存里少碰磁盘”。你可以把它理解成超市里的货架进货越多东西越齐顾客查询拿得越快但货架本身要占大量仓库空间。默认情况下MySQL 8.0安装完后innodb_buffer_pool_size是128M说实话这数值在现在动辄64G内存的服务器上显得很抠门。但很多云厂商的镜像或者一键脚本会把innodb_buffer_pool_size直接推到物理内存的70%甚至80%如果你的机器只有8G内存还跑着别的服务那系统直接卡成PPT是完全可能的。除了这个主角还有一堆全局的缓存会占用内存比如MyISAM引擎的key_buffer_size虽然MyISAM用得越来越少了但如果你还在用系统表或者老业务它一样会按你配的数值吃内存。另外还有table_open_cache和table_definition_cache这俩控制的是表描述符和数据字典相关对象的缓存数量。表一多这两个值如果设得太大光缓存的元数据就是几百M的级别。还有一类容易被忽略的全局内存是Percona或者MariaDB里比较常见的performance_schema内存池以及二进制日志相关的binlog_cache和binlog_stmt_cache。这些虽然不是大头但累积起来你从ps里会看到真正的RES比预期高出一截。1.2 线程缓冲连接数一多内存翻倍如果说全局缓冲是“固定支出”那线程缓冲就是“按人头算的弹性成本”。MySQL每建立一个连接都会给这个连接分配一组私有的缓冲区。最典型的就是sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size和net_buffer_length。这里有一个特别容易被新手忽视的内存放大效果。假设你把sort_buffer_size设成8M心想这不就8M嘛没多少。但是连接数一旦涨到100光是排序缓冲就占了800M如果再叠加join_buffer_size的4M、read_buffer_size的256K粗略算一下200个连接就是接近2.5G。更可怕的是这些缓冲并不是说你不用它就省下来MySQL往往是按配置值直接分配的尤其sort_buffer_size这类在排序操作时才会真正用满但连接栈本身依然会占用一定的虚拟内存。所以我每次遇到“MySQL内存占用过大”的问题第一件事除了看buffer_pool就是去看max_connections和这几个*_buffer_size的值。很多人一台16G的机器连接数上限配个1000线程缓冲也配得很大那内存真是有多少吃多少。1.3 那些容易被忽略的“隐性内存”还有一种情况是你看参数配置都没问题但内存就是持续偏高。这时候要留个心眼MySQL不是只有表数据和索引才会吃内存下面这几类隐性开销很容易让人误判为“内存泄漏”。首先就是Performance Schema。MySQL 5.7及8.0默认开启performance_schema它会根据表的数量、连接数、语句数量动态扩展内存。它内部有大量的events_statements_summary_by_digest、memory_summary_by_thread这类统计表监控项一多内存就会明显上去。我见过一台MySQL在4G内存的机器上光是performance_schema就用掉了500多M。其次是预处理语句Prepared Statement和游标。如果业务代码里频繁使用prepare但没有释放或者连接的max_prepared_stmt_count没限制服务器端累积的预编译指令对象会一直吃掉内存时间长了就会造成一个缓慢但持续的涨势。再一个某些存储引擎的内存表和临时表也会占内存。MEMORY引擎直接把数据放到临时表里就是你建表时用的ENGINEMEMORY只要数据量控制不住MySQL进程的RSS就会一路飙升。还有排序和分组操作如果结果集大于tmp_table_size或者max_heap_table_sizeMySQL会先在内存临时表里处理实在放不下才转磁盘临时表但在转之前内存已经实实在在用出去了。2. 排查前的准备先量化再动手2.1 从系统层面确认现状在改任何参数之前第一步永远是看系统当前的状态。我习惯先敲这么几条命令把内存底数和MySQL进程占用量确认清楚free -h top -c ps aux --sort-rss | head -10free -h能让你快速看到物理内存、已用、剩余和buff/cache的情况。这里有一个容易踩的坑Linux的buff/cache是会被回收的你看到free显示内存所剩无几并不代表系统马上要崩而是说明大量内存被当作文件缓存用掉了。但MySQL进程的RES才是真正主动占用的内存不能跟buff/cache混淆。接着用ps aux --sort-rss把进程按照内存占用排序找到mysqld进程的PID再用pmap看看它的内存映射细节pmap -x pid | sort -k3 -n -r | head -20pmap输出的内容虽然琐碎但你能看到heap、anon、mmap这些匿名内存映射的空间大小。如果发现某个大块匿名内存已经超过了buffer_pool的配置值那就要怀疑是不是连接数、临时表或者performance_schema吃掉了。2.2 用SQL和视图定位内存去向系统命令只能看到进程整体的内存真要拆到MySQL内部还得靠它自己的统计视图。我先给出一套可以直接抄的定位SQL-- 查看全局内存相关变量 SHOW VARIABLES LIKE %buffer%; SHOW VARIABLES LIKE %cache%; -- 查看当前实际生效的连接数 SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Threads_running; -- 查看InnoDB Buffer Pool的使用情况 SELECT pool_id, pool_size, free_buffers, database_pages, modified_database_pages FROM performance_schema.innodb_buffer_pool_stats; -- 按事件类型汇总内存消耗 SELECT event_name, current_alloc, current_number_of_bytes_used FROM performance_schema.memory_summary_global_by_event_name ORDER BY current_alloc DESC LIMIT 20;重点看第二个查询里的current_alloc字段。它会按照内存事件的名称把内存消耗列出来比如memory/innodb/buf_buf_pool、memory/sql/THD、memory/performance_schema/events_statements_summary_long等等。这一下子就能看出来到底是InnoDB的缓冲池占了大头还是连接线程相关的内存占了大头。还有一个超级实用的命令是SHOW ENGINE INNODB STATUS里面有一段BUFFER POOL AND MEMORY会直接列出Buffer pool size、Free buffers、Database pages这类关键数据。你可以判断buffer_pool是否已经满到需要频繁做页面淘汰。再加上这几条基本就能定位占内存的原因了-- 平均栈大小 SHOW STATUS LIKE Max_used_connections; SELECT max_connections, thread_stack, thread_cache_size; -- 临时表使用情况 SHOW STATUS LIKE Created_tmp_disk_tables; SHOW STATUS LIKE Created_tmp_tables;比值如果很高说明SQL需要好好优化也说明临时内存表在用完前可能出了大问题。2.3 判断是“缓存策略”还是“异常泄漏”排查到这一步我倾向于先把问题定性。也就是说要区分当前的内存占用是MySQL正常的缓存策略还是逻辑错误导致的持续增长。看RSS曲线的走势是最直观的方式。如果MySQL进程的内存在一周内保持平稳只在业务高峰期波动那基本属于正常范围不需要紧张。真正要警惕的是两类曲线一类是“楼梯型”上涨比如每次批量任务跑完后内存就跳高一个台阶且永远降不下来另一类是“指数型”暴涨十几分钟内存从3G飙到20G多半是某个会话或者某条SQL在疯狂申请内存比如超大的排序、JOIN缓冲或者内存临时表。这种定性不用多么高深的技术在Zabbix、Prometheus或者云监控里拉几条时间序列就行。关键是要在动手改参数之前先确认“病”和“药”对得上。很多人一看到内存高就盲改innodb_buffer_pool_size结果如果是连接数暴增导致的你调buffer pool不仅没用反而会让磁盘IO雪上加霜。3. 核心参数调整与实操配置3.1 重中之重压对innodb_buffer_pool_size从实际经验看innodb_buffer_pool_size的调整是内存治理最核心的动作。它占据MySQL总内存的70%-80%是很常见的事所以它的合理取值直接决定你的“内存预算”够不够用。我的做法是这样的先明确这台服务器是专用MySQL还是混布。如果是专用机器且数据量和热数据量都很大那可以把innodb_buffer_pool_size设置为物理内存的60%-70%如果机器上还跑着应用服务、监控Agent或者日志采集组件那Buffer Pool的比例就要降到50%以下给系统留出足够的余量。这个参数的合理设置有个数学直觉如果你发现Innodb_buffer_pool_read_requests和Innodb_buffer_pool_reads两个状态值之间的缓存命中率长期低于95%而内存却还有富余那说明Buffer Pool偏低可以调高一些。反过来如果缓存命中率已经很高再调高意义也不大反而占内存。在MySQL 8.0里这个参数是支持动态调整的-- 动态修改为4G SET GLOBAL innodb_buffer_pool_size 4294967296;但要注意动态修改只能保证新的内存分配不会立即释放已存在的页面。因为InnoDB是按照innodb_buffer_pool_instances来分池管理的调整时要尽量在业务低峰期做避免内存重新分配时短暂的抖动。还有个经验是Buffer Pool设得越大InnoDB的预读和淘汰策略就越重要。一定要关注innodb_old_blocks_time这个参数控制的是冷数据在旧子列表中停留的时间。我一般设置为1000毫秒让全表扫描的大块数据不会轻易把真正的热数据挤出缓存能减少无效的内存置换。3.2 连接和线程缓冲如何在性能和内存之间找平衡max_connections是另外一个需要重点审查的参数。很多默认配置或者某些历史遗留下来的配置会把max_connections直接设成1000或者2000但实际上日常连接数可能只有几十。问题在于每次连接分配的那组线程缓冲都要吃内存连接上限管得越宽内存上限就越高。我先教你一个计算连接线程内存总量的简化公式这比记住一堆官方文档更实用每连接线程缓冲 ≈ sort_buffer_size join_buffer_size read_buffer_size read_rnd_buffer_size net_buffer_length thread_stack举个实际例子默认情况下sort_buffer_size256K、join_buffer_size256K、read_buffer_size128K、read_rnd_buffer_size256K、thread_stack256K保守算一个连接至少占用1M级别的分配。如果max_connections1000那理论上的线程缓冲上限就是1GB。当然实际并不会全部同时分配但连接一爆炸起来内存和CPU同时飙高是常有的事。所以我一般建议把max_connections压到实际需要的1.5到2倍。比如业务高峰期连接数在200左右那就设成400到500别再高了。同时把sort_buffer_size这类参数控制在合适区间宁可让排序做慢点也别让内存崩掉。值越大并不代表SQL跑得越快因为大排序缓冲意味着每个连接都会占用大量内存。另外要设好wait_timeout和interactive_timeout尽量避免空闲连接长时间占用线程资源。别忘了max_used_connections这个状态值它记录的是历史最高的并发连接数你可以用它来衡量自己设置的max_connections是否合理。3.3 排序、临时表与堆外内存的治理连接数控制住了接下来处理临时表和排序相关的内存。这一块特别容易出现“看似什么都没配内存却莫名其妙涨”的情况。tmp_table_size和max_heap_table_size这两个参数控制的是内存临时表的上限。默认值一个16M一个16M但有些环境会被调得很大比如256M甚至512M。问题来了一条大SQL做GROUP BY或者ORDER BY时如果结果集超过这个上限MySQL会先把结果放到内存临时表超过后再转磁盘。转磁盘的过程虽然不需要持续占内存但内存临时表本身是一次性分配的。如果并发执行的多条SQL同时触发内存临时表最后占用的内存就是吞吐量乘以单条临时表大小。我见过一个真实案例一条查询的执行计划生成一个约80M的内存临时表正常情况下没问题但是业务方同时开了几个窗口跑同类查询瞬间内存就冲高了4-5G。解决思路不复杂要么SQL层面优化掉临时表如果暂时改不了SQL就把tmp_table_size调到合理值避免单条SQL吃掉太多内存。顺带一提binlog_cache_size它管的是事务在提交前暂存二进制日志变更的内存缓冲。如果业务有大量大事务binlog_cache不够用就会创建磁盘临时文件但那也不会明显减少内存占用。所以通常情况下保持默认16M/32M就够了不要为了优化写入去调它内存收益很低。最后再提一个有点偏门但杀伤力很大的点performance_schema。如果不需要细化到语句级的内存统计可以把performance_schema里用不到的instrument和consumer关掉。具体操作是这样-- 关闭不必要的内存统计 UPDATE performance_schema.setup_consumers SET ENABLED NO WHERE NAME LIKE %memory%;关闭之后performance_schema的内存开销会明显下降。但注意如果你正在排查内存问题就别急着关毕竟要靠它来定位。3.4 动态调整与重启生效的选择在MySQL 5.7和8.0中大部分Buffer Pool相关的参数都能动态调整但也有部分参数需要写进配置文件后重启才生效。我建议把修改分成两步走先在线调整动态参数应用一段时间观察状态确认稳定后再把配置固化到my.cnf里免得下次重启恢复原状。需要写在配置文件里的典型参数包括innodb_buffer_pool_size、max_connections、performance_schema、table_open_cache。在线改的时候也要注意SET GLOBAL只对新连接生效已经存在的连接通常不会重新分配线程缓冲。所以改完连接相关参数之后最好还是让连接平滑重建比如在低峰期重启一下服务或者让应用端重新建立连接池。另外一个很容易被遗漏的操作如果你调整了Buffer Pool大小建议同时设置innodb_buffer_pool_dump_at_shutdownON和innodb_buffer_pool_load_at_startupON。这两个开关的作用是在关停时把Buffer Pool中的页面映射信息保存下来启动时再快速预热。这样调整完配置重启MySQL也不会经历一段“冷缓存”期导致性能骤降。4. 常见问题与排查技巧实录4.1 问题速查表我把平时遇到过的MySQL内存问题整理成一张速查表你在现场排查时可以直接对照找方向。症状可能原因解决方向内存持续缓慢上涨重启才回落预处理语句未释放、performance_schema统计累计、内存碎片限制prepare数量、关闭不需要的内存consumer、定期整理碎片内存突发暴涨伴随CPU升高大排序、大临时表、连接数瞬间增多优化SQL、降低sort/join buffer、限制连接数内存长期高占用业务压力不大innodb_buffer_pool_size设置过大按业务实际和命中率下调MySQL启动后内存就不小全局缓冲整体配置过高重新评估buffer_pool、key_buffer、table_cache空闲之后内存也不下降连接被sleep线程占住、线程缓存过大调整wait_timeout、interactive_timeout内存涨到接近上限但不kill系统内存分配与MySQL内部参数叠加检查max_connections与per-thread buff的乘积物理内存有大量page cache正常缓冲可回收不用紧张这张表不能覆盖所有场景但至少能帮你把排查方向从“盲猜”变成“对照”。4.2 实战案例一内存持续增长像极了“泄漏”有一个印象很深的客户MySQL版本5.7内存从3G起步两周时间涨到了7.8G重启后又能回落。刚开始大家都以为是泄漏但重新排查时我做了两件事。第一件事用performance_schema.memory_summary_global_by_event_name查看内存分配来源发现memory/sql/Prepared_stmt_map和memory/sql/THD::main_mem_root占据的比例很高。这说明预处理语句没有正常释放。进一步查看performance_schema.prepared_statements_instances果然找到一堆已经执行完毕但还挂在连接上的SQL语句。第二件事检查线上连接池配置发现连接池里设置了prepStmtCacheSize和cachePrepStmtstrue但应用端没有正确检测MySQL连接失效机制导致连接在服务端留下了大量未释放的预处理语句。解决方案是把服务端的max_prepared_stmt_count合理限制一下同时推动应用侧增加连接有效性检查和变更限制。这个案例告诉我内存持续上涨不一定是“泄漏”很可能是某个生命周期很长的对象在不断累积。MySQL的Prepared_stmt_map和events_statements_summary_long类结构不会在语句结束后清理干净需要主动关注。4.3 实战案例二突发内存暴涨撑爆服务器另一个案例更加惊险。某个周六晚上10点监控直接报警MySQL服务器可用内存只剩下不到200M同时负载飙到了20以上。我在控制台先查了一下Threads_running发现瞬间近百个线程在跑。再看慢查询日志发现同一时间有大量针对一张大表的ORDER BY和GROUP BY每条SQL都要排序2亿行左右的数据。那会儿sort_buffer_size被设成了4M听着不算大但上百个并发同时排序光排序缓冲就冲到400M左右。再加上InnoDB的Buffer Pool和大临时表转换总内存直接爆了。当时我做了三步第一临时调低max_connections同时把sort_buffer_size降到1M先止住雪崩第二kill掉一部分长时间运行的排序查询第三优化掉那种全表排序的SQL用索引来消除filesort。最终内存稳定在60%以下。这个案例里参数本身不是罪魁祸首SQL本身的设计才是参数只是放大了问题。4.4 避坑清单与行业习惯最后分享几条基于长期踩坑的经验。第一不要一上来就动innodb_buffer_pool_size。我见过太多人看到内存光鲜就把Buffer Pool调高或者调低结果要么性能下降要么问题依旧。判断依据必须是缓存命中率、磁盘IO、实际内存余量这几个指标。第二排查内存问题前先拍照记录。所有参数修改之前用SHOW VARIABLES和SHOW GLOBAL STATUS把当前状态存下来再结合监控记录做对比。改参数不记录等于把自己丢进黑箱里谁也帮不了你。第三重视监控的粒度。别只盯着MySQL进程的总内存建议把Threads_connected、Created_tmp_disk_tables、Innodb_buffer_pool_reads、Prepared_stmt_count这些指标都配上告警。很多内存问题从来不是突然出现的它早就在指标里露出了苗头。第四遇到Windows机器别把问题都甩给业务。MySQL在Windows上的内存表现和Linux下会有些差异但排查路径是一致的。顺便说一句我见过有人在Windows装了MySQL后顺手把innodb_buffer_pool_size调到4G可机器总共才8G内存系统一开机就卡。这种情况下调整配置比优化SQL更紧迫。在我实际排查这类问题的时候最深的体会是MySQL没有“无缘无故的大内存”。每一个看起来过高的内存占用背后都有一个参数、一个连接、或者一条SQL在支撑。如果你愿意花半小时把一个进程的内存来源拆解清楚再去动参数基本就不会再做无用功了。最后再提醒一句改完参数记得观察一整轮业务周期确认稳定后再固化到配置里。内存治理是一个持续校准的过程一次就能调到完美的情况很少见。
返回列表