Kubernetes实战(第二版)----第2章 理解容器(续4)

2.3 理解:什么使容器成为可能

您应该在本地计算机上使容器保持运行,以便在以下练习中使用它,在这些练习中,将研究容器如何在不使用虚拟机的情况下实现进程隔离。Linux内核的几个特性使这成为可能,现在是了解它们的时候了。

2.3.1 使用Namespaces定制进程环境

第一个特性称为Linux命名空间( Linux Namespaces ),它确保每个进程都有自己的系统视图。这意味着在容器中运行的进程将只看到系统上的一些文件、进程和网络接口,以及不同的系统主机名,就像它在单独的虚拟机中运行一样。

默认情况下,Linux OS中可用的所有系统资源,如文件系统、进程id、用户id、网络接口和其他资源,都在所有进程可看到和使用的相同bucket(桶)中。但是内核允许创建称为命名空间的额外bucket,并将资源移入其中,以便将它们组织成较小的资源集合。可以使每个资源集合仅对一个进程或一组进程可见。创建新进程时,可以指定它应该使用的命名空间。此时进程只看到这个命名空间中的资源,而看不到其他命名空间中的资源。

引入可用的命名空间类型

更具体地说,并不是只有单一类型的命名空间。实际上有几种类型—每种命名空间对应一种资源类型。因此,进程不仅使用一个命名空间,而且对每种类型都使用一个命名空间。

命名空间存在以下类型:

  • 挂载命名空间(mnt)隔离挂载点(文件系统)。

  • 进程ID命名空间(pid)隔离进程ID。

  • 网络命名空间(net)隔离网络设备、堆栈、端口等。

  • 进程间通信命名空间(ipc)隔离进程间的通信(这包括隔离消息队列、共享内存和其他)。

  • UNIX分时系统(UTS)命名空间隔离系统主机名和网络信息服务(NIS)域名。

  • 用户ID命名空间(User)隔离用户和组ID。

  • Cgroup命名空间隔离控制组的根目录。你将在本章后面了解cgroups。

使用网络命名空间为进程提供一组专用的网络接口

进程运行的网络命名空间决定了进程可以看到哪些网络接口。每个网络接口完全属于一个命名空间,但是可以从一个命名空间移动到另一个命名空间。如果每个容器使用自己的网络命名空间,那么每个容器将看到自己的一组网络接口。

查看图2.13,了解如何使用网络命名空间创建容器。假设您希望运行一个容器化的进程,并为其提供一组专用的网络接口,该网络接口只能由该进程使用。

Kubernetes实战(第二版)----第2章 理解容器(续4)

图2.13 网络命名空间限制了进程使用的网络接口

最初,只有默认的网络命名空间存在。然后为容器创建两个新的网络接口和一个新的网络命名空间。然后可以将网络接口从默认命名空间移到新的命名空间。一旦到了那里,就可以重新命名它们,因为名称必须在每个命名空间中唯一。最后,进程可以在这个网络命名空间中启动,这允许它只看到为它创建的两个网络接口。

如果只查看可用的网络接口,进程无法分辨它是在容器、VM还是直接运行在裸机上的操作系统中。

使用UTS命名空间为进程提供专用主机名

要使进程看起来像在自己的主机上运行,另一个示例是使用UTS命名空间。它决定了在此命名空间内运行的进程所看到的主机名和域名。通过给两个不同的进程分配两个不同的UTS命名空间,可以使它们看到不同的系统主机名。对于这两个进程来说,它们似乎运行在两台不同的计算机上。

理解命名空间如何隔离进程

通过为所有可用的命名空间类型创建一个专用的命名空间实例并将其分配给进程,可以使进程相信它在自己的操作系统中运行。这样做的主要原因是每个进程都有自己的环境。进程只能在自己的命名空间中查看和使用资源。它不能在其他命名空间中使用。同样地,其他进程也不能使用它的资源。容器就是这样隔离在其中运行的进程的环境的。

在多个进程之间共享命名空间

在下一章中,您将了解到,并不总是希望将容器彼此完全隔离。相关容器可能想要共享某些资源。下图显示了共享相同网络接口、主机和系统域名(但不共享文件系统)的两个进程的示例。

Kubernetes实战(第二版)----第2章 理解容器(续4)

图2.14 每个进程都与多个命名空间类型相关联,其中一些可以共享。

首先关注共享的网络设备。这两个进程看到并使用相同的两个设备(eth0和lo),因为它们使用相同的网络命名空间。这允许它们绑定到相同的IP地址并通过环回设备进行通信,就像它们在不使用容器的机器上运行时一样。这两个进程还使用相同的UTS命名空间,因此看到相同的系统主机名。但是,它们各自使用自己的挂载命名空间,这意味着它们有各自的文件系统。

总之,进程可能希望共享某些资源,而不是共享所有资源。这是可能的,因为存在不同的命名空间类型。进程为每种类型都有一个关联的命名空间。

考虑到这些,有人可能会问,容器到底是什么?一个“在容器中”运行的进程不会运行在一个类似于虚拟机的实际封闭环境中。它只是一个进程,为它分配了七个命名空间(每个命名空间对应一个类型)。有些命名空间与其他进程共享,而有些命名空间则不共享。这意味着进程之间的边界并不都在同一条线上。

在后面的章节中,您将了解如何通过使用现有容器的网络命名空间直接在主机操作系统上运行新进程来调试容器,同时对其他所有内容使用主机的默认命名空间。这将允许您使用主机上可用的工具来调试容器的网络系统,而这些工具在容器中可能不可用。

2.3.2 探索正在运行的容器的环境

如果您想看看容器内的环境是什么样子的呢?系统主机名是什么,本地IP地址是什么,文件系统上有哪些二进制文件和库可用,等等?

要在VM的情况下研究这些特性,通常需要通过ssh远程连接到VM,并使用shell执行命令。这个过程与容器非常相似。您在容器中运行一个shell。

提示:shell的可执行文件必须在容器的文件系统中可用。在生产环境中运行的容器并不总是如此。

在现有容器中运行shell

你的镜像所基于的Node.js镜像提供了bash shell,这意味着你可以在容器中运行以下命令:

 

$ docker exec -it kubia-container bash
#这是shell的命令提示符
[email protected]:/#

这个命令在kubia-container容器中作为附加进程运行bash。该进程与主容器进程(运行的Node.js服务器)具有相同的Linux命名空间。通过这种方式,您可以从内部探索容器,查看Node.js和您的应用程序在容器中运行时是如何查看系统的。-it选项是两个选项的简写:

  • -i 告诉Docker在交互模式下运行命令。

  • -t 告诉Docker分配一个伪终端(TTY),这样您就可以正确地使用shell。

如果您想按照您习惯的方式使用shell,则需要这两个选项。如果省略第一个,就不能执行任何命令,如果省略第二个,就不会出现命令提示符,一些命令可能会抱怨没有设置TERM变量。

列出容器中正在运行的进程

让我们通过在容器中运行的shell中执行ps aux命令来列出在容器中运行的进程。下面的清单显示了该命令的输出。

 

Listing 2.8 Listing processes running in the container
[email protected]:/# ps aux
USER PID %CPU %MEM   VSZ   RSS TTY STAT START TIME COMMAND
root    1  0.0  0.1 676380 16504 ?   Sl   12:31 0:00 node app.js
root   10  0.0  0.0  20216  1924 ?   Ss   12:31 0:00 bash
root   19  0.0  0.0  17492  1136 ?   R+   12:38 0:00 ps aux

该列表只显示了三个进程。它们是在容器中运行的仅有的三个进程。您无法看到在主机OS或其他容器中运行的其他进程,因为容器在自己的进程ID命名空间中运行。

在主机进程列表中查看容器进程

如果您现在打开另一个终端并列出主机OS本身中的进程,您还将看到在容器中运行的进程。这确认了容器中的进程实际上是在主机OS中运行的常规进程,如下面的清单所示。

 

#清单2.9 容器的进程在主机操作系统中运行
$ ps aux | grep app.js
USER PID %CPU %MEM   VSZ   RSS TTY STAT START TIME COMMAND
root  382  0.0  0.1 676380 16504 ?   Sl   12:31 0:00 node app.js

提示:如果使用macOS或Windows,则必须列出驻留Docker守护进程的VM中的进程,因为容器在那里运行。在Docker Desktop中,可以使用以下命令进入VM:

 

docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine chroot /host

如果您有敏锐的眼光,您可能会注意到容器中的进程id与主机上的不同。因为容器使用自己的进程ID命名空间,所以它有自己的进程树和自己的ID号序列。如图所示,树是主机的完整进程树的一个子树。因此,每个进程都有两个id。

Kubernetes实战(第二版)----第2章 理解容器(续4)

图2.15  PID命名空间使进程子树显示为具有自己编号序列的单独进程树

容器的文件系统与主机和其他容器隔离

与隔离的进程树一样,每个容器也有一个隔离的文件系统。如果列出容器根目录的内容,则只显示容器中的文件。这包括来自容器映像的文件和在容器操作期间创建的文件,比如日志文件。下一个清单显示了kubia container中的文件。

 

#清单2.10容器有自己的文件系统
[email protected]:/# ls /
app.js boot etc   lib   media opt   root sbin sys usr
bin     dev   home lib64 mnt   proc run   srv   tmp var

它包含app.js文件和其他系统目录,它们是node:12基本镜像的一部分。欢迎您浏览容器的文件系统。您将看到,无法从主机的文件系统查看文件。这很好,因为它阻止了潜在的攻击者通过Node.js服务器中的漏洞访问它们。

要离开容器,请通过运行exit命令或按Control-D离开shell,您将返回到您的主机计算机(类似于从ssh会话中注销)。

提示:在调试容器中运行的应用程序时,像这样进入一个正在运行的容器非常有用。当出现故障时,首先要调查的是应用程序看到的系统的实际状态。

2.3.3 通过Linux控制组(Linux Control Groups)限制进程的资源

Linux命名空间使得进程可以只访问主机的一些资源,但是它们没有限制每个进程可以消耗多少单个资源。例如,可以使用命名空间只允许进程访问特定的网络接口,但是不能限制进程消耗的网络带宽。同样,也不能使用命名空间来限制进程可用的CPU时间或内存。您可能希望这样做,以防止某个进程消耗所有CPU时间,影响关键的系统进程正常运行。为此,我们需要Linux内核的另一个特性。

(未完待续......)  欢迎关注公众号,及时获得最新翻译内容:

Kubernetes实战(第二版)----第2章 理解容器(续4)