如何看待CPU资源?
由于进程和线程在Linux的CPU调度看来没啥区别,所以本文后续都会用进程这个名词来代表内核的调度对象,一般来讲也包括线程
如果要分配资源,我们必须先搞清楚这个资源是如何存在的,或者说是如何组织的。我想CPU大家都不陌生,我们都在系统中用过各种工具查看过CPU的使用率,比如说以下这个命令和它的输出:
mpstat -P ALL 1 1
根据显示内容我们知道,这个计算机有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/}]