什么是容器?

upgrade vr

所谓容器,其实就像上图:
把 VR 玩家放在一个其以为是家的地方(chroot),创造目标所需环境(namespaces),再定期定量的提供水、食物(cgroups)供其维续生命。

chroot、namespace、cgroups,就是容器的核心技术。

本文将使用几个有限的命令,用 15 分钟的时间,让读者直观的了解容器

chroot

chroot 比较简单,不演示了。

namespaces

namespaces,wikipedia 定义: Namespaces are a feature of the Linux kernel that partitions kernel resources such that one set of processes sees one set of resources while another set of processes sees a different set of resources.

文档见 namespaces

不同的进程,可以处于不同的 namespaces 中,这样它们就被隔离了。

namespaces 从类型来讲,分为 6 种,分别用于不同场景。这 6 种 namespaces 分别是:

名称 宏定义 隔离内容
Mount namespaces CLONE_NEWNS Mount points
UTS namespaces CLONE_NEWUTS Hostname and NIS domain name
IPC namespaces CLONE_NEWIPC System V IPC, POSIX message queues
PID namespaces CLONE_NEWPID Process IDs
Network namespaces CLONE_NEWNET Network devices, stacks, ports, etc.
User namespaces CLONE_NEWUSER User and group IDs

uts namespaces

我们先从最简单的 uts namespaces开始。

下面开始我们的第一个目标:隔离hostname

  1. 查看当前 namespaces 的 hostname 和当前进程所在的 namespaces:
1
2
3
4
[root@outside ~]# hostname
outside
[root@outside ~]# readlink /proc/$$/ns/uts
uts:[4026531838]
  1. 解除 namespaces share, 创建新的进程:
1
2
3
4
5
6
7
[root@outside ~]# unshare --uts /bin/sh
sh-4.2# hostname inside
sh-4.2# hostname
inside
sh-4.2# readlink /proc/$$/ns/uts
uts:[4026532328]
sh-4.2# exit

以上可看到,新的 hostname 在内部已经生效,并且内部进程的 uts namespaces 已发生变化(4026531838 -> 4026532328)。

  1. 验证外部的 namespaces,其 hostname 未受到影响:
1
2
[root@outside ~]# hostname
outside

那是如何实现的?下例说明:

1
2
3
4
[root@outside ~]# strace -fe unshare,execve unshare --uts  /bin/sh
execve("/usr/bin/unshare", ["unshare", "--uts", "/bin/sh"], [/* 27 vars */]) = 0
unshare(CLONE_NEWUTS)                   = 0  # <-- 关键点
execve("/bin/sh", ["/bin/sh"], [/* 27 vars */]) = 0

对于unshare(2), 请参考 man page

pid namespaces

如果只是修改 hostname 而不影响宿主,那也没什么意思。下面我们来看看如何隔离pid 资源

实现目标:在新的进程中看到全新一套 pids

1
2
3
4
5
[root@outside ~]# unshare --fork --pid --mount-proc  /bin/sh
sh-4.2# ps
   PID TTY          TIME CMD
     1 pts/5    00:00:00 sh
     2 pts/5    00:00:00 ps

如此,我们就在新的 namespaces 中,隔离了 pids,在这个容器内部看来,就是全新的一套 pids(第一个 pid 为 1)。

如何实现的呢?仍使用strace验证:

1
2
3
4
5
6
7
[root@outside ~]# strace -fe unshare,execve,mount unshare --fork --pid --mount-proc  /bin/sh
execve("/usr/bin/unshare", ["unshare", "--fork", "--pid", "--mount-proc", "/bin/sh"], [/* 27 vars */]) = 0
unshare(CLONE_NEWNS|CLONE_NEWPID)       = 0 # <-- 关键点
Process 97554 attached
[pid 97554] mount("none", "/proc", NULL, MS_REC|MS_PRIVATE, NULL) = 0
[pid 97554] mount("proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL) = 0
[pid 97554] execve("/bin/sh", ["/bin/sh"], [/* 27 vars */]) = 0

cgroups

cgroups,wikipedia 定义: cgroups is a Linux kernel feature that limits, accounts for, and isolates the resource usage (CPU, memory, disk I/O, network, etc.) of a collection of processes

我们只需要知道 cgroups,能限制资源就行了。下面我们来直观感受一下 cgroup 的功能。

限制目标: 进程数

新启动一个sh:

1
2
3
[root@outside ~]# sh
sh-4.2# echo $$
14838 # <-- 记住这个 pid

先挂载 cgroups(pids子系统):

1
2
[root@outside ~]# mkdir -p cgroup/pids
[root@outside ~]# mount -t cgroup -o pids pids ./cgroup/pids

配置 cgroups:

1
2
3
4
[root@outside ~]# cd ./cgroup/pids/
[root@outside pids]# mkdir x
[root@outside pids]# cd x
[root@outside x]# echo 3 > pids.max # <-- 限制只能启动 3 个进程

将上面的 sh 加入到我们新建立的 cgroups 中(默认也影响其子进程):

1
[root@outside x]# echo 14838 > cgroup.procs

下面来看看效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sh-4.2# pstree -p $$
sh(14838)───pstree(15421)
sh-4.2# sleep 100 & # <-- 启动第 1 个进程
[1] 15427
sh-4.2# sleep 100 & # <-- 启动第 2 个进程
[2] 15429
sh-4.2# sleep 100 & # <-- 第 3 个进程启动失败, 因为最开始的 sh 进程也算
sh: fork: retry: Resource temporarily unavailable
sh: fork: retry: Resource temporarily unavailable
sh: fork: retry: Resource temporarily unavailable
sh: fork: retry: Resource temporarily unavailable
sh: fork: Resource temporarily unavailable

可以查看一下当前的pids数量:

1
2
[root@outside x]# cat pids.current
3  # <-- 的确是 3 个

总结

以上使用unshare命令与cgroups,初步了解了容器的几个基本技术。

关于容器更深入的内容,有待读者自己去学习。

玩得开心 :)

-EOF-