MySQL 5.7 CPU 抖动问题的不完全处理过程

问题

我们内网有台机器,MySQL 版本为:

Server version: 5.7.17-11 Percona Server (GPL), Release 11, Revision f60191c  

读写压力并不大,大概是每十分钟有个定时任务写入一定数量的数据:

然而 CPU 抖动却非常频繁:

上图还没有完全说明问题,因为我们的监控系统是 30 秒收集一次,上图是个两分钟的平均值展示。如果从 top 指令看,则是时不时地飚高到 80% - 90% 甚至 100% 以上。

按照以往的经验来看,十分钟一两兆这样的数据量写入,不至于会长期占用这么高的 CPU。

perf top

执行 perf top 查看到如下情况:

看到 MySQL 的几个函数占用了较高的 CPU:

buf_get_n_pending_read_ios()  
ut_time_ms()  
buf_flush_page_cleaner_coordinator  
srv_check_activity(unsigned long, unsigned long)  

查看代码,可以看到 storage/innobase/buf/buf0flu.cc 里的 buf_flush_page_cleaner_coordinator 线程,刚好有这三个函数:

这个是 innodb buffer pool 后台刷新协调线程,看起来在这个 while 循环里,srv_check_activitybuf_get_n_pending_read_ios 这两个函数在 if 判断里面,一定是会被执行到的,既然后者占用的比例较高,所以先来看下后者究竟在做什么。

buf_get_n_pending_read_ios

通过 perf top 的 Annotate 功能,可以查看到这个函数的 TEXT 段:

对应源码 storage/innobase/buf/buf0buf.cc 中的该函数:

对照上面的汇编,30: 那段比较明显是 for 循环;而

mov %rsp,%rbp  

是将被调函数的栈帧栈底地址放入 bp 寄存器中,是准备开始调用函数了。明显地,在这个函数里,就只有 buf_pool_from_array(i) 这个函数了,那么我们需要查的占了 64% CPU 时间的这两行指令:

mfence  
mov typeinfo for TaoCrypt:AbstractGroup+0x7920,%rax  

看起来跟

os_rmb;  

这行有比较大的关系。(以上是我不靠谱的推测 ...)

再来看下 os_rmb 这个宏是干什么的:

可以看到是编译的时候根据编译环境来定义的。

查看了 MySQL 启动日志,发现问题机器上是:

2017-11-15T10:49:50.445980+08:00 0 [Note] InnoDB: GCC builtin __sync_synchronize() is used for memory barrier  

而刚好,在另外一台正常机器,我看到的是:

2017-03-02T18:32:47.595244+08:00 0 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used for memory barrier  

看了下网上的资料,memory barrier 是内存屏障,编译器用来保证指令执行顺序的。__sync_synchronize 属于比较 legacy 一点的实现,而 __atomic_thread_fence 是比较新一点的实现。

至于为什么问题机器上用了 __sync_synchronize ,是因为同事之前因为某种原因自己编译了 Percona Server,而不是用 Percona 的二进制包,这是编译环境导致的问题!

我重新在我们的系统上编译了下,果然是看到了这些输出:

-- Performing Test HAVE_GCC_ATOMIC_BUILTINS
-- Performing Test HAVE_GCC_ATOMIC_BUILTINS - Failed
-- Performing Test HAVE_GCC_SYNC_BUILTINS
-- Performing Test HAVE_GCC_SYNC_BUILTINS - Success

__atomic_thread_fence 实现的 os_rmb 是这样的:

问题环境用官方包替换之后,我们看到 perf top 里,buf_get_n_pending_read_ios 已经在第一页看不到了:

从 MySQL 进程来看,buf_get_n_pending_read_ios 占用 CPU 的比例也下降了 (之前是 35% 左右) :

ut_time_ms

这是个获取时间的函数,先用 systemtap 确认下是否它的调用方 ( Percona 官方包支持 function 探测):

probe process("/usr/local/mysql/bin/mysqld").function("ut_time_ms@*").return {  
        printf ("%s <- %s\n", thread_indent(-1), probefunc())
}

probe timer.s(10) {  
        exit()
}

执行:

# stap -v mysql.stp > res.txt
# awk '/<-/{a[$NF]++}END{for(i in a)print i, a[i]}' res.txt 
buf_flush_page_cleaner_coordinator 100952  
buf_lru_manager 40  
_Z15buf_page_createRK9page_id_tRK11page_size_tP5mtr_t 1  
_ZL13pc_flush_slotv 28  

可以看到这个函数基本上都是 buf_flush_page_cleaner_coordinator 调用的。

查看了后台刷新协调线程的代码,共有 8 处 ut_time_ms 函数调用。

谷歌了一下,有搜到 gettimeofday 的优化,都是提到了使用 VDSO,这是我们环境中默认开启的:

# cat /proc/sys/kernel/vsyscall64
1  

确实用 strace -c 追踪后台刷新协调线程只能看到一些 IO 方面的系统调用:

代码太长,没看懂,借下这篇文档中的分析:

如果单纯从减少 buf_flush_page_cleaner_coordinatorut_time_ms 调用来说,让调度进程多去等待 buffer pool instance 处理,而不是一直在线程里不停执行 ut_time_ms,应该也算一种思路吧。(这又是我的不靠谱主观臆测 ...)

在 MySQL 5.7 中,如果不显式设置 innodb_buffer_pool_instances 这个参数,当innodb buffer size 大于 1G 的时候,就会默认会分成 8 个 instances,如果小于 1G,就只有 1 个 instance

环境上的 MySQL,innodb buffer size 只设置了 256M,将其改大成 1G 之后,可以看到 buf_flush_page_cleaner_coordinator 调用 ut_time_ms 的次数确实减少了:

buf_flush_page_cleaner_coordinator 41140  
buf_lru_manager 80  
_ZL13pc_flush_slotv 40  

这其实是个玄学,因为不知道是不是改大了 innode buffer size 带来的。不过呢,测试过单纯改大 innode buffer size 并不能改善 MySQL CPU 抖动的情况。

暂时的结果

同样的读写负载下,可以看到比优化之前的均值 60% 好了很多,但其实从 top 看到有时候还是会飚高到 50% - 70% 左右,但频率比优化前低了很多。

perf top 的结果:

或许之后可以测试下 5.7.17 之后的版本:

https://bugs.mysql.com/bug.php?id=87637

参考资料

https://github.com/percona/percona-server/blob/f9157b81b9d86b3dacb72267deeeee3660895f75/storage/innobase/buf/buf0flu.cc
http://www.yunweipai.com/archives/17404.html
https://as.h5con.cn/articles/41146
http://blog.csdn.net/lubing20044793/article/details/38897729
http://www.trueeyu.com/2015/03/18/gettimeofday/
https://bugs.mysql.com/bug.php?id=87637

最后

有官方包用官方包,没事不要自己编译软件包玩!!