2

Docker原理实战-2:Cgroups

 2 years ago
source link: https://jasonkayzk.github.io/2021/08/29/Docker%E5%8E%9F%E7%90%86%E5%AE%9E%E6%88%98-2%EF%BC%9ACgroups/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

讲述Docker底层原理的第二篇文章,本文讲述了Docker和K8S中限制容器硬件资源的技术:Linux内核中的Cgroups;

系列文章:

Docker原理实战-2:Cgroups

Cgroups

Namespace用来隔离自己单独的空间,而Cgroups(Control Groups)用来限制每个空间的硬件资源,如:CPU、内存等;

通过Cgroups可以方便的限制某个进程的资源占用,并且可以实时的监控进程和统计信息;

Cgroups三大组件

Cgroups中一共包括了3大组件:

① cgroup

对进程进行分组管理的一种机制,即:一个cgroup中包含了一组进程;

而我们可以在整个cgrouo上增加Linux Subsystem的各种参数配置,将一整组进程和一整组subsystem的系统配置关联起来!

② subsystem

subsystem是一组资源控制的模块,包括了:

名称 说明 blkio 设置对块设备(比如硬盘)输入输出的访问控制; cpu 设置 cgroup 中进程的 CPU 被调度的策略; cpuacct 统计 cgroup 中进程的 CPU 占用 ; cpuset 在多核机器上设置 cgroup 中进程可以使用的 CPU 和内存(此处内存仅使用于UMA 架构); devices 控制 cgroup 中进程对设备的访问; freezer 用于挂起( suspend )和恢复( resume) cgroup 中的进程; memory 控制 cgroup 中进程的内存占用; net_cls 用于将 cgroup 中进程产生的网络包分类,以便 Linux 的 tc (traffic controller )可以根据分类区分出来自某个 cgroup 的包并做限流或监控; net_prio 设置 cgroup 中进程产生的网络流量的优先级; ns 这个 subsystem 比较特殊,它的作用是:使 cgroup 中的进程在新的 Namespace 中 fork 新进程 ( NEWNS )时,创建出一个新的 cgroup,这个 cgroup 包含新的 Namespace 中的进程;

每个 subsystem 会关联到定义了相应限制的 cgroup 上,并对这个 cgroup 中的进程做相应的限制和控制!

可以通过安装 cgroup 命令行工具查看当前内核支持哪些subsystem:

  root@jasonkay:~# apt install cgroup-tools(原:cgroup-bin)

  root@jasonkay:~# lssubsys -a
  cpuset
  cpu,cpuacct
  blkio
  memory
  devices
  freezer
  net_cls,net_prio
  perf_event
  hugetlb
  pids
  rdma

③ hierarchy

hierarchy的功能是将一组croup串成一个树状结构,通过这个树状结构,可以实现 cgroups 的继承;

系统对一组定时任务 cgroup-1 限制了CPU使用率,而其中的一个定时任务还需要限制磁盘IO;

为了避免影响到其他进程,可以创建继承自 cgroup-1的cgroup-2,并设置 cgroup-2 限制磁盘IO;

三个组件的关系

Cgroups主要是通过cgroup、subsystem和hierarchy三个组件协作完成的;

三个组件的关系:

  • 系统在创建了新的 hierarchy 之后,系统中所有的进程都会加入这个 hierarchy 的 cgroup根节点,这个 cgroup 根节点是 hierarchy 默认创建的;在 hierarchy 中创建的 cgroup 都是这个 cgroup 根节点的子节点;
  • 一个 subsystem 只能附加到 一 个 hierarchy 上面;
  • 一个 hierarchy 可以附加多个 subsystem;
  • 一个进程可以作为多个 cgroup 的成员,但是这些 cgroup 必须在不同的 hierarchy 中;
  • 一个进程 fork 出子进程时,子进程是和父进程在同一个 cgroup 中的,也可以根据需要将其移动到其他 cgroup 中;

Linux Kernel接口

在Cgroups中,hierarchy是一个树状的结构;

因此,Linux内核为了使 Cgroups 的配置更加直观,直接通过虚拟的树状文件系统对 Cgroups 进行配置;

下面来实际创建一个 Cgroups;

创建并挂载一个hierarchy

首先,创建并挂载一个hierarchy(cgroup树):

################### 创建cgroup ###################
cd ~ && pwd # /root

# 创建一个hierarchy挂载点
mkdir cgroup-test

# 挂载一个hierarchy
sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test

# 查看文件:
ls ./cgroup-test 

# 输出:
# cgroup.clone_children  cgroup.sane_behavior  release_agent cgroup.procs notify_on_release tasks

完成hierarchy挂载后,目录下会生成一些默认文件,这些文件就是这个hierarchy中的cgroup根节点的配置:

① cgroup.clone_children

cpuset 的 subsystem 会读取这个配置文件:

如果这个值是1(默认是 0),子 cgroup 才会继承父 cgroup 的 cpuset 的配置;

② cgroup.procs

树中当前节点 cgroup 中的进程组ID;

现在的位置是在根节点,这个文件中会有现在系统中所有进程组的 ID!

root@jasonkay:~# cat ./cgroup-test/cgroup.procs 
1
2
3
4
……

③ notify_on_release 和 release_agent

notify_on_release 和 release_agent 会一起使用:

notify_on_release 标识当这个 cgroup 最后一个进程退出的时候是否执行了 release_agent:

release_agent 则是一个路径,通常用作进程退出之后自动清理掉不再使用的 cgroup;

④ tasks

tasks 标识该 cgroup 下面的进程 ID:

如果把一个进程 ID 写到 tasks 文件中,便会将相应的进程加入到这个 cgroup 中!

扩展cgroup子节点

然后在刚刚创建好的 hierarchy 上的 cgroup 根结点上扩展出两个子 cgroup:

# 当前路径:
root@jasonkay:~# cd ./cgroup-test && pwd 
# /root/cgroup-test

# 创建子cgroup
sudo mkdir cgroup-1     # 创建子cgroup "cgroup-1"
sudo mkdir cgroup-2     # 创建子cgroup "cgroup-2"

# 查看目录结构:
root@jasonkay:~/cgroup-test# tree
.
├── cgroup-1
│   ├── cgroup.clone_children
│   ├── cgroup.procs
│   ├── notify_on_release
│   └── tasks
├── cgroup-2
│   ├── cgroup.clone_children
│   ├── cgroup.procs
│   ├── notify_on_release
│   └── tasks
├── cgroup.clone_children
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks

2 directories, 14 files

可以看到:

在一个 cgroup 的目录下创建子目录时,内核会将该目录直接标记为这个cgroup的子目录,并继承父 cgroup 的属性!

在cgroup中添加和移动进程

一个进程在一个Cgroups的hierarchy中,只能在一个cgroup节点上存在,系统进程都会默认在根节点上存在!

可以将进程移动到其他cgroup节点,只需要将进程的PID写入到对应cgroup节点的task文件即可!

################### 在cgroup中添加和移动进程 ###################

# 查看当前路径
cd cgroup-1/ && pwd # /root/cgroup-test/cgroup-1

# 查看当前Shell进程PID
echo $$ # 6758

# 将所在终端检测移动至cgroup-1中
sudo sh -c "echo $$ >> tasks" 

# 查看配置
cat /proc/6758/cgroup

# 输出:
# 13:name=cgroup-test:/cgroup-1
# 12:blkio:/user.slice
# 11:rdma:/
# 10:cpuset:/
# 9:memory:/user.slice/user-0.slice/session-1.scope
# 8:hugetlb:/
# 7:devices:/user.slice
# 6:net_cls,net_prio:/
# 5:pids:/user.slice/user-0.slice/session-1.scope
# 4:freezer:/
# 3:perf_event:/
# 2:cpu,cpuacct:/user.slice
# 1:name=systemd:/user.slice/user-0.slice/session-1.scope
# 0::/user.slice/user-0.slice/session-1.scope

# 查看tasks
cat tasks 

# 输出:
# 6758
# 47828

可以看到当前6758进程,已经被加入到cgroup-1中!

通过subsystem限制cgroup中的进程资源

上面在创建hierarchy时,并没有关联到任何的subsystem,因此没办法通过该hierarchy中的cgroup对进程的资源进行限制;

注:系统默认已经为每个subsystem创建了默认的hierarchy;

例如:memory的hierarchy:

  root@jasonkay:~/cgroup-test/cgroup-1# mount | grep memory
  cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)

可以看到,/sys/fs/cgroup/memory目录便是挂在了memory subsystem的hierarchy上!

下面通过在memory subsystem的hierarchy中创建cgroup,限制进程占用内存!

过程如下:

################### 通过subsystem限制cgroup中进程的资源 ###################

# 进入memory subsystem的hierarchy目录中
cd /sys/fs/cgroup/memory && pwd # /sys/fs/cgroup/memory

# 不做限制的情况下进行测试
stress --vm-bytes 200m --vm-keep -m 1 &

# 输入top,并查看stress内存占用 /stress 为 200M(15985.9 * 1.3%)
top - 16:57:58 up 1 day, 42 min,  2 users,  load average: 0.23, 0.09, 0.02
Tasks: 260 total,   2 running, 258 sleeping,   0 stopped,   0 zombie
%Cpu(s): 12.9 us,  0.1 sy,  0.0 ni, 87.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  15985.9 total,  11990.0 free,   2460.8 used,   1535.1 buff/cache
MiB Swap:  12288.0 total,  12287.7 free,      0.3 used.  13210.3 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                    
  48060 root      20   0  208660 204872    208 R 100.0   1.3   0:13.70 stress

注:测试需要使用stress命令;

  sudo apt install stress

下面创建对进程内存进行限制:

# 创建cgroup
sudo mkdir test-limit-memory && cd test-limit-memory && pwd # /sys/fs/cgroup/memory/test-limit-memory

# 设置cgroup的最大内存占用为100M
sudo sh -c "echo "100m" > memory.limit_in_bytes"

# 将当前进程移动至该cgroup中
sudo sh -c "echo $$ > tasks"

# 再次运行stress
stress --vm-bytes 200m --vm-keep -m 1 &

# 输入top,并查看stress内存占用 /stress 此时为100M(15985.9 * 0.6%)了
top - 17:00:46 up 1 day, 45 min,  2 users,  load average: 0.37, 0.29, 0.11
Tasks: 260 total,   2 running, 258 sleeping,   0 stopped,   0 zombie
%Cpu(s):  2.3 us, 12.8 sy,  0.0 ni, 83.5 id,  1.5 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  15985.9 total,  12088.5 free,   2361.8 used,   1535.7 buff/cache
MiB Swap:  12288.0 total,  12183.7 free,    104.3 used.  13309.2 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                    
  48151 root      20   0  208660  99812    208 R  81.2   0.6   0:03.11 stress

可以看到,通过cgroup,成功将stress进程最大内存占用限制到了100M!

注1:试验结束后,将stress进程kill掉!

注2:试验结束后,将目录umount:

  # 查看挂载情况:
  root@jasonkay:~# mount
  sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
  ……
  cgroup-test on /root/cgroup-test type cgroup (rw,relatime,name=cgroup-test)

  # 取消挂载:
  root@jasonkay:~# umount ./cgroup-test

  # 再次查看挂载情况:
  root@jasonkay:~# mount
  sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
  …… # 已不存在cgroup-test

注3:清除内存限制配置:

我们将当前进程加入了test-limit-memory中,因此,在删除这个目录之前,需要将当前进程重新加入至其他hierarchy中:

  # 切换到默认内存限制hierarchy
  cd /sys/fs/cgroup/memory/

  # 将当前进程放入默认内存限制tasks
  echo $$ >> tasks

  # 删除目录(它会将这个目录下的全部文件也都删除!)
  rmdir test-limit-memory/

Docker是如何使用Cgroups的

Docker就是通过Cgroups实现容器资源限制和监控的;

# 使用 -m 设置内存限制
root@jasonkay:/sys/fs/cgroup/memory# docker run -itd -m 128m busybox:1.34.0
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
0bf66ace3af2a1cd57e7a8a12bbf33940ba63722e18dbd4c1e82517c8889af0c

# 查看容器cgroup
cd /sys/fs/cgroup/memory/docker/0bf66ace3af2a1cd57e7a8a12bbf33940ba63722e18dbd4c1e82517c8889af0c/

ls
cgroup.clone_children           memory.kmem.tcp.failcnt             memory.oom_control
……

# 查看cgroup内存限制
root@jasonkay:/sys/fs/cgroup/memory/docker/0bf66ace3af2a1cd57e7a8a12bbf33940ba63722e18dbd4c1e82517c8889af0c# cat memory.limit_in_bytes 
134217728

# 查看cgroup中当前进程使用的内存大小
root@jasonkay:/sys/fs/cgroup/memory/docker/0bf66ace3af2a1cd57e7a8a12bbf33940ba63722e18dbd4c1e82517c8889af0c# cat memory.usage_in_bytes 
2863104

可以看到,Docker通过为每个容器创建cgroup来配置资源限制和资源监控!

注:试验结束别忘了关闭容器!

通过Go使用Cgroups

下面我们通过Go代码来配置Cgroups,以实现限制容器内存;

chapter2_basic/cgroups/cgroup_demo.go

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "os/exec"
    "path"
    "strconv"
    "syscall"
)

const (
    // 挂载了memory subsystem的hierarchy的根目录位置
    cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
)

func main() {
    // 容器进程
    if os.Args[0] == "/proc/self/exe" {
        fmt.Printf("current pid %d\n", syscall.Getpid())

        cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`)
        cmd.SysProcAttr = &syscall.SysProcAttr{
        }
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        if err := cmd.Run(); err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
    }

    cmd := exec.Command("/proc/self/exe")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Start(); err != nil {
        fmt.Println("ERROR", err)
        os.Exit(1)
    }
    // 得到fork出来进程映射在外部命名空间的pid
    fmt.Printf("%v", cmd.Process.Pid)

    // 在系统默认创建挂载了memory subsystem的hierarchy上创建cgroup
    _ = os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
    // 将容器进程加入这个cgroup中
    _ = ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
    // 限制cgroup中进程使用
    _ = ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes"), []byte("100m"), 0644)

    _, _ = cmd.Process.Wait()
}

执行代码并查看:

pwd # /root/workspace/my_docker/chapter2_basic

# 运行代码:
root@jasonkay:~/workspace/my_docker/chapter2_basic# go run cgroups/cgroup_demo.go 
49750current pid 1
stress: info: [7] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd

# 在一个新的shell中查看内存:
top

# top - 17:36:51 up 1 day,  1:21,  2 users,  load average: 0.56, 0.18, 0.09
# Tasks: 267 total,   2 running, 265 sleeping,   0 stopped,   0 zombie
# %Cpu(s):  0.6 us,  7.3 sy,  0.0 ni, 91.6 id,  0.0 wa,  0.0 hi,  0.6 si,  0.0 st
# MiB Mem :  15985.9 total,  11840.2 free,   2439.3 used,   1706.4 buff/cache
# MiB Swap:  12288.0 total,  12181.0 free,    107.0 used.  13233.0 avail Mem 
# 
#    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND          #                             
#  49763 root      20   0  208660 100996    272 R  75.0   0.6   0:33.54 stress

可以看到,只占用了100M内存!

注:最后别忘了清理cgroup配置:

  rmdir /sys/fs/cgroup/memory/testmemorylimit/

小结

本篇主要讲述了Docker和K8S中限制容器硬件资源的技术:Cgroups;

Cgroups主要包括三个部分:

  • cgroup;
  • subsystem;
  • hierarchy;

最后通过Shell操作和Go代码,分别进行了进程的内存限制操作;

本系列的下一小节将讲述和 Docker 存储相关的基本知识:UnionFS;

附录

系列文章:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK