为什么 Percona MySQL 开启 NUMA 选项后启动很慢

最近在使用新版本 Percona MySQL 的时候, 发现启动巨慢, 且启动时间随 innodb_buffer_pool_size 的大小成正比, 配置的 innodb_buffer_pool_size 值越大, 启动时间就越长, 所耗时间约为 T = innodb_buffer_pool_size(GB), 如下所示, 我们将 innodb_buffer_pool_size 配置的足够大, 就会发现实例启动耗时约 440s:

Jul  7 12:00:44 mysqld-3301[95109]: InnoDB: Initializing buffer pool, total size = 432G, instances = 24, chunk size = 256M
Jul  7 12:00:44 mysqld-3301[95109]: InnoDB: Setting NUMA memory policy to MPOL_INTERLEAVE
Jul  7 12:08:04 mysqld-3301[95109]: InnoDB: Setting NUMA memory policy to MPOL_DEFAULT
Jul  7 12:08:04 mysqld-3301[95109]: InnoDB: Completed initialization of buffer pool

可以看到消耗的时间主要在设置 NUMA 策略这步:

InnoDB: Setting NUMA memory policy to MPOL_INTERLEAVE

为什么要修改 NUMA 策略?

目前主流的服务器基本都是 NUMA 架构, 比起过去的 SMP 架构, NUMA 在 CPU 和内存利用率方面都有很大的提升. 不过 NUMA 架构对内存占用较多的应用(比如数据库等)存在一些性能方面的影响, 比较知名的有 MySQL swap insanity, 简单概括此问题即是 当你把主机大部分内存分配给InnoDB时,你会发现明明操作系统还有很多内存,但是却有很多内存被交换到了 SWAP 分区.

为什么会发生这样的问题? 主要的原因在于 MySQL 使用的内存超过了 NUMA 架构中单颗 CPU 对应的内存, 如下所示:

# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6 8 10
node 0 size: 32722 MB
node 0 free: 24803 MB
node 1 cpus: 1 3 5 7 9 11
node 1 size: 32768 MB
node 1 free: 28378 MB

每颗 CPU node 对应一半的内存(32G). 如果 MySQL 的内存超过 32G 就会产生上述的 swap insanity 问题. 更多描述可以参考以前的文章 MySQL numa交叉模式启动说明.

避免产生此类问题的方式目前主要就是修改 MySQL 的占用内存的策略, 保证可以使用每颗 CPU 对应的内存. MySQL 对 NUMA 的支持主要经过了以下阶段:

 numactl(命令行)  ->  numa_interleave(mysqld_safe 脚本) -> innodb_numa_interleave(mysqld 选项)

新版本的选项 innodb_numa_interleave 即可设置 MySQL 在启动的时候将 NUMA 内存策略从默认的 MPOL_DEFAULT 策略修改为 MPOL_INTERLEAVE 交互模式, 更多策略见 numa_memory_policy. 通过 INTERLEAVE 策略完成内存分配后, MySQL 又会将策略修改回默认的 DEFAULT 策略, 如下官方说明:

 Enables the NUMA interleave memory policy for allocation of the InnoDB buffer pool. 
 When innodb_numa_interleave is enabled, the NUMA memory policy is set to MPOL_INTERLEAVE 
 or the mysqld process. After the InnoDB buffer pool is allocated, the NUMA memory policy
 is set back to MPOL_DEFAULT. 

为什么启动很慢?

percona-PS-3967 的问题中, 自从 Oracle 官方引入 innodb_numa_interleave 功能后, Percona 版本之前的 NUMA 策略并没有生效, 为了解决这个问题, Percona 从以下版本开始在启用 NUMA INTERLEAVE 策略的时候一并通过 mmapMAP_POPULATE 标记预先分配 innodb_buffer_pool_size 指定的内存:

主版本 生效版本
5.6 >= 5.6.40-83.2
5.7 >= 5.7.22-22

这意味着 MySQL 在启动的时候就会预先占用 innodb_buffer_pool_size 指定的大小的内存, 指定的越大, 分配的时间就会越长, 如下所示:

Feature reverted from the upstream implementation back to the one ported from Percona Server 5.6, 
in which innodb_numa_interleave variable not only enables NUMA memory interleaving at InnoDB buffer
pool allocation, but allocates buffer pool with MAP_POPULATE, forcing interleaved allocation at 
the buffer pool initialization time.

再回到我们最开始的问题, 分配的 innodb_buffer_pool_size 为 432G, 启动时间约为 440s, mmap 平均每秒分配约 1G 左右的内存.

如何避免启动慢的问题?

实际上我们很少对数据库进行重启操作, 即便启动很慢, 影响也是很小. 不过如果想加快启动速度, 可以试着从以下几方面着手:

1. 降低 innodb_buffer_pool_size

尽量不要将此参数设置的过大, 如果数据很多能拆库尽量拆库.

2. 关闭 innodb_numa_interleave

如果实例使用的内存远小于单颗 cpu 对应的内存, 可以考虑关闭 numa 策略. 很多云主机实例仅一颗 CPU node, 无法开启 numa 的平衡策略(kernel.numa_balancing), 这种情况下就无需开启 innodb_numa_interleave 选项.

3. 保存 buffer pool

参考以下配置, 通过保存 buffer pool 状态加快启动过程, 同时也节省了不少数据预热的时间:

innodb_buffer_pool_load_at_startup = 1
innodb_buffer_pool_dump_at_shutdown = 1
innodb_buffer_pool_dump_pct = 60

更多见: Saving and Restoring the Buffer Pool State

4. 使用低版本 MySQL

如上所述的版本变更, 我们可以使用较低的版本, 不过可能出现 PS-3967 不生效的问题. 从笔者的使用经验来看, 可以参考以下版本:

percona mysql 5.6.38
percona mysql 5.7.19

5. 开启 large-page

可以考虑将共享缓存分配给 innodb buffer pool, MySQL 指定 large page 功能后, 就不需要通过 mmap 预分配指定的 innodb_buffer_pool_size 大小的缓存. 这种方式的启动时间和使用低版本的 MySQL 等同. 开启 large-page 的过程可参考: configuring-huge-page-for-MySQL, large-page-support.

参考

a-brief-update-on-numa-and-mysql
5.6-innodb_numa_support
5.7-innodb_numa_support
percona_PS-3967
taobao-2015-07-05
numa_memory_policy