乐趣区

关于linux:一篇搞懂容器技术的基石-cgroup

大家好,我是张晋涛。

目前咱们所提到的容器技术、虚拟化技术(不管何种抽象层次下的虚拟化技术)都能做到资源层面上的隔离和限度。

对于容器技术而言,它实现资源层面上的限度和隔离,依赖于 Linux 内核所提供的 cgroup 和 namespace 技术。

咱们先对这两项技术的作用做个概括:

  • cgroup 的次要作用:治理资源的调配、限度;
  • namespace 的次要作用:封装形象,限度,隔离,使命名空间内的过程看起来领有他们本人的全局资源;

本篇,咱们重点来聊 cgroup。

为什么要关注 cgroup & namespace

云原生 / 容器技术的井喷式增长

自 1979 年,Unix 版本 7 在开发过程中引入 Chroot Jail 以及 Chroot 零碎调用开始,直到 2013 年开源出的 Docker,2014 年开源进去的 Kubernetes,直到现在的云原生生态的炽热。容器技术曾经逐渐成为支流的根底技术之一。

在越来越多的公司、集体抉择了云服务 / 容器技术后,资源的调配和隔离,以及安全性变成了人们关注及探讨的热点话题。

其实容器技术应用起来并不难,但要真正把它用好,大规模的在生产环境中应用,那咱们还是须要把握其外围的。

以下是容器技术 & 云原生生态的大抵倒退历程:

图 1,容器技术倒退历程

从图中,咱们能够看到容器技术、云原生生态的倒退轨迹。容器技术其实很早就呈现了,但为何在 Docker 呈现后才开始有了较为显著的倒退?晚期的 chroot、Linux VServer 又有哪些问题呢?

Chroot 带来的安全性问题

图 2,chroot 示例

Chroot 能够将过程及其子过程与操作系统的其余部分隔离开来。然而,对于 root process,却 能够任意退出 chroot

package main

import (
    "log"
    "os"
    "syscall"
)

func getWd() (path string) {path, err := os.Getwd()
    if err != nil {log.Println(err)
    }
    log.Println(path)
    return
}

func main() {RealRoot, err := os.Open("/")
    defer RealRoot.Close()
    if err != nil {log.Fatalf("[ Error] - /: %v\n", err)
    }
    path := getWd()

    err = syscall.Chroot(path)
    if err != nil {log.Fatalf("[ Error] - chroot: %v\n", err)
    }
    getWd()

    err = RealRoot.Chdir()
    if err != nil {log.Fatalf("[ Error] - chdir(): %v", err)
    }
    getWd()

    err = syscall.Chroot(".")
    if err != nil {log.Fatalf("[ Error] - chroot back: %v", err)
    }
    getWd()}

别离以普通用户和 sudo 的形式运行:

➜  chroot go run main.go 
2021/11/18 00:46:21 /tmp/chroot
2021/11/18 00:46:21 [Error] - chroot: operation not permitted
exit status 1
➜  chroot sudo go run main.go
2021/11/18 00:46:25 /tmp/chroot
2021/11/18 00:46:25 /
2021/11/18 00:46:25 (unreachable)/
2021/11/18 00:46:25 /

能够看到如果是应用 sudo来运行的时候,程序在当前目录和零碎本来的根目录下进行了切换。而普通用户则无权限操作。

Linux VServer 的安全漏洞

Linux-VServer 是一种基于 Security Contexts 的软分区技术,能够做到虚构服务器隔离,共享雷同的硬件资源。次要问题是 VServer 应用程序针对 “chroot-again” 类型的攻打没有很好的进行平安爱护,攻击者能够利用这个破绽脱离限度环境,拜访限度目录之外的任意文件。(自 2004 年开始,国家信息安全破绽库就登出了相干破绽问题)

现代化容器技术带来的劣势

  • 轻量级,基于 Linux 内核所提供的 cgroup 和 namespace 能力,创立容器的老本很低;
  • 肯定的隔离性;
  • 标准化,通过应用容器镜像的形式进行应用程序的打包和散发,能够屏蔽掉因为环境不统一带来的诸多问题;
  • DevOps 撑持(能够在不同环境,如开发、测试和生产等环境之间轻松迁徙利用,同时还可保留利用的全副性能);
  • 为基础架构削减防护,晋升可靠性、可扩展性和信赖度;
  • DevOps/GitOps 撑持(能够做到疾速无效地持续性公布,治理版本及配置);
  • 团队成员间能够无效简化、减速和编排利用的开发与部署;

在理解了为什么要关注 cgroup 和 namespace 等技术之后,那咱们就进入到本篇的重点吧,来一起学习下 cgroup。

什么是 cgroup

cgroup 是 Linux 内核的一个性能,用来限度、管制与拆散一个过程组的资源(如 CPU、内存、磁盘输入输出等)。它是由 Google 的两位工程师进行开发的,自 2018 年 1 月正式公布的 Linux 内核 v2.6.24 开始提供此能力。

cgroup 到目前为止,有两个大版本,cgroup v1 和 v2。以下内容以 cgroup v2 版本为主,波及两个版本差异的中央会在下文具体介绍。

cgroup 次要限度的资源是:

  • CPU
  • 内存
  • 网络
  • 磁盘 I/O

当咱们将可用系统资源按特定百分比调配给 cgroup 时,残余的资源可供零碎上的其余 cgroup 或其余过程应用。

图 4,cgroup 资源分配及残余可用资源示例

cgroup 的组成

cgroup 代表“控制组”,并且不会应用大写。cgroup 是一种分层组织过程的机制,沿层次结构以受控的形式调配系统资源。咱们通常应用复数模式用于指定整个特色,也用作限定符如“cgroup controller”。

cgroup 次要有两个组成部分:

  • core – 负责分层组织过程;
  • controller – 通常负责沿层次结构调配特定类型的系统资源。每个 cgroup 都有一个 cgroup.controllers 文件,其中列出了所有可供 cgroup 启用的控制器。当在 cgroup.subtree_control 中指定多个控制器时,要么全副胜利,要么全副失败。在同一个控制器上指定多项操作,那么只有最初一个失效。每个 cgroup 的控制器销毁是异步的,在援用时同样也有着提早援用的问题;

所有 cgroup 外围接口文件都以 cgroup 为前缀。每个控制器的接口文件都以控制器名称和一个点为前缀。控制器的名称由小写字母和“_”组成,但永远不会以“_”结尾。

cgroup 的外围文件

  • cgroup.type –(单值)存在于非根 cgroup 上的可读写文件。通过将“threaded”写入该文件,能够将 cgroup 转换为线程 cgroup,可抉择 4 种取值,如下:
  • 1) domain – 一个失常的无效域 cgroup
  • 2) domain threaded – 线程子树根的线程域 cgroup
  • 3) domain invalid – 有效的 cgroup
  • 4) threaded – 线程 cgroup,线程子树
  • cgroup.procs –(换行分隔)所有 cgroup 都有的可读写文件。每行列出属于 cgroup 的过程的 PID。PID 不是有序的,如果过程挪动到另一个 cgroup,雷同的 PID 可能会呈现不止一次;
  • cgroup.controllers –(空格分隔)所有 cgroup 都有的只读文件。显示 cgroup 可用的所有控制器;
  • cgroup.subtree_control –(空格分隔)所有 cgroup 都有的可读写文件,初始为空。如果一个控制器在列表中呈现不止一次,最初一个无效。当指定多个启用和禁用操作时,要么全副胜利,要么全副失败。
  • 1) 以“+”为前缀的控制器名称示意启用控制器
  • 2) 以“-”为前缀的控制器名称示意禁用控制器
  • cgroup.events – 存在于非根 cgroup 上的只读文件。
  • 1) populated – cgroup 及其子节点中蕴含流动过程,值为 1;无流动过程,值为 0.
  • 2) frozen – cgroup 是否被解冻,解冻值为 1;未解冻值为 0.
  • cgroup.threads –(换行分隔)所有 cgroup 都有的可读写文件。每行列出属于 cgroup 的线程的 TID。TID 不是有序的,如果线程挪动到另一个 cgroup,雷同的 TID 可能会呈现不止一次。
  • cgroup.max.descendants –(单值)可读写文件。最大容许的 cgroup 数量子节点数量。
  • cgroup.max.depth –(单值)可读写文件。低于以后节点最大容许的树深度。
  • cgroup.stat – 只读文件。

    • 1) nr_descendants – 可见后辈的 cgroup 数量。
    • 2) nr_dying_descendants – 被用户删除行将被零碎销毁的 cgroup 数量。
  • cgroup.freeze –(单值)存在于非根 cgroup 上的可读写文件。默认值为 0。当值为 1 时,会解冻 cgroup 及其所有子节点 cgroup,会将相干的过程关停并且不再运行。解冻 cgroup 须要肯定的工夫,当动作实现后,cgroup.events 管制文件中的“frozen”值会更新为“1”,并收回相应的告诉。cgroup 的解冻状态不会影响任何 cgroup 树操作(删除、创立等);
  • cgroup.kill –(单值)存在于非根 cgroup 上的可读写文件。惟一允许值为 1,当值为 1 时,会将 cgroup 及其所有子节点中的 cgroup 杀死(过程会被 SIGKILL 杀掉)。个别用于将一个 cgroup 树杀掉,避免叶子节点迁徙;

cgroup 的归属和迁徙

零碎中的每个过程都属于一个 cgroup,一个过程的所有线程都属于同一个 cgroup。一个过程能够从一个 cgroup 迁徙到另一个 cgroup。过程的迁徙不会影响现有的后辈过程所属的 cgroup。

图 5,过程及其子过程的 cgroup 调配;跨 cgroup 迁徙示例

跨 cgroup 迁徙过程是一项代价低廉的操作并且有状态的资源限度(例如,内存)不会动静的利用于迁徙。因而,常常跨 cgroup 迁徙过程只是作为一种伎俩。不激励间接利用不同的资源限度。

如何实现跨 cgroup 迁徙

每个 cgroup 都有一个可读写的接口文件“cgroup.procs”。每行一个 PID 记录 cgroup 限度治理的所有过程。一个过程能够通过将其 PID 写入另一 cgroup 的“cgroup.procs”文件来实现迁徙。

然而这种形式,只能迁徙一个过程在单个 write(2) 上的调用(如果一个过程有多个线程,则会同时迁徙所有线程,但也要参考线程子树,是否有将过程的线程放入不同的 cgroup 的记录)。

当一个过程 fork 出一个子过程时,该过程就诞生在其父亲过程所属的 cgroup 中。

一个没有任何子过程或流动过程的 cgroup 是能够通过删除目录进行销毁的(即便存在关联的僵尸过程,也被认为是能够被删除的)。

什么是 cgroups

当明确提到多个独自的控制组时,才应用复数模式“cgroups”。

cgroups 造成了树状构造。(一个给定的 cgroup 可能有多个子 cgroup 造成一棵树构造体)每个非根 cgroup 都有一个 cgroup.events 文件,其中蕴含 populated 字段批示 cgroup 的子层次结构是否具备实时过程。所有非根的 cgroup.subtree_control 文件,只能蕴含在父级中启用的控制器。

图 6,cgroups 示例

如图所示,cgroup1 中限度了应用 cpu 及 内存资源,它将管制子节点的 CPU 周期和内存调配(即,限度 cgroup2、cgroup3、cgroup4 中的 cpu 及内存资源分配)。cgroup2 中启用了内存限度,然而没有启用 cpu 的资源限度,这就导致了 cgroup3 和 cgroup4 的内存资源受 cgroup2 中的 mem 设置内容的限度;cgroup3 和 cgroup4 会自由竞争在 cgroup1 的 cpu 资源限度范畴内的 cpu 资源。

由此,也能够显著的看出 cgroup 资源是自上而下散布束缚的。只有当资源曾经从上游 cgroup 节点分发给上游时,上游的 cgroup 能力进一步散发束缚资源。所有非根的 cgroup.subtree_control 文件只能蕴含在父节点的 cgroup.subtree_control 文件中启用的控制器内容。

那么,子节点 cgroup 与父节点 cgroup 是否会存在外部过程竞争的状况呢

当然不会。cgroup v2 中,设定了非根 cgroup 只能在没有任何过程时能力将域资源分发给子节点的 cgroup。简而言之,只有不蕴含任何过程的 cgroup 能力在其 cgroup.subtree_control 文件中启用域控制器,这就保障了,过程总在叶子节点上。

挂载和委派

cgroup 的挂载形式

  • memory_recursiveprot – 递归地将 memory.min 和 memory.low 爱护利用于整个子树,无需显式向下流传到叶节点的 cgroup 中,子树内叶子节点能够自由竞争;
  • memory_localevents – 只能挂载时设置或者通过从 init 命名空间从新挂载来批改,这是零碎范畴的选项。只用以后 cgroup 的数据填充 memory.events,如果没有这个选项,默认会计数所有子树;
  • nsdelegate – 只能挂载时设置或者通过从 init 命名空间从新挂载来批改,这也是零碎范畴的选项。它将 cgroup 命名空间视为委托边界,这是两种委派 cgroup 的形式之一;

cgroup 的委派形式

  • 设置挂载选项 nsdelegate;
  • 受权用户对目录及其 cgroup.procscgroup.threadscgroup.subtree_control 文件的写访问权限

两种形式的后果雷同。一旦被委派,用户就能够在目录下建设子层次结构,所有的资源分配都受父节点的制约。目前,cgroup 对委托子层次结构中的 cgroup 数量或嵌套深度没有任何限度(之后可能会受到明确限度)。

后面提到了跨 cgroup 迁徙,从委派中,咱们能够很明确的得悉跨 cgroup 迁徙对于普通用户来讲,是有限度条件的。即,是否对目前 cgroup 的“cgroup.procs”文件具备写访问权限以及是否对源 cgroup 和指标 cgroup 的独特先人的“cgroup.procs”文件具备写访问权限。

委派和迁徙

图 7,委派权限示例

如图,普通用户 User0 具备 cgroup[1-5] 的委派权限。

为什么 User0 想将过程 从 cgroup3 迁徙至 cgroup5 会失败呢?

这是因为 User0 的权限只到 cgroup1 和 cgroup2 层,并不具备 cgroup0 的权限。而委派中的受权用户明确指出 须要独特先人的 “cgroup.procs”文件具备写访问权限!(即,须要图中 cgroup0 的权限,才能够实现)

资源分配模型及性能

以下是 cgroups 的资源分配模型:

  • 权重 – (例如,cpu.weight) 所有权重都在 [1, 10000] 范畴内,默认值为 100。依照权重比率来分配资源。
  • 限度 – [0, max] 范畴内,默认为“max”,即 noop(例如,io.max)。限度能够被适度应用(子节点限度的总和可能超过父节点可用的资源量)。
  • 爱护 – [0, max] 范畴内,默认为 0,即 noop(例如,io.low)。爱护能够是硬保障或尽力而为的软边界,爱护也可能被适度应用。
  • 调配 – [0, max] 范畴内,默认为 0,即没有资源。调配不能被适度应用(子节点调配的总和不能超过父节点可用的资源量)。

cgroups 提供了如下性能:

  • 资源限度 – 下面 cgroup 局部曾经示例,cgroups 能够以树状构造来嵌套式限度资源。
  • 优先级 – 产生资源争用时,优先保障哪些过程的资源。
  • 审计 – 监控及报告资源限度及应用。
  • 管制 – 管制过程的状态(起、停、挂起)。

cgroup v1 与 cgroup v2

被弃用的外围性能

cgroup v2 和 cgroup v1 有很大的不同,咱们一起来看看在 cgroup v2 中弃用了哪些 cgroup v1 的性能:

  • 不反对包含命名档次在内的多个层次结构;
  • 不反对所有 v1 装置选项;
  • “tasks”文件被删除,“cgroup.procs”没有排序
    • 在 cgroup v1 中线程组 ID 的列表。不保障此列表已排序或没有反复的 TGID,如果须要此属性,用户空间应排序 / 对立列表。将线程组 ID 写入此文件会将该组中的所有线程挪动到此 cgroup 中;
  • cgroup.clone_children 被删除。clone_children 仅影响 cpuset controller。如果在 cgroup 中启用了 clone_children(设置:1),新的 cpuset cgroup 将在初始化期间从父节点的 cgroup 复制配置;
  • /proc/cgroups 对于 v2 没有意义。改用根目录下的“cgroup.controllers”文件;

cgroup v1 的问题

cgroup v2 和 v1 中最显著的不同就是 cgroup v1 容许任意数量的层次结构, 但这会带来一些问题的。咱们来具体聊聊。

挂载 cgroup 层次结构时,你能够指定要挂载的子系统的逗号分隔列表作为文件系统挂载选项。默认状况下,挂载 cgroup 文件系统会尝试挂载蕴含所有已注册子系统的层次结构。

如果曾经存在具备完全相同子系统集的流动层次结构,它将被从新用于新装置。

如果现有层次结构不匹配,并且任何申请的子系统正在现有层次结构中应用,则挂载将失败并显示 -EBUSY。否则,将激活与申请的子系统相关联的新层次结构。

以后无奈将新子系统绑定到流动 cgroup 层次结构,或从流动 cgroup 层次结构中勾销绑定子系统。当 cgroup 文件系统被卸载时,如果在顶级 cgroup 之下创立了任何子 cgroup,即便卸载,该层次结构仍将放弃活动状态;如果没有子 cgroup,则层次结构将被停用。

这就是 cgroup v1 中的问题,在 cgroup v2 中就很好的进行了解决。

cgroup 和容器的分割

这里咱们以 Docker 为例。创立一个容器,并对其可应用的 CPU 和内存进行限度:

➜  ~ docker run --rm -d  --cpus=2 --memory=2g --name=2c2g redis:alpine 
e420a97835d9692df5b90b47e7951bc3fad48269eb2c8b1fa782527e0ae91c8e
➜  ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/cpu.max
200000 100000
➜  ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/memory.max
2147483648
➜  ~ 
➜  ~ docker run --rm -d  --cpus=0.5 --memory=0.5g --name=0.5c0.5g redis:alpine
8b82790fe0da9d00ab07aac7d6e4ef2f5871d5f3d7d06a5cdb56daaf9f5bc48e
➜  ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/cpu.max       
50000 100000
➜  ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/memory.max
536870912

从下面的示例能够看到,当咱们应用 Docker 创立出新的容器并且为他指定 CPU 和 内存限度后,其对应的 cgroup 配置文件的 cpu.maxmemory.max都设置成了相应的值。

如果你想要对一些曾经在运行的容器进行资源配额的查看的话,也能够间接去查看其对应的配置文件中的内容。

总结

以上就是对于容器技术的基石之一的 cgroup 的具体介绍了。接下来我还会写对于 namespace 以及其余容器技术相干的内容,敬请关注!


欢送订阅我的文章公众号【MoeLove】

退出移动版