一、背景介绍
在对 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和pagecache
echo 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 NAME
73443720 73407245 99% 0.19K 3672186 20 14688744K dentry
15713530 13234322 84% 0.10K 424690 37 1698760K buffer_head
303044 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-8192
118177 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 周期执行。针对误操作修改调度配置问题,由于该类操作执行频率较低,之前未纳入运维操作规范流程,按照"操作前评审+操作中检查+操作后验证"流程进行操作,以降低运维误操作概率。
六、参考资料
评论