MySQL 8.0 资源组(Resource Groups)深度解读

原创 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 直接挂掉,为了解决这个问题,通过我们会用下面两个办法来解决:

  1. 设置 max_execution_time 来阻止长时间运行的 SQL。当然,后果就是当你确实有个 SQL 就是要跑这么久的时候,会被一视同仁的干掉;

  2. 自己通过外部工具或脚本,周期性检查,并杀掉相应 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 主要实现三个功能:

  1. 通过简单的 SELECT +1 死循环快速占用大量 CPU;

  2. 存储过程将执行 2 分钟;

  3. 存储过程会统计实际计算次数,并保存至存储过程的 OUT 变量。

其中 2 和 3 主要是为了测试 RG 的线程优先级用的(THREAD PRIORITY)。

如何使用 Resource Groups

特别留意:对于启用了线程池的 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 的操作主要涉及两个权限:

  1. 创建、删除、修改 RG,需要有 RESOURCE_GROUP_ADMIN 权限
    (https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html#priv_resource-group-admin) ;

  2. 设置资源组(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 绑定和线程优先级控制方面的能力。

  1. 为特定线程或当前 connection 绑定指定 CPU。下图我们创建了一个名为 rg 的 RG,并指定当前 connection/session 使用这个 RG,通过上述的 CPU 炸弹和 Linux 自带的 top 命令,可以看到 CPU 1 被占用。

  1. 创建一个低优选级 RG low,线程优先级为 15,创建一个高优先级 RG high,线程优先级为 5(请注意优先级数字越低,表示优先级越高),两个 RG 绑定了同一个 CPU:CPU0。我们通过上述的 CPU 炸弹来看一下分别绑定这两个 RG 时,线程优先级是否起到有效作用(可以看到高优先级的会话计算数量是低优先级会话的 8 倍多):

注意事项

  1. 当一个 RG 被绑定后,如 SET RESOURCE GROUP rg,该 RG 默认不能被删除,除非当前连接断开或具体绑定的任务执行完成;

  2. 创建、删除、修改 RESOURCE GROUP 是否会在主从之间同步:不同步,也不生成 binlog 日志;

  3. 为了保障系统线程的优先级:类型为 SYS 的系统后台线程,thread priority 只能设置为 -20 ~0 之间,而普通 user 线程,则在 0~19 之间;

  4. 除了上述 SET RESOURCE GROUP 的方式绑定 RG,也可以通过 SQL HINT 的方式进行 RG 绑定,如:select /* + RESOURCE_GROUP(rg) */ * from performance_schema.threads;

  5. 通过 SET RESOURCE GROUP ${rg} FOR ${thread_id} 为特定线程绑定 RG 时,需要注意,${thread_id} 是 performance_schema.threads 里看到的 THREAD_ID 而非 SHOW PROCESSLIST 看到的 Id

  6. 启用了线程池的 MySQL 目前暂时无法使用 RG;

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 限制

  1. 目前 RG 仅支持对 CPU 的绑定和线程优先级,暂不支持其它资源的控制调度。其实从目前 Linux 的发展来看,cgroups 在进行 io 等调度方面还有比较多的瓶颈,短期可能也期望不了 RG 在这一块会有突破。但是,CPU 调度已经可以解决很多问题了,不是么。

  2. 在特定环境下,比如通过 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<resourcegroups::Range> *vcpu_range_vector,
       const Mem_root_array<resourcegroups::Range> *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 <stdio.h>
    #define __USE_GNU
    #include <sched.h>
    #include <unistd.h>
    #include <pthread.h>
    
    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。

使用场景

  1. 实现熔断机制,比如为 SYS 资源组分配更高的优先级或绑定特定的 CPU,降低用户线程压力太大可能造成的 CRASH 风险;

  2. 对于可以预见的线程或会话,分配更高或更低的优先级。比如对于监控 session,可以分配较高优先级,当系统本身负载较高时,可以尽量确保监控、健康检查等行为有效。对于不紧急的批处理操作,可以绑定特定 CPU 并降低优先级,最大程度降低对业务响应的影响;

  3. 根据我们的测试,在特定场景下,为系统线程绑定专用资源,可能可以带来意想不到的性能正反馈:MySQL8.0中为 log_writer 和 log_flush 两个操作线程设置较高的 CPU 优先级后(确保资源不被大量用户线程挤兑),在部分场景对 QPS 会有明显提升。 

参考文档

  1. 8.12.5 Resource Groups
    https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html

  2. Resource Groups Restrictions
    https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html#resource-group-restrictions

  3. sched_setaffinity
    https://linux.die.net/man/2/sched_setaffinity

  4. taskset.c
    https://github.com/karelzak/util-linux/blob/master/schedutils/taskset.c

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐