[作者]
王栋: 携程技术保障中心数据库专家, 对数据库疑难问题的排查和数据库自动化智能化运维工具的开发有强烈的兴趣.
[问题描述]
我们知道当 mysqld 进程使用到 SWAP 时, 就会严重影响到 MySQL 的性能. SWAP 的问题比较复杂, 本文会从 SWAP 的原理开始, 分享我们碰到的案例和分析思路.
[SWAP 原理]
swap 是把一部分磁盘空间或文件, 当作内存来使用. 它有换出和换入两种方式, 换出是进程把不活跃的内存数据存储到磁盘上, 并释放数据占用的内存空间, 换入是进程再次访问这部分数据的时候, 从磁盘读到内存中.
swap 扩展了内存空间, 是为了回收内存. 内存回收的机制, 一种是当内存分配没有足够的空间时, 系统需要回收一部分内存, 称为直接内存回收. 另外还有一个专门的 kswapd0 进程用来定期回收内存. 为了衡量内存的使用情况, 定义了三个内存阀值, 分为页最小水位 (min), 页低水位 (low), 页高水位 (high)
执行下面命令, 可以看到水位线对应的值, 如下图所示
cat /proc/zoneinfo |grep -E "Node|pages free|nr_inactive_anon|nr_inactive_file|min|low|high"|grep -v "high:"
内存回收行为主要有
1, 当系统剩余内存低于 low 时, kswapd 开始起作用进行内存回收, 直到内存达到 high 水位.
2, 当剩余内存达到 min 时就会触发直接回收.
3, 当触发全局回收, 并且 file+free<=high 时, 一定会进行针对匿名页的 swap.
[NUMA 与 SWAP]
有些案例我们发现系统还有大量剩余空间的情况下, 已经使用了 swap. 这正是 NUMA 架构导致的. NUMA 架构下每个 Node 都有本地的内存空间, Node 间内存使用不均衡, 当某个 Node 的内存不足时, 就可能导致 swap 的产生.
[swappiness]
我们大概理解了内存回收的机制, 回收的内存包括文件页和匿名页. 对文件页的回收就是直接回收缓存, 或者把脏页写回到磁盘再进行回收. 对匿名页的回收, 就是通过 swap, 将数据写入磁盘后再释放内存.
通过调整 / proc/sys/vm/swappiness 的值, 可以调整使用 swap 的积极程度, swappiness 值从 0-100, 值越小, 倾向于回收文件页, 尽量少的使用 swap. 我们最初将这个值调整为 1, 但发现并不能避免 swap 的产生. 实际上即使将这个值设置 0, 当满足 file+free<=high 时, 还是会发生 swap.
[关闭 NUMA 的方案]
在 NUMA 开启的情况, 由于 NUMA 节点间内存使用不均衡, 可能导致 swap, 解决这个问题主要有下面一些方案
, 在 mysqld_safe 脚本中加上 "numactl -interleave all" 来启动 mysqld
, Linux Kernel 启动参数中加上 numa=off, 需要重启服务器
, 在 BIOS 层面关闭 NUMA
, MySQL 5.6.27/5.7.9 开始引用 innodb_numa_interleave 选项
对于 2,3,4 关闭 NUMA 的方案比较简单, 不做详细描述, 下面重点描述下方案 1
[开启 numa interleave 访问的步骤]
1, yum install numactl -y
2, 修改 / usr/bin/mysqld_safe 文件
cmd="`mysqld_ld_preload_text`$NOHUP_NICENESS" 下新增一条脚本
- cmd="/usr/bin/numactl --interleave all $cmd"
- 3,service MySQL stop
4, 写入硬盘, 防止数据丢失
sync;sync;sync
5, 延迟 10 秒
sleep 10
6, 清理 pagecache,dentries 和 inodes
- sysctl -q -w vm.drop_caches=3
- 7,service MySQL start
8, 验证 numactl -interleave all 是否生效, 可以通过下面命令, interleave_hit 是采用 interleave 策略从该节点分配的次数, 没有启动 interleave 策略的服务器, 这个值会很低
numastat -mn -p `pidof mysqld`
至此我们 MySQL5.6 的服务器通过上面方案解决了由于 NUMA Node 间内存分配不均导致的 swap 的问题. 对于 MySQL5.7.23 版本的服务器, 我们使用了 innodb_numa_interleave 选项, 但问题并没有彻底解决.
[使用 MySQL5.7 新增 innodb_numa_interleave 选项的问题]
在开启 innodb_numa_interleave 选项的服务器中, 仍然会存在 NUMA Node 间内存分配不均衡的问题, 会导致 swap 产生. 针对这个问题做了进一步分析:
1, MySQL 版本为 5.7.23, 已经开启了 innodb_numa_interleave
2, 使用命令查看 mysqld 进程的内存使用情况, numastat -mn `pidof mysqld`
可以看出 Node 0 使用了约 122.5G 内存, Node 1 使用了约 68.2G 内存, 其中 Node0 上的可用空间只剩 566M, 如果后面申请 Node 0 节点分配内存不足, 就可能产生 swap
- Per-node process memory usage (in MBs) for PID 1801 (mysqld)
- Node 0 Node 1 Total
- --------------- --------------- ---------------
- Huge 0.00 0.00 0.00
- Heap 0.00 0.00 0.00
- Stack 0.01 0.07 0.09
- Private 125479.61 69856.82 195336.43
- ---------------- --------------- --------------- ---------------
- Total 125479.62 69856.90 195336.52
3, 是 innodb_numa_interleave 没有生效吗, 通过分析 / proc/1801/numa_maps 文件可以进一步查看 mysqld 进程的内存分配情况
以其中一条记录为例,
7f9067850000 表示内存的虚拟地址
interleave:0-1 表示内存所用的 NUMA 策略, 这里使用了 Interleave 方式
anon=5734148 匿名页数量
dirty=5734148 脏页数量
active=5728403 活动列表页面的数量
N0=3607212 N1=2126936 节点 0,1 分配的页面数量
kernelpagesize_kB=4 页面大小为 4K
7f9067850000 interleave:0-1 anon=5734148 dirty=5734148 active=5728403 N0=3607212 N1=2126936 kernelpagesize_kB=4
4, 通过解析上面文件, 对 Node 0 和 Node 1 节点分配的页面数量做统计, 可以计算出 Node 0 通过 interleave 方式分配了约 114.4G 内存, Node 1 通过 interleave 方式分配了约 64.7G 内存
说明 innodb_numa_interleave 开关是实际生效的, 但是即使 MySQL 使用了 interleave 的分配方式, 仍然存在不均衡的问题
5, 通过 innodb_numa_interleave 相关的源码, 可以看出当开关开启时, MySQL 调用 Linux 的 set_mempolicy 函数指定 MPOL_INTERLEAVE 策略跨节点来分配内存 set_mempolicy(MPOL_INTERLEAVE, numa_all_nodes_ptr->maskp, numa_all_nodes_ptr->size)
当开关关闭时, set_mempolicy(MPOL_DEFAULT, NULL, 0), 使用默认的本地分配策略
- my_bool srv_numa_interleave = FALSE;
- #ifdef HAVE_LIBNUMA
- #include <numa.h>
- #include <numaif.h>
- struct set_numa_interleave_t
- {
- set_numa_interleave_t()
- {
- if (srv_numa_interleave) {
- ib::info() <<"Setting NUMA memory policy to"
- "MPOL_INTERLEAVE";
- if (set_mempolicy(MPOL_INTERLEAVE,
- numa_all_nodes_ptr->maskp,
- numa_all_nodes_ptr->size) != 0) {
- ib::warn() << "Failed to set NUMA memory"
- "policy to MPOL_INTERLEAVE:"
- << strerror(errno);
- }
- }
- }
- ~set_numa_interleave_t()
- {
- if (srv_numa_interleave) {
- ib::info() << "Setting NUMA memory policy to"
- "MPOL_DEFAULT";
- if (set_mempolicy(MPOL_DEFAULT, NULL, 0) != 0) {
- ib::warn() << "Failed to set NUMA memory"
- "policy to MPOL_DEFAULT:"
- << strerror(errno);
- }
- }
- }
- };
[测试对比开启 innodb_numa_interleave 开关和 numactl -interleave=all 启动 mysqld 进程两种方式 NUMA 节点的内存分配情况]
场景一, numactl --interleave=all 启动 mysqld 进程的方式
1, 修改 systemd 配置文件, 删除 my.cnf 中 innodb_numa_interleave=on 开关配置, 重启 MySQL 服务
/usr/bin/numactl --interleave=all /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS
2, 运行 select count(*) from test.sbtest1 语句, 这个表中有 2 亿条记录, 运行 14 分钟, 会将表中的数据读到 buffer pool 中
3, 运行结束后, 分析 numa_maps 文件可以看到 mysqld 进程采用了 interleave 跨节点访问的分配方式, 两个 Node 间分配的内存大小基本一致
- 7f9a3c5b3000 interleave:0-1 anon=1688811 dirty=1688811 N0=842613 N1=846198 kernelpagesize_kB=4
- 7f9a3c5b3000 interleave:0-1 anon=2497435 dirty=2497435 N0=1247949 N1=1249486 kernelpagesize_kB=4
4,mysqld 进程总的分配也是均衡的
场景二, 开启 innodb_numa_interleave 的方式
1, 增加 my.cnf 中 innodb_numa_interleave=on 开关配置, 重启 MySQL 服务, 执行与场景一相关的 SQL 语句
2, 运行结束后, 分析 numa_maps 文件可以看到 mysqld 进程采用 interleave 方式分配的在不同 Node 间是基本平衡的
- 7f71d8d98000 interleave:0-1 anon=222792 dirty=222792 N0=111652 N1=111140 kernelpagesize_kB=4
- 7f74a2e14000 interleave:0-1 anon=214208 dirty=214208 N0=107104 N1=107104 kernelpagesize_kB=4
- 7f776ce90000 interleave:0-1 anon=218128 dirty=218128 N0=108808 N1=109320 kernelpagesize_kB=4
3, 不过仍有部分内存使用了 default 的本地分配策略, 这部分内存全部分配到了 Node 0 上
7f31daead000 default anon=169472 dirty=169472 N0=169472 kernelpagesize_kB=4
4, 最终 mysqld 进程分配的内存 Node 0 比 Node 1 大了约 1G
[MySQL5.7.23 启用 numactl -interleave=all 的方法]
MySQL5.7 版本不再使用 mysqld_safe 文件, 所以启用 numactl -interleave=all 的方式, 与 MySQL 5.6 的方法不同, 总结如下:
1, 修改 VIM /etc/my.cnf 文件, 删除 innodb_numa_interleave 配置项
2, 修改 systemd 的本地配置文件, VIM /usr/lib/systemd/system/mysqld.service, 增加 / usr/bin/numactl --interleave=all 命令
- # Start main service
- ExecStart=/usr/bin/numactl --interleave=all /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS
3, 停止 MySQL 服务
systemctl stop mysqld.service
4, 重新加载配置文件
systemctl daemon-reload
5, 写入硬盘, 防止数据丢失
sync;sync;sync
6, 延迟 10 秒
sleep 10
7, 清理 pagecache,dentries 和 inodes
sysctl -q -w vm.drop_caches=3
8, 启动 MySQL 服务
systemctl start mysqld.service
9, 验证是否生效,
首先确认 show global variables like 'innodb_numa_interleave'; 开关为关闭状态
正常情况下 mysqld 进程会全部采用 interleave 跨节点访问的分配方式, 如果可以查询到其他访问方式的信息, 表示 interleave 方式没有正常生效
Less /proc/`pidof mysqld`/numa_maps|grep -v 'interleave'
[结论]
numactl -interleave=all 启动 mysqld 进程的方式 NUMA 不同 Node 间分配的内存会更加均衡.
这个差异是与 innodb_numa_interleave 参数执行的策略有关, 开启后, 全局内存采用了 interleave 的分配方式, 但线程内存采用了 default 的本地分配方式.
而如果使用 numactl -interleave=all 启动 mysqld 进程, 所有内存都会采用 interleave 的分配方式.
来源: https://www.cnblogs.com/CtripDBA/p/11541680.html