写点什么

我们找回了泄露的内存

  • 2022 年 2 月 14 日
  • 本文字数:5100 字

    阅读完需:约 17 分钟

我们找回了泄露的内存

 一、背景介绍

在对 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 (空闲内存)四个指标,并根据内存使用率(已使用物理内存/总物理内存)进行告警,当收到内存使用率报警后,由于"惯性思维",我们按照之前处理"类似故障"的操作流程来进行缓存清理:

## 刷新磁盘sync## 删除Inode和dentries和pagecacheecho 3 > /proc/sys/vm/drop_caches
复制代码

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

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

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

## 查看所有进程使用的总物理内存(包含共享物理内存)grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%4.2f GB\n", total/1024/1024 }' ## 执行命令cat /proc/meminfo ## 输出结果## 服务器总内存MemTotal:       132030344 kB## 服务器空闲内存MemFree:         1396884 kB## Buffer使用的内存Buffers:          409812 kB## Cache使用的内存Cached:         53136072 kB## 使用Slab分配的内存。Slab:           16681824 kB## 使用Slab分配且可回收内存。SReclaimable:   16592540 kB## 使用Slab分配但不可以回收内存。SUnreclaim:        89284 kB
复制代码

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 命令来查看:

## 执行命令获取内存使用Top 10类型slabtop --sort=c -o |head -n 17 ## 输出结果 Active / Total Objects (% used)    : 87179810 / 89818826 (97.1%) Active / Total Slabs (% used)      : 4166722 / 4166821 (100.0%) Active / Total Caches (% used)     : 105 / 184 (57.1%) Active / Total Size (% used)       : 15364477.91K / 15662436.88K (98.1%) Minimum / Average / Maximum Object : 0.02K / 0.17K / 4096.00K   OBJS      ACTIVE      USE       OBJ SIZE      SLABS       OBJ/SLAB    CACHE SIZE      NAME73443720    73407245    99%         0.19K       3672186         20      14688744K       dentry15713530    13234322    84%         0.10K       424690          37      1698760K        buffer_head303044      244340      80%         0.55K       43292           7       173168K         radix_tree_node 41660      41411       99%         0.98K       10415           4       41660K          ext4_inode_cache  1053      1053        100%        16.00K      1053            1       16848K          size-16384   183      183         100%        32.12K      183             1       11712K          kmem_cache  1129      1079        95%         8.00K       1129            1       9032K           size-8192118177      74869       63%         0.06K       2003            59      8012K           size-64 28260      28104       99%         0.19K       1413            20      5652K           size-192  7452      7259        97%         0.64K       1242            6       4968K           proc_inode_cache
复制代码

警告:请勿在内存分配比较频繁或负载较高服务器如 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 来分析,通过分析发现:

## 查看MySQL数据目录下ib_logfile文件缓存情况/vmtouch -v /mysql_xxxx/data/ib_logfile* ## 命令输出结果/mysql_xxxx/data/ib_logfile0[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 262144/262144/mysql_xxxx/data/ib_logfile1[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 262144/262144/mysql_xxxx/data/ib_logfile2[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 262144/262144/mysql_xxxx/data/ib_logfile3[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 262144/262144            Files: 4     Directories: 0  Resident Pages: 1048576/1048576  4G/4G  100%         Elapsed: 0.11502 seconds ## 查看MySQL数据目录下binlog文件缓存情况./vmtouch -v /mysql_xxxx/binlog/ ## 命令输出结果(部分内容)/mysql_xxxx/binlog/relay-bin.index[O] 1/1/mysql_xxxx/binlog/mysql-bin.002587[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 91158/91158/mysql_xxxx/binlog/mysql-bin.002588[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 7973/7973/mysql_xxxx/binlog/mysql-bin.002589[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 3717/3717/mysql_xxxx/binlog/mysql-bin.002590[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 2343/2343           Files: 104     Directories: 1  Resident Pages: 4922439/5315467  18G/20G  92.6%         Elapsed: 0.61802 seconds
复制代码

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

四、目录项排查

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

while true;do    date_str=`date "+%Y-%m-%d %H:%M:%S"`;    memory_info=`cat /proc/meminfo |grep SReclaimable`;    dentry_info=`cat /proc/slabinfo  |grep dentry`;    echo "${date_str}    ${memory_info}";    echo "${date_str}    ${dentry_info}";    sleep 1;done
复制代码

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

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

## dentry.stp## 监控kernel上dentry内存分配和内存释放的进程信息probe kernel.function("d_alloc") {    printf("%s[%ld] %s %s\n", execname(), pid(), pp(), probefunc())}probe kernel.function("d_free") {    printf("%s[%ld] %s %s\n", execname(), pid(), pp(), probefunc())}probe timer.s(5) {    exit()}
复制代码

执行脚本并分析结果:

## 执行脚本并输入到日志文件stap dentry_chek.stp 1>/tmp/dentry_chek.log 2>&1 ## 分析日志文件命令cat /tmp/dentry_chek.log | awk '{print $1"-->"$3}' | sort  | uniq -c | sort  -k1 -n -r| head -n 50 ## 分析日志文件输出结果## BackgrProcPool进程的d_alloc与d_free相差较大是由于脚本执行时间点导致。  15049 BackgrProcPool[13267]-->d_alloc  14707 BackgrProcPool[13267]-->d_free   8367 yum[8606]-->d_alloc    265 agent_daemon.py[5830]-->d_free    262 ZooKeeperRecv[13267]-->d_free    262 ZooKeeperRecv[13267]-->d_alloc    254 agent[7269]-->d_alloc    166 clickhouse-serv[13267]-->d_alloc    162 clickhouse-serv[13267]-->d_free    131 ZooKeeperSend[13267]-->d_free    131 ZooKeeperSend[13267]-->d_alloc    122 agent_daemon.py[5830]-->d_alloc 
复制代码

从上面的日志分析输出结果可发现 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 周期执行。针对误操作修改调度配置问题,由于该类操作执行频率较低,之前未纳入运维操作规范流程,按照"操作前评审+操作中检查+操作后验证"流程进行操作,以降低运维误操作概率。

六、参考资料

发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2020.11.28 加入

还未添加个人简介

评论

发布
暂无评论
我们找回了泄露的内存