《自己动手写Docker》书摘之二: Linux Cgroups

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: Linux Cgroups介绍 上面是构建Linux容器的namespace技术,它帮进程隔离出自己单独的空间,但Docker又是怎么限制每个空间的大小,保证他们不会互相争抢呢?那么就要用到Linux的Cgroups技术。

Linux Cgroups介绍

上面是构建Linux容器的namespace技术,它帮进程隔离出自己单独的空间,但Docker又是怎么限制每个空间的大小,保证他们不会互相争抢呢?那么就要用到Linux的Cgroups技术。

概念

Linux Cgroups(Control Groups) 提供了对一组进程及将来的子进程的资源的限制,控制和统计的能力,这些资源包括CPU,内存,存储,网络等。通过Cgroups,可以方便的限制某个进程的资源占用,并且可以实时的监控进程的监控和统计信息。 

Cgroups中的三个组件:

  • cgroup
    cgroup 是对进程分组管理的一种机制,一个cgroup包含一组进程,并可以在这个cgroup上增加Linux subsystem的各种参数的配置,将一组进程和一组subsystem的系统参数关联起来。
  • subsystem
    subsystem 是一组资源控制的模块,一般包含有:

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

    每个subsystem会关联到定义了相应限制的cgroup上,并对这个cgroup中的进程做相应的限制和控制,这些subsystem是逐步合并到内核中的,如何看到当前的内核支持哪些subsystem呢?可以安装cgroup的命令行工具(apt-get install cgroup-bin),然后通过lssubsys看到kernel支持的subsystem。 

    # / lssubsys -a
    cpuset
    cpu,cpuacct
    blkio
    memory
    devices
    freezer
    net_cls,net_prio
    perf_event
    hugetlb
    pids
  • hierarchy
    hierarchy 的功能是把一组cgroup串成一个树状的结构,一个这样的树便是一个hierarchy,通过这种树状的结构,Cgroups可以做到继承。比如我的系统对一组定时的任务进程通过cgroup1限制了CPU的使用率,然后其中有一个定时dump日志的进程还需要限制磁盘IO,为了避免限制了影响到其他进程,就可以创建cgroup2继承于cgroup1并限制磁盘的IO,这样cgroup2便继承了cgroup1中的CPU的限制,并且又增加了磁盘IO的限制而不影响到cgroup1中的其他进程。

三个组件相互的关系:

通过上面的组件的描述我们就不难看出,Cgroups的是靠这三个组件的相互协作实现的,那么这三个组件是什么关系呢? 

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

这几句话现在不理解暂时没关系,后面我们实际使用过程中会逐渐的了解到他们之间的联系的。

kernel接口:

上面介绍了那么多的Cgroups的结构,那到底要怎么调用kernel才能配置Cgroups呢?上面了解到Cgroups中的hierarchy是一种树状的组织结构,Kernel为了让对Cgroups的配置更直观,Cgroups通过一个虚拟的树状文件系统去做配置的,通过层级的目录虚拟出cgroup树,下面我们就以一个配置的例子来了解下如何操作Cgroups。 

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

     ~ mkdir cgroup-test # 创建一个hierarchy挂载点
     ~ sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test # 挂载一个hierarchy
     ~ ls ./cgroup-test # 挂载后我们就可以看到系统在这个目录下生成了一些默认文件
    cgroup.clone_children  cgroup.procs  cgroup.sane_behavior  notify_on_release  release_agent  tasks

    这些文件就是这个hierarchy中根节点cgroup配置项了,上面这些文件分别的意思是:

    • cgroup.clone_children cpuset的subsystem会读取这个配置文件,如果这个的值是1(默认是0),子cgroup才会继承父cgroup的cpuset的配置。
    • cgroup.procs是树中当前节点的cgroup中的进程组ID,现在我们在根节点,这个文件中是会有现在系统中所有进程组ID。
    • notify_on_releaserelease_agent会一起使用,notify_on_release表示当这个cgroup最后一个进程退出的时候是否执行release_agentrelease_agent则是一个路径,通常用作进程退出之后自动清理掉不再使用的cgroup。
    • tasks也是表示该cgroup下面的进程ID,如果把一个进程ID写到tasks文件中,便会将这个进程加入到这个cgroup中。
  • 然后,我们创建在刚才创建的hierarchy的根cgroup中扩展出两个子cgroup:

     cgroup-test sudo mkdir cgroup-1 # 创建子cgroup "cgroup-1"
     cgroup-test sudo mkdir cgroup-2 # 创建子cgroup "cgroup-1"
     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

    可以看到在一个cgroup的目录下创建文件夹,kernel就会把文件夹标记会这个cgroup的子cgroup,他们会继承父cgroup的属性。

  • 在cgroup中添加和移动进程:
    一个进程在一个Cgroups的hierarchy中只能存在在一个cgroup节点上,系统的所有进程默认都会在根节点,可以将进程在cgroup节点间移动,只需要将进程ID写到移动到的cgroup节点的tasks文件中。

     cgroup-1 echo $$
    7475
     cgroup-1 sudo sh -c "echo $$ >> tasks" # 将我所在的终端的进程移动到cgroup-1中
     cgroup-1 cat /proc/7475/cgroup
    13:name=cgroup-test:/cgroup-1
    11:perf_event:/
    10:cpu,cpuacct:/user.slice
    9:freezer:/
    8:blkio:/user.slice
    7:devices:/user.slice
    6:cpuset:/
    5:hugetlb:/
    4:pids:/user.slice/user-1000.slice
    3:memory:/user.slice
    2:net_cls,net_prio:/
    1:name=systemd:/user.slice/user-1000.slice/session-19.scope

    可以看到我们当前的7475进程已经被加到了cgroup-test:/cgroup-1中。

  • 通过subsystem限制cgroup中进程的资源
    上面我们创建hierarchy的时候,但这个hierarchy并没有关联到任何subsystem,所以没办法通过那个hierarchy中的cgroup限制进程的资源占用,其实系统默认就已经把每个subsystem创建了一个默认的hierarchy,比如memory的hierarchy:

      ~ mount | grep memory
    cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory,nsroot=/)

    可以看到,在/sys/fs/cgroup/memory目录便是挂在了memory subsystem的hierarchy。下面我们就通过在这个hierarchy中创建cgroup,限制下占用的进程占用的内存:

     memory stress --vm-bytes 200m --vm-keep -m 1 # 首先,我们不做限制启动一个占用内存的stress进程
     memory sudo mkdir test-limit-memory && cd test-limit-memory # 创建一个cgroup
     test-limit-memory sudo sh -c "echo "100m" > memory.limit_in_bytes" sudo sh -c "echo "100m" > memory.limit_in_bytes" # 设置最大cgroup最大内存占用为100m
     test-limit-memory sudo sh -c "echo $$ > tasks" # 将当前进程移动到这个cgroup中
     test-limit-memory stress --vm-bytes 200m --vm-keep -m 1 # 再次运行占用内存200m的的stress进程

    运行结果如下(通过top监控):

    PID  PPID     TIME+ %CPU %MEM  PR  NI S    VIRT    RES   UID COMMAND
    8336  8335   0:08.23 99.0 10.0  20   0 R  212284 205060  1000 stress
    8335  7475   0:00.00  0.0  0.0  20   0 S    7480    876  1000 stress
    
    PID  PPID     TIME+ %CPU %MEM  PR  NI S    VIRT    RES   UID COMMAND
    8310  8309   0:01.17  7.6  5.0  20   0 R  212284 102056  1000 stress
    8309  7475   0:00.00  0.0  0.0  20   0 S    7480    796  1000 stress

    可以看到通过cgroup,我们成功的将stress进程的最大内存占用限制到了100m。 

    Docker是如何使用Cgroups的:

    我们知道Docker是通过Cgroups去做的容器的资源限制和监控,我们下面就以一个实际的容器实例来看下Docker是如何配置Cgroups的:

     ~ # docker run -m 设置内存限制
     ~ sudo docker run -itd -m  128m ubuntu
    957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11
     ~ # docker会为每个容器在系统的hierarchy中创建cgroup
     ~ cd /sys/fs/cgroup/memory/docker/957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 
     957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 # 查看cgroup的内存限制
     957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 cat memory.limit_in_bytes
    134217728
     957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 # 查看cgroup中进程所使用的内存大小
     957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 cat memory.usage_in_bytes
    430080

    可以看到Docker通过为每个容器创建Cgroup并通过Cgroup去配置的资源限制和资源监控。

用go语言实现通过cgroup限制容器的资源

下面我们在上一节的容器的基础上加上cgroup的限制,下面这个demo实现了限制容器的内存的功能:

package main

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

const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"

func main() {
    if os.Args[0] == "/proc/self/exe" {
        //容器进程
        fmt.Printf("current pid %d", syscall.Getpid())
        fmt.Println()
        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)
    } else {
        //得到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()
}

通过对Cgroups虚拟文件系统的配置,我们让容器中的把stress进程的内存占用限制到了100m

 PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
10861 root      20   0  212284 102464    212 R  6.2  5.0   0:01.13 stress

小结

本节我们主要介绍了Linux Cgroups,通过Linux Cgroups的三种结构,可以随意的定制对资源的限制和做资源的监控,最后用GO语言做了一个Cgroups的限制资源的demo,介绍了怎么样用GO语言去操控容器的Cgroups而限制容器的资源。


相关图书推荐<<自己动手写docker>>

_Docker_01_


相关链接:
《自己动手写Docker》书摘之一: Linux Namespace
《自己动手写Docker》书摘之二: Linux Cgroups  
《自己动手写Docker》书摘之三: Linux UnionFS  
《自己动手写Docker》书摘之四: 构造简单容器  
《自己动手写Docker》书摘之五: 增加容器资源限制

目录
相关文章
|
6天前
|
Linux Docker 容器
Centos安装docker(linux安装docker)——超详细小白可操作手把手教程,包好用!!!
本篇博客重在讲解Centos安装docker,经博主多次在不同服务器上测试,极其的稳定,尤其是阿里的服务器,一路复制命令畅通无阻。
64 4
Centos安装docker(linux安装docker)——超详细小白可操作手把手教程,包好用!!!
|
1月前
|
Linux Docker 容器
Linux的namespace和cgroups简介
本文介绍了Linux的Namespace技术和cgroups,解释了它们如何帮助实现容器的隔离和资源限制。
65 7
Linux的namespace和cgroups简介
|
19天前
|
Linux Docker 容器
linux之docker命令
linux之docker命令
|
1月前
|
NoSQL 关系型数据库 Redis
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo
mall在linux环境下的部署(基于Docker容器),docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongodb、minio详细教程,拉取镜像、运行容器
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo
|
5天前
|
Linux 开发工具 Docker
各个类linux服务器安装docker教程
各个类linux服务器安装docker教程
26 0
|
2月前
|
Kubernetes Linux Docker
【Azure 应用服务】使用Docker Compose创建App Service遇见"Linux Version is too long. It cannot be more than 4000 characters"错误
【Azure 应用服务】使用Docker Compose创建App Service遇见"Linux Version is too long. It cannot be more than 4000 characters"错误
|
2月前
|
开发框架 .NET Linux
【Azure 应用服务】 部署到App Service for Linux 服务的Docker 镜像,如何配置监听端口呢?
【Azure 应用服务】 部署到App Service for Linux 服务的Docker 镜像,如何配置监听端口呢?
|
2月前
|
Linux 持续交付 虚拟化
在Linux中,Docker和容器虚拟概念是什么?
在Linux中,Docker和容器虚拟概念是什么?
|
2月前
|
运维 Kubernetes Linux
docker和kubectl客户端安装Linux
通过上述步骤,您应该已经在Linux环境中成功安装了Docker和kubectl客户端。安装完成后,您可以开始使用Docker来创建和管理容器,使用kubectl来管理Kubernetes集群。这些工具的安装和使用,对于现代云原生应用的开发和运维来说,是一个基础且关键的步骤。
33 0
|
2月前
|
存储 Kubernetes Linux
在Linux中,如何使用Docker和Kubernetes管理容器?
在Linux中,如何使用Docker和Kubernetes管理容器?