GaussDB(DWS)的CPU资源隔离管控能力
1. 前言
- 适用版本:【8.1.3及以上】
GaussDB(DWS)使用cgroup实现了两种cpu管控能力,基于cpu.shares的共享配额管控和基于cpuset的专属限额管控。本文首先对内核cgroup相关功能进行了简要介绍,然后介绍了GaussDB(DWS)的cgroup层级结构,最后简要介绍了两种cpu管控方式,并分析了两者的优缺点。
2. cgroup概述
cgroup全称control group,是linux内核提供的用于对进程/线程使用的资源进行隔离、管控以及记录的组件。
相关概念:
- 任务(task):对应系统中的一个进程/线程;
- 控制组(control group):进行资源限制隔离的基本单位,一个任务加入到控制组任务列表(tasks)后即受控制组资源控制,支持在线将一个任务从一个控制组迁移到另外一个控制组。
- 层级(hierarchy):控制组是一种树形结构,一个父控制组可以有多个子控制组,一个子控制组只能属于一个父控制组。同属一个父控制组的子控制组间按照资源配置进行资源争抢、隔离,子控制组继承父控制组的资源配置。
- 子系统(subsytem):一个子系统对应一种资源的资源控制器,比如CPU子系统是控制CPU时间分配的控制器,CPUSET子系统是控制CPU核分配的控制器。
cgroup主要子系统介绍:
- cpu子系统:限制任务的cpu使用率;
- cpuacct 子系统:统计cgroup中所有任务使用cpu的累积信息,单位ns;
- cpuset子系统:限制任务能够使用的cpu核;
- memory子系统:限制任务的memory使用;
- blkio子系统:限制任务磁盘IO;
这里我们着重介绍GaussDB应用到的cgroup子系统,涉及的子系统包括:cpu子系统、cpuacct 子系统以及cpuset子系统。
2.1 cgroup文件系统
VFS (Virtual File System) 虚拟文件系统是系统内核非常强大的一个功能,它把文件系统的具体实现细节隐藏起来,给用户态进程提供一个统一的文件系统API接口。cgroup的接口操作也是基于VFS实现的,可以通过mount命令查看cgroup挂载信息,每个目录对应cgroup的一个子系统。
2.1.1 cpu & cpuacct 子系统
cpu子系统和cpuacct子系统相辅相成,cpu子系统限制cgroup使用的cpu时间,cpuacct统计cgroup使用的cpu时间,同时cpu子系统与cpuacct子系统挂载路径一致,因此可以把这两个子系统放在一起讨论:
tasks
tasks记录着关联到该cgroup的任务(进程/线程)pid,只有加入tasks的任务才受cgroup控制。
cpu子系统用于控制cgroup中所有任务可以使用的cpu时间,主要包含以下几个接口:
cpu.cfs_period_us & cpu.cfs_quota_us
cfs_period_us 与cfs_quota_us 需要组合使用,cfs_period_us用来配置进行cpu限制的单位时间周期,cfs_quota_us用来配置在设置的时间周期内所能使用的CPU时间。二者单位都是微秒(us),cfs_period_us的取值范围为1毫秒(ms)到1秒(s),cfs_quota_us的取值大于1ms即可,如果cfs_quota_us的值为-1(默认值),表示不受cpu周期的限制。举例说明:
1. 限制只能使用1个CPU(每100ms能使用100ms的CPU时间,即可以使用一个cpu)
# echo 100000 > cpu.cfs_quota_us /* quota = 100ms */
# echo 100000 > cpu.cfs_period_us /* period = 100ms */
2. 限制使用3个CPU(内核)(每100ms能使用300ms的CPU时间,即可以使用两个cpu)
# echo 300000 > cpu.cfs_quota_us /* quota = 300ms */
# echo 100000 > cpu.cfs_period_us /* period = 100ms */
3. 限制使用1个CPU的30%(每100ms能使用30ms的CPU时间,即可以使用30%的cpu)
# echo 30000 > cpu.cfs_quota_us /* quota = 30ms */
# echo 100000 > cpu.cfs_period_us /* period = 100ms */
cpu.shares
shares用来设置cgroup中任务CPU可用时间的相对比例,针对所有可用cpu,默认值是1024,在cpu出现满负载争抢时,各控制组按照shares设置的相对比例争抢cpu。假如系统中有两个cgroup,分别是A和B,A的shares值是10000,B的shares值是20000,那么A和B出现cpu争抢时A将获得10000/(10000+20000)=33.3%的CPU资源,而B将获得66.7%的CPU资源。shares作为一种共享配额的cpu管控方式,具备以下几个特点:
-
cpu空闲情况下,shares不起作用,只有在cpu出现满负载争抢时,各控制组任务才按照shares配置比例争抢cpu
-
空闲cpu其他控制组可以使用,如果A没有使用到33.3%的cpu时间,那么剩余的cpu时间将会被分配给B,即B的CPU使用率可以超过66.7%
-
如果添加了一个新的控制组C,且它的shares值是10000,那么A的配额比例变成10000/(10000+20000+10000)=25%,B的cpu配额比例变成50%
-
由于shares是一个权重值,需要和其它控制组的权重值进行对比才能得到自己的配额比例,在一个负载多变的环境上,cgroup数量和权重可能是多变的,这样给cgroup配置的配额比例,可能因为增加cgroup或修改其他cgroup权重导致该cgroup配额比例发生变化,无法精确控制cpu使用率。
cpu.stat
stat包含以下三项统计结果
- nr_periods: 表示经历了多少个cpu.cfs_period_us里面配置的时间周期
- nr_throttled: 在上面的这些周期中,有多少次cpu受到了限制(即cgroup中的进程在指定的时间周期中用光了它的配额)
- throttled_time: cgroup中的进程被限制使用CPU持续了多长时间(纳秒)
cpu.rt_runtime_us & cpu.rt_period_us
rt_runtime_us与rt_period_us组合使用可以对cgroup中的实时调度任务进行cpu时间限制,只可用于实时调度任务。rt_period_us用来配置进行cpu限制的单位时间周期,设置每隔多久cgroup对cpu资源的存取进行重新分配,rt_runtime_us用来配置在设置的时间周期内任务对cpu资源的最长连续访问时间,二者单位都是微秒(us)。
cpuacct(cpu accounting)子系统用于统计cgroup中任务所使用的cpu时间,主要包含以下接口:
cpuacct.usage
统计cgroup中所有任务使用的cpu时间,单位ns,可以通过写入0值重置统计信息
cpuacct.usage_percpu
统计cgroup中所有任务在每个cpu核上使用的cpu时间,单位ns
cpuacct.stat
统计cgroup中所有任务使用的用户态和系统态cpu时间,单位USER_HZ,格式如下:
- user:cgroup中所有任务使用的用户态cpu时间
- system:cgroup中所有任务使用的内核态cpu时间
2.1.2 cpuset 子系统
puset子系统可以为cgroup分配专属的cpu核和内存节点,cgroup中所有任务只能运行在分配的cpu和内存节点上。GaussDB仅应用了cpuset子系统的cpu控制能力,因此我们这里仅介绍cpu相关控制接口。
cpuset.cpus
设置cgroup中任务可以使用的cpu,使用小横线‘-’设置连续cpu,不连续cpu之间使用逗号‘,’分隔。例如:0-1,11-12,17 表示cgroup可以使用cpu 0、1、11、12、17。
cpuset.cpu_exclusive
包含标签0和1,可以控制其他cpuset及其父子cpuset是否可以共享该cpuset的cpu,默认值为0,cpu不会专门分配给某个cpuset。
cpuset.sched_load_balance
包含标签0和1,设定内核是否可以在该cpuset的cpu上进行负载均衡,默认值1,表示内核可以将超载cpu上的任务移动至低负载cpu上以平衡负载。
注意:如果父cgroup启用了负载均衡,则其所有子cgroup默认开启负载均衡,因此如果要禁用cgroup的负载均衡,则其所有上层cgroup都需要关闭负载均衡,同时需要考虑其他同层cgroup是否能够关闭负载均衡。
cpuset.sched_relax_domain_level
表示内核进行负载均衡的策略,如果禁用负载均衡则该值无意义。不同系统框架下,该值意义可能不同,以下为常用值的含义:
值 | 均衡策略 |
---|---|
-1 | 负载均衡默认值 |
0 | 不执行负载均衡;负载均衡是阶段性的 |
1 | 对同一cpu上的线程进行负载均衡 |
2 | 对同一软件包中的线程进行负载均衡 |
3 | 对同一节点或扇叶中的线程进行负载均衡 |
4 | 对不使用统一内存访问(NUMA)构架中的多个 CPU 进行负载平衡 |
5 | 对使用统一内存访问(NUMA)构架中的多个 CPU 进行直接负载平衡 |
3. GaussDB的cpu管控
GaussDB(DWS)支持两种cpu管控能力,基于cpu.shares的共享配额管控和基于cpuset的专属限额管控。在介绍GaussDB(DWS)的cpu管控之前,我们首先介绍GaussDB(DWS)中的cgroup结构。
3.1 GaussDB的cgroup层级模型
GaussDB(DWS)的cpu管控需要考虑其他进程与GaussDB(DWS)之间的cpu管控,GaussDB(DWS)内核后台线程与用户线程之间的cpu管控,用户之间的cpu管控。为了应对不同层级的cpu隔离管控需求,GaussDB(DWS)设计了基于cgroup层级特点的cpu分层隔离管控,GaussDB(DWS)的cgroup层级模型如下图所示:
以上cgroup均适配了cpu子系统和cpuset子系统,每一个cgroup都包含两个值:cpu.shares和cpuset.cpus。GaussDB(DWS)借助cgroup提供了三个维度的cpu隔离管控能力:
- GaussDB(DWS)与其他进程之间的隔离管控
- 数据库常驻后台线程与作业线程的隔离管控
- 数据库用户之间的隔离管控
GaussDB与其他进程之间的隔离管控
数据库集群的每个节点在cgroup的cpu子系统和cpuset子系统内均包含一个专属目录:“GaussDB:gaussdba”,作为GaussDB(DWS)的主cgroup节点(目录),用于限制和记录GaussDB(DWS)内所有线程使用的cpu,GaussDB(DWS)进程内所有线程均直接或间接的受到该cgroup的限制。通过限制GaussDB(DWS)内核使用的cpu,可防止数据库系统对其他应用程序造成影响。
数据库常驻后台线程与作业线程的隔离管控
GaussDB(DWS)主cgroup节点之下包含两个cgroup:Backend控制组和Class控制组。Backend控制组用于GaussDB(DWS)常驻后台线程的cpu隔离管控,Class控制组用于作业线程的cpu隔离管控。大部分后台常驻线程cpu资源占用较少,但AutoVacuum线程cpu资源占用可能较多,因此在Backend控制组单独提供Vacuum控制组用于AutoVaccum线程的cpu资源限制,而除AutoVacuum之外的其他后台常驻线程均受到DefaultBackend控制组的cpu资源限制。
数据库用户之间的隔离管控
通常情况下数据库系统会同时运行多种类型的作业,不同类型作业之间可能出现cpu资源争抢。GaussDB(DWS)资源管理为用户提供了双层的cgroup层级结构用于cpu隔离管控,用户可按需创建和修改cgroup配置实现不同类型作业之间的cpu资源限制。用户双层cgroup包含以下控制组:
- UserClass控制组:用户创建的父控制组,主要实现cpu资源的初步划分,如:不同的父控制组可以属于不同的部门/分公司
- RemainWD控制组:包含UserClass控制组创建UserWD控制组后剩余的cpu资源
- Timeshare控制组:创建UserClass控制组时默认创建的优先级控制组,包含四个优先级的控制组:Rush:High:Medium:Low,资源配比为:8:4:2:1
- UserWD控制组:用户创建的子控制组,在父控制组基础上实现cpu资源的细粒度划分,如:不同的子控制组可以属于同一部门下的不同类型作业
3.2 GaussDB作业的cpu管控
数据库系统中运行多种类型作业出现cpu争取时,不同用户可能有不同的诉求,主要诉求如下:
- 实现cpu资源的充分利用,不在意单一类型作业的性能,主要关注cpu整体吞吐量
- 允许一定程度的cpu资源争抢和性能损耗,在cpu空闲情况下实现cpu资源充分利用,在cpu满负载情况下希望各类型按比例使用cpu
- 部分作业对性能敏感,不在意cpu资源的浪费
对于第一种诉求,不建议进行用户之间的cpu隔离管控,不管控哪一种的cpu管控都或多或少的对cpu整体使用率产生影响;对于第二种诉求可以采用基于cpu.shares的共享配额管控方式,实现满负载cpu隔离管控前提下尽量提高cpu整体使用率;对于第三种诉求可以采用基于cpuset.cpus的专属限额管控方式,实现不同类型作业之间的cpu绝对隔离。下面对这两种作业cpu管控方式进行简要介绍,具体细节和使用中的疑问,我们后面文章再详细介绍,用兴趣的朋友也可以自己动手实验。
3.2.1 CPU的共享配额管控
共享配额有两层含义:
共享:cpu是所有控制组共享的,空闲的cpu资源其他控制组能够使用
配额:业务繁忙cpu满负载情况下,控制组之间按照配额比例进行cpu抢占
共享配额基于cpu.shares实现,通过上面cgroup的介绍我们可知这种管控方式只有在cpu满负载情况下生效,因此在cpu空闲情况下并不能保证控制组能够抢占到配额比例的cpu资源。cpu空闲是不是可以理解为没有cpu资源争抢,控制组内任务可以任意使用cpu,因此不会有性能影响呢?答案是错误的,虽然cpu平均使用率可能不高,但是某个特定时刻还是可能存在cpu资源争抢的。示例:
10个cpu上运行10个作业,每个cpu上运行一个作业,这种情况下各作业在任意时刻请求cpu都可以瞬间得到响应,作业之间没有任何cpu资源的争抢;但是假如10个cpu上运行20个作业,因为作业不会一直占用cpu,在某些时间可能等待IO、网络等,因此cpu使用率可能并不高,此时cpu资源看似空闲,但是在某个时刻可能出现2~N作业同时请求一个cpu的情况出现,此时即会导致cpu资源争抢,影响作业性能。
通过测试验证,在cpu满负载情况下,控制组之间基本可以按照配额比例占用cpu,实现cpu资源的配额管控。
3.2.2 CPU的专属限额管控
专属限额有两层含义:
专属:cpu是某个控制组专属的,空闲的cpu资源其他控制组不能使用
限额:只能使用限额配置的cpu资源,其他控制组空闲的cpu资源,也不能抢占
专属限额基于cpuset.cpus实现,通过合理的限额设置可以实现控制组之间cpu资源的绝对隔离,各控制组间任务互不影响。但是因为cpu的绝对隔离,因此在控制组空闲时就会导致cpu资源的极大浪费,因此限额设置不能太大。那从作业性能来看是不是限额越大越好呢?答案是不完全正确,示例:
假设10个作业运行在10个cpu上,cpu平均使用率5%左右;10个作业运行在5个cpu上,cpu平均使用率10%左右。通过上面共享配额的性能分析我们可知:虽然10个作业运行在5个cpu上cpu使用率很低,看似空闲,但是相对10个作业运行在10个cpu上还是存在某种程度的cpu资源争抢的,因此10个作业运行在10个cpu上性能要好于运行在5个cpu上。那是不是越多越好呢?10个作业运行在20个cpu上,在任意一个时刻,总会至少10个cpu是空闲的,因此理论上10个作业运行在20个cpu上并不会比运行在10个cpu上性能更好。
因此我们可以得知,对于并发为N的控制组,分配cpus小于N的情况下,cpu越多作业性能越好;但是当分配cpus大于N的情况下,性能就不会有任何提升了。
3.2.3 共享配额与专属限额对比
cpu共享配额和专属限额的管控方式各有优劣,共享配额能够实现CPU资源的充分利用,但是各控制组之间资源隔离不彻底,可能影响查询性能;专属限额的管控方式可以实现cpu资源的绝对隔离,但是在cpu资源空闲时会造成cpu资源的浪费。相对专属限额来说,共享配额拥有更高的cpu使用率和更高的整体作业吞吐量;相对共享配额来说,专属限额cpu隔离彻底,更满足性能敏感用户的使用诉求。
从上面GaussDB(DWS)的cgroup层级结构我们得知,用户cgroup是包含父子两层控制的,那可不可以父控制组一层使用专属限额,而子控制组一层使用共享配额呢?答案是肯定的,另外同一层控制组也可以同时有使用专属限额和共享配额的控制存在。具体使用本文不做介绍,有兴趣的可以自己试验。
4. 总结
本文首先对cgroup进行概述,并着重介绍了GaussDB(DWS)应用到的cpu子系统、cpuacct 子系统以及cpuset子系统。然后介绍了GaussDB(DWS)中的cgroup结构,并对基于cpu.shares的共享配额管控和基于cpuset的专属限额管控二种管控能力进行详细介绍,并对比其优缺点。
- 点赞
- 收藏
- 关注作者
评论(0)