原创 tiredboy 网易游戏运维平台
tiredboy
网易游戏运维工程师,目前主要负责数据库相关的运维工作。
熟悉容器化(docker/k8s)的同学,可能经常听到 cgroups 或者 taskset,通过 cgroups 我们可以在系统层面为不同的进程分配不同的 CPU,以实现对进程使用资源的精细化控制。MySQL 8.0 也给我们带来了一个新的特性:Resource Groups
(https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html)。
后文简称 RG,通过 RG 我们也可以实现对 MySQL 内部线程资源的精细化控制(目前仅支持 CPU 方面的绑定调度)。
相信经常使用 MySQL 的同学都碰到过让自己头痛的问题:某个「烂」 SQL 占用了大量的 CPU 资源,导致其它正常查询不能被响应,甚至导致 MySQL 直接挂掉,为了解决这个问题,通过我们会用下面两个办法来解决:
设置 max_execution_time
来阻止长时间运行的 SQL。当然,后果就是当你确实有个 SQL 就是要跑这么久的时候,会被一视同仁的干掉;
自己通过外部工具或脚本,周期性检查,并杀掉相应 thread。且不说在负载高的时候,可能这个脚本都无法正常连接 MySQL Server,周期性检查这种明显具备滞后性的操作,对业务的影响是不可避免的。
RG 的引入可以在很大程度上解决这个问题,对于复杂、执行时间长、消耗资源多,但又不希望它们影响业务的任务(比如一些计算、统计性质的批处理),我们可以对这部份任务设置特定的资源组,限制任务的使用资源,避免对其它业务线程的影响,保证服务质量。
为了更直观的观察 CPU 占用情况,我利用存储过程制造了一个 MySQL CPU 炸弹 bomb
,样例如下:
mysql> DELIMITER //
mysql> CREATE PROCUDURE bomb(OUT ot BIGINT)
-> BEGIN
-> DECLARE cnt BIGINT DEFAULT 0;
-> SET @FUTURE = (SELECT NOW() + INTERVAL 120 SECOND);
-> WHILE NOW() < @FUTURE DO
-> SET cnt = (SELECT cnt + 1);
-> END WHILE;
-> SELECT cnt INTO ot;
-> END
-> //
mysql> DELIMITER ;
该存储过程 bomb
主要实现三个功能:
通过简单的 SELECT +1
死循环快速占用大量 CPU;
存储过程将执行 2 分钟;
存储过程会统计实际计算次数,并保存至存储过程的 OUT
变量。
其中 2 和 3 主要是为了测试 RG 的线程优先级用的(THREAD PRIORITY)。
特别留意:对于启用了线程池的 MySQL 目前是用不了 RG 的,当然了,我们这里用的是官方开源版,线程池当然是没有了。
MySQL 的 RG 信息保存在 infromation_schema.resource_groups
通过命令:
mysql> SELECT * FROM INFORMATION_SCHEMA.RESOURCE_GROUPS\G
可以看到系统默认已经有了两个 RG:URS_default 和 SYS_default,分别对应用户(前台)线程和系统(后台)线程。可以通过查询 performance_schema.threads
可以查看当前 MySQL 线程分别对应的资源组:
mysql> select * from performance_schema.threads\G
对 Resource Groups 的操作主要涉及两个权限:
创建、删除、修改 RG,需要有 RESOURCE_GROUP_ADMIN 权限
(https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html#priv_resource-group-admin) ;
设置资源组(SET RESOURCE GROUP
)需要有 RESOURCE_GROUP_USER
权限
(https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html#priv_resource-group-user) ;
环境要求与配置
这里仅涉及 Linux 操作系统的说明,官方对于各平台(MacOS/FreeBSD/Windows/Linux)均有详细的说明,可以参考这里:Resource Groups Restrictions
(https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html#resource-group-restrictions)。
Linux 系统上为了使用线程优先级(THREAD PRIORITY)功能,需要给予 MySQLD 二进制文件 CAP_SYS_NICE
能力。由于我们使用的是 systemd,配置也相对简单:
sudo systemctl edit mysqld
在自动打开的编辑器里增加以下内容:
[Service]
AmbientCapabilities=CAP_SYS_NICE
保存退出,并通过 systemctl restart mysql
重启 MySQL Server 即可。
请注意:未设置 CAP_SYS_NICE
时,修改资源组涉及 THREAD_PRIORITY
的操作也并不会报错,但会有 warnings,通过 SHOW WARNINGS
可以看到 THREAD_PRIORITY
默认被忽略了:
Attribute thread_priority is ignored (using default value).
使用
对 Resouce Groups 的增删改查等操作,在文档 8.12.5 Resource Groups(https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html) 有非常详细的描述。
本节不再赘述,主要是通过两个案例来检查 RG 在 CPU 绑定和线程优先级控制方面的能力。
为特定线程或当前 connection 绑定指定 CPU。下图我们创建了一个名为 rg
的 RG,并指定当前 connection/session 使用这个 RG,通过上述的 CPU 炸弹和 Linux 自带的 top
命令,可以看到 CPU 1 被占用。
创建一个低优选级 RG low
,线程优先级为 15,创建一个高优先级 RG high
,线程优先级为 5(请注意优先级数字越低,表示优先级越高),两个 RG 绑定了同一个 CPU:CPU0。我们通过上述的 CPU 炸弹来看一下分别绑定这两个 RG 时,线程优先级是否起到有效作用(可以看到高优先级的会话计算数量是低优先级会话的 8 倍多):
注意事项
当一个 RG 被绑定后,如 SET RESOURCE GROUP rg
,该 RG 默认不能被删除,除非当前连接断开或具体绑定的任务执行完成;
创建、删除、修改 RESOURCE GROUP 是否会在主从之间同步:不同步,也不生成 binlog 日志;
为了保障系统线程的优先级:类型为 SYS 的系统后台线程,thread priority 只能设置为 -20 ~0 之间,而普通 user 线程,则在 0~19 之间;
除了上述 SET RESOURCE GROUP
的方式绑定 RG,也可以通过 SQL HINT 的方式进行 RG 绑定,如:select /* + RESOURCE_GROUP(rg) */ * from performance_schema.threads;
通过 SET RESOURCE GROUP ${rg} FOR ${thread_id}
为特定线程绑定 RG 时,需要注意,${thread_id} 是 performance_schema.threads
里看到的 THREAD_ID
而非 SHOW PROCESSLIST
看到的 Id
;
启用了线程池的 MySQL 目前暂时无法使用 RG;
RG 整体的实现可以参考 WL#9467
(https://dev.mysql.com/worklog/task/?id=9467)
根据 sql/resourcegroups/platofrm (https://github.com/mysql/mysql-server/tree/8.0/sql/resourcegroups/platform)下的文件来看,提供了对 Linux/FreeBSD/Apple/Win 的支持,我们这里主要关注 Linux 上的实现。
Linux 的实现直接使用了 sched_setaffinity 来完成 CPU 的绑定
这个如果用过 taskset (https://github.com/karelzak/util-linux/blob/master/schedutils/taskset.c)命令的同学可能比较好理解,举个例子,我们可以通过命令 taskset -pc 0,1 1888
为 PID 为 1888 的进程绑定使用 0 和 1 两个 CPU。
RG 这里的实现是相同的,代码主要在 sql/resourcegroups/platform/thread_attrs_api_linux.cc
(https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/resourcegroups/platform/thread_attrs_api_linux.cc#L72) :
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
for (const auto &cpu_id : cpu_ids) CPU_SET(cpu_id, &cpu_set);
int rc = ::sched_setaffinity(thread_id, sizeof(cpu_set), &cpu_set);
if (rc != 0) {
char errbuf[MYSQL_ERRMSG_SIZE];
LogErr(ERROR_LEVEL, ER_RES_GRP_SET_THR_AFFINITY_FAILED, thread_id,
my_errno(), my_strerror(errbuf, MYSQL_ERRMSG_SIZE, my_errno()));
return true;
}
return false;
实现上来看没有太多的黑科技,把系统的接口封装成 SQL 语句提供给用户,底层的实现还是通过系统 API 给线程直接绑定 CPU。这也可以理解为什么启用线程池后目前还无法使用 RG,毕竟这里 CPU 绑定的最小粒度是线程。
目前 RG 仅支持对 CPU 的绑定和线程优先级,暂不支持其它资源的控制调度。其实从目前 Linux 的发展来看,cgroups 在进行 io 等调度方面还有比较多的瓶颈,短期可能也期望不了 RG 在这一块会有突破。但是,CPU 调度已经可以解决很多问题了,不是么。
在特定环境下,比如通过 cgroups 限制了 CPU 的容器环境,可能无法有效进行资源调度。针对这一点,下面做详细说明:
举个例子,我们通过 LXC 启动了一个容器,并分配了 3、4、7、8 共 4 个 CPU。在容器内启动 MySQL Server。我们先来看一下,默认的两个 RG 分别是什么样的:
mysql> select * from information_schema.resource_groups\G
*************************** 1. row ***************************
RESOURCE_GROUP_NAME: USR_default
RESOURCE_GROUP_TYPE: USER
RESOURCE_GROUP_ENABLED: 1
VCPU_IDS: 0-3
THREAD_PRIORITY: 0
*************************** 2. row ***************************
RESOURCE_GROUP_NAME: SYS_default
RESOURCE_GROUP_TYPE: SYSTEM
RESOURCE_GROUP_ENABLED: 1
VCPU_IDS: 0-3
THREAD_PRIORITY: 0
2 rows in set (0.01 sec)
mysql>
可以看到 VCPU_IDS 都是 0-3 ,也就是 0、1、2、3 共 4 个 CPU,我们来尝试一下绑定第二个 CPU,也就是 CPU1:
mysql> CREATE RESOURCE GROUP rg
-> TYPE = USER
-> VCPU = 1;
Query OK, 0 rows affected (0.00 sec)
mysql>
RG 创建成功了。但是我们来绑定一下呢:
mysql> SET RESOURCE GROUP rg;
ERROR 3661 (HY000): Unable to bind resource group rg with thread id (308485).(Failed to apply thread resource controls).
可以看到绑定失败了。这是因为当 MySQL 尝试去绑定 CPU1 时,宿主系统并没有分配 CPU1 给 MySQL。那如果我们来尝试直接使用系统预分配的 CPU ID 来绑定呢,比如 CPU4,我们直接修改原 rg 为 CPU4,看看会是什么情况:
mysql> ALTER RESOURCE GROUP rg VCPU=4;
ERROR 3652 (HY000): Invalid cpu id 4
可以看到这个时候也是无法绑定的。到这里基本上可以确认 MySQL 8.0 RG 在容器环境基本上无法正常使用。那么为什么呢?
我们在 sql/resourcegroups/resource_group_sql_cmd.cc 找到如下代码:
(https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/resourcegroups/resource_group_sql_cmd.cc#L121)
bool validate_vcpu_range_vector(
std::vector *vcpu_range_vector,
const Mem_root_array *cpu_list, uint32_t num_vcpus) {
...
if (vcpu_range.m_start >= num_vcpus || vcpu_range.m_end >= num_vcpus) {
my_error(ER_INVALID_VCPU_ID, MYF(0),
vcpu_range.m_start >= num_vcpus ? vcpu_range.m_start
: vcpu_range.m_end);
return true;
}
...
return false;
}
MySQL 在对资源组变更的时候会判断 CPU 是否合理,比如上述我们希望绑定 VCPU=4,但是 MySQL 只看到 4 个 CPU(最大 CPU 号为 3),导致无法绑定。VCPU=1 时虽然创建成功了,但在实际绑定的时候,由于宿主系统并未分配 CPU1 给这个容器,导致绑定的时候系统拒绝,出错了。
这里我个人是认为 MySQL 在实现这一块逻辑的时候偷懒的了,更合理的做法应该是通过看到的具体的 CPU 来去判断,而不是通过看到的 CPU 数量直接限制,这一块的逻辑并不复杂,写了一个 demo 来在容器中获取实际宿主分配的 CPU:
/*
* gcc -lpthread -o get_cpus get_cpus.c
*/
#include
#define __USE_GNU
#include
#include
#include
void main() {
/*
* sys_vcpu_cnt: system cpu numbers
* thread_vcpu_cnt: affinity cpu from the thread itself
*/
int sys_vcpu_cnt, thread_vcpu_cnt;
// GET VCPUS FROM sysconf
#ifdef _SC_NPROCESSORS_ONLN
sys_vcpu_cnt = sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(_SC_NPROCESSORS_CONF)
sys_vcpu_cnt = sysconf(_SC_NPROCESSORS_CONF);
#endif
printf("SYSTEM VCPU COUNT: %d\n", sys_vcpu_cnt);
// GET CPU FROM THREAD SELF
cpu_set_t set;
if (pthread_getaffinity_np(pthread_self(), sizeof(set), &set) == 0)
thread_vcpu_cnt = CPU_COUNT(&set);
printf("THREAD VCPU COUNT: %d\n", thread_vcpu_cnt);
int j;
// CAN BE TESTED BY: taskset -c 0,2 ./get_cpus
for (j=0; j < sys_vcpu_cnt; j++)
if (CPU_ISSET(j, &set))
printf("CPU %d\n", j);
}
我们来运行一下试试,./get_cpus
:
SYSTEM VCPU COUNT: 40
THREAD VCPU COUNT: 4
CPU 3
CPU 4
CPU 7
CPU 8
可以看到,通过系统提供的接口拿到正确的 CPU ID,并不复杂。我们可以认为是开发者没有考虑到容器化的情况(说坏话就是偷懒)。
LXC/ 容器想用 RG,怎么办?
从上面的内容来看,MySQL 阻止分配不合理的 VCPU 主要是通过看到的 CPU 数量 num_vcpus
来判定的,那么有没有可能我们让 MySQL 看到所有的 CPU 呢,这样首先就可以突破 MySQL 在 SQL 层面的检查并正常创建 RG 了。唯一要注意的是,我们在操作时,要确保分配的 CPU ID 是与宿主分配的一致(否则实际绑定时还是会被系统拒绝)。
我们来看一下 MySQL RG 里 CPU 数量是怎么计算的,代码在 sql/resourcegroups/platform/thread_attrs_api_common.cc
(https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/resourcegroups/platform/thread_attrs_api_common.cc#L37)
具体追溯,最后的计算发生在 sql/resourcegroups/platform/thread_attrs_api_linux.cc
(https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/resourcegroups/platform/thread_attrs_api_linux.cc#L149) :
#ifdef HAVE_PTHREAD_GETAFFINITY_NP
cpu_set_t set;
if (pthread_getaffinity_np(pthread_self(), sizeof(set), &set) == 0)
num_vcpus = CPU_COUNT(&set);
#endif // HAVE_PTHREAD_GETAFFINITY_NP
return num_vcpus;
}
uint32_t num_vcpus_using_config() {
cpu_id_t num_vcpus = 0;
#ifdef _SC_NPROCESSORS_ONLN
num_vcpus = sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(_SC_NPROCESSORS_CONF)
num_vcpus = sysconf(_SC_NPROCESSORS_CONF);
#endif
return num_vcpus;
}
由于有 pthread_getaffinity_np
的支持,根据上面的 get_cpus
样例,MySQL 最终引用的是该函数来计算 CPU,也就是 4 个。
思路到这里其实会比较明确了,如果我们希望在容器环境里正常使用该功能,最快的方式就是把 configure.cmake(https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/configure.cmake#L297) 里这句注释掉并重新编译:
CHECK_FUNCTION_EXISTS (pthread_getaffinity_np HAVE_PTHREAD_GETAFFINITY_NP)
更新 MySQL 后,我们再来看一下现在的绑定情况:
mysql> CREATE RESOURCE GROUP rg
-> TYPE = USER
-> VCPU = 10;
Query OK, 0 rows affected (0.00 sec)
mysql> SET RESOURCE GROUP rg;
ERROR 2013 (HY000): Lost connection to MySQL server during query
mysql>
mysql> ALTER RESOURCE GROUP rg
-> VCPU = 3,4,7,8;
Query OK, 0 rows affected (0.01 sec)
mysql> SET RESOURCE GROUP rg;
Query OK, 0 rows affected (0.00 sec)
mysql>
可以看到,VCPU 我们现在可以在宿主范围内指定了,但实际使用的时候超出范围(比如这里 CPU10)还是会被拒绝并出错的,所以要确保分配的 CPU ID 是与宿主分配的一致。当我们指定的 CPU 与系统分配的一致时(如这里的 3、4、7、8),这次我们顺利的绑定 RG。
实现熔断机制,比如为 SYS 资源组分配更高的优先级或绑定特定的 CPU,降低用户线程压力太大可能造成的 CRASH 风险;
对于可以预见的线程或会话,分配更高或更低的优先级。比如对于监控 session,可以分配较高优先级,当系统本身负载较高时,可以尽量确保监控、健康检查等行为有效。对于不紧急的批处理操作,可以绑定特定 CPU 并降低优先级,最大程度降低对业务响应的影响;
根据我们的测试,在特定场景下,为系统线程绑定专用资源,可能可以带来意想不到的性能正反馈:MySQL8.0中为 log_writer 和 log_flush 两个操作线程设置较高的 CPU 优先级后(确保资源不被大量用户线程挤兑),在部分场景对 QPS 会有明显提升。
8.12.5 Resource Groups
https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html
Resource Groups Restrictions
https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html#resource-group-restrictions
sched_setaffinity
https://linux.die.net/man/2/sched_setaffinity
taskset.c
https://github.com/karelzak/util-linux/blob/master/schedutils/taskset.c