我们找回了泄露的内存

1,348 阅读7分钟

一、背景介绍

在对 MySQL 数据库服务器日常巡检或故障排查中,一般会先分析操作系统级别 CPU /内存/存储/网络四大类性能指标,若发现操作系统级别指标异常则有针对性地对该指标进行系统排查。比如使用 TOP 命令发现数据库服务器 CPU 负载较高,需进一步观察 us (用户态 CPU 使用率)/ sy (内核态 CPU 使用率)/ wa (等待 IO 的 CPU 使用率)等指标,当 sy (内核态 CPU 使用率)较高时,可重点是否由数据库高并发导致系统上下文频繁切换引起。

为保证数据库服务稳定性,去哪儿网使用专用服务器+单机多实例方式来部署 MySQL 数据库实例,通过 MySQL 参数 innodb_buffer_pool_size 和 PXC 参数 gcache.size 等参数严格控制每个实例的内存使用,并且为操作系统和基础服务预留足够的内存,因此内存使用率报警在去哪儿网数据库报警中占比极低,但在某段时间该类报警数量和报警占比急剧上涨,严重影响到数据库服务稳定性,通过我们定制的数据库告警大盘能方便查看最近N天不同种类的报警占比:

、问题分析

针对操作系统内存项,我们分别监控 used (已使用内存)/ cache ( Cache 使用内存)/ bufferef ( Buffer 使用内存)/ free (空闲内存)四个指标,并根据内存使用率(已使用物理内存/总物理内存)进行告警,当收到内存使用率报警后,由于"惯性思维",我们按照之前处理"类似故障"的操作流程来进行缓存清理:

735ff0a98bea168225bfba0accbb034.png

执行" drop caches "后,服务器"空闲物理内存"和"内存使用率"均恢复正常,但数天后该服务器再次触发内存使用率报警,通过监控发现服务器"已使用物理内存"按照 3GB /天的速度上升:

同时服务器"空闲物理内存"按照 3GB /天的速度下降:

为彻底解决问题和探明 "drop caches" 操作清理掉的数据内容,我们挑选一台存储历史数据的数据库服务器来进行分析,由于 top/ free/vmstat 等命令都是通过 /proc/memoryinfo 文件和 /proc/pid/smaps 来获取内存使用情况,因此我们首先查看该文件并挑选出关键信息:

5ade133103be7ad932d01df1a776e21.png

Buffers 是对原始磁盘块的临时存储,通过缓存磁盘存储的数据,内核可用将分散的读写操作集中起来进行合并优化,如将多次小的写操作合并为单次大的写操作,以提升磁盘存储的访问性能。通常情况下 Buffer 缓存数据较少,不会占用太多物理内存。

Cache 是对文件系统的数据页缓存,如在第一次从文件读取数据时将读取到的数据缓存在内存中,在后续读取时直接从内存中读取数据,避免再次访问缓存的磁盘存储。由于数据库服务IO 读写操作频繁,当物理内存充足时会使用大量的 Cache 来缓存数据。

Slab 是 Linux 操作系统的一种内存分配机制,其工作是针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,分配速度较慢且会产生大量的内存碎片,slab 分配器是基于对象进行管理的,相同类型的对象归为一类,每当要申请这样一个对象, slab 分配器就从一个 slab 列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内碎片。SReclaimable 表示 Slab 分配器管理的可回收内存,SUnreclaim 表示 Slab 分配器管理的不可回收内存。MySQL 数据库通过 buffer pool 来内部实现数据缓存机制,并不使用 Slab 分配器,因此通常数据库服务器 Slab 内存使用较低,上述示例中 Slab 使用 16.6GB 内存属于异常情况,需重点排查。

通过 /proc/slabinfo 文件可用获得 Slab 内存分配的详细信息,但该文件内容可读性较差,需要自行计算不同类型使用的内存情况,因此推荐使用 slabtop 命令来查看:

1535b55bcdda64853c22a619c4187b0.png

1535b55bcdda64853c22a619c4187b0.png

警告:请勿在内存分配比较频繁或负载较高服务器如 Redis 服务器上运行 slabtop 命令,该命令可能导致服务器长时间挂起无法正常服务。

其中 dentry 对象占用的内存最多,约 14.6GB 。在 Linux 中一切皆文件,无论是普通文件或网络套接字,都使用统一的文件系统来管理,Linux 文件系统为每个文件都分配两个数据结构:

  • 索引节点( Index Node ),用来记录文件的元数据如文件编号、文件大小、访问时间等信息。索引节点和文件一一对应,会被持久化存储到磁盘中。
  • 目录项( Directory Entry ),用来记录文件名称、索引节点指针以及与其他目录项的关联关系,多个关联的目录项构成了文件系统的目录结构。目录项是由内核在内存数据结构中管理维护,做目录项缓存。

缓冲区 (Buffer)/页缓存(Cache)/索引节点(Index Node)/目录项(Directory Entry)在 Linux 操作系统中的架构如下:

三、页缓存分析

在 Linux 内核版本为 4.1 或更高版本中,可用通过 bcc 软件包中的 cachestat 和 cachetop 来分析缓存使用情况。对于内核版本较低的系统,可用 github 开源工具 hcache 或 vmtouch 来分析,通过分析发现:

38ca7971703265481fc6d0d97b67f18.png

该数据库服务器上页缓存(Cache)主要缓存ib_logfile文件和binlog文件,工具vmtouch不仅提供缓存查看功能,还提供清理文件已使用的页缓存或将文件内容加载到页缓存的功能。

四、目录项排查

通过slabtop定位到"已使用物理内存"增长主要由Dentry导致后,通过简易脚本来抓起dentry的变化情况:

417c979efd919a9374aee7490ba6636.png

发现dentry使用内存每分钟增长1次,每次增长约2MB缓存,通过分析本机crontab作业和远程调度作业找出每分钟调度一次的业务待进一步分析。

为进一步定位具体原因,通过监控dentry内存分配(d_alloc)和内存释放(d_free)来定位,创建dentry_chek.stp文件:

545691c743580b929bebd45ac66741f.png

执行脚本并分析结果:

d816ab8a1e4c0cc604df7969d4de49c.png

从上面的日志分析输出结果可发现yum进程执行d_alloc的次数远大于执行d_free的次数,结合调度作业情况最终定位到问题原因:

* */1 * * * root yum makecache --enablerepo=xxxxxx_repo;yum -q -y update xxxxxx --enablerepo=xxxxxx_repo;``

命令yum makecache通过本地缓存远程服务器安装包信息来提升安装包查询搜索速度,早期某个运维项目需要保证服务器及时安装部署最新的软件包,采用通过crontab调度+yum makecache方案来实现,在近期某次crontab调度配置修改过程中,误操作将该作业从"每小时执行一次"调整为"每分钟执行一次",每小时执行一次的正确调度配置为:

0` `*/1 * * * root yum makecache --enablerepo=xxxxxx_repo;yum -q -y update xxxxxx --enablerepo=xxxxxx_repo;``

调度作业配置调整后执行频率提升60倍,使得yum makecache产生大量dentry的问题被加速暴露出来。

五、解决方案

针对yum makecache问题,由于目前运维随着运维需求不断更新迭代,当前运维操作已不强依赖该软件包且该软件包更新频率极低,因此先恢复该调度作业的正常执行频率,后期会通过主动推送方案来彻底避免yum makecache周期执行。针对误操作修改调度配置问题,由于该类操作执行频率较低,之前未纳入运维操作规范流程,按照"操作前评审+操作中检查+操作后验证"流程进行操作,以降低运维误操作概率。

六、参考资料