容器技术Linux Namespaces和Cgroups

对操作系统了解多少,仅仅敲个命令吗

  1. 操作系统虚拟化(容器技术)的发展历程

1979 年,UNIX 的第 7 个版本引入了 Chroot 特性。Chroot 现在被认为是第一个操作系统虚拟化(Operating system level virtualization)技术的原型,本质是一种操作系统文件系统层的隔离技术。

2006 年,Google 发布了在 Linux 上运行的 Process Container(进程容器)技术,其目标是提供一种类似于 Virtual Mahine(计算机虚拟化技术)的、但主要针对 Process 的操作系统级别资源限制、优先级控制、资源审计能力和进程控制能力。

2007 年,Google 推动 Process Container 代码合入 Linux Kernel。同时由于 Container 这一命名在 Kernel 具有许多不同的含义,所以为了避免代码命名的混乱,就将 Process Container 更名为了 Control Groups,简称:Cgroups

2008 年,Linux 社区整合了 Chroot、Cgroups、Namespaces、SELinux、Seccomp 等多种技术并发布了 LXC(Linux Container)v0.1.0 版本。LXC 通过将 Cgroups 的资源配额管理能力和 Namespace 的资源视图隔离能力进行组合,实现了完备的轻量级操作系统虚拟化。

2013 年 3 月 15 日,在加利福尼亚州圣克拉拉召开的 Python 开发者大会上,DotCloud 的创始人兼首席执行官 Solomon Hvkes 在一场仅 5 分钟的微型演讲中,首次发布了基于 LXC 封装的 Docker Container,并于会后将其源码开源并托管到 Github。

容器技术Linux Namespaces和Cgroups_第1张图片

2.   容器的优势

传统模式的部署,直接将多个应用运行在物理服务器上,如果其中一个应用占用了大部分资源,可能会导致其他应用的性能下降。

虚拟化部署时代,可以在单个物理服务器的 CPU 上运行多个虚拟机(VM),每个 VM 是一台完整的计算机,在虚拟化硬件之上运行所有组件(包括了操作系统)。因此,可以让不同的应用在 VM 之间安全地隔离运行,更好地利用物理服务器上的资源。

容器与 VM 类似,具有自己的文件系统、CPU、内存、进程空间等,但与 VM 不同的是,容器之间共享操作系统(OS)。 所以,容器被认为是一种轻量级的操作系统层面的虚拟化技术。

相比于 VM ,轻量级的容器更适合云原生模式的实践。

 容器技术Linux Namespaces和Cgroups_第2张图片

 3.  容器的本质

容器技术Linux Namespaces和Cgroups_第3张图片

容器是一种轻量级的操作系统层面的虚拟化技术。

重点是 “操作系统层面” ,即容器本质上是利用操作系统提供的功能来实现虚拟化。

容器技术的代表之作 Docker ,则是一个基于 Linux 操作系统,使用 Go 语言编写,调用了 Linux Kernel 功能的虚拟化工具。

为了更好地理解容器的本质,我们来看看容器具体使用了哪些 Linux Kernel 技术,以及在 Go 中应该如何去调用。

容器技术Linux Namespaces和Cgroups_第4张图片

3.1  Chroot

Chroot 是一个可供 User Process 调用的 System Call 接口,可以让一个 Process 把指定的目录作为根目录(Root Directory),随后 Process 所有的文件系统操作都只能在这个指定目录中进行。故称之为 Change Root。

chroot() 的函数原型非常简单:

  • 调用权限:Root 用户。

  • 形参列表

    • path:一个指向字符串的指针,是一个绝对路径,表示将 Process 的根目录更改为的该目录路径。

  • 函数返回

    • 成功:返回 0;

    • 失败:返回 -1。

#include 

int chroot(const char *path);

需要注意的是,在更改了 Process 的根目录后,Process 只能访问新的根目录以及其子目录中的文件和资源。因此,在调用 chroot() 后,应确保 Process 所需要访问的所有文件和资源都存在于新的根目录下。

chroot() 目前主要主要用于:

安全隔离场景:限制将 Process 的访问范围,以此提高系统的安全性。

调试环境场景:创建一个与主系统隔离的环境,用于调试、测试和运行 Process。

系统救援场景:在 Linux 操作系统损坏或遭受攻击时,可以使用 chroot 将 Process 切换到受损系统的根目录中,以便进行修复和救援操作。

可见,chroot() 确实在 Linux File System(文件系统)层面提供了针对 Process 的隔离性,但并不提供完全的安全隔离,无法阻止其他方式的攻击。因此,要想实现 Processes 之间的安全隔离,还需要需采取其他安全措施。

3.2  NameSpaces

Linux Namespaces(命名空间)是一种操作系统层级的资源视图隔离技术,能够将 Linux 的全局资源,划分为 Namespace 范围内可见的资源。

由于容器之间共享 OS ,对于操作系统而言,容器的实质就是进程,多个容器运行,对应操作系统也就是运行着多个进程。

当进程运行在自己单独的命名空间时,命名空间的资源隔离可以保证进程之间互不影响,大家都以为自己身处在独立的一个操作系统里。这种进程就可以称为容器。

Namespaces 具有多种类型,基本上涵盖了构成一个操作系统所需要的基本元素:

命名空间 系统调用参数 作用
Mount (mnt) CLONE_NEWNS 文件目录挂载隔离。用于隔离各个进程看到的挂载点视图
Process ID (pid) CLONE_NEWPID 进程 ID 隔离。使每个命名空间都有自己的初始化进程,PID 为 1,作为所有进程的父进程
Network (net) CLONE_NEWNET 网络隔离。使每个 net 命名空间有独立的网络设备,IP 地址,路由表,/proc/net 目录等网络资源
Interprocess Communication (ipc) CLONE_NEWIPC 进程 IPC 通信隔离。让只有相同 IPC 命名空间的进程之间才可以共享内存、信号量、消息队列通信
UTS CLONE_NEWUTS 主机名或域名隔离。使其在网络上可以被视作一个独立的节点而非主机上的一个进程
User ID (user) CLONE_NEWUSER 用户 UID 和组 GID 隔离。例如每个命名空间都可以有自己的 root 用户
Control group (cgroup) Namespace CLONE_NEWCGROUP Cgroup 信息隔离。用于隐藏进程所属的控制组的身份,使命名空间中的 cgroup 视图始终以根形式来呈现,保障安全
Time Namespace CLONE_NEWTIME 系统时间隔离。允许不同进程查看到不同的系统时间

通过 /proc/{pid}/ns 文件可以查看指定 Process 运行在哪些 Namespaces Instance 中,并且每个 Namespace Instance 都具有一个唯一的标识。

[root@192 dongguangming]#  ls -l --time-style='+' /proc/$$/ns
total 0
lrwxrwxrwx. 1 root root 0  ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0  mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0  net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0  pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0  user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0  uts -> uts:[4026531838]
[root@192 dongguangming]# 

最终,用户可以通过创建多种不同类型的 Namespaces Instance 来提供的操作系统资源的隔离,再结合创建多种不同类型的 cgroups 来提供操作系统的资源配额,就构成了一个最基本的操作系统容器,即:Process Container。

UTS namespace

UTS namespace 为 Container 提供了 Hostname 和 Domain Name 的隔离。

Container 中的 Process 可以根据需要调用 sethostname 和 setdomainname 指令来进行配置,让每个 Container 都可以被视为网络中的一个独立的节点。

PID namespace

PID namespace 为 Container 提供了进程号的隔离。

每个 Containers 都拥有自己的进程环境,Container 的 init Process 都是 PID 1 号进程,它作为所有子进程的父进程。要想做到进程的隔离,首先需要创建出 PID 1 号进程,它具有以下特性:

  • 如果某个子进程脱离了父进程(父进程没有 wait 它),那么 init Process 就会负责回收资源并结束这个子进程。

  • 如果 init Process  被终止,那么 Kernel 就会调用 SIGKILL 终止此 PID namespace 中的所有进程。

IPC namespace

IPC namespace 为 Container 提供了 IPC(进程间)通信机制的隔离,包括信号量、消息队列、共享内存等机制。

每个 Containers 都拥有以下 /proc 文件接口:

  • /proc/sys/fs/mqueue:POSIX Message Queues 接口类型;

  • /proc/sys/kernel:System V IPC 接口类型;

  • /proc/sysvipc:System V IPC 接口类型。

Mount namespace

Mount namespace 为 Container 提供了 Filesystem 挂载点的隔离,继而实现了 VFS 的隔离。

每个 Containers 都拥有以下 /proc 文件接口,可以构成一个独立的 rootfs(Root 文件系统):

  • /proc/[pid]/mounts

  • /proc/[pid]/mountinfo

  • /proc/[pid]/mountstats

实际上,Mount namespace 是基于 Chroot 的不断改良而开发出来的。为 Container 创建的 rootfs 只是一个操作系统发行版所包含的文件、目录和配置,并不包括 Kernel 的文件。

Network namespace

Network namespace 为 Container 提供了网络资源的隔离,包括:

  • Network devices(网络设备)

  • IPv4 and IPv6 protocol stacks(IPv4、IPv6 的协议栈)

  • IP routing tables(IP 路由表)

  • Firewall rules(防火墙规则)

  • Sockets 套接字

  • /proc/[pid]/net

  • /sys/class/net

  • /proc/sys/net

需要注意的是,同一个 Network device 只能存在于一个 Namespace Instance 中,所以常常结合虚拟网络设备来使用。

容器技术Linux Namespaces和Cgroups_第5张图片

User namespace

User namespace 为 Container 提供了用户权限和安全属性相关的隔离,包括:User ID、User Group ID、Root 目录以及特殊的权限。

每个 Containers 都拥有以下 /proc 文件接口:

  • /proc/[pid]/uid_map

  • /proc/[pid]/gid_map

NameSpace 的具体描述可以查看 Linux man 手册中的 namespaces(7) - Linux manual page,手册中还描述了几个 NameSpace API ,主要是和进程相关的系统调用函数。

容器技术Linux Namespaces和Cgroups_第6张图片

clone()
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
                 /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

clone() 用于创建新进程,通过传入一个或多个系统调用参数( flags 参数)可以创建出不同类型的 NameSpace ,并且子进程也将会成为这些 NameSpace 的成员。

setns()
int setns(int fd, int nstype);

setns() 用于将进程加入到一个现有的 Namespace 中。其中 fd 为文件描述符,引用 /proc/[pid]/ns/ 目录里对应的文件,nstype 代表 NameSpace 类型。

unshare()
int unshare(int flags);

unshare() 用于将进程移出原本的 NameSpace ,并加入到新创建的 NameSpace 中。同样是通过传入一个或多个系统调用参数( flags 参数)来创建新的 NameSpace 。

ioctl()
int ioctl(int fd, unsigned long request, ...);

ioctl() 用于发现有关 NameSpace 的信息。

上面的这些系统调用函数,我们可以直接用 C 语言调用,创建出各种类型的 NameSpace ,这是最直观的做法。而对于 Go 语言,其内部已经帮我们封装好了这些函数操作,可以更方便地直接使用,降低心智负担。

先来看一个简单的小工具

package main

import (
 "os"
 "os/exec"
)

func main() {
 switch os.Args[1] {
 case "run":
  run()
 default:
  panic("help")
 }
}

func run() {
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 must(cmd.Run())
}

func must(err error) {
 if err != nil {
  panic(err)
 }
}

 这个程序接收用户命令行传递的参数,并使用 exec.Command 运行,例如当我们执行 go run main.go run echo hello 时,会创建出 main 进程, main 进程内执行 echo hello 命令创建出一个新的 echo 进程,最后随着 echo 进程的执行完毕,main 进程也随之结束并退出。

容器技术Linux Namespaces和Cgroups_第7张图片

容器技术Linux Namespaces和Cgroups_第8张图片

但是上面创建的进程太快退出了,不便于我们观察。如果让 main 进程启动一个 bash 进程会怎样呢?

为了直观对比,我们先看看当前会话的进程信息。

[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2057 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
1407
[root@192 dongguangming]# 

 当前我们正处于 PID 1407 的 bash 会话进程中,继续下一步操作:

[root@192 dongguangming]# go run main.go run /bin/bash
[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2058 pts/1    00:00:00 go
 2076 pts/1    00:00:00 main
 2079 pts/1    00:00:00 bash
 2089 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
2079
[root@192 dongguangming]# exit
exit
[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2090 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
1407
[root@192 dongguangming]# 

在执行 go run main.go run /bin/bash 后,我们的会话被切换到了 PID 2709的 bash 进程中,而 main 进程也还在运行着(当前所处的 bash 进程是 main 进程的子进程,main 进程必须存活着,才能维持 bash 进程的运行)。当执行 exit 退出当前所处的 bash 进程后,main 进程随之结束,并回到原始的 PID 1407的 bash 会话进程。

我们说过,容器的实质是进程,你现在可以把 main 进程当作是 “Docker” 工具,把 main 进程启动的 bash 进程,当作一个 “容器” 。这里的 “Docker” 创建并启动了一个 “容器”。

为什么打了双引号,是因为在这个 bash 进程中,我们可以随意使用操作系统的资源,并没有做资源隔离。

要想实现资源隔离,也很简单,在 run() 函数增加 SysProcAttr 配置,先从最简单的 UTS 隔离开始,传入对应的 CLONE_NEWUTS 系统调用参数,并通过 syscall.Sethostname 设置主机名:

func run() {
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 cmd.SysProcAttr = &syscall.SysProcAttr{
  Cloneflags: syscall.CLONE_NEWUTS,
 }
 must(syscall.Sethostname([]byte("mycontainer")))
 must(cmd.Run())
}

 

这段代码看似没什么问题,但仔细思考一下。

syscall.Sethostname 这一行到底是哪个进程在执行?main 进程还是 main 进程创建的子进程?

不用想,子进程都还没 Run 起来呢!现在调用肯定是 main 进程在执行,main 进程可没进行资源隔离,相当于直接更改宿主机的主机名了。

子进程还没 Run 起来,还不能更改主机名,等子进程 Run 起来后,又会进入到阻塞状态,无法再通过代码方式更改到子进程内的主机名。那有什么办法呢?

看来只能把 /proc/self/exe 这个神器请出来了。

在 Linux 2.2 内核版本及其之后,/proc/[pid]/exe 是对应 pid 进程的二进制文件的符号链接,包含着被执行命令的实际路径名。如果打开这个文件就相当于打开了对应的二进制文件,甚至可以通过重新输入 /proc/[pid]/exe 重新运行一个对应于 pid 的二进制文件的进程。

对于 /proc/self ,当进程访问这个神奇的符号链接时,可以解析到进程自己的 /proc/[pid] 目录。

合起来就是,当进程访问 /proc/self/exe 时,可以运行一个对应进程自身的二进制文件。

这有什么用呢?继续看下面的代码:

package main

import (
 "os"
 "os/exec"
 "syscall"
)

func main() {
 switch os.Args[1] {
 case "run":
  run()
 case "child":
  child()
 default:
  panic("help")
 }
}

func run() {
 cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 cmd.SysProcAttr = &syscall.SysProcAttr{
  Cloneflags: syscall.CLONE_NEWUTS,
 }
 must(cmd.Run())
}

func child() {
 must(syscall.Sethostname([]byte("mycontainer")))
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 must(cmd.Run())
}

func must(err error) {
 if err != nil {
  panic(err)
 }
}

 

在 run() 函数中,我们不再是直接运行用户所传递的命令行参数,而是运行 /proc/self/exe ,并传入 child 参数和用户传递的命令行参数。

同样当执行 go run main.go run echo hello 时,会创建出 main 进程, main 进程内执行 /proc/self/exe child echo hello 命令创建出一个新的 exe 进程,关键也就是这个 exe 进程,我们已经为其配置了 CLONE_NEWUTS 系统调用参数进行 UTS 隔离。也就是说,exe 进程可以拥有和 main 进程不同的主机名,彼此互不干扰。

进程访问 /proc/self/exe 代表着运行对应进程自身的二进制文件。因此,按照 exe 进程的启动参数,会执行 child() 函数,而 child() 函数内首先调用 syscall.Sethostname 更改了主机名(此时是 exe 进程执行的,并不会影响到 main 进程),接着和本文最开始的 run() 函数一样,再次使用 exec.Command 运行用户命令行传递的参数。

总结一下就是, main 进程创建了 exe 进程(exe 进程已经进行 UTS 隔离,exe 进程更改主机名不会影响到 main 进程), 接着 exe 进程内执行 echo hello 命令创建出一个新的 echo 进程,最后随着 echo 进程的执行完毕,exe 进程随之结束,exe 进程结束后, main 进程再结束并退出。

容器技术Linux Namespaces和Cgroups_第9张图片

那经过 exe 这个中间商所创建出来的 echo 进程和之前由 main 进程直接创建的 echo 进程,两者有何不同呢。

我们知道,创建 exe 进程的同时我们传递了 CLONE_NEWUTS 标识符创建了一个 UTS NameSpace ,Go 内部帮我们封装了系统调用函数 clone() 的调用,我们也说过,由 clone() 函数创建出的进程的子进程也将会成为这些 NameSpace 的成员,所以默认情况下(创建新进程时无继续指定系统调用参数),由 exe 进程创建出的 echo 进程会继承 exe 进程的资源, echo 进程将拥有和 exe 进程相同的主机名,并且同样和 main 进程互不干扰。

因此,借助中间商 exe 进程 ,echo 进程可以成功实现和宿主机( main 进程)资源隔离,拥有不同的主机名。

容器技术Linux Namespaces和Cgroups_第10张图片

再次通过启动 /bin/bash 进行验证主机名是否已经成功隔离:

[root@192 dongguangming]# hostname
192.168.0.103
[root@192 dongguangming]# touch main.go
[root@192 dongguangming]# vi main.go 
[root@192 dongguangming]# go run main.go run /bin/bash
[root@mycontainer dongguangming]# hostname
mycontainer
[root@mycontainer dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2112 pts/1    00:00:00 go
 2133 pts/1    00:00:00 main1
 2136 pts/1    00:00:00 exe
 2140 pts/1    00:00:00 bash
 2150 pts/1    00:00:00 ps
[root@mycontainer dongguangming]# exit
exit
[root@192 dongguangming]# hostname
192.168.0.103
[root@192 dongguangming]# 

 当执行 go run main.go run /bin/bash 时,我们也可以在另一个 ssh 会话中,使用 ps afx 查看bash 会话进程的层次信息:

容器技术Linux Namespaces和Cgroups_第11张图片

以此类推,新增资源隔离只要继续传递指定的系统调用参数即可:

package main

import (
 "fmt"
 "os"
 "os/exec"
 "syscall"
)

func main() {
 switch os.Args[1] {
 case "run":
  run()
 case "child":
  child()
 default:
  panic("help")
 }
}

func run() {
 fmt.Println("[main]", "pid:", os.Getpid())
 cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 cmd.SysProcAttr = &syscall.SysProcAttr{
  Cloneflags: syscall.CLONE_NEWUTS |
   syscall.CLONE_NEWPID |
   syscall.CLONE_NEWNS,
  Unshareflags: syscall.CLONE_NEWNS,
 }
 must(cmd.Run())
}

func child() {
 fmt.Println("[exe]", "pid:", os.Getpid())
 must(syscall.Sethostname([]byte("mycontainer")))
 must(os.Chdir("/"))
 must(syscall.Mount("proc", "proc", "proc", 0, ""))
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 must(cmd.Run())
 must(syscall.Unmount("proc", 0))
}

func must(err error) {
 if err != nil {
  panic(err)
 }
}

 

Cloneflags 参数新增了 CLONE_NEWPID 和 CLONE_NEWNS 分别隔离进程 pid 和文件目录挂载点视图,Unshareflags: syscall.CLONE_NEWNS 则是用于禁用挂载传播(如果不设置该参数,container 内的挂载会共享到 host ,挂载传播不在本文的探讨范围内)。

当我们创建 PID Namespace 时,exe 进程包括其创建出来的子进程的 pid 已经和 main 进程隔离了,这一点可以通过打印 os.Getpid() 结果或执行 echo $$ 命令得到验证。但此时还不能使用 ps 命令查看,因为 ps 和 top 等命令会使用 /proc 的内容,所以我们才继续引入了 Mount Namespace ,并在 exe 进程挂载 /proc 目录。

[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2195 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
1407
[root@192 dongguangming]# go run main.go run /bin/bash
[main] pid: 2217
[exe] pid: 1
[root@mycontainer /]# ps
  PID TTY          TIME CMD
    1 pts/1    00:00:00 exe
    4 pts/1    00:00:00 bash
   13 pts/1    00:00:00 ps
[root@mycontainer /]#  echo $$
4
[root@mycontainer /]# exit
exit
[root@192 dongguangming]# 

通过 /proc/{pid}/ns 文件可以查看指定 Process 运行在哪些 Namespaces Instance 中,并且每个 Namespace Instance 都具有一个唯一的标识。

此时,exe 作为初始化进程,pid 为 1 ,创建出了 pid 4 的 bash 子进程,而且已经看不到 main 进程了。

剩下的 IPC 、NET、 USER 等 NameSpace 就不在本文一一展示了。

3.3  Cgroups

借助 NameSpace 技术可以帮进程隔离出自己单独的空间,成功实现出最简容器。但是怎样限制这些空间的物理资源开销(CPU、内存、存储、I/O 等)就需要利用 Cgroups 技术了。

限制容器的资源使用,是一个非常重要的功能,如果一个容器可以毫无节制的使用服务器资源,那便又回到了传统模式下将应用直接运行在物理服务器上的弊端。这是容器化技术不能接受的。

Cgroups 的全称是 Control groups 即控制组,最早是由 Google 的工程师(主要是 Paul Menage 和 Rohit Seth)在 2006 年发起,一开始叫做进程容器(process containers)。在 2007 年时,因为在 Linux Kernel 中,容器(container)这个名词有许多不同的意义,为避免混乱,被重命名为 cgroup ,并且被合并到 2.6.24 版本的内核中去。

Cgroups 是对进程分组管理的一种机制,提供了对一组进程及它们的子进程的资源限制、控制和统计的能力,并为每种可以控制的资源定义了一个 subsystem (子系统)的方式进行统一接口管理,因此 subsystem 也被称为 resource controllers (资源控制器)。

几个主要的 subsystem 如下( Cgroups V1 ):

子系统 作用
cpu 限制进程的 cpu 使用率
cpuacct 统计进程的 cpu 使用情况
cpuset 在多核机器上为进程分配单独的 cpu 节点或者内存节点(仅限 NUMA 架构)
memory 限制进程的 memory 使用量
blkio 控制进程对块设备(例如硬盘) io 的访问
devices 控制进程对设备的访问
net_cls 标记进程的网络数据包,以便可以使用 tc 模块(traffic control)对数据包进行限流、监控等控制
net_prio 控制进程产生的网络流量的优先级
freezer 挂起或者恢复进程
pids 限制 cgroup 的进程数量
更多子系统参考 Linux man cgroups[3]文档 https://man7.org/linux/man-pages/man7/cgroups.7.html

借助 Cgroups 机制,可以将一组进程(task group)和一组 subsystem 关联起来,达到控制进程对应关联的资源的能力。如图:

容器技术Linux Namespaces和Cgroups_第12张图片

Cgroups 的层级结构称为 hierarchy (即 cgroup 树),是一棵树,由 cgroup 节点组成。

系统可以有多个 hierarchy ,当创建新的 hierarchy 时,系统所有的进程都会加入到这个 hierarchy 默认创建的 root cgroup 根节点中,在树中,子节点可以继承父节点的属性。

对于同一个 hierarchy,进程只能存在于其中一个 cgroup 节点中。如果把一个进程添加到同一个 hierarchy 中的另一个 cgroup 节点,则会从第一个 cgroup 节点中移除。

hierarchy 可以附加一个或多个 subsystem 来拥有对应资源(如 cpu 和 memory )的管理权,其中每一个 cgroup 节点都可以设置不同的资源限制权重,而进程( task )则绑定在 cgroup 节点中,并且其子进程也会默认绑定到父进程所在的 cgroup 节点中。

基于 Cgroups 的这些运作原理,可以得出:如果想限制某些进程的内存资源,就可以先创建一个 hierarchy ,并为其挂载 memory subsystem ,然后在这个 hierarchy 中创建一个 cgroup 节点,在这个节点中,将需要控制的进程 pid 和控制属性写入即可。

接下来我们就来实践一下。

Linux 一切皆文件。

在 Linux Kernel 中,为了让 Cgroups 的配置更直观,使用了目录的层级关系来模拟 hierarchy ,以此通过虚拟的树状文件系统的方式暴露给用户调用。

创建一个 hierarchy ,并为其挂载 memory subsystem ,这一步我们可以跳过,因为系统已经默认为每个 subsystem 创建了一个默认的 hierarchy ,我们可以直接使用。

例如 memory subsystem 默认的 hierarchy 就在 /sys/fs/cgroup/memory 目录。

[root@host go]# mount | grep memory
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
[root@host go]# cd /sys/fs/cgroup/memory
[root@host memory]# pwd
/sys/fs/cgroup/memory
[root@host memory]#

只要在这个 hierarchy 目录下创建一个文件夹,就相当于创建了一个 cgroup 节点:

[root@host memory]# mkdir hello
[root@host memory]# cd hello/
[root@host hello]# ls
cgroup.clone_children           memory.kmem.slabinfo                memory.memsw.failcnt             memory.soft_limit_in_bytes
cgroup.event_control            memory.kmem.tcp.failcnt             memory.memsw.limit_in_bytes      memory.stat
cgroup.procs                    memory.kmem.tcp.limit_in_bytes      memory.memsw.max_usage_in_bytes  memory.swappiness
memory.failcnt                  memory.kmem.tcp.max_usage_in_bytes  memory.memsw.usage_in_bytes      memory.usage_in_bytes
memory.force_empty              memory.kmem.tcp.usage_in_bytes      memory.move_charge_at_immigrate  memory.use_hierarchy
memory.kmem.failcnt             memory.kmem.usage_in_bytes          memory.numa_stat                 notify_on_release
memory.kmem.limit_in_bytes      memory.limit_in_bytes               memory.oom_control               tasks
memory.kmem.max_usage_in_bytes  memory.max_usage_in_bytes           memory.pressure_level
[root@host hello]#

其中我们创建的 hello 文件夹内的所有文件都是系统自动创建的。常用的几个文件功能如下:

文件名 功能
tasks cgroup 中运行的进程( PID)列表。将 PID 写入一个 cgroup 的 tasks 文件,可将此进程移至该 cgroup
cgroup.procs cgroup 中运行的线程群组列表( TGID )。将 TGID 写入 cgroup 的 cgroup.procs 文件,可将此线程组群移至该 cgroup
cgroup.event_control event_fd() 的接口。允许 cgroup 的变更状态通知被发送
notify_on_release 用于自动移除空 cgroup 。默认为禁用状态(0)。设定为启用状态(1)时,当 cgroup 不再包含任何任务时(即,cgroup 的 tasks 文件包含 PID,而 PID 被移除,致使文件变空),kernel 会执行 release_agent 文件(仅在 root cgroup 出现)的内容,并且提供通向被清空 cgroup 的相关路径(与 root cgroup 相关)作为参数
memory.usage_in_bytes 显示 cgroup 中进程当前所用的内存总量(以字节为单位)
memory.memsw.usage_in_bytes 显示 cgroup 中进程当前所用的内存量和 swap 空间总和(以字节为单位)
memory.max_usage_in_bytes 显示 cgroup 中进程所用的最大内存量(以字节为单位)
memory.memsw.max_usage_in_bytes 显示 cgroup 中进程的最大内存用量和最大 swap 空间用量(以字节为单位)
memory.limit_in_bytes 设定用户内存(包括文件缓存)的最大用量
memory.memsw.limit_in_bytes 设定内存与 swap 用量之和的最大值
memory.failcnt 显示内存达到 memory.limit_in_bytes 设定的限制值的次数
memory.memsw.failcnt 显示内存和 swap 空间总和达到 memory.memsw.limit_in_bytes 设定的限制值的次数
memory.oom_control 可以为 cgroup 启用或者禁用“内存不足”(Out of Memory,OOM) 终止程序。默认为启用状态(0),尝试消耗超过其允许内存的任务会被 OOM 终止程序立即终止。设定为禁用状态(1)时,尝试使用超过其允许内存的任务会被暂停,直到有额外内存可用。
更多文件的功能说明可以查看 kernel 文档中的 cgroup-v1/memory[4] https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt

在这个 hello cgroup 节点中,我们想限制某些进程的内存资源,只需将对应的进程 pid 写入到 tasks 文件,并把内存最大用量设定到 memory.limit_in_bytes 文件即可。

[root@host hello]# cat memory.oom_control
oom_kill_disable 0
under_oom 0
[root@host hello]# cat memory.failcnt
0
[root@host hello]# echo 100M > memory.limit_in_bytes
[root@host hello]# cat memory.limit_in_bytes
104857600
[root@host hello]#

hello cgroup 节点默认启用了 OOM 终止程序,因此,当有进程尝试使用超过可用内存时会被立即终止。查询 memory.failcnt 可知,目前还没有进程内存达到过设定的最大内存限制值。

我们已经设定了 hello cgroup 节点可使用的最大内存为 100M ,此时新启动一个 bash 会话进程并将其移入到 hello cgroup 节点中:

[root@host hello]# /bin/bash
[root@host hello]# echo $$
4123
[root@host hello]# cat tasks
[root@host hello]# echo $$ > tasks
[root@host hello]# cat tasks
4123
4135
[root@host hello]# cat memory.usage_in_bytes
196608
[root@host hello]#

后续在此会话进程所创建的子进程都会加入到该 hello cgroup 节点中(例如 pid 4135 就是由于执行 cat 命令而创建的新进程,被系统自动加入到了 tasks 文件中)。

同样篇幅问题,剩下的 subsystem 也不在本文一一展示了。

其实到这里,我们已经通过 NameSpace 技术帮进程隔离出自己单独的空间,并使用 Cgroups 技术限制和监控这些空间的资源开销,这种特殊的进程就是容器的本质

4. Docker 对 Cgroups 和 Namespaces 的应用

当我们创建了一个 Docker Container 之后就可以查看这个 Container 所具有的 cgroups 和 namespaces 了。

  1. 查看 Container 的 ID(cfca1212d140)和 PID(2240)配置。

$ docker ps
CONTAINER ID   IMAGE                   COMMAND   CREATED         STATUS       PORTS     NAMES
cfca1212d140   centos:centos7.9.2009   "bash"    18 months ago   Up 2 hours             vim-ide

$ docker inspect --format='{{.State.Pid}}' cfca1212d140
2240
  1. 查看 Container 的 cgroups 配置。

$ ll /sys/fs/cgroup/memory/docker/
总用量 0
drwxr-xr-x. 2 root root 0 6月   2 03:40 cfca1212d1407a89632a439e974e246d1f6edd0bbef9079f06addf2613e1d46f

$ cat /sys/fs/cgroup/memory/docker/cfca1212d1407a89632a439e974e246d1f6edd0bbef9079f06addf2613e1d46f/cgroup.procs 
2240

$ cat /sys/fs/cgroup/memory/docker/cfca1212d1407a89632a439e974e246d1f6edd0bbef9079f06addf2613e1d46f/memory.limit_in_bytes
9223372036854771712
  1. 查看 Container 的 namespaces 配置。

$ ls -l --time-style='+' /proc/2240/ns
总用量 0
lrwxrwxrwx. 1 root root 0  ipc -> ipc:[4026532433]
lrwxrwxrwx. 1 root root 0  mnt -> mnt:[4026532431]
lrwxrwxrwx. 1 root root 0  net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0  pid -> pid:[4026532434]
lrwxrwxrwx. 1 root root 0  user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0  uts -> uts:[4026532432]

 

总结

其实容器的底层原理并不难,本质上就是一个特殊的进程,特殊在为其创建了 NameSpace 隔离运行环境,用 Cgroups 为其控制了资源开销,这些都是站在 Linux 操作系统的肩膀上实现的,包括 Docker 的镜像实现也是利用了 UnionFS 的分层联合技术。

参考:

  1. Linux Namespace和Cgroup  Linux Namespace和Cgroup - Linux程序员 - SegmentFault 思否

  2. linux namespace and cgroup linux namespace and cgroup - 掘金

  3. Linux Namespace 入门系列:Namespace API Linux Namespace 入门系列:Namespace API - 知乎

  4. Docker 工作原理及容器化简易指南 http://dockone.io/article/8788

  5. 昨天有读者说他不会 docker,今天就给你肝出来了 https://mp.weixin.qq.com/s?__biz=MzU2NDg0OTgyMA==&mid=2247492639&idx=1&sn=ac607fbbe221e50c24db1a7ccb12518b

  6. Docker 系列:核心原理和实现(2) Docker 系列:核心原理和实现(2) | 郭宁的个人博客

  7. What Are Linux Namespaces and What Are They Used for? What Are Linux Namespaces and What Are They Used for?

  8. namespaces(7) — Linux manual page namespaces(7) - Linux manual page

  9. 极好 A deep dive into Linux namespaces A deep dive into Linux namespaces – Chord Simple

  10. Linux – cgroup & namespace Linux – cgroup & namespace – Benjr.tw

  11. cgroup_namespaces(7) — Linux manual pages  https://manpages.courier-mta.org/htmlman7/cgroup_namespaces.7.html
  12. Overview of Linux Namespace  Linux namespaces pid,network,mount,ipc,uts,user,cgroup

  13. Container and Cgroups https://enqueuezero.com/container-and-cgroups.html

  14. Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation  Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation | Linux Journal

  15. Anatomy of a Container: Namespaces, cgroups & Some Filesystem Magic - LinuxConf https://www.slideshare.net/jpetazzo/anatomy-of-a-container-namespaces-cgroups-some-filesystem-magic-linuxcon

  16. What even is a container: namespaces and cgroups What even is a container: namespaces and cgroups

  17. https://events.static.linuxfound.org/sites/events/files/slides/cgroup_and_namespaces.pdf
  18. Resource management: Linux kernel Namespaces and cgroups  https://sites.cs.ucsb.edu/~rich/class/cs293b-cloud/papers/lxc-namespace.pdf,http://www.haifux.org/lectures/299/netLec7.pdf
  19. #14 - Introduction to Linux Control Groups (Cgroups)

    Introduction to Linux Control Groups (Cgroups)

  20. Control Group v2 https://www.kernel.org/doc/Documentation/cgroup-v2.txt
    
  21. Docker 背后的内核知识——cgroups 资源限制 Docker背后的内核知识——cgroups资源限制_语言 & 开发_孙健波_InfoQ精选文章

  22. 在Linux中使用namespace和cgroup实现进程的资源隔离和限制 在Linux中使用namespace和cgroup实现进程的资源隔离和限制 | 滩之南

  23. CGroup Namespaces https://lwn.net/Articles/618873/

  24. CGroup 介绍、应用实例及原理描述 IBM Developer

  25. Restricting process CPU usage using nice, cpulimit, and cgroups Restricting process CPU usage using nice, cpulimit, and cgroups | Scout APM Blog

  26. Docker DCA – Linux Namespaces and cgroups

    Docker DCA - Linux Namespaces and cgroups - buildVirtual
  27. Demystifying Containers - Part I: Kernel Space https://medium.com/@saschagrunert/demystifying-containers-part-i-kernel-space-2c53d6979504

  28. Containers: Namespaces and Dockers http://www.i3s.unice.fr/~urvoy/docs/VICC/1_2_vicc.pdf
  29. Docker Namespace and Cgroups https://medium.com/@kasunmaduraeng/docker-namespace-and-cgroups-dece27c209c7

  30. Understanding Linux Container Scheduling  Understanding Linux Container Scheduling — Squarespace / Engineering

  31. Linux Container Primitives: PID and Network Namespaces - SCHUTZWERK Linux Container Primitives: PID and Network Namespaces - SCHUTZWERK
  32. Containers: cgroups, Linux kernel namespaces, ufs, Docker, and intro to Kubernetes pods Containers: cgroups, Linux kernel namespaces, ufs, Docker, and intro to Kubernetes pods - IBM MediaCenter

  33. Container Creation Using Namespaces and Bash Container Creation Using Namespaces and Bash | Nicolas Mesa

  34. Linux containers in 500 lines of code Linux containers in 500 lines of code

  35. Linux Container Primitives: cgroups, namespaces, and more! https://www.youtube.com/watch?v=x1npPrzyKfs

  36. Container Creation Using Namespaces and Bash  Container Creation Using Namespaces and Bash | Nicolas Mesa

  37. Containers from Scratch Containers from Scratch | posts

  38. Hands on Linux sandbox with namespaces and cgroups https://blogs.rdoproject.org/2015/08/hands-on-linux-sandbox-with-namespaces-and-cgroups/

  39. How to run Docker containers using common Linux tools (without Docker)  unshare – I Learned How To…

  40. Understanding containers (Part 1/3) Understanding containers (Part 1/3) – Bibi's blog

  41. Docker 学习笔记10 容器技术原理 PID Namespace Docker 学习笔记10 容器技术原理 PID Namespace | 码农家园

  42. Understanding Linux containers  Containers explained: What they are and why you should care

  43. runc source code——bootstrap分析2 runc source code——bootstrap分析2 – freesky-edward

  44. Demystifying Containers – Part I: Kernel Space Demystifying Containers - Part I: Kernel Space | SUSE Communities

  45. Linux Namespaces  Linux Namespaces

  46. Containerization Mechanisms: Namespaces Containerization Mechanisms: Namespaces - Selectel Blog

  47. Runc 容器初始化和容器逃逸 Runc 容器初始化和容器逃逸_Linux云计算网络-商业新知

  48. chroot, cgroups and namespaces — An overview https://itnext.io/chroot-cgroups-and-namespaces-an-overview-37124d995e3d

  49. Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation | Linux Journal

  50. Namespaces and Cgroups – the basis of Linux Containers  Meetup - We are what we do
  51. LXC, Cgroups and Advanced Linux Container Technology Lecture http://www.novell.com/feeds/nih/wp-content/uploads/2012/05/SUS15_lec.pdf
  52. Linux Containers https://www.cl.cam.ac.uk/~lc525/files/Linux_Containers.pdf
  53. Linux Container Primitives: cgroups, namespaces, and more! (LinuxFest Northwest 2019)

    https://speakerdeck.com/samuelkarp/linux-container-primitives-cgroups-namespaces-and-more-linuxfest-northwest-2019

  54. Container Performance Analysis  http://www.brendangregg.com/Slides/LISA2017_Container_Performance_Analysis.pdf

  55. Understanding cgroups https://www.grant.pizza/blog/understanding-cgroups/

  56. cgroups  cgroups - ArchWiki

  57. Understanding Linux Container Scheduling Understanding Linux Container Scheduling — Squarespace / Engineering

  58. Understanding Linux Container Scheduling Understanding Linux Container Scheduling — Kevin Lynch

  59. Linux Kernel Isolation Features From the Frog's mouth - JFrog Blog

  60. Introduction to Linux Containers Introduction to Linux Containers | CloudNativeLab

  61. All things Linux containers containerz.info

  62. src/syscall/exec_linux.go - The Go Programming Language https://golang.org/src/syscall/exec_linux.go

 

你可能感兴趣的:(linux,docker,linux,docker,容器)