容器的寿命

2021-07-29 23:44:01

免责声明:我上周在德里 Kubernetes 论坛上发表了相同标题的演讲。如果您愿意,可以在 YouTube 上观看视频。 (更新:原始视频已被 CNCF 删除。已在此处重新上传)此外,此帖子还可作为演示中使用的命令的参考。我已经编程近 6 年了,几乎所有时间都在使用容器。作为一名程序员,好奇心占了上风,我开始问这个问题,什么是容器?答案之一是这样的:“哦,它们就像虚拟机,但只是它们没有自己的内核并共享主机的内核。”这让我很长一段时间相信容器是一种更轻的虚拟机形式。他们对我来说就像魔法一样。直到后来我开始深入研究容器的内部结构时,我才意识到这句话感觉非常真实:而且我一直试图找到一种方法来解释乍一看或第二眼看起来像以下内容我了解到,实际上根本没有容器之类的东西。我发现我们所知道的容器由两个 Linux 原语组成:在我们研究它们是什么以及它们如何帮助形成称为容器的抽象之前,了解如何创建和管理新进程很重要在 Linux 中。让我们来看看下图:

在上图中,父进程可以被认为是一个活动的shell会话,子进程可以被认为是在shell中运行的任何命令,例如:ls、pwd。现在,当运行新命令时,会创建一个新进程。这是由父进程通过调用函数 fork 来完成的。当它创建一个新的独立进程时,它将子进程的进程 ID (PID) 返回给调用函数 fork 的父进程。在适当的时候,父母和孩子都可以继续执行他们的任务并终止。子PID对于父进程跟踪新创建的进程很重要。我们稍后将在本博文中回到这一点。如果您有兴趣深入了解 fork 的语义,我在过去写了一篇更详细的博客文章,描述了这一点以及如何使用代码做到这一点。你可以在这里阅读。既然已经了解了如何在 Linux 中创建新进程,让我们尝试了解哪些命名空间可以帮助我们实现。命名空间是一种隔离原语,可以帮助我们隔离各种类型的资源。在 Linux 中,目前可以对七种不同类型的资源执行此操作。他们是,没有特定的顺序:我不会在这篇文章中详细介绍他们每个人的作用,因为已经有很多关于这方面的文献,手册页可能是他们最好的资源。相反,我将尝试在这篇文章中解释网络命名空间,看看它如何帮助我们隔离网络资源。但在此之前,需要注意的是,默认情况下,这些命名空间中的每一个都已经存在于系统中,并且被称为主机命名空间或默认命名空间。例如,系统中的默认网络命名空间包含用于 WIFI 和/或以太网端口的网络接口卡(如果有的话)。关于进程的所有信息都包含在 procfs 下,它通常安装在 /proc 上。运行 echo $$ 将为我们提供当前正在运行的进程的 PID:如果查看 /proc/<PID>/ns,我们将获得该进程使用的命名空间列表。例如: $ ls /proc/448884/ns -lhtotal 0lrwxrwxrwx 1 root root 0 Feb 23 19:00 cgroup -> 'cgroup:[4026531835]'lrwxrwxrwx 1 root root 0 Feb 23 ip->'02 4026531839]'lrwxrwxrwx 1 root root 0 Feb 23 19:00 mnt -> 'mnt:[4026531840]'lrwxrwxrwx 1 root root 0 Feb 23 19:00 net -> '65x rootwxr3r20 Feb20 19:00 pid -> 'pid:[4026531836]'lrwxrwxrwx 1 root root 2 月 23 日 19:00 pid_for_children -> 'pid:[4026531836]'lrwxrwxrwx 1 root root 1>2 月 20 日4026531837]'lrwxrwxrwx 1 root root 0 Feb 23 19:00 uts -> 'uts:[4026531838]'

对于每个命名空间,都有一个文件,它是指向命名空间 ID 的符号链接 [1]。所以对于网络命名空间,上例中命名空间的ID是net:[4026532008],而4026532008是inode号。对于同一个命名空间中的两个进程,这个数字是相同的。在 Linux 上,要创建新的命名空间,我们可以使用系统调用 unshare。为了创建一个新的网络命名空间,我们需要添加标志 -n。因此,在具有 root 权限的 shell 会话中,我们将执行以下操作: 我们可以查看 /proc/<PID>/ns 目录以验证我们确实创建了一个新的命名空间:命名空间 ID 与我们在上面看到的不同主机网络命名空间。在此之后运行命令 ip link 只会向我们显示环回接口:# ip link1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00: 00 brd 00:00:00:00:00:00 如果有WIFI卡或以太网口等网络接口,则根本不显示。事实上,如果我们尝试运行 ping 127.0.0.1,我们通常认为理所当然的事情也不会起作用:首先我们创建了一个新的网络命名空间,这个行为隔离了默认命名空间中已有的网络资源。在这个新的命名空间中,我们唯一可用的接口是环回接口。然而,它还没有分配给它的 IP 地址,因此 ping 127.0.0.1 不能正常工作。这可以通过运行来验证:

# ip address1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 这说明不仅这个接口目前没有IP地址,它的状态也设置为DOWN。运行以下命令可以解决这个问题:首先,我们将 IP 地址 127.0.0.1 分配给该接口并将接口状态设置为 UP,从而使其可用于侦听传入的网络数据包。现在 ping 将按预期工作:# ping 127.0.0.1PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.020 bytes of 127.0.0.1: icmp_seq=1 ttl=64 time=0.020 bytes of 127.0.0.1 .0.1: icmp_seq=2 ttl=64 time=0.060 ms64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.071 ms 为了理解隔离的概念,我们将继续尝试获取这个新的网络接口,(让我们称其为 CHILD)以与主机网络命名空间对话,反之亦然。为了帮助我们理解,我们将这个 shell 中的 PS1 变量设置为易于识别的变量:我们还将生成一个具有 root 访问权限的新终端,以便在其中运行的 shell 属于主机网络命名空间。我们将再次设置 PS1 变量以帮助轻松识别主机命名空间:

在此接口上运行 ip link 命令将显示系统中当前安装的网络接口。例如:[netns: HOST]# ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00: 00:00:00:00:003: enp0s31f6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000 link/ether 0e:94:18:de:da:b3 ff:ff:ff:ff:ff:ff4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:ad:0f:83:cc brd ff:ff:ff:ff:ff:ff11: wlp61s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DORMANT group default qlen 1000 link/ether fa:3d:a9:90:95: 5d brd ff:ff:ff:ff:ff:ff 但是如果读者一直跟着,这将产生一个空的输出。那么这是否意味着该命令不起作用或者我们在那里做错了什么,即使我们之前创建了一个新的网络命名空间?这两个问题的答案都是否定的。由于在 UNIX [2] 中一切都是文件,ip 命令在目录 /var/run/netns 中查找网络名称空间。目前该目录是空的。因此,我们将首先创建一个空文件,然后再次尝试运行该命令: [netns: HOST]# touch /var/run/netns/child[netns: HOST]# ip netns listError: Peer netns reference is invalid.Error: Peer netns 引用无效。我们确实在列表中看到了子命名空间,但我们也看到了一个错误。这是因为我们还没有将运行新命名空间的 shell 映射到这个文件。为此,我们将挂载 /proc/<PID>/ns/net 文件绑定到我们上面创建的新文件。这可以通过在运行子网络命名空间的 shell 中执行以下命令来完成: 这次列出网络命名空间的命令没有任何错误。这意味着我们已将 ID 为 4026533490 的命名空间与位于 /var/run/netns/child 的文件相关联,并且该命名空间现在是持久的。现在我们需要找到一种方法让主机和子网络命名空间相互通信。为此,我们将在主机网络命名空间中创建一对虚拟以太网设备:

在此命令中,我们创建了一个名为 veth0 的虚拟以太网设备,而该对设备的另一端称为 veth1。我们可以通过运行来验证这一点:[netns: HOST]# ip link | grep veth35: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 100036: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 mode DEFAULTDOWNc group default qlen 1000 目前,这两个设备都存在于主机命名空间中。如果我们在子网络命名空间中运行 ip link,它将只显示之前的环回地址: [netns: CHILD]# ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 那么我们怎样才能让 veth 设备之一出现在子命名空间中呢?为此,我们将在主机网络命名空间中运行以下命令,因为这是当前存在 veth 设备的位置: 这里我们指示将 veth1 网络设备分配给命名空间子级。查看此命名空间中的 ip link 将不再显示 veth1 设备:[netns: HOST]# ip link | grep veth36: veth0@if35: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000

[netns: CHILD]# ip 链接 | grep veth35: veth1@if36: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 我们还有两个步骤才能让它们相互通信,即为每个veth分配一个IP地址设备并将状态设置为 up。所以让我们快速完成:[netns: HOST]# ip address add dev veth0 local 10.16.8.1/24[netns: HOST]# ip link set veth0 up [netns: HOST]# ip address | grep veth -A 536: veth0@if35: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000 link/ether 32:c7:79:c7:e2:e0:brd ff: ff:ff:ff:ff link-netns child inet 10.16.8.1/24 scope global veth0 valid_lft永远首选_lft永远[netns:CHILD]#ip address add dev veth1 local 10.16.8.2/24[netns: CHILD]#ip link set veth1 up [netns: CHILD]# ip 地址 | grep veth -A 535: veth1@if36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 5a:62:dd:40:a6:f1 brd ff:ff:ff: ff:ff:ff 链接-netnsid 0 inet 10.16.8.2/24 范围全局 veth1 valid_lft 永远首选_lft 永远 inet6 fe80::5862:ddff:fe40:a6f1/64 范围链接 valid_lft 永远首选_lft 永远 [netns: HOST]# ping.10. 8.2PING 10.16.8.2 (10.16.8.2) 56(84) 字节数据.64 字节来自 10.16.8.2: icmp_seq=1 ttl=64 time=0.086 ms64 字节来自 10.16.8.2: icmp_seq=049来自 10.16.8.2 的 ms64 字节:icmp_seq=3 ttl=64 time=0.100 ms

[netns: CHILD]# ping 10.16.8.1PING 10.16.8.1 (10.16.8.1) 56(84) bytes of data.64 bytes from 10.16.8.1: icmp_seq=1 ttl=64 time=0.057 ms.014 bytes from. icmp_seq=2 ttl=64 time=0.090 ms64 bytes from 10.16.8.1: icmp_seq=3 ttl=64 time=0.118 ms 瞧!我们做到了!我希望这有助于更好地理解命名空间。我们上面所做的事情可以通过两个孩子用由锡罐和长绳组成的绳子电话互相交谈的图像来最好地描述。在这张图片中,孩子们可以被认为是命名空间,而锡罐类似于我们创建并用于发送和接收网络流量的虚拟以太网设备。接下来是 cgroups。它们帮助我们控制进程可以消耗的资源量。最好的例子是 CPU 和内存。这样做的最佳用例是避免进程意外使用所有可用的 CPU 或内存并阻止整个系统执行任何其他操作。 cgroup 位于 /sys/fs/cgroup 目录下。我们来看一下内容: # ls /sys/fs/cgroup/ -lhtotal 0dr-xr-xr-x 5 root root 0 Feb 17 01:05 blkiolrwxrwxrwx 1 root root 11 Feb 17 01:05 cpu -> cpu ,cpuacctlrwxrwxrwx 1 root root 11 Feb 17 01:05 cpuacct -> cpu,cpuacctdr-xr-xr-x 5 root root 0 Feb 17 01:05 cpu,cpuacctdr-xr-xr-x 17 root 01 Feb 0 cpusetdr-xr-xr-x 5 root root 0 Feb 17 01:05 devicesdr-xr-xr-x 2 root root 0 Feb 17 01:05 freezerdr-xr-xr-x 2 root root 0 Feb 17 01:05 Hugetlbdr- xr-xr-x 9 root root 0 Feb 20 00:24 memorylrwxrwxrwx 1 root root 16 Feb 17 01:05 net_cls -> net_cls,net_priodr-xr-xr-x 2 root root 0 Feb 17 01:05 root_cls, root 16 Feb 17 01:05 net_prio -> net_cls,net_priodr-xr-xr-x 2 root root 0 Feb 17 01:05 perf_eventdr-xr-xr-x 5 root root 0 Feb 17 01:05 pidsdr-xr-xr- x 2 root root 0 Feb 17 01:05 rdmadr-xr-xr-x 5 root root 0 Feb 17 01:05 systemddr-xr-xr-x 5 root root 0 Feb 17 01:06 统一 每个目录都是一个资源,它的用途可以控制。要创建一个新的 cgroup,我们需要在这些资源之一中创建一个新目录。例如,如果我们打算创建一个新的 cgroup 来控制内存使用,我们将在 /sys/fs/cgroups/memory 路径下创建一个新目录(名称由我们决定)。所以让我们这样做: 让我们看看这个目录的内部。如果您在想为什么要麻烦因为我们刚刚创建了目录并且它应该是空的,请继续阅读:# ls -lh /sys/fs/cgroup/memory/demo/total 0-rw-r--r-- 1 root root 0 Feb 24 12:29 cgroup.clone_children--w--w--w- 1 root root 0 Feb 24 12:29 cgroup.event_control-rw-r--r-- 1 root root 0 Feb 24 12:29 cgroup.procs-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.failcnt--w------- 1 root root 0 Feb 24 12:29 memory.force_empty-rw-r --r-- 1 root root 0 Feb 24 12:29 memory.kmem.failcnt-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.limit_in_bytes-rw-r--r- - 1 root root 0 Feb 24 12:29 memory.kmem.max_usage_in_bytes-r--r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.slabinfo-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.failcnt-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.limit_in_bytes-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.max_usage_in_bytes-r--r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.usage_in_bytes-r--r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.usage_in_bytes- rw-r--r-- 1 root root 0 Feb 24 12:29 memory.limit_in_bytes-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.max_usage_in_bytes-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.failcnt-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.limit_in_bytes-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.max_usage_in_bytes-r--r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.usage_in_bytes-rw-r--r-- 1 root root 0 Feb 24 12 :29 memory.move_charge_at_immigrate-r--r--r-- 1 root root 0 Feb 24 12:29 memory.numa_stat-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.oom_control-- -------- 1 root root 0 Feb 24 12:29 memory.pressure_level-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.soft_limit_in_bytes-r--r--r- - 1 root root 0 Feb 24 12:29 memory.stat-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.swappiness-r--r--r-- 1 root root 0 Feb 24 12:29 memory.usage_in_bytes-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.use_hierarchy-rw-r--r-- 1 root root 0 Feb 24 12:29 notify_on_release-rw-r --r-- 1 root root 0 Feb 24 12:29 任务

原来操作系统为每个新目录创建了一大堆文件。让我们看一看其中一个文件:这个文件中的值表示一个进程可以使用的最大内存,如果它是这个 cgroup 的一部分。让我们将此值设置为一个小得多的数字,例如 4MB,但以字节为单位:虽然这不是我们写入文件的确切内容,但它大约为 3.99 MB。我的猜测是这与由操作系统管理的内存对齐有关。我暂时没有进一步研究这个。 (如果你知道答案,请告诉我!)这将启动一个新的 shell 进程。让我们尝试运行一个命令,比如 wget,我知道它需要超过 4MB 的内存才能运行:# wget wikipedia.orgURL 由于 HSTS 策略而转换为 HTTPS--2020-02-24 12:36:58-- https: //wikipedia.org/加载的CA证书'/etc/ssl/certs/ca-certificates.crt'Resolving wikipedia.org (wikipedia.org)... 103.102.166.224, 2001:df2:e500:ed1a::1Connecting to wikipedia.org (wikipedia.org)|103.102.166.224|:443...已连接。HTTP 请求已发送,正在等待响应... 301 已永久移动位置:https://www.wikipedia.org/ [以下]--2020- 02-24 12:36:58-- https://www.wikipedia.org/Resolving www.wikipedia.org (www.wikipedia.org)... 103.102.166.224, 2001:df2:e500:ed1a::1Connecting到 www.wikipedia.org (www.wikipedia.org)|103.102.166.224|:443...已连接。HTTP 请求已发送,正在等待响应... 200 OKLength: 76776 (75K) [text/html]正在保存到:' index.html'index.html 100%[==================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ====>] 74.98K 362KB/s 在 0.2s2020-02-24 12:36:59 (362 KB/s) - 'index.html' 保存 [76776/7 6776] 现在我们注意到该命令有效。这是因为这个进程是默认 cgroup 的一部分。为了使它成为新 cgroup 的一部分,我们需要将这个进程的 PID 写入 cgroup.procs 文件:这里似乎有两个条目。第一个条目是我们写入文件的 shell 进程的 PID。另一个是我们运行的cat进程的PID。这是因为默认情况下,所有子进程都与父进程属于同一个 cgroup。一旦进程终止,PID 会自动从文件中删除。如果我们再次运行相同的命令,我们仍然会找到两个条目,但第二个会有所不同:

该进程立即被杀死,因为它试图使用比当前允许的 cgroup 更多的内存。我会说很整洁。因此,虽然命名空间和 cgroup 允许隔离和控制资源的使用并形成通常称为容器的抽象核心,但还有两个概念可用于进一步增强隔离: 功能:它限制了 root 权限的使用。有时我们需要运行需要提升权限才能做一件事的进程,但以 root 身份运行它是一种安全风险,因为这样该进程几乎可以对系统执行任何操作。为了限制这种情况,功能提供了一种分配特殊权限的方法,而无需向进程授予系统范围的 root 权限。一个例子是,如果我们需要一个程序来管理网络接口和相关操作,我们可以授予该程序CAP_NET_ADMIN 能力。 Seccomp:它限制了系统调用的使用。为了进一步降低安全性,可以使用它们来阻止可能造成额外伤害的系统调用。例如,阻止 kill syscall 将阻止进程终止或向其他进程发送信号。所以虽然......