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

熟悉虚拟化(docker/k8s)的同学,可能经常听到 cgroups 或者 taskset,通过 cgroups 我们可以在系统层面为不同的进程分配不同的 CPU,以实现对进程使用资源的精细化控制。MySQL 8.0 也给我们带来了一个新的特性:Resource Groups。后文简称 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 权限;
  2. 设置资源组(SET RESOURCE GROUP)需要有 RESOURCE_GROUP_USER 权限;

环境要求与配置

这里仅涉及 Linux 操作系统的说明,官方对于各平台(MacOS/FreeBSD/Windows/Linux)均有详细的说明,可以参考这里:Resource Groups 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 有非常详细的描述。本节不再赘述,主要是通过两个案例来检查 RG 在 CPU 绑定和线程优先级控制方面的能力。

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

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

20200421160103.png
20200421160140.png

注意事项

  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,根据 sql/resourcegroups/platofrm 下的文件来看,提供了对 Linux/FreeBSD/Apple/Win 的支持,我们这里主要关注 Linux 上的实现。

Linux 的实现直接使用了 sched_setaffinity 来完成 CPU 的绑定,这个如果用过 taskset 命令的同学可能比较好理解,举个例子,我们可以通过命令 taskset -pc 0,1 1888 为 PID 为 1888 的进程绑定使用 0 和 1 两个 CPU。RG 这里的实现是相同的,代码主要在 sql/resourcegroups/platform/thread_attrs_api_linux.cc� :

  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 找到如下代码:

    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,具体追溯,最后的计算发生在 sql/resourcegroups/platform/thread_attrs_api_linux.cc

#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 里这句注释掉并重新编译:

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. 根据我们的测试,在特定场景下,为系统线程绑定专用资源,可能可以带来意想不到的性能正反馈:https://g.126.fm/01AMb2U

参考文档

  1. 8.12.5 Resource Groups

  2. Resource Groups Restrictions

  3. sched_setaffinity

  4. taskset.c

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容