kubernetes in action 第二版----第二章翻译
2
理解容器
本章涵盖
- 理解容器是什么
- 容器与虚拟机的区别
- 创建,运行,在Docker中共享容器镜像
- 使容器成为可能的Linux内核特性
Kubernetes主要管理运行在容器中的应用 – 所有在你开始探索Kubernetes之前,你需要对容器是什么有很好的理解。这一章解释了典型Kubernetes用户需要知道的Linux容器的基础知识。
2.1 容器介绍
在第一章你了解了在同一个操作系统中运行不同的微服务可能需求也不一样,可能需要的动态库版本有冲突或者有不同的环境要求。
当一个系统由许多小的应用组成时,可以为每一个应用分配一个专用的虚拟机,让每个应用在自己的操作系统中运行。但是随着微服务变得更小,微服务的数量开始增长,如果你想保持较低的硬件开支并且不想浪费资源。那么你可能无法为每一个微服务分配一个虚拟机。
不仅仅是浪费硬件资源,每个虚拟机需要单独配置和管理,意味着运行更多数量的虚拟机的同时也需要更多的高级配置人员,以及对更好、通常更复杂的自动化系统的需求。由于向微服务架构的转变,系统由数百个已部署的应用程序实例组成,因此需要虚拟机的替代方案。容器是另一种选择。
2.1.1容器与虚拟机的比较
现在大多数开发和运维团队更愿意使用容器,而不是用虚拟机隔离单个微服务的环境(或一般的软件进程)。容器允许你在同一台主机上允许多个服务,同时还让他们保持隔离。像虚拟机一样,但开销少的多。
每个虚拟机都运行一个带有多个系统进程的独立操作系统,与之不同的是,在容器中运行的进程在现有的主机操作系统中运行。因为仅有一个操作系统,所以没有重复的系统进程存在。虽然所有的应用进程运行在同样的操作系统上,但它们的环境是隔离的,尽管分离性没有虚拟机好。对于容器中的进程,隔离使进程看起来在计算机上没有其他进程存在。接下来的几节你将了解这是怎么实现的,但是首先让我们更深入的看看容器和虚拟机的区别。
比较容器和虚拟机的开销
与虚拟机相比,容器更轻量级,因为容器不要求分离的资源池或额外的操作系统级别进程。而每个虚拟机在它自己的系统进程运行,除了用户应用消耗的进程外还要求额外的计算资源,容器仅仅是一个运行在主机操作系统上的隔离的进程,仅消耗应用所需要消耗的资源。它们几乎没有开销。
图2.1 展示两台裸机计算机,一台运行2个虚拟机,另外一台运行容器。后者有空间容纳容器,它只允许一个操作系统,而第一台允许了3个操作系统-一个主机和两台客机。

图2.1 用虚拟机隔离应用组vs用容器隔离多个app
由于vm的资源开销,您经常将多个应用程序分组到每个应用程序中VM。你不能为每个应用分配完成的虚拟机。但是容器没有引入额外开销,这意味着你可以为每个应用创建分离的容器。实际上,你不应该在同一个容器能运行多个应用,这样做使得容器中的进程管理变得更困难。此外,所有处理容器的现有软件,包括Kubernetes本身,都是在容器中只有一个应用程序的前提下设计的。
比较容器和虚拟机的启动时间
除了更低地运行时开销外,容器同时启动地更快,因为仅有应用进程需要启动。没有额外地系统进程需要先启动,就像启动一个新地虚拟机一样。
比较容器和虚拟机地隔离性
您会同意容器在使用资源方面明显更好,但是它也有缺点。当你在虚拟机中运行进程,每个虚拟机运行自己地操作系统和内核。在VM之下是管理程序(可能是个额外地操作系统),它把物理硬件资源分割成小的虚拟资源集合,每个虚拟机中地操作系统能使用这些虚拟资源。如图2.2所示,在这些虚拟机中运行的应用程序对客户机操作系统内核进行系统调用(sys-calls)VM,然后内核在虚拟CPU上执行的机器指令通过管理程序转发到主机的物理CPU。

图2.2 运行在虚拟机中vs容器中的apps如何使用硬件
注意 存在两种类型的管理程序。管理程序类型一不要运行在主机操作系统,而类型二运行在主机操作系统上。
另一方面,容器,在运行在主机操作系统上单内核上做系统调用。这个内核是唯一在主机CPU上执行指令的内核。CPU不需要像处理vm那样处理任何类型的虚拟化。
请查看图2.3,了解在裸机上运行三个应用程序、在两个独立的虚拟机中运行它们或在三个容器中运行它们之间的区别。

图2.3 在裸机,虚拟机,容器上运行应用的区别
第一个用例中,3个应用使用同一个内核根本没有隔离.在第二个用例中,应用A和B运行在同一个虚拟机上,因此功效内核,而应用C与A和B完全隔离,所有C使用自己的内核。C仅与AB共享硬件.
第三个用例展示运行在容器中的3个应用。虽然它们使用通用的内核,但是它们彼此完全隔离,根本意识不到彼此的存在。隔离性由内核提供。每个应用只能看到物理硬件的一部分并且认为自己是运行在操作系统上的唯一进程,虽然它们都运行在同一个操作系统上。
理解隔离的安全含义
跟容器相比,使用虚拟机的主要优势是它提供的完全隔离,因为每个虚拟机由自己的内核,而容器使用同样的内核。这显然会带来安全风险。如果内核有个bug,一个容器中的应用可以利用bug读取运行在其他容器中应用的内存。如果apps运行在不同的虚拟机并且仅共享硬件,发送上述工具的可能性小很多。当然,完全隔离只能通过在不同的物理机上运行应用获得。
此外,容器共享内存空间,而每个虚拟机使用自己的内存块。因此,如果你不限制一个容器能使用的内存量,这可能导致其他容器无内存可用或者导致其他容器的数据被换出至内存。
注意 在Kubernetes中不可能发生swap,因为它要求在所有的节点上禁用swap
理解容器如何启用,虚拟机如何启用
虚拟机是通过CPU中的虚拟化支持和主机上的虚拟化软件来启用的,而容器是由Linux内核本身启用的,你将在稍后亲自尝试容器技术时了解它们。为此,您需要安装Docker,因此让我们了解它如何适合容器故事。
2.1.2 介绍Docker容器平台
容器技术已经存在了很长时间,当Docker出现之后才广为人知。Docker是第一个可以很容易的在不同计算机之间移植的容器系统。它简化了应用的打包,应用的所有库和依赖-甚至是整个操作系统文件系统打包进一个可移植的包,该包可以被部署在任何运行了Docker的计算机上。
介绍容器,镜像与注册中心
Docker是一个打包,发布与运行应用的平台,就像之前提到的,它允许你将应用及其依赖的环境整个打包。这可以是应用程序所需的几个动态链接库,也可以是操作系统通常附带的所有文件。Docker允许通过公共仓库把包分发给其他启用了Docker的计算机。

图2.4 3个主要的Docker概念是镜像,注册与容器
图2.4显示了在我刚刚描述的过程中出现的三个主要Docker概念。
下面是它们各自是什么:
- 镜像-容器镜像是将应用程序及其环境打包到其中的东西。像一个压缩文件。它包含应用将使用的整个文件系统已经额外的元数据,比如当镜像执行的时候,可执行文件运行的路径,应用监听的端口,和其他关于镜像的信息。
- 注册-注册中心是容器镜像的存储库,它允许在不同的人和计算机之间交换镜像。在你构建完你的镜像后,你可以在相同的计算机上运行,也可以把惊醒推(上传)到注册中心,然后再另一台计算书上拉(下载)下来。一些注册中心是公共的,允许任何人从它上面拉取镜像,而其他的是私有的,仅允许通过授权的个人、组织或计算机访问。
- 容器-容器是容器镜像的实例化。运行的容器是主机操作系统上的一个进程,但是它的环境与主机上的其他进程是隔离的。容器的文件系统源自容器镜像,但也可以将其他文件系统挂载到容器中。容器通常是受资源限制的,这意味着它只能访问和使用分配给它的资源,比如CPU和内存
构建,分发,运行容器镜像
为了理解容器,镜像,注册中心如何产生联系,让我们看看如何构建一个容器镜像,通过注册中心发布镜像与如何使用镜像创建运行的容器。图2.5到2.7展示了这三个过程。

图2.5 构建一个容器镜像
如图2.5,开发者先构建一个镜像,然后把它推到注册中心,如图2.6.镜像对任何能访问注册中心的人可用。

图2.6 上传容器镜像到注册中心
如图2.7,另外一个人限制能从任何运行了Docker的计算机中拉取镜像然后运行它。Docker基于镜像创建一个隔离的容器,并调用镜像中指定的可执行文件。

图2.7 在不同的计算机上运行容器
由于应用程序的环境与主机环境解耦,因此可以在任何计算机上运行应用程序。
理解应用能看到的环境
当你在容器中运行应用程序,它看的是你到打包进容器文件内容,同样的你也可以把其他的文件系统挂载进容器。无论应用程序是在笔记本电脑上还是在成熟的生产服务器上运行,它都会看到相同的文件,即使生产服务器使用完全不同的Linux发行版。应用没有访问主机系统上的文件,所有服务器上是否安装了和你开发机上不同的库集合,没有影响。
比如,如果你用整个Red Hat Enterprise Linux(RHEL)操作系统中的文件打包你的应用然后运行它,应用将想它运行在RHEL内部,不管你运行的是基于Fedora还是基于Debian系统的计算机。与主机上安装的Linux发行版无关。仅有的重要的事情可能是内核版本以及内核加载的模块。后面我会解释为什么。
与通过VM镜像创建新的VM类似,先安装操作系统然后在它上面安装应用,之后分发整个VM镜像,其他人可用在不同的主机上运行它。Docker实现了同样的效果,但它没有使用vm来隔离应用程序,而是使用Linux容器技术来实现(几乎)相同级别的隔离。
理解镜像分层
虚拟机镜像是安装在VM中的操作系统所需的整个文件系统的大块,与之不同的是,容器镜像由通常小得多的层组成。这些层可用跨镜像共享和重用。这意味着,如果镜像的其余部分已经作为包含相同层的另一个镜像的一部分下载到主机,则只需要下载镜像的某些层。
分层使镜像分布非常有效,同时也有助于减少图像的存储空间。Docker每层只存储一次。如图2.8,从包含相同层的两个镜像创建的两个容器使用相同的文件

图2.8 容器可用共享多个镜像层
图展示了镜像A和B共享一个镜像层,这意味着应用A和B读一些同样的文件。此外,它们也与C共享底层。但是如果三个容器访问同样的文件,那它们彼此怎么能做到完全隔离?A对已存储的与B共享文件做改变,对于应用B是否可见?它们部署。这是为什么。
文件系统通过写时复制机制实现隔离。容器的文件系统由来自容器镜像的只读层和一个额外的读/写层组成。当一个运行在A容器中的应用改变只读层的文件,整个文件拷贝进容器的读/写层,文件内容在这层改变。因为每个容器由它自己的可写层,改变共享文件对其他容器来说是不可见的。
当前删除一个文件,它仅在读/写层标记为删除,但它仍然存在于下面的一个或多个层中。接下来的是,删除文件永远不会减少层的大小。
警告:即使是看似无害的操作,如更改文件的权限或所有权,也会导致在读/写层创建整个文件的新副本。如果对一个大文件或多个文件执行此类操作,则镜像大小可能会显著膨胀。
理解容器镜像的移植限制
理论上,基于Docker镜像可用运行在任何其他运行了Docker的Linux计算机上,但是有一点需要注意,因为容器没有它自己的内核。如果一个应用容器要求指定版本的内核,那它不可能在每个计算机上都能工作,如果一个计算机运行不同版本的Linux内核或没有加载需要的内核模块,app就不能在上面运行。这个场景如图描述。
图2.9 如果一个容器需要内核的指定特性或模块,那么不能再任何地方运行
容器B需要指定的内核模块才能运行。这个模块再第一个计算机的内核中加载了,但是没有再第二个种加载。你可以在第二个计算机种运行你的应用,但是它可能失败当它取访问确实的模块。
不仅仅是内核和内核模块。同样要明确,如果容器app在特定的硬件架构商构建的,那么它也只能在特定的架构上运行。你不可能仅仅是因为ARM架构的机器上安装了Docker就期望一个在X86Cpu架构上编译的应用镜像能在基于ARM的计算机上运行。这种情况你需要一个能仿真x86架构的虚拟机。
2.1.3 介绍Docker的替代和开放容器计划
Docker的第一个使容器变成主要的容器平台。我希望我以及解释清楚了Docker本身没有提供进程隔离。容器的实际隔离是在Linux内核级别使用它提供的机制进行的。Docker仅仅是使得那些机制更易用同时运行你把容器镜像发布给不同得主机。
介绍容器开放计划
在Docker成功之后,围绕着容器格式和运行时诞生了开放容器计划。Docker是该计划的一部分,其他容器运行时和许多对容器技术感兴趣的组织也是如此。
OCI成员创建了OCI镜像格式规范(OCI Image Format Specification)和OCI运行时规范(OCI Runtime Specification),前者规定了容器镜像的标准格式,后者定义了容器运行时的标准接口,目的是标准化容器的创建、配置和执行
介绍容器运行时接口(cri)及其实现(cri - o)
这本书的重点是使用Docker作为Kubernetes的容器运行时,因为它最初是Kubernetes唯一支持的,并且仍然是最广泛使用的。但是Kubernetes现在通过容器运行时接口(container Runtime Interface, CRI)支持许多其他容器运行时。
CRI的一种实现是CRI- o,它是Docker的轻量级替代品,允许您在Kubernetes中利用任何oci兼容的容器运行时。兼容ocic1的运行时示例包括rkt(发音为Rocket)、runC和Kata Containers。
2.2 探索容器
对容器是什么你已经有感觉了,但是我还没有解释它们真没工作的,在我们解释之前,你需要创建一个应用,把它打包进容器镜像然后运行它,你需要Dokcer,所有安装Dokcer然后在容器种运行Hello world先。
2.2.1 安装镜像和运行Hello World容器
理想的,你直接在Linux计算机上安装Docker,这样你就不用处理运行在你主机上的虚拟机里面运行容器的额外复杂性。但是,如果你正在用macOS或者Windows并且不知道如何取安装虚拟机,Docker桌面应用将为你创建。你将用安装在你主机上的Docker工具运行容器,但是Docker daemon将运行在VM中,就像它创建的其他容器一样。
Docker平台由许多组件组成,但你仅仅需要安装用于运行容器的引擎。如果你用macOS或者Windows,安装Docker桌面。详细安装细节见docs.docker.com/install.
在容器中运行HELLO World
注意 Windows桌面Docker可以运行Window或Linux容器。确认你是用Linux容器做的配置。
安装完成后,用docker CLI工具运行Docker命令。首先,尝试从Docker Hub拉取并运行镜像,公共镜像注册中心包含许多知名的随时可用的容器镜像软件包。其中之一是busybox镜像,你将在你的第一个容器中运行简单的回声“Hello world“命令。
如果你不熟悉busybox,它是一个混合了许多UNIX标准命令行工具的单个可执行文件,比如echo,ls,gzip等等。替代busybox镜像,你也可以用任何其他操作系统容器镜像,如Fedora,Ubuntu,或者其他包含echo可执行文件的镜像。
运行busybox你不需要下载或安装任何东西,通过指定要下载的镜像和要运行镜像的命令,单个docker run命令可以搞定这些事情。
为了运行简单的Hello world容器,执行如下列表中的命令。

这看起来并不令人印象深刻,但请记住,整个“应用程序”是用一个命令下载并执行的,而无需安装该应用程序或其任何依赖项。
在这个用例,app仅仅是一单个可执行文件,但是也可能有包含几十个库与额外文件复杂的难以置信的app,整个安装和运行app的流程是一样的。不那么明显的是运行在容器中的app,与计算机上的其他进程隔离。在接下来的练习你将印证这点。

理解当你运行一个容器的时候发生了什么
图2.10展示了当你执行docker run命令的时候发生了什么

图2.10 在基于busybox容器镜像的容器中运行echo “Hello World”
Docker CLI工具向Docker守护进程发送一个运行容器的指令,Docker守护进程检测busybox镜像是否已经在本机镜像缓存中,如果没有,Docker守护进程从Docker Hub注册中心拉取到本地。
在下载镜像到本地计算机后,Docker守护进程用镜像创建一个容器并且在容器中执行echo命令。命令向标准输出中打印文本,然后进程终止,容器停止。
如果本机运行Linux操作系统,Docker CLI工具与守护进程都运行在该操作系统上。如果本机运行的是macOS或者Windows,守护进程和容器运行在linux虚拟机中。
运行其他镜像
运行其他镜像与运行busybox镜像类似。实际上,经常更简单,正常来说你甚至不用像之前示例中运行echo命令一样指定要执行什么命令。执行的命令应该写在镜像中,但是在运行的时候你可以运行你的命令覆盖容器中的命令。
比如,如果你像运行一个Redis数据库,你可以在http://hub.docker.com或其他公共注册中心上找到镜像名。在Redis的示例中,镜像之一叫redis:alpine,所有你可以通过以下指令运行它docker run redis:alpine
按ctrl+c(或在Mac上运行Command-C命令),可以停止退出容器。
注意 如果你像从不同的注册中心运行一个镜像,除了指定镜像名外,还要指定注册中心。比如,如果你想从Quay.io注册中心运行一个镜像,这是另外一个可公开访问的镜像注册中心,运行命令: docker run quay.io/shome/image.
理解镜像标签
如果你在Docker Hub上搜索Redis镜像,你已经注意到,有许多的标志可选。对于Redis,标签不仅有latest,buster,alpine,也有5.0.7-buster,5.0.7-alpine,还有其他。
Docker允许你以相同的名称拥有相同映像的多个版本或变体,每个变体有唯一的标签。如果你没有为引用的镜像直接指定标签,Docker假设你指定的latest标签。当上传镜像的一个新版本,镜像作者一般用实际上的版本或latest作为镜像的标签,当你用允许镜像的latest版本,用latest代替版本号。
注意 docker run命令仅拉取以前没有拉取过的镜像。当你首次运行镜像的时候,latest标签确保,确保你获取运行的是最近的版本。从这个时间点开始以后用的就是缓存镜像.
即使是单一版本,也有几个变体。对于Redis,我之前提过5.0.7-buster和5.0.7-alpine。两个都包含同个版本的Redis,但是它们基于不同的基镜像。5.0.7-buster基于Debian版本的”Buster”,而5.0.7-alpine基于Alpine基镜像,一个非常精简的镜像-大小仅5M-它仅包含典型linux发布版的很小一部分安装二进制文件。
为了运行指定版本或进行的变体,要在镜像名上指定标签。比如,运行5.0.7-alpine标签,要执行下面的命令,docker run redis:5.0.7-alpine
2.2.2 创建一个容器化的Node.js web应用
现在你已经有了一个能运行的Docker设置,你将创建一个你将在本书中使用的应用程序。你创建一个小的Node.js网络应用并且将它打包进容器镜像。应用接收HTTP请求并且相应容器运行的计算机的主机名。
这样,您将看到在容器中运行的应用程序看到的是不同的主机名,而不是主机的主机名,即使它像任何其他进程一样在主机上运行。之后这将非常有用,当你部署app在Kubernetes上并且扩展它(水平扩展; 运行app的多个实例)。你将看到HTTP请求,请求到不同的app实例.
App由单个文件app.js组成,文件内容如下。
列表中的代码应该很容易理解。它在端口8080启动一个HTTP服务器。对于每一个请求,它把请求信息写到标准输出并且发送200OK响应状态码与如下文本:
Hey there, this is <server-hostname>. Your IP is <client-IP>.
注意 响应中的主机名是实际上的主机名,而不是客户端发送的请求主机头。在之后这个细节非常重要。
现在,你应该下载并且在本地安装Node.js,直接测试你的app,但是并不一定要这样做。很容把它打包进容器镜像并在容器中运行它。这允许你在任何其他启动Docker同时没有安装Node.js的主机运行app.
2.2.3 创建一个构建容器镜像的Dockerfile文件
为了把你的app打包成一个镜像,首先,你必须创建Dockerfile文件,其包构建镜像时应该执行的指令列表。在app.js文件同一目录创建Dockerfile文件,确保该文件包含下面列表中的3个目录

在容器镜像中定义的FROM行将作为启动点(你在上面构建的基础镜像)。在本例中,你使用node容器镜像,标签为12.在第二行,你从本地目录把app.js添加进镜像根目录,用同样的名字(app.js). 最后,在第3行,你指定当你执行镜像时Docker应该执行的命令。在本例中,命令是node app.js。
—————————————————————————————————————
选择基础镜像
你可能好奇为什么用指定的镜像作为你的基础镜像,因为你的app是一个Node.js应用,你需要你的镜像包含运行app的node二进制文件。你可能已经使用了包含node二进制文件的其他镜像,或者你可能甚至已经使用一个Linux发布基础镜像,比如fedora或者ubuntu,当你构建镜像的时候已经把Node.js安装进了容器。但是因为node镜像已经包含运行Node.js应用所需要的一切,那从头开始构建镜像就没有什么意义。然而,在一些组织,使用特定的基础镜像并且在构建时添加软件可能是强制的。
2.2.4 构建基础镜像
Dockerfile与app.js包含你需要构建的镜像的一切。现在你将使用下面列表的命令构建名为kubia:latest的镜像:

-t选项指定所需的映像名称和标签,末尾的点指定包含Dockerfile和构建上下文(构建过程所需的所有工件)的目录的路径。
当构建过程完成时,新创建的镜像在你本机的进行仓库中变得可用。通过列出本地进行,你可用看到它,就像如下列表。

理解如何构建镜像
图2.11展示构建的过程中发生了什么。你告诉Docker基于当前目录的内容创建名为kubia的奖项。Docker读取目录中的Dockerfile并且基于目录中的文件构建镜像.

图2.11 使用Dockerfile构建一个新的容器镜像
构建本身不是由docker CLI工具执行的。作为替代,真个目录的内容上传给Docker守护进程,然后通过它构建镜像。你已经了解了CLI工具和daemon并不一定要在同一台计算机上。如果你在非Linux系统,比如macOS或者Windows上使用Docker,那客户端是你的主机操作系统,而守护进程运行在Linux虚拟机中。但是守护进程可用运行在远程主机上。
建议 不要添加非必要的文件到构建目录,因为它们回放慢构建速度-特别是如果Docker守护进程在远程主机上。
为了构建镜像,除非本地仓库中已经有了镜像,否则Docker首先从公共镜像仓库(本例中是Docker Hub)拉取基础镜像(node:12)。然后,它从镜像中创建一个新的容器,并从Dockerfile中执行下一个指令。容器最后产生一个有自己ID的新镜像。构建过程继续处理Dockerfile中的剩余目录。每一个创建一个新的镜像。最后阶段打上你在docker build命令中通过-t标志指定的标签。
理解镜像中的层级是什么
几页之前,你学到了镜像由几个层级组成。有人可能会想每个镜像仅仅是由基础镜像的层以及基础镜像之上的一个新层组成,但是实际情况不是这样的。当构建一个镜像的时候,会为Dockerfile中的每个指令创建一个新层。
在构建kubia的过程中,在它拉取了基础镜像的所有层级之后,Docker创建了一个新层并把app.js文件添加进新层。然后它创建了另外一层,该层仅仅当镜像执行时要执行的命令。然后给最后一层打上标签kubia:latest。
你可以通过运行命令docker history查看一个镜像的多个层与每层的大小,如下列表所示.首先打印的是第一层.

你看到的大部分层来自于node:12镜像(也包含镜像本身拥有的基础镜像)。最上面的两层是Dockerfile中的第二和第三个命令(ADD和ENTRYPOINT)。
你能看Created BY列,每一层通过执行容器中的一个命令创建。除了用ADD命令添加文件外,你也可以用Dockerfile中的其他命令。比如,RUN命令在构建过程中执行容器中的一个命令。在上述列表,可以找到一层执行apt-get update和一些其他apt-get命令。Apt-get是Ubuntu包管理器的一部分,其备用于软件包的安装.列表中展示的命令,把一些包安装进镜像的文件系统.
了解在Dockerfile中你能使用的RUN及其他命令,参考Dockerfile的引用https://docs.docker.com/engine/reference/builder/。
建议 每个命令创建一个新层。之前提过当你删除一个文件的时候,仅仅是在新层上标记删除,实际上没有从下层删除。因此,删除有后续命令的文件并不会减小镜像的大小,如果你使用RUN命令,确保该命令在它终止之前,删除了所有它创建的临时文件。
2.2.5 运行容器镜像
当镜像构建准备好后,现在可以用以下命令运行容器:
$ docker run –name kubia-container -p 1234:8080 -d kubia
该命令告诉Docker从kubia镜像运行一个叫kubia-container的新容器。容器与控制台分离(-d标志)并且允许在后台。主机端口1234映射容器端口8080(通过-p 1234:8080选择指定),然后你通过http://localhost:1234访问app.
下图可以帮助你视图化一切是怎么组合起来的。注意Linux虚拟机仅当你使用macOS和Windows存在。如果直接用Linux,没有虚拟机,描述1234端口在本地计算机的边缘。

访问你的APP
现在使用curl或浏览器通过http://localhost:1234访问你的容器:
$curl localhost:1234
注意 如果Docker守护进程运行在不同的机器,你必须用守护进程所在的IP替代localhost。你可以通过DOCKER_HOST环境变量查找IP。
如果所有都对了,你应该可以看到应用程序发送的响应。在我这里,它返回44d76963e8e1做为它的主机名。在你那里,你将看到一个不同的16进制数。它是容器的ID。
列出所有运行的容器
列出你电脑上正在运行的容器,运行如下列表中的命令。为了适应页面展示,命令的输出被编辑过-输出的最后两行是前两行的继续。

对于每个容器,Docker打印它的ID和名称,使用的镜像以及执行的命令。它也展示了容器什么时候创建的,是什么状态,映射容器端口的主机端口。
获取容器的额外信息
Docker ps命令展示了容器的最基本的信息。要看额外信息,你可以使用docker inspect:
$docker inspect kubia-container
Docker打印包含容器许多信息的长的JSON格式的稳定。比如它的状态,配置,网络设置,包括它的地址。
查看应用日志
Docker捕获并且存储应用写至标准输出与标准错误流的所有东西。标准输出和标准错误是写日志的典型位置。你可以使用docker logs命令看输出,就像如下列表。

现在你知道了基础命令与观测容器应用。接下来,你将了解如何去发布它。
2.2.6 分发容器镜像
你刚才构建的镜像只再本地可用。为了在其他计算机上运行它,你首先必须把它推送至外部的镜像注册中心。让我们把它推到公共的Docker Hub注册中心,这样你就不需要设置一个私有注册表。你也可用用其他注册中心,比如我已经提过的Quay.io,或者Google容器注册中心。
在你推送镜像之前,你必须使用Docker Hub的镜像名模式给镜像重新打标签。镜像名必须包含你的Docker Hub编号,当你在http://hub.docker.com上注册的时候选择的编号。在接下来的例子我将用我自己的ID(luksa),但是当你自己尝试命令的时候,你要把ID替换成你自己的ID。
在附加标签下标记镜像
一旦你有你自己的ID,你已经可用为你的镜像添加额外的便签。当前它的名称是kubia,现在你可用给它打上标签yourid/kubia:1.0(用你实际上的Docker Hub ID替换yourid)。这是我使用的命令:
$docker tag kubia luksa/kubia:1.0
再次确认你的镜像现在有列表中的两个名字,就像如下列表。

如你所见,kubia与luska/kubia:1.0指向同样的镜像ID,意外中它们不是两个镜像,而是一个有两个名字的镜像。
把镜像推送至DOCKER HUB
在你推送镜像至Docker Hub之前,你必须用你的user ID登录dockers hub,使用如下的命令
$docker login -u youid -p yourpassword docker.io
登录后,使用以下命令推送yourid/kubia:1.0镜像至Docker Hub:
$ docker push yourid/kubia:1.0
在其他主机上运行镜像
当你把镜像推送到Docker Hub之后,镜像对所有人都可用。你可以在任何启用了Docker的主机上通过以下命令运行镜像:
$docker run -p 1234:8080 -d luska/kubia:1.0
如果容器在你的主机上正确运行,它应该也可用运行在任意其他的Linux计算机上,前提是 Node.js 二进制文件不需要任何特殊的内核功能(它不需要)。
2.2.7 停止删除容器
如果您已经在另一台主机上运行了容器,那么现在可以终止它,因为您只需要本地计算机上的容器就可以进行下面的练习。
停止容器
用如下命令告诉Docker去停止容器: $docker stop kubia-container
该指令向容器中的主进程发送一个终止信号,一边它可用优雅的关闭。如果进程没有响应终止新型号或者没有在指定时间内关闭,Docker将杀死它。当容器中的顶级进程终止时,没有其他进程在容器中运行,因此容器停止。
删除容器
容器不再运行,但它仍然存在。Docker会保存它,以防你决定再次启动它。您可以通过运行docker ps -a查看停止的容器。-a选项打印所有容器——那些正在运行的和那些已经停止的。作为练习,您可以通过运行docker start kubia-container.
您可以安全地删除另一台主机上的容器,因为您不再需要它。删除命令:docker rm: $ docker rm kubia-container
该命令删除容器。容器的所有内容都被移除并且不在可用启动。虽然镜像还在。如果你决定再次创建容器,镜像不需要再次下载. 如果你向删除镜像,使用docker rmi命令:
$docker rmi kubia:latest
2.3 理解是什么使容器变得可能
您应该在本地计算机上保持容器运行,以便可以在以下练习中使用它,在练习中,您将了解没有使用虚拟机的容器如何允许进程隔离。Linux内核的几个特性使这成为可能,现在是了解它们的时候了。
2.3.1 使用命名空间定制进程环境
第一个特性叫Linux命令空间,它确保每个进程有它自己的系统视角。这意味着运行在容器中的的进程将只看到系统上的一些文件,进程和网卡,同样有不同的主机名,就像它运行在分开的虚拟机中。
最初,Linux操作系统上的所有可用系统资源,比如文件系统,进程ID,用户ID,网卡,与其他,所有的进程看到和使用同一个桶里的资源。但是内核允许您创建称为名称空间的其他存储桶,并将资源移动到其中,以便将它们组织成更小的集合。这允许你每个集合只对一个或一个进程组可见。当你创建一个新进程的时候,你可以指定它所用哪个命名空间。这个进程只能看到这个命名空间中的资源,而看不到其他命名空间中的资源。
介绍可用的资源类型
更具体地说,不仅仅存在一个命名空间类型。时间上存在几个类型-每种资源类型一个。因此,一个进程使用不止一个命名空间,而是每种类型一个命名空间。
存在接下类地命名空间类型:
- 挂载命名空间(mnt)隔离挂载点(文件系统)
- 进程ID命名空间(pid)隔离进程ID
- 网络命名空间(net)隔离网络设备,栈,端口等
- 进程交互命名空间隔离进程间地通信(包括隔离消息队列、共享内存与其他)
- UNIX时分共享系统(UTS)命名空间隔离系统主机名与网络信息服务域名。
- User ID命名空间(*user)隔离用户和用户组ID
- Cgroup命名空间隔离根目录控制组,你将在本章后续了解groups
创建网络名称空间,为进程提供一组专用的网络接口
网络命名空间确定一个运行进程能看到那些网卡。每个网络实际上术语一个命名空间,但是可以从一个命名空间移动到另外一个命名空间。如果每个容器使用它自己地网络命名空间,每个容器看到它自己地网卡组。
查看图2.13,更好地了解如何使用网络名称空间创建容器。假设您想运行一个容器化的进程,并为它提供一组专用的网络接口,只有该进程可以使用。

图2.13 网络命名空间限制进程能使用那些网卡
最初,只存在默认地命名空间。然后,你为容器创建了两个新地网络与一个新地网络命名空间。网卡可以从默认地命名空间移动到新的命名空间。一旦移动完成,网卡可以重命名,英文每个命名空间中网卡的名称是唯一的。最后,进程可以启用网络命名空间,进程只能看到为它创建的两个网卡。
单独看可以网卡,进程不知道它是运行在容器中还是在虚拟机或操作系统中,又或者直接运行在裸机上。
使用UTS命名空间为进程赋一个专用的主机名
如何使进程看起来像是在自己的主机上运行的另一个示例是使用UTS名称空间。它决定进程运行的命名空间能看到什么主机名与域名。通过为两个进程赋两个不同的UTS命名空间,你可以让它们看到不同的系统主机名。对于这两个进程,它们就像运行在两台不同的计算机上。
理解命名空间如何隔离进程
通过为所有命令空间类型创建专用的命名空间实例并将其赋给进程,你可以使得进程相信它运行在自己的操作系统里。主要原因是每个进程有它自己的环境。一个进程进能看到和使用他自己的命名空间。它不能使用任何其他的命名空间。与此类似,其他进程也不能看到它的资源。这就是容器隔离在其中运行的进程的环境的方式。
在多个进程间哦共享命名空间
在下一张你将了解到你并不想让容器完全隔离。相关的容器可能想共享特定的资源。下图展示了一个实例,两个进程共享同一的网络接口、主机以及域名系统,但是没有共享文件系统

图2.14 每个进程关联多个命名空间类型,有一些是共享的。
首先聚焦在网络设备的共享。两个进程看到和使用同样的设备引文它们使用同样的网络命名空间。这允许它们绑定到相同的IP地址并通过环回设备进行通信,就像它们在不使用容器的机器上运行一样。两个进程同时也有同样的UTS命名空间,因此,它们看到的是一个主机命名,与此相反,它们使用自己的挂载命名空间,这意味着它们有分离的文件系统。
总之,进程可能想共享一些资源而不共享其他资源。因为分离命名空间类型的存在使之变得可能。一个进程对每种类型都有一个关联的名称空间。
鉴于这一切,有人可能问容器到底是什么?“在容器中”运行的进程并不像运行在真实的虚拟机中。容器仅仅是被赋了7个不同命名空间(每种类型一个)的进程。一些命名空间与其他进程共享,而其他没有。这意味着进程边界并不都在同一条线上。
在后续的章节,在后面的章节中,您将学习如何通过直接在主机操作系统上运行一个新进程来调试容器,但是使用现有容器的网络命名空间,同时使用主机的默认命名空间。这将允许你使用在主机上可用但在容器中不可用的工具调试容器的网络系统。
2.3.2 探索运行容器的环境
如果你想看看容器内部是什么样子呢?系统主机名是什么,本地IP地址是多少,在文件系统上有那些二进制与库可用,诸如此类?
在虚拟机中探索这些特性,一般通过ssh远程连接到虚拟机并且使用shell执行命令。这个过程与容器非常类似。你在容器里运行shell。
注意 shell执行文件必须在容器的文件系统上可用。生产环境上运行的容器并不总是这样。
在容器中运行Shell
Node.js镜像的基础镜像提供了bash shell,这意味着你可用在容器中运行如下的命令
$ docker exec -it kubia-container bash
运行的命令bash作为另外一个进程存在容器kubia-container中.这个进程与主容器进程有同样的linux命名空间(运行Node.js的服务器)。通过这种方式,你可以从容器内部探索并了解Node.js和你的应用在容器中运行时如何看到系统。-it选项是两个选项的缩写:
- -i告诉Docker以交互的方式运行命令
- -t告诉Docker分配一个伪终端(TTY),你可以适当的使用shell
如果你想按照你过去的习惯使用shell,你需要使用两个shell。如果你省略第一个,你不能执行任何命令,如果你省略第二个,命令提示不会出现并且一些命令可能抱怨没有设置TERM变量。
列出容器中运行的进程
通过运行在容器中的shell执行 ps aux命令,列出容器中的所有进程。输入如下列表

列表仅仅展示3个进程。它们是运行在容器中仅有的进程。你看不到运行在主机操作系统或其他容器的其他进程,因为容器运行在它自己的进程ID命名空间。
在主机的进程列表中查看容器进程
如果您现在打开另一个终端并列出主机操作系统本身的进程,您还将看到在容器中运行的进程。这证实了容器中的进程实际上是在主机操作系统中运行的常规进程,如您在下面的清单中看到的那样。

如果有一双锐利的眼睛,一定注意到了容器的进程ID与主机上的进程ID不一样。因为容器用它自己的进程ID命名空间,它有自己的ID编号顺序的进程树。如下图所示,它的进程树是主机全进程数的子树,因此每个进程有两个ID。

图2.15 PID名称空间使进程子树显示为具有自己编号序列的独立进程树
容器的文件系统与主机和其他容器隔离
像隔离进程树一样,每个容器有自己的隔离文件系统。如果你列出容器根目录的内容,仅仅展示容器的文件。

作为node:12基础镜像的一部,它包含app.js文件和其他文件目录。你可以随意浏览容器的文件系统。你将看到你没有办法看到主机的文件系统。这很好,因为它阻止潜在的攻击者,通过Node.js中的漏洞访问主机的文件系统。
离开容器,通过运行shell命令exit命令或按ctrl+D,你将返回到你的主机(类似于从ssh会话中登出)。
建议 当调试容器中的app是,这样进入正在运行的容器非常有用。当出现故障时,你想调查的第一件事情是你的应用看到的实际系统状态。
2.2.3 使用Linux控制组限制进程的资源使用
Linux命名空间使得进程仅访问主机的一部分资源变得可能,但是它们没有限制每个进程能占用多少单个资源。比如,你可以使用命名空间限制进程访问部分网卡,但是你不能限制每个进程消耗多少网络带宽。与此类似,你不能使用命名空间限制进程的CPU时间或可用内存。你可能想这样做,阻止一个进程占用所有的CPU时间并保证关键的系统进程正常运行。为此,我们需要Linux内核的另外一个特性。
介绍CGROUP
第二个使得容器变得可能的Linux内核特性叫Linux控制组(cgroups)。它限制,解释,隔离系统资源,如CPU,内存,硬盘或网络带宽。比如当使用cgroups时,一个进程或进程组只能使用分配给它的CPU时间,内存和网络带宽。通过这种方式,进程不能占用为其他进程保留的资源.
当前,你不需要知道控制组怎么做到这一起的,但是它可能值得了解你怎么请求Docker限制一个容器能使用的CPU和内存的数量。
限制容器的CPU使用
如果你没有给容器能使用的CPU加任何限制,那么它可用无限制的访问主机上的所有CPU核心。你可用直接通过使用Docker的—cpuset-cpus选项指定容器能使用哪些核。比如,运行容器只能用核心1与核心2,可以带如下的选项运行容器:
$ docker run –cpuset-cpus=”1,2” …
你也可以通过使用—cpus,--cpu-period, --cpu-quota与—cpu-shares选项限制CPU的使用时间。比如,运行容器只能使用一般的CPU核心,可以如下这样运行容器:
$ docker run –cpu=”0.5” …
限制容器的内存使用
像CPU一样,一个容器可以是公用系统所有的可用内存,就像任何正常的操作系统进程,但是你可能想限制内存的使用。Docker提供如下的选项限制容器能使用的内存和交换区:--memory, --memory-reservation,--kernel-memory,-memory-swap,和—memory-swappiness.
比如,设置容器最多能使用100M内存,加如下选(m代表兆字节):
$ docker run –memory=”100m” …
在幕后,所有的Docker选项只配置cgroups进程。内核负责限制进程可用的资源。关于更多的内存和CPU限制选项,请参照Docker文档。
2.3.4 加强容器之间的隔离
Linux命名空间与Cgroups分隔容器环境,组织容器占用其提供容器的计算机资源。在容器中的进程使用相同的内核,所有我们不能说它们是真正的隔离。一个流氓容器可能做影响其他容器的恶意系统调用。
想象一个运行了几个容器的Kubernetes节点。每个容器有自己的网络设备和文件,每个容器只能消耗限定的CPU和内存。乍看之下,容器中的流氓程序无法毁坏其他容器。但是如果流氓程序修改了所有容器共享的时钟呢?
视应用而定,改变时间可能没有太大问题,但是运行程序做任意系统调用事实上就允许它做任何事情。系统调用允许它修改内核内存,增加或删除内核模块,还能做一些正常容器不被允许做的事情。
这就引出了使容器成为可能的第三组技术。完全解释它们超出了本书的范围。所有请引用其他聚焦特定容器或使容器更安全的资源上。这一节简短的介绍以下那些技术。
为提供容器系统的全部权限
操作系统内核为程序提供一组系统调用结合,应用程序通过它与操作系统及底层硬件交互。这些系统调用包括创建进程,操作文件与设备,在应用之间建立通信渠道,及其他。
一些系统调用相当安全,任意进程都可以用,而另外一些保留给能提升权限的进程使用。如果你查看早期的例子,允许在Kubernetes节点上的应用程序首先被允许打开本地文件,但是没有改变系统时钟或以修改内核的方式破坏其他容器。
大多数容器应该在没有提升权限的情况下运行,仅有那些你信任的程序,并且需要额外权限的程序才可以在提升权限的容器中运行。
注意 Docker通过使用—privileged标志提升容器的权限。
使用功能为容器提供所有特权的子集
如果一个应用程序只需要调用部分要求提升权限的系统调用,创建一个完全提升权限的容器并不是理想的选择。幸运的是,Linux内核把权限划分称为能力的单元。能力单元的例子:
- CAP_NET_ADMIN 允许进程执行网络相关的操作,
- CAP_NET_BIND_SERVICE允许容器绑定小于1024的端口号
- CAP_SYS_TIME允许容器修改系统时钟,诸如此类。
当你创建容器的时候,能力可以容器中添加或删除。Docker与Kubernetes删除所有的能力除了应用程序需要的能力,但是如果用户被授权,就可以添加或删除能力。
注意 运行容器的时候一般遵循最小权限原则。不要给容器任何它们不需要的能力。这样做可以阻止攻击者通过使用那些权限获取访问操作系统的能力。
使用seccomp配置文件过滤单个系统调用
如果你需要更好的控制程序能做那些系统调用,你可以使用seccomp(计算机安全模式)。您可以通过创建一个JSON文件来创建一个自定义的seccomp配置文件,该JSON文件列出了使用该配置文件的容器允许进行的sys调用。当你创建容器的使用向Docker提供创建的JSON文件。
使用apparmor和selinux加固容器
如果迄今为止讨论的技术还不能满足需求,容器也提供额外的两种托管访问控制机制:SELinux(安全增强的LINXU)与AppArmor(应用甲)。
使用SELinux,你给文件和系统资源贴上标签,也给用户和进程贴上标签。只有当涉及的所有主题和对象的标签符合一组策略时,用户或进程才能访问文件或资源。AppArmor类似,但是使用文件路径而不是标签,聚焦在进程而不是用户。
SELinux和AppArmor大大提升了操作系统的安全性,SELinux和AppArmor都大大提高了操作系统的安全性,但是如果您对所有这些与安全性相关的机制感到不知所措,也不要担心。本节的目的是阐明适当隔离容器所涉及的所有内容,但目前对名称空间的基本理解应该绰绰有余。
2.4 总结
如果在读本章之前是一个容器小白,现在你应该理解了容器是什么,为什么使用,以及为可以实现容器Linux内核提供了什么特性。如果你之前使用过容器,我希望本章有助于澄清您对容器如何工作的不确定,现在你理解了容器与正常的操作系统一样,只不过Linux内核为这些进程提供了隔离。
在读完这一章后,你应该知道:
- 容器是正常的进程,但是彼此隔离同时与其他操作系统进程隔离。
- 容器比虚拟机更轻量级,因为它们使用相同的Linux内核,它们不像虚拟机一样完全隔离。
- Docker是第一个使得容器变得受欢迎的平台,也是Kubernetes支持第一个容器运行时。现在通过CRI也支持其他的容器运行时。
- 一个容器进行包含用户应用程序和它的所有依赖。它通过容器注册中心分发,用于创建运行容器。
- 容器可以通过单个docker run命令下载和执行。
- Docker使用Dockerfile构建进行,Dockerfile包含构建过程中Docker应该执行的命令。镜像由可以在多个镜像间共享的层组成。每一个层仅需要传输与存储一次。
- 容器通过Linux命令空间,控制组,能力集,seccomp,AppArmor和/或Selinux特性提供隔离性。命名空间确保一个容器只能看到主机上的一部分可用资源,控制组现在它能使用的资源,而其他特性则加强了容器之间的隔离。
在经过了检查容器的旅途后,现在,你准备起锚航向下一章,你将了解使用Kubernetes运行容器。
更多推荐



所有评论(0)