关于后端:精准限制CPUCgroups

2次阅读

共计 4246 个字符,预计需要花费 11 分钟才能阅读完成。

如何对待 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/}]
正文完
 0