Docker核心技术:Docker原理之Cgroups

云原生学习路线导航页(持续更新中)

本文是 Docker核心技术 系列文章:Docker原理之Cgroups,其他文章快捷链接如下:

4.4.Cgroups:资源管控

4.4.1.Cgroups是什么

  • Cgroups是谷歌Borg系统内部做资源管控的,后来将之提供到了Linux Kernel中

  • Linux内核中,Cgroups实现

    • 在一个task中,还有另外一个属性 cgroups

4.4.2.Cgroups工作机制

  • Cgroups实际上就是一个文件系统,在Linux的 /sys/fs/cgroup

  • Cgroup本身又有很多的子系统,包括cpu子系统 /sys/fs/cgroup/cpu、memory子系统 /sys/fs/cgroup/memory等等,每个子系统中又包括各种配置文件,配置子系统的参数

    • 比如cpu子系统中,包含多个配置文件,负责控制cpu绝对值、cpu相对值、cpu limit等等

    • 如果想要控制一个进程的cpu绝对值,就修改cgroup目录下相应的文件,就可以立即做到cpu的限额

  • 如果是容器,每个容器都有自己的cgroup

  • 【🌟】cgroups的结构:树形结构

    • /sys/fs/cgroup
      • cpu子系统
        • 当前子系统的cpu配置文件
        • 每个目录:又是关联到cpu子系统 的 子系统,目录内部又会包含当前子系统的cpu配置文件
      • memory子系统
      • ...

4.4.3.Cgroups包含哪些子系统

4.4.4.Cgroups CPU子系统

  • cpu子系统下,有很多配置,常用的配置文件如下:
    • cpu.shares:设置cpu的相对值
      • 假设一个主机有3个CPU,创建了2个cgroup
      • Cgroup1 中 cpu.shares 写512,cgroup2 中 cpu.shares写1024
      • 意味着,这两个cgroup,可以按照 512: 1024=1: 2 的比例分摊cpu的时间,即Cgroup1 会分到1个CPU、Cgroup2 会分到2个CPU
      • 不过1:2只是一个相对值,如果Cgroup2 中压根没有进程,则 Cgroup1中的进程,就可以使用超过1个CPU
    • cpu.cfs_period_us、:cpu.cfs_quota_us:相互配合,设置cpu的绝对值
      • cpu.cfs_period_us:控制cpu时间周期的长度,默认是10万,100000
      • cpu.cfs_quota_us:控制当前cgroup能拿到一个cpu时间周期的多少。
        • 默认是-1,即不限制当前cgroups下进程可使用的cpu绝对值
        • 可如果你设置 cpu.cfs_period_us为 10万,cpu.cfs_quota_us为 1万,则表示 当前cgroup中的进程,一共可以得到 1万/10万=0.1个CPU
        • 这是一个绝对值,当前cgroup下的 所有进程 占用cpu的总时间,不可超过0.1个CPU时间
    • cpuacct.usage:进行cgroup及其子cgroup下的,cpu使用情况的统计分析,可用做监控

4.4.5.拓展:Linux中多个进程如何共享cpu时间片

4.4.5.1.Linux内核的调度器
  • Linux Kernel 2.6.23 以后,默认的是CFS(Completely Fair Scheduler)完全公平调度器
4.4.5.2.CFS调度器
4.4.5.3.vruntime红黑树
  • 进程初始化时,为进程初始化它的vruntime值,插入树中,最小值放在左边,最大值放右边
  • 每次进程调度,都拿最左边的那个进程去运行。
  • 每次调度之后,时钟周期会为被调度进程重新计算一遍vruntime,公式:vruntime=实际运行时间*1024/进程权重
    • 进程权重相当于cpu_share的那个比例值
    • 被调度的进程,vruntime会一直涨;未被调度的进程,vruntime就不会变
    • 当被调度的进程,vruntime不再是最小的了,红黑树结构就会发生变化,将下次应该调度的进程放在最左边
  • 按照这样的模式,就可以 按照进程权重的设置,为每个进程分配 公平的cpu时间
4.4.5.4.CFS进程调度在Linux Kernel中的实现
  • 感兴趣的可以深入

4.4.6.Cgroup Memory子系统

  • memory子系统下,有很多配置,常用的配置文件如下:

    • memory.soft_limit_in_bytes:内存软限制
    • memory.limit_in_bytes:内存硬限制,进程使用超过就会OOM
    • memory.oom_control :当发生OOM时,对进程执行什么操作,默认是 kill 进程。memory.oom_control中默认包含3个值:
      • oom_kill_disable 0:该参数表示是否禁用 OOM 杀死进程的功能。如果值为 0,则表示未禁用,即允许内核在 OOM 事件发生时杀死进程以释放内存。如果值为 1,则表示已禁用,即内核不会杀死进程
      • under_oom 0:该参数表示是否处于 OOM 状态。如果值为 0,则表示系统当前未处于 OOM 状态。如果值为 1,则表示系统当前处于 OOM 状态
      • oom_kill 0:该参数表示是否已经执行了 OOM 杀死进程的操作。如果值为 0,则表示尚未执行 OOM 杀死进程的操作。如果值为 1,则表示已经执行了 OOM 杀死进程的操作

4.4.7.Cgroup CPU 子系统练习

  • 练习:使用Cgroups控制进程的资源开销
  • 进入主机的 /sys/fs/cgroup/cpu目录下,mkdir cpudemo

    • 相当于我们创建了一个新的cgroup,并将其与 CPU 子系统相关联
    • linux会自动为cpudemo目录中,创建cpu的各种配置文件
  • 准备一个死循环的程序,用来占用cpu资源

    • 该程序会吃掉两个cpu
    go 复制代码
    package main
    
    func main(){
      // 协程1:死循环会吃掉一个cpu
      go func(){
        for {
        }
      }()
      // 主协程:死循环也会吃掉一个cpu
      for {
        
      }
    }
  • 将上面的程序写入一个main.go,然后build一下

    sh 复制代码
    [root@VM-226-235-tencentos ~/zgy/code/test_cpu]# go build main.go 
    [root@VM-226-235-tencentos ~/zgy/code/test_cpu]# ls
    main  main.go
  • 在终端前台执行一下这个程序,终端会卡住,因为是死循环,没有任何输出

  • 另外打开一个终端,使用top命令查看现在的cpu使用情况

    • 可以看到,main程序果然吃掉了2个CPU
    • main程序的pid为:19963
  • 现在我们去cpudemo cgroup下,为这个进程设置CPU限额

    sh 复制代码
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cd /sys/fs/cgroup/cpu/cpudemo
    
    # 将main程序的pid,写入cgroup.procs,表示将main进程加入这个cgroup控制
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 19963 > cgroup.procs
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cgroup.procs
    19963
    
    # 使用绝对值控制cpu使用额度,cpu.cfs_period_us默认是10万,向cpu.cfs_quota_us写入1万,则main进程的cpu额度将会被限制在0.1个CPU
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 10000 > cpu.cfs_quota_us
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cpu.cfs_period_us
    100000
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cpu.cfs_quota_us
    10000
  • 再次top查看 main进程的cpu使用,果然被限制在0.1附近

  • 可以用同样的方法,将cpu.cfs_quota_us值改成1.5万,则cpu将会被限制在0.15个

  • 删除子系统

    • 注意,直接使用rm是删不掉cgroup子系统的

    • 需要先安装cgroup-tools的工具,然后执行cgdelete命令,才可以删除子系统

      sh 复制代码
      # ubuntu下用apt,centos下用yum
      apt install cgroup-tools
      cd /sys/fs/cgroup/cpu
      cgdelete cpu:cpudemo

4.4.8.Cgroup Memory子系统练习

  • 进入主机的 /sys/fs/cgroup/memory目录下,mkdir memorydemo

    • 相当于我们创建了一个新的cgroup,并将其与 Memory 子系统相关联
    • linux会自动为memorydemo目录中,创建memory的各种配置文件
  • 准备一个死循环的程序,用来占用memory资源。

    • 程序效果:

      • 该程序每隔1分钟就申请100MB内存,并将其填充为字符 'A'。共申请10次,最多申请1000MB。

      • 可以通过 watch 命令,定期执行给定的命令并显示其输出,监视内存使用情况

        sh 复制代码
        watch 'ps -aux | grep malloc | grep -v grep'
        1. watch 命令:watch 是一个 Linux 命令,用于定期执行给定的命令并显示其输出。
        2. ps aux 命令:ps 是一个用于查看当前运行进程的命令。aux 选项用于显示所有用户的所有进程,并以紧凑的格式显示进程的信息。
        3. grep malloc 命令:grep 是一个用于在文本中搜索指定模式的命令。在这里,它用于过滤出包含 "malloc" 字符串的行,即查找正在运行的进程中使用 malloc 函数进行内存分配的进程。
        4. grep -v grep 命令:-v 选项用于反向匹配,即排除包含 "grep" 字符串的行。这是为了避免在结果中显示 grep 命令本身的进程。
      • 然后我们通过修改cgroup下的配置文件,将该进程的内存使用量,限制在100MB,超过时会OOM。

    • 创建一个main.go文件

      go 复制代码
      package main
      
      //#cgo LDFLAGS:
      //char* allocMemory();
      import "C"
      import (
      	"fmt"
      	"time"
      )
      
      func main() {
      	// only loop 10 times to avoid exhausting the host memory
      	holder := []*C.char{}
      	for i := 1; i <= 10; i++ {
      		fmt.Printf("Allocating %dMb memory, raw memory is %d\n", i*100, i*100*1024*1025)
      		// hold the memory, otherwise it will be freed by GC
      		holder = append(holder, (*C.char)(C.allocMemory()))
      		time.Sleep(time.Minute)
      	}
      }
    • 创建一个 malloc.c 文件

      c 复制代码
      #include <stdlib.h>
      #include <stdio.h>
      #include <string.h>
      
      #define BLOCK_SIZE (100*1024*1024)
      char* allocMemory() {
          char* out = (char*)malloc(BLOCK_SIZE);
          memset(out, 'A', BLOCK_SIZE);
          return out;
      }
    • 创建一个Makefile

      makefile 复制代码
      build:
      	CGO_ENABLED=1 GOOS=linux CGO_LDFLAGS="-static" go build
  • 程序写完,make build一下

    sh 复制代码
    [root@VM-226-235-tencentos ~/zgy/code/test_mem]# go mod init example.com/memory-test
    [root@VM-226-235-tencentos ~/zgy/code/test_mem]# go mod tidy
    [root@VM-226-235-tencentos ~/zgy/code/test_mem]# make build
    [root@VM-226-235-tencentos ~/zgy/code/test_mem]# ls
    go.mod  main.go  Makefile  malloc.c  memory-test
  • 在终端前台执行一下这个程序,终端会卡住,因为进程每次循环都会sleep 1min

  • 另外打开一个终端,使用 watch 'ps -aux | grep malloc | grep -v grep' 命令查看现在的 malloc 内部的进程情况

    • 可以看到,有一个进程memory-test,果然malloc了100MB的内存
    • memory-test进程的pid为:13433
  • 现在我们去memorydemo cgroup下,为这个进程设置Memory限额

    sh 复制代码
    [root@VM-226-235-tencentos /sys/fs/cgroup/memory/memorydemo]# cd /sys/fs/cgroup/memory/memorydemo
    
    # 将memory-test进程的pid,写入cgroup.procs,表示将 memory-test 进程加入这个cgroup控制
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 13433 > cgroup.procs
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cgroup.procs
    13433
    
    # 向 memory.limit_in_bytes 写入 104960000,大约是100MB
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 104960000 > memory.limit_in_bytes
    [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat memory.limit_in_bytes
    104960000
  • 在程序执行的终端可以看到,进程已经被kill掉了,因为申请内存超过了100MB,被执行OOM了。默认的OOM策略就是kill process

4.4.9.kubernetes Pod的cgroup怎么查看

  • 方法一:进入pod内部查看
    • 进入pod内部,在 /sys/fs/cgroup下面,可以看到当前pod的所有 cgroup设置
    • 比如 /sys/fs/cgroup/cpu 中,cpu.cfs_period_us、cpu.cfs_quota_us 就以绝对值的方式,设置了pod的 cpu limit
    • 再比如,在 /sys/fs/cgroup/memory 中,memory.limit_in_bytes 就设置了pod的 memory limit
  • 方法二:在pod所在主机上查看
    • 进入主机的 /sys/fs/cgroup,每一个子系统中,都有一个kubepods的目录,里面存储着所有pod的cgroup
    • kubepods里面,还有一个burstable目录,这就是所有pod cgroup的存储位置
    • 比如,查看一个pod的cpu cgroup,就应该进入:/sys/fs/cgroup/cpu/kubepods/burstable/podxxxx-xxxx/
      • 其中 podxxxx-xxxx 表示pod的uid,可以使用kubectl get pods podxxx -oyaml找到

4.4.10.kubelet启动时,报错cgroup driver不一致

  • 因此,启动kubelet时,有时会报错:cgroup driver不一致,kubelet启动失败。原因如下:

    • kubelet默认使用 systemd 作为 cgroup driver
    • 如果你本地还装了docker,且docker默认使用的是 cgroupfs 作为 cgroup driver。
    • 二者cgroup driver不同,kubelet出于保护,会禁止启动
  • 解决方法:将docker的cgroup driver改成和kubelet一样,比如将docker的cgroup driver改成systemd,操作如下:

相关推荐
猫猫的小茶馆7 分钟前
【C语言】指针常量和常量指针
linux·c语言·开发语言·嵌入式软件
朝九晚五ฺ30 分钟前
【Linux探索学习】第十五弹——环境变量:深入解析操作系统中的进程环境变量
linux·运维·学习
ernesto_ji1 小时前
Jenkins下载安装、构建部署到linux远程启动运行
linux·servlet·jenkins
李迟1 小时前
某Linux发行版本无法使用nodejs程序重命名文件问题的研究
java·linux·服务器
酷酷学!!!2 小时前
Linux基础指令(汇总)
linux·运维·服务器
xiangshangdemayi2 小时前
Windows环境GeoServer打包Docker极速入门
windows·docker·容器·geoserver·打包·数据挂载
枫叶丹42 小时前
【在Linux世界中追寻伟大的One Piece】手写序列化与反序列化
linux·运维·网络
韦德斯2 小时前
嵌入式Linux的RTC读写操作应用
linux·运维·c语言·arm开发·实时音视频
程序员JerrySUN3 小时前
熟悉的 Docker,陌生的 Podman
linux·docker·容器·系统架构·podman