ARTICLE DETAIL

资讯详情

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

MySQL内存占用居高不下?排查RSS虚高的完整链路与调优指南

MySQL内存占用居高不下?排查RSS虚高的完整链路与调优指南 凌晨两点被监控电话吵醒是每个数据库运维都逃不掉的宿命。电话那边只有一句话MySQL所在服务器内存使用率已超过95%。我披上衣服坐到电脑前看了下free -h8G的机器只剩300多M可用再看top里mysqld那一行的RES稳稳停在5.2G。这台实例跑的是MySQL 5.7.44凌晨两点连个像样的查询都没有。奇怪的地方就在这里我在配置文件里明明只给了innodb_buffer_pool_size2G为什么mysqld进程能吃掉5.2G如果你也遇到过类似的情况——监控面板上mysqld的RSS一路走高、调小buffer pool也没用、甚至重启后过几天又涨回去——那这篇文章就是按我当时的排查链路一步步拆给你看。整个过程没有高深的东西关键是把账算平。1. 先别急着调innodb_buffer_pool_size内存告警后的第一件事是取证很多人收到内存告警后的第一反应是把innodb_buffer_pool_size砍半这是错误的顺序。buffer pool是InnoDB的缓存池砍它等于让热点数据无处落脚业务高峰期磁盘IO会先把你搞死。正确的顺序是先弄清楚内存到底去哪了再决定动谁。1.1 五分钟完成现场快照别让告警白响告警响的那一刻现场数据是最值钱的。登录服务器后按下面顺序执行三分钟内就能拿到完整现场free -h top -b -n 1 -p $(pidof mysqld) cat /proc/$(pidof mysqld)/status | grep -E VmRSS|VmSize|Threadsmysql -uroot -p -e SHOW GLOBAL STATUS LIKE Threads_connected; mysql -uroot -p -e SHOW GLOBAL VARIABLES WHERE Variable_name IN (innodb_buffer_pool_size,max_connections,performance_schema,sort_buffer_size,join_buffer_size,read_buffer_size,read_rnd_buffer_size,tmp_table_size,max_heap_table_size,table_open_cache,table_definition_cache);这里有个常见误区free -h里的buff/cache列不是需要担心的内存。Linux会把空闲内存拿来做文件缓存进程需要时会自动让出来。真正要注意的是used列和mysqld进程的RSSResident Set Size常驻内存。top里的RES就是这个值/proc/PID/status里的VmRSS也一样。另外一个关键数据是Threads_connected。这个值决定了连接私有缓冲这个大头能膨胀到什么程度后面算账全靠它。如果业务是连接池模式这个值通常稳定在几十如果是直连模式可能冲到几百。当场把它记下来别等告警消失了再后悔。1.2 建一张内存基线表后面排查全靠它现场快照拿到之后顺手建一张基线表。我排查这类问题时会先把下面这些信息填进一个文档项目记录值说明实例版本5.7.44不同版本默认值差异很大物理内存8G整机可用内存mysqld RSS5.2G告警时的常驻内存innodb_buffer_pool_size2G配置文件实际值max_connections300配置值而非默认151Threads_connected47告警时的实际连接数performance_schemaON5.7默认开启忙时连接数峰值约180从监控历史里取这张表的价值在于后续每一步计算都能对着它做减法而不是凭感觉猜。特别是配置值和实际运行值常常不一致——比如max_connections配了300但实际连接峰值只有180那么按300去估算内存上限就会偏大容易误判。2. MySQL的内存去哪儿了一张图五个去向先把手里的账算平MySQL的内存去向可以分成五类搞清楚每一类的归属才算真正把账算平了全局共享区innodb_buffer_pool_sizeInnoDB缓冲池、innodb_log_buffer_size日志缓冲、key_buffer_sizeMyISAM索引缓冲、表缓存、performance_schema缓存。连接私有区每个连接各自的sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size、net_buffer_length、thread_stack。临时分配内部临时表、排序操作、filesort过程占用的内存。执行上下文线程栈、预处理语句缓存、记录集缓冲等零零碎碎的开销。系统层滞留glibc malloc的arena碎片、jemalloc的缓存区——这部分MySQL自己的统计工具根本看不到却经常是内存虚高的真凶。2.1 InnoDB缓冲池它是蓄水池不是泄漏点innodb_buffer_pool_size是MySQL内存的最大头。buffer pool的工作方式很像一个蓄水池数据页和索引页读进来之后会留在池子里下次查询直接命中内存不再走磁盘。它只增不减除非你主动改变大小。所以如果mysqld的RSS大约等于buffer pool大小 几百M这完全正常不用慌。真正需要警惕的是RSS远大于这个和值。比如我的实例buffer pool只有2GRSS却到了5.2G这个差额就是问题所在。想看buffer pool内部情况可以用SHOW ENGINE INNODB STATUS\G在BUFFER POOL AND MEMORY这一段里Total memory allocated会显示InnoDB请求的总内存包含buffer pool、自适应哈希索引、锁结构等。这个数字通常比innodb_buffer_pool_size高几个百分点属正常现象。2.2 每连接缓冲的乘法陷阱这是最容易踩坑的地方。sort_buffer_size、join_buffer_size这些参数是连接私有的不是全局的一份。它们虽然不会在建立连接时立刻全部分配但一旦连接执行过排序、join操作分配出来的高水位内存就会一直留在线程里直到连接关闭。算一笔账就清楚了。5.7默认值大概是sort_buffer_size 256K join_buffer_size 256K read_buffer_size 128K read_rnd_buffer_size 256K thread_stack 256K net_buffer_length 16K ----------------------- 每条连接合计 ≈ 1.1Mmax_connections默认151时理论上限约166M看着不多。但如果你为了跑报表把sort_buffer_size调到4M那每条连接的高水位就变成了4M1M300条连接就是1.5G。这就是改一个参数所有连接一起涨价的乘法陷阱。后面我们会专门讲怎么调它。2.3 performance_schema和表缓存没人注意的固定支出5.7默认开启performance_schema8.0也一样它本身就要吃内存。活跃连接数越多、表的数量越多、等待事件越密集这个内存占用就越高。我见过performance_schema吃掉400M的实例这在共享的内存预算里不是小数目。表缓存同样容易被忽略。table_open_cache2000意味着最多缓存的表对象数量每个条目都会占用内存table_definition_cache1400则缓存表定义结构。如果实例里有几千张表这块开销就能到几十M。想确认这些数字用下面两条SQL就能看到内存分配排行SELECT event_name, ROUND(CURRENT_NUMBER_OF_BYTES_USED/1024/1024, 2) AS current_mb, ROUND(HIGH_NUMBER_OF_BYTES_USED/1024/1024, 2) AS high_mb FROM performance_schema.memory_summary_global_by_event_name ORDER BY current_mb DESC LIMIT 20;也可以直接用sys库的视图可读性更好SELECT event_name, ROUND(current_alloc/1024/1024, 2) AS current_mb FROM sys.memory_global_by_current_bytes WHERE current_alloc 0 ORDER BY current_mb DESC LIMIT 20;这两条SQL是整个排查过程中最值的命令建议收藏。3. 完整排查链路复盘RSS 5.2G是怎么一步步现出原形的理论铺垫完了回到开头那个案例。8G机器MySQL 5.7.44buffer pool 2Gmax_connections 300告警时连接数47mysqld RSS 5.2G。下面是我完整的排查过程每一步都对应一个对账动作。3.1 第一关理论上限算出来只有2.9G账对不上先按配置项把理论内存上限算出来项目计算值说明innodb_buffer_pool_size2048MInnoDB主体innodb_log_buffer_size16M重做日志缓冲key_buffer_size8MMyISAM索引缓冲连接私有区300 × 1.1M ≈ 330M按max_connections上限算表缓存约30Mtable_open_cachedef_cacheperformance_schema约400M从内存统计中实测杂项100M线程缓存、各种context合计≈ 2932M理论最大值注意我用的是max_connections300去乘已经把最坏情况算进去了。就算performance_schema再膨胀一点总理论值也就3G出头。但实际RSS是5.2G中间差了2G多完全对不上。这说明一定有某些内存花在了MySQL自己的统计面板之外。3.2 第二关让performance_schema自报家门用前面那段SQL查memory_summary_global_by_event_name把结果按大类和明细各看一遍。当时得到的关键数据是memory/innodb约2.3Gbuffer poollog buffer内部结构、memory/sql约90M、memory/performance_schema约410M、memory/mysys约70M所有被instrumentation统计到的内存加起来约2.9G。这一步非常重要它证明MySQL自己认为它只用了2.9G。但操作系统告诉我们它占用5.2G。两者的差值2.3G就是MySQL看不见的内存。3.3 第三关RSS和instrumentation之间的差额指向了mallocMySQL看不见的内存来自哪里答案通常在内存分配器。在Linux上glibc的malloc实现会为每个线程维护独立的分配区arena。默认情况下arena数量上限是CPU核心数的8倍。我这台机器是4核最多可以创建32个arena每个arena可以保留最大64MB的堆块粗算上限就是2GB。这些arena一旦被分配即使MySQL内部已经把小块内存释放了glibc也未必还给操作系统而是留在arena里备着下次分配用。这就表现为RSS居高不下但MySQL自己的内存统计却看不到任何异常。验证方法很简单看进程的地址空间映射pmap -x $(pidof mysqld) | tail -5输出里会出现一大片64MB一组的匿名内存区域数量很多这就是arena堆块。再执行cat /proc/$(pidof mysqld)/status看VmRSS把数值和步骤3.2里算出的2.9G做减法差额基本就和这些匿名段的累积值吻合。3.4 第四关对症下药RSS从5.2G降到3.1G定位到glibc arena之后解决方案很干净。限制arena数量即可不需要动任何MySQL参数如果MySQL由systemd管理在service文件里加一行环境变量然后daemon-reload并重启EnvironmentMALLOC_ARENA_MAX2如果用的是mysqld_safe可以在启动脚本前设置export MALLOC_ARENA_MAX2再启动。也可以换成jemalloc效果更彻底但要额外安装库并配置[mysqld_safe] malloc-lib/usr/lib64/libjemalloc.so.1。我当时给几个实例统一设置了MALLOC_ARENA_MAX2重启观察mysqld的RSS从5.2G降到3.1G业务毫无影响。内存告警直接消失buffer pool一点没动。这里要提醒一句MALLOC_ARENA_MAX设成1虽然能最大程度压制内存但可能导致线程互相争锁CPU使用率反而上升。设2或4是比较平衡的选择既要看内存也要看CPU曲线。4. 调优实操哪些参数真正值得动哪些是自我安慰如果你也遇到了RSS虚高但glibc调整解决不了那就要认真审视MySQL参数本身了。这一节把值得动的参数逐个过一遍并说明为什么这么调。4.1 buffer pool按数据量算而不是按内存总量拍脑袋先说最核心的innodb_buffer_pool_size。很多人喜欢把物理内存的70%直接给它这是错的。正确做法是先看你的数据到底多大SELECT ROUND(SUM(data_length index_length)/1024/1024/1024, 2) AS data_index_gb FROM information_schema.tables WHERE table_schema NOT IN (mysql,information_schema,performance_schema,sys);buffer pool存在的意义是缓存热点数据页。如果数据总量只有50G给buffer pool配60G就是浪费如果数据有200G物理内存只有64G那Buffer pool给到48G之后剩下的热点数据依赖磁盘也正常。我的经验公式buffer pool (数据量索引量) × 1.2这是下限如果是专用数据库服务器物理内存的70%是常见上限。注意另外要预留操作系统和mysqld其他部分的内存别顶到100%。5.7支持动态调整SET GLOBAL innodb_buffer_pool_size 3221225472; -- 3G但动态调整只对当前运行生效重启前必须同步改配置文件。8.0多了SET PERSIST一条命令同时持久化方便不少。4.2 连接私有缓冲先看峰值连接数再决定每个连接给多大连接私有缓冲的调整原则是先确认真实峰值连接数再反推每个连接能分多少。先查历史监控里Threads_connected的峰值假设是180。如果给每个连接留4M的sort_buffer那仅sort_buffer一项就是720M加上其他私有缓冲轻轻松松破G。大多数OLTP业务sort_buffer_size给256K~512K足够排序量大的报表查询可以单独在会话级别临时调大而不是全局改。join_buffer_size同理默认256K在很多场景下都够除非有大量不走索引的join。这几个参数是改动影响面最大的类型谨慎。一个容易忽略的点tmp_table_size和max_heap_table_size是取两者较小值生效的。比如tmp_table_size64M但max_heap_table_size还是默认的16M那临时表只能用到16M。要调就一起调。4.3 performance_schema用不上就关用得上就瘦身如果实例上根本没有监控依赖performance_schema也不需要等待事件分析那直接在配置文件里performance_schemaOFF重启能立刻省下几百M。这是性价比最高的减内存操作之一。如果依赖它做监控那就保留开关但可以关闭那些高开销的consumer比如events_statements_history_long这类历史长队列表UPDATE performance_schema.setup_consumers SET ENABLED NO WHERE NAME LIKE events_statements_history_long%;注意performance_schema的开关只能重启生效不能动态切换。8.0版本的内存统计更细致同样的开关状态下8.0的内存占用往往比5.7还要高一些这点升级前要有心理准备。4.4 动态调整与持久化别让重启后回到解放前参数调完一定要确认能活过重启。5.7的SET GLOBAL只管当前运行写配置文件才能持久化。我见过不止一次同事线上SET GLOBAL调完隔天机房断电重启配置直接回到旧值所有优化归零。一个适合多数中大型实例的5.7配置参考按你的业务裁剪[mysqld] performance_schema OFF max_connections 150 innodb_buffer_pool_size 3G innodb_log_buffer_size 32M sort_buffer_size 1M join_buffer_size 256K read_buffer_size 256K read_rnd_buffer_size 512K tmp_table_size 64M max_heap_table_size 64M table_open_cache 2000 table_definition_cache 1400配上前面说过的MALLOC_ARENA_MAX2环境变量中低配机器上的内存压力会明显缓解。5. 调完参数内存还是降不下来四个隐性钉子户逐一击破有时候参数看起来都合理了RSS还是会缓慢上涨或是在某个业务动作后突然跳升。以下四个场景是我实测中翻过车的地方列出来供你对照。5.1 大事务和长连接连接一多binlog_cache也跟着膨胀每次事务提交前DML语句会先写进会话自己的binlog cache。binlog_cache_size默认32K超过之后会溢出到磁盘临时文件但max_binlog_cache_size这个上限默认大得惊人。如果一个连接跑了一个批量更新几百万行的事务它一个连接就能轻松占用几百M甚至上G的binlog cache。这事的隐蔽之处在于它不是常驻内存但事务没提交时它就是实实在在的占用。如果有几十个这样的长事务并发内存瞬间飙升表现和泄漏一模一样。排查命令SHOW GLOBAL STATUS LIKE Binlog_cache_use; SHOW GLOBAL STATUS LIKE Binlog_cache_disk_use;Binlog_cache_use增长很快说明有大量事务在写binlog cacheBinlog_cache_disk_use增长说明溢出到磁盘了IO压力也会上来。对策是把max_binlog_cache_size设成合理值比如1G~2G同时推动业务拆分大事务。5.2 一条大查询就能吃掉几个G临时表和Filesort的聚集效应GROUP BY、ORDER BY、DISTINCT这类操作MySQL会在内存里建内部临时表。tmp_table_size就是这块的上限超过之后会转成磁盘临时表。问题在于并发情况下即便单条SQL只用几十M几十条大查询同时跑临时表内存就是几十个几十M地叠加。排查时先看累计状态SHOW GLOBAL STATUS LIKE Created_tmp%;重点比较Created_tmp_disk_tables和Created_tmp_tables的比值。如果磁盘临时表比例超过10%说明临时表频繁溢出要么调大tmp_table_size要么优化SQL的排序和分组逻辑。更精细的做法是用sys库定位具体语句SELECT query, tmp_tables, tmp_disk_tables FROM sys.statements_with_temp_tables ORDER BY tmp_disk_tables DESC LIMIT 10;抓到语句之后从索引和SQL改写入手比直接调参数健康得多。5.3 实例一开就是半年内存碎片像滚雪球连接频繁建立断开、临时操作反复分配释放这些会让分配器内部积累大量碎片。MySQL自己释放的内存和操作系统实际回收的内存中间永远存在一个差值。跑得越久这个差值越大但通常不会无限增长而是在某个高位震荡。这类问题的特征RSS缓慢上行后趋于平稳重启后大幅下降过段时间又慢慢回来。对策是确认分配器设置没有问题的话就把它当作正常现象如果持续上涨到逼近物理内存那要排查是否有真正的泄漏比如存储引擎插件、自定义函数等第三方的内存管理缺陷。5.4 8.0和5.7的默认值差异升级后内存变大的真正原因从5.7升到8.0很多人发现内存占用明显变大第一反应是8.0有内存泄漏。其实多数情况是默认值变了。8.0的table_open_cache默认4000比5.7的2000翻了一倍performance_schema的埋点更密同样负载下内存开销更高innodb_buffer_pool_size默认虽然还是128M但如果你沿用旧的my.cnf没调整整体的内存预算就会紧张。另外5.7里如果开了query cache8.0直接移除了这个功能配置文件里残留的query_cache_size会被忽略。如果你原来query cache设得很大升级后这部分内存直接释放反而会降。所以升级前后别急着下结论先对比两边配置和实际内存统计。6. 别等电话再响监控指标、巡检SQL与容量规划的落地清单排查完了问题也解决了但如果没有后续的监控和巡检下个月同一时间你还会被同一个电话吵醒。把下面的清单落地内存问题就变成可预期的事。6.1 四个必须盯的指标指标获取方式告警阈值建议mysqld进程RSSnode_exporter采集进程内存连续10分钟 物理内存70%Threads_connectedmysqld_exporter采集超过max_connections的80%磁盘临时表比例状态变量计算Created_tmp_disk / Created_tmp 10%Buffer pool命中率InnoDB状态解析低于95%且持续下滑如果服务器出现swap使用量持续增长这是最危险信号说明物理内存已经不够分配进程开始向磁盘交换此时必须马上介入而不是等告警自动恢复。6.2 每周巡检三句话SELECT event_name, ROUND(current_alloc/1024/1024, 2) AS current_mb FROM sys.memory_global_by_current_bytes WHERE current_alloc 0 ORDER BY current_mb DESC LIMIT 15;SELECT query, tmp_tables, tmp_disk_tables FROM sys.statements_with_temp_tables ORDER BY tmp_disk_tables DESC LIMIT 10;SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Created_tmp%; SHOW GLOBAL STATUS LIKE Binlog_cache%;这三组SQL分别覆盖当前内存花在哪谁在制造临时表连接与事务缓存状态每周跑一遍能提前发现大部分隐患。6.3 容量规划的粗算公式新实例上线前用一个保守的公式估算内存需求实例总内存预估 innodb_buffer_pool_size innodb_log_buffer_size key_buffer_size max_connections × (sort_buffer_size join_buffer_size read_buffer_size read_rnd_buffer_size thread_stack net_buffer_length) (table_open_cache table_definition_cache) × 1.5K performance_schema开销(200M~500M) 操作系统及文件缓存余量(物理内存15%~20%)举例一台16G的专用数据库服务器计划给innodb_buffer_pool_size配10Gmax_connections设150每个连接平均按1.5M算10G 16M 8M 150×1.5M 80M 300M 2.5G ≈ 13.5G16G物理内存装得下。如果计算结果逼近物理乃至超过优先砍max_connections或者降低连接私有缓冲再考虑buffer pool。这次排查之后我把MALLOC_ARENA_MAX2写进了所有MySQL节点的systemd单元文件里算是用最小代价解决了最大问题。后来我又陆续遇到几次内存缓慢上涨几乎都是大查询临时表堆积和binlog缓存在做怪套路跟这次是同一套先算账再让performance_schema指路最后查分配器。说实话MySQL的内存问题十有八九不是真的内存泄漏而是配置、分配器和业务查询三方合谋的结果。下次再收到告警别急着把buffer pool砍半先静下心把账算平——大多数时候答案已经写在账本里了。
返回列表