是如何查看CPU资源的?由于Linux的CPU调度并没有进程和线程的区别,所以本文后面会用进程这个词来表示内核的调度对象。一般来说,它也包括线程。我们要分配资源,首先要搞清楚资源是怎么来的。存在,或者它是如何组织的。我想每个人都熟悉CPU。我们都使用过各种工具来检查系统中的CPU使用率。例如下面的命令及其输出:mpstat-PALL11根据显示的内容,我们知道这台电脑有4个cpu核,当前cpu利用率几乎为0,说明系统整体比较闲置的。从这个例子可以看出,我们对cpu资源的评价一般有两个观察角度:核心数和百分比。现在的计算机基本上都是多核甚至多cpu系统。一个服务器有几个到几十个cpu核是很常见的。因此,从这个角度来说,cgroup应该提供一种手段来指定进程可以占用的cpu核数,从而实现cpu计算资源的隔离。我们需要多解释一下百分比的概念:这个百分比是怎么来的?每个cpu核心的计算能力是不是像一个有刻度的水杯?一个进程要占用它会占用一定的量吗?当然不是!此cpu百分比计算为时间比率。基本思想是:一个CPU一般只有两种状态,要么被占用,要么没有被占用。当有多个进程占用CPU时,操作系统在一个CPU核上进行分时处理。比如我们把一秒分成1000份,那么每份就是1毫秒,假设有5个进程需要使用cpu,那么我们让他们依次使用5个,比如每个人有1毫秒,那么1秒后,每个进程只占用这个CPU200ms,使用率为20%。整体cpu使用率为100%。同理,如果只有一个进程被占用,只需要300ms,那么在这一秒的尺度上,cpu占用了30%的时间。所以显示的状态是占用30%的CPU时间。这就是内核查看和分配计算资源的方式。当然,实际情况比这复杂得多,但基本思路是这样的。Linux内核通过CPU调度器CFS来调度CPU时间——完全公平的调度器。由于本文的重点是cgroups而不是CFS,所以对这个话题感兴趣的同学可以到这里进一步学习。CFS是内核真正实现CPU资源隔离的核心手段。因此,了解CFS对于理解CPU资源隔离会有很大的帮助。如何隔离CPU资源?根据CPU资源的组织形式,我们可以理解cgroup是如何隔离CPU资源的。无非是两种思路,一种是分配核数进行隔离,一种是分配CPU使用时间进行隔离。Cgroups简介Cgroups是linux的重要组成部分之一。它可以隔离和限制进程或用户。限制、隔离和统计功能。比如可以通过cgroup限制特定进程的资源使用,比如使用特定数量的cpu核,特定大小的内存。如果资源超过限制,它将被挂起或杀死。cgroups核心概念任务(task)在cgroup中,一个任务就是一个进程。控制组(controlgroup)cgroup的资源控制是以控制组的形式实现的,控制组表示资源的配额限制。进程可以添加到一个控制组或迁移到另一个控制组。Hierarchy(层次结构)控制组具有层级关系,类似于树状结构,子节点的控制组继承父控制组的属性(资源配额、限制等)。子系统(subsystem)一个子系统实际上是一个资源控制器。例如,内存子系统可以控制进程内存的使用。子系统需要添加到某一层级,然后该层级的所有控制组都由该子系统控制。cgroups用于限制CPU。我们的机器带有cgproups。可以使用命令验证mount-tcgroupcgroup暴露给用户的API是一个文件系统,所有对cgroups的操作都可以通过修改文件完成,cgroupAPI对应的路径为:/sys/fs/cgroup/,作为用户,只需要编辑文件系统中的内容就可以达到配置对应的cgroup需求。创建cgroupcd/sys/fs/cgroup/cpumkdirtest创建文件夹后,cgroup会自动初始化该文件夹下的配置文件:其中,有3个文件需要注意,即:cgroup的限制逻辑是如下:1limit所有tasks中pid的进程在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%echo30000>cpu.cfs_quota_us5启动测试程序,将pid添加到tasks文件中,然后观察cpu情况,可以清楚查看被限制在30%echopid(loglistener进程号)>/sys/fs/cgroup/cpu/rocket/testGoclientusingcgroups这是一个用Golang封装的操作cgroups的工具包,支持创建、管理、检查和销毁cgroup。使用go提供的client,可以在server上提供一个daemon进程,daemon进程收到请求后会管理cgroup。相关的核心代码如下:packagemainimport("flag""github.com/containerd/cgroups""github.com/opencontainers/runtime-spec/specs-go""log""os/exec""strings""time")varkb=1024varmb=1024*kb//main//调用一些进程并将此进程添加到cgroupfuncmain(){varcgPath=flag.String("cgroup_path","","cg-pathiscgrouppathname")varperiod=flag.Uint64("cpu_period",100000,"cpulimitvalue,defaultis100%")varquota=flag.Int64("cpu_quota",-1,"cpulimitvalue,defaultis100%")varmemLimit=flag.Int("mem_limit",100,"memlimitvalue,defaultis100mb")varcmd=flag.String("cmd","","yourapplicationcmd")varargs=flag.String("args","","cmdargs")flag.Parse()cpuLimit:=float32(*quota)/float32(*period)*100limit:=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)控制,err:=cgroups.New(cgroups.V1,cgroups.StaticPath(*cgPath),&specs.LinuxResources{CPU:&specs.LinuxCPU{配额:quota,Period:period,},Memory:&specs.LinuxMemory{Limit:&limit,},})iferr!=nil{log.Fatal(err)return}defercontrol.Delete()pid:=run(*cmd,strings.Split(*args,"")...)log.Printf("运行进程完成,pid:%v,添加到cgroup任务\n",pid)iferr=control.AddTask(cgroups.Process{Pid:pid});err!=nil{log.Fatal(err)return}任务,err:=control.Tasks(cgroups.Freezer,false)iferr!=nil{log.Fatal(err)}log.Printf("Currenttasks:%v",tasks)time.Sleep(10*time.Second)}//后台运行cmd,返回pidfuncrun(cmdstring,args...string)int{log.Printf("[run],cmd:%s,args:%v",cmd,args)command:=exec.Command(cmd,args...)错误:=command.Start()iferr!=nil{log.Fatalf("开始错误,%v",err)return0}for{ifcommand.Process!=nil{returncommand.Process.Pid}time.Sleep(1000*time.Microsecond)}}gorunmain.go-cgroup_pathtest-cmd/root/pi/main-cpu_quota300000-cpu_period10000002022/08/0812:21:23cgroup_path:test,cpu_quota:300000,cpu_period:1000000,max(30.000002%),mem_limit:100m(104857600),cmd:/root/pi/main,args:2022/08/0812:21:23[run],cmd:/root/pi/main,args:[]2022/08/0812:21:23runprocessdone,pid:11855,addtocgrouptask2022/08/0812:21:23Current任务:[{freezer11855/sys/fs/cgroup/freezer/test/}]
