意见箱
恒创运营部门将仔细参阅您的意见和建议,必要时将通过预留邮箱与您保持联络。感谢您的支持!
意见/建议
提交建议

精准限制CPU:Cgroups

来源:恒创科技 编辑:恒创科技编辑部
2024-02-08 02:31:59
如何看待CPU资源?

由于进程和线程在Linux的CPU调度看来没啥区别,所以本文后续都会用进程这个名词来代表内核的调度对象,一般来讲也包括线程

如果要分配资源,我们必须先搞清楚这个资源是如何存在的,或者说是如何组织的。我想CPU大家都不陌生,我们都在系统中用过各种工具查看过CPU的使用率,比如说以下这个命令和它的输出:

mpstat -P ALL 1 1

在这里插入图片描述


精准限制CPU:Cgroups

根据显示内容我们知道,这个计算机有4个cpu核心,目前的cpu利用率几乎是0,就是说系统整体比较闲。

从这个例子大概可以看出,我们对cpu资源的评估一般有两个观察角度:核心个数百分比

目前的计算机基本都是多核甚至多cpu系统,一个服务器上存在几个到几十个cpu核心的情况都很常见。所以,从这个角度看,cgroup应该提供一种手段,可以给进程们指定它们可以占用的cpu核心,以此来做到cpu计算资源的隔离。
百分比这个概念我们需要多解释一下:这个百分比究竟是怎么来的呢?难道每个cpu核心的计算能力就像一个带刻度表的水杯一样?一个进程要占用就会占用到它的一定刻度么?

当然不是!这个cpu的百分比是按时间比率计算的。基本思路是:一个CPU一般就只有两种状态,要么被占用,要么不被占用。当有多个进程要占用cpu的时候,那么操作系统在一个cpu核心上是进行分时处理的。比如说,我们把一秒钟分成1000份,那么每一份就是1毫秒,假设现在有5个进程都要用cpu,那么我们就让它们5个轮着使用,比如一人一毫秒,那么1秒过后,每个进程只占用了这个CPU的200ms,使用率为20%。整体cpu使用比率为100%。
同理,如果只有一个进程占用,而且它只用了300ms,那么在这一秒的尺度看来,cpu的占用时间是30%。于是显示出来的状态就是占用30%的CPU时间。

这就是内核是如何看待和分配计算资源的。当然实际情况要比这复杂的多,但是基本思路就是这样。Linux内核是通过CPU调度器CFS--完全公平调度器对CPU的时间进行调度的,由于本文的侧重点是cgroup而不是CFS,对这个题目感兴趣的同学可以到这里进一步学习。CFS是内核可以实现真对CPU资源隔离的核心手段,因此,理解清楚CFS对理解清楚CPU资源隔离会有很大的帮助。

如何隔离CPU资源?

根据CPU资源的组织形式,我们就可以理解cgroup是如何对CPU资源进行隔离的了。

无非也是两个思路,一个是分配核心进行隔离,另一个是分配CPU使用时间进行隔离。

Cgroups介绍

Cgroups是linux的重要组件之一,可以对进程或用户进行隔离和限制

Cgroups全称Control Groups,是Linux内核提供的物理资源隔离机制,通过这种机制,可以实现对Linux进程或者进程组的资源限制、隔离和统计功能。比如可以通过cgroup限制特定进程的资源使用,比如使用特定数目的cpu核数和特定大小的内存,如果资源超限的情况下,会被暂停或者杀掉。

cgroups核心概念

任务(task)
在cgroup中,任务就是一个进程。
控制组(control group)
cgroup的资源控制是以控制组的方式实现,控制组指明了资源的配额限制。进程可以加入到某个控制组,也可以迁移到另一个控制组。
层级(hierarchy)
控制组有层级关系,类似树的结构,子节点的控制组继承父控制组的属性(资源配额、限制等)。
子系统(subsystem)
一个子系统其实就是一种资源的控制器,比如memory子系统可以控制进程内存的使用。子系统需要加入到某个层级,然后该层级的所有控制组,均受到这个子系统的控制

cgroups进行CPU限制

我们的机器自带cgproups,可以使用命令验证
mount -t cgroup
在这里插入图片描述

cgroup暴露给用户的API为文件系统,所有对cgroup的操作均可以通过对文件的修改完成,cgroup API对应的路径为:/sys/fs/cgroup/,作为使用方,仅需要对文件系统中的内容进行编辑,即可达到配置对应的cgroup的要求。

创建cgroup
cd /sys/fs/cgroup/cpu
mkdir test

在这里插入图片描述
创建文件夹后,cgroup会自动在该文件夹下初始化出配置文件:
在这里插入图片描述

其中,需要关注的文件有3个,分别为:

cgroup的限制逻辑如下:

1 限制所有pid在tasks中的进程,
2 在 cpu.cfs_period_us 周期内,只能使用最多 cpu.cfs_quota_us 的cpu资源。
3 默认情况下,cpu.cfs_period_us的单位为微秒,默认值为100ms。cpu.cfs_quota_us的值为-1,暨不做限制。
4 例如: 限制在100ms中,只能使用30ms的cpu资源,暨限制cpu占用率为30%
echo 30000 > cpu.cfs_quota_us
5 启动测试程序,并添加pid到tasks文件中后,再观察CPU情况,可以清晰的看到被限制在了30%
echo pid(loglistener的进程号) > /sys/fs/cgroup/cpu/rocket/test

使用cgroups的go客户端

这是一个使用Golang封装的用来操作cgroups的工具包,支持创建、管理、检查和销毁cgroups。使用go提供的客户端,可以在服务器上提供一个守护进程,由守护进程接收请求后,进行cgroup管理。相关的核心代码如下:

package main

import (
    "flag"
    "github.com/containerd/cgroups"
    "github.com/opencontainers/runtime-spec/specs-go"
    "log"
    "os/exec"
    "strings"
    "time"
)

var kb = 1024
var mb = 1024 * kb

// main
// call some process and add this process into cgroup
func main() {
    var cgPath = flag.String("cgroup_path", "", "cg-path is cgroup path name")
    var period = flag.Uint64("cpu_period", 100000, "cpu limit value, default is 100% ")
    var quota = flag.Int64("cpu_quota", -1, "cpu limit value, default is 100% ")
    var memLimit = flag.Int("mem_limit", 100, "mem limit value, default is 100mb ")

    var cmd = flag.String("cmd", "", "your application cmd")
    var args = flag.String("args", "", "cmd args")

    flag.Parse()

    cpuLimit := float32(*quota) / float32(*period) * 100
    limit := int64(*memLimit * mb)

    log.Printf("cgroup_path: %s, cpu_quota: %v, cpu_period: %v,max (%v%%), mem_limit: %vm (%d), cmd: %s, args: %v \n",
        *cgPath, *quota, *period, cpuLimit, *memLimit, limit, *cmd, *args)

    control, err := cgroups.New(cgroups.V1, cgroups.StaticPath(*cgPath), &specs.LinuxResources{
        CPU: &specs.LinuxCPU{
            Quota:  quota,
            Period: period,
        },
        Memory: &specs.LinuxMemory{
            Limit: &limit,
        },
    })
    if err != nil {
        log.Fatal(err)
        return
    }
    defer control.Delete()

    pid := run(*cmd, strings.Split(*args, " ")...)
    log.Printf("run process done, pid: %v, add to cgroup task\n", pid)
    if err = control.AddTask(cgroups.Process{Pid: pid}); err != nil {
        log.Fatal(err)
        return
    }

    tasks, err := control.Tasks(cgroups.Freezer, false)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Current tasks: %v", tasks)

    time.Sleep(10 * time.Second)
}

// run cmd in background, and return pid
func run(cmd string, args ...string) int {
    log.Printf("[run], cmd:%s, args: %v", cmd, args)

    command := exec.Command(cmd, args...)
    err := command.Start()
    if err != nil {
        log.Fatalf("Start error, %v", err)
        return 0
    }
    for {
        if command.Process != nil {
            return command.Process.Pid
        }
        time.Sleep(1000 * time.Microsecond)
    }
}
go run main.go -cgroup_path test -cmd /root/pi/main -cpu_quota 300000 -cpu_period 1000000 


2022/08/08 12:21:23 cgroup_path: test, cpu_quota: 300000, cpu_period: 1000000,max (30.000002%), mem_limit: 100m (104857600), cmd: /root/pi/main, args:  
2022/08/08 12:21:23 [run], cmd:/root/pi/main, args: []
2022/08/08 12:21:23 run process done, pid: 11855, add to cgroup task
2022/08/08 12:21:23 Current tasks: [{freezer 11855 /sys/fs/cgroup/freezer/test/}]
上一篇: Linux常用的四种配置网卡方式 下一篇: 手机怎么远程登录云服务器?