在某种意义上,容器与您已经学会从仓库安装的Linux包非常相似。一个容器镜像大致上是一个压缩归档(例如,.tar.gz文件)的应用程序,以及应用程序所需的所有配置文件和依赖项。那个小包——一个镜像——可以通过Docker执行。这种容器的革命性之处在于,它将一切整齐地包含在一个单一的制品中,并且可以在任何安装了容器运行时(如Docker)的Linux系统上运行。
这一章很容易成为一本书——Docker和Linux容器通常是相当大的主题。然而,就像这本书中的其他一切一样,我们只关注基本理论和实践技能,这些对于您舒适地与基于Docker的基础设施在应用程序中交互是必要的。
在这一章中,您将了解到以下内容:

容器解决的应用程序开发和运维问题
什么是容器,以及它们与Linux包的相似之处
Docker镜像和Docker容器之间的区别
在开发工作流程中使用Docker的所有实用基础知识
如何使用Dockerfile构建自己的容器镜像(您将容器化一个真实的Python Web应用程序)
一些更高级的主题,如虚拟机和容器之间的区别,以及Linux如何通过命名空间创建容器抽象
一些来之不易的容器技巧、窍门和最佳实践
让我们开始吧。
容器作为包的工作方式
当目标是包括一个已知的工作设置的系统时,Docker成为了打包软件的标准工具。一个Docker容器通常包含您想要运行的软件以及一个完整的(尽管经常是缩减的)Linux系统作为其执行环境。这个执行环境提供了库和工具,以及其他一些东西,如基本的系统配置,以便它可以作为一个独立的实体运行,独立于运行容器的系统。主要目标是确保应用程序可以在开发人员的机器上、生产和测试环境以及其他任何地方成功运行,而无需处理诸如操作系统版本、安装的库等细节。
重要的是要记住,操作系统和库并没有消失。库中的错误可能仍然存在,任何打包的依赖项应该出于安全和其他原因进行更新。然而,打包软件的消费者,无论是最终用户还是您的系统操作员,以及任何编排软件,现在都提供了一个通用的包,并且不需要关心系统依赖性。虽然软件的运行和配置的细节仍然依赖于软件,但执行的方式(在容器中)在某种程度上是标准化的。
总之,这意味着任何特定的环境设置,如安装依赖项,现在被描述为Dockerfile的一部分,一旦创建了一个工作容器镜像,除了特定的配置外,容器预计将在任何能够运行Docker或更广泛地说,OCI镜像的系统上运行。
OCI(Open Container Initiative的缩写)提供了标准来指定诸如镜像格式和Linux容器的执行等事项。有时它与Docker同义使用,这意味着开发人员可能使用Docker创建镜像,但在编排器上的执行可能根本不使用Docker。
没有比安装Docker并开始尝试一些命令更好的开始方式了,所以让我们这样做。
先决条件:Docker安装
首先,下载并安装Docker Desktop。您可以在 https://docs.docker.com/get-docker/ 上找到这方面的说明。
还有一个非常好的官方入门教程 https://docs.docker.com/get-started/,但我们建议您在阅读完本章之后再阅读它。我们将涵盖一些基础知识,但关注特定命令行标志的焦点较少,而更多地关注您将如何作为应用程序开发人员使用这些命令和工作流程。
现在您已经安装了Docker,让我们开始启动我们的第一个容器吧!
Docker速成课程
Docker镜像是我们比喻中的“包”——它是一个静态的制品,被保存、存储和移动。当它在机器上执行时,它变成了一个容器。这很重要,因为您有时会听到这些术语被错误地使用:Docker镜像是从一个容器——一个运行中的、命名空间化的过程——启动的不可变基础。镜像是生成运行时容器的预构建模板。
镜像被设计成不可变的:如果您下载了一个nginx Web服务器镜像并运行它,您对生成的容器所做的任何更改都不会影响底层镜像。这是大多数习惯于一次性配置并多次启动和停止的长寿命虚拟机的开发人员所困扰的部分。
Docker容器是不同的。理想情况下,它们被设计成短暂和无状态的,而它们所衍生的镜像则作为一个长期存在的蓝图,可以在许多不同的执行环境中生成无限数量的容器。
下面是一个基本的Docker工作流程示例,旨在让您熟悉最重要的Docker命令。不用担心记住命令;我们将在本章后面深入介绍它们。现在,我们将只解释每个步骤中发生的事情,这样您可以习惯于在下一个微服务工作中在屏幕上看到的内容。
首先,让我们启动nginx容器(docker run)并在其中交互式地(-it)运行Bash shell(/bin/bash):
→ ~ docker run -it nginx /bin/bash
这为我们提供了一个从nginx镜像启动的独特容器的shell提示。容器的Bash shell现在连接到我们的终端:
root@e96107c9a58e:/#
让我们写一个名为test.txt的文件并验证它是否存在:
root@e96107c9a58e:/# echo "I am immutable" >> test.txt
root@e96107c9a58e:/# cat test.txt
I am immutable
我们可以用通常的命令,ctrl-d退出shell:
root@e96107c9a58e:/#
exit
容器退出,我们现在回到了我们的常规shell。这是大多数首次使用Docker的用户感到困惑的地方:让我们再次运行第一个命令,从nginx Docker镜像启动一个容器,并检查我们的文件:
→ ~ docker run -it nginx /bin/bash
root@c3b4d95ab9e6:/# cat test.txt
cat: test.txt: No such file or directory
FILE A BUG REPORT! DOCKER IS BROKEN!
实际上,不是的。这不是您在其中写入test.txt文件的同一个容器。如果您仔细观察,您可能已经注意到第二个容器的shell提示中的主机名与第一个容器不同。那是因为每个docker run命令都从指定的镜像启动一个新的容器。容器被设计为运行、退出并永远消失。
原来的容器实际上仍然存在:
→ ~ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3b4d95ab9e6 nginx "/docker-entrypoint.…" 14 minutes ago Exited (1) 6 minutes ago agitated_hofstadter
e96107c9a58e nginx "/docker-entrypoint.…" 14 minutes ago Exited (0) 14 minutes ago nervous_gould
要摆脱它们,您可以使用docker rm和要删除的容器的ID:
→ ~ docker rm c3b4d95ab9e6
c3b4d95ab9e6
可以使用docker start启动已停止的容器:
→ ~ docker start e96107c9a58e
e96107c9a58e
此时,您将看到容器在您的Docker进程列表中运行:
→ ~ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e96107c9a58e nginx "/docker-entrypoint.…" 18 minutes ago Up 1 second 80/tcp nervous_gould
然后,您可以使用docker exec在该容器内执行命令,再次启动并附加到Bash shell程序与-it。从那里,您可以查看我们修改的文件系统状态(test.txt):
→ ~ docker exec -it e96107c9a58e /bin/bash
root@e96107c9a58e:/# cat test.txt
I am immutable
然而,长时间保留容器——修改它们的状态并停止和重新启动它们,而不是始终从镜像启动新容器——是不被鼓励的,并且会导致Docker帮助解决的许多相同错误。
让我们避免所有这些错误,并通过强制删除永远删除这个运行中的容器:
→ ~ docker rm -f e96107c9a58e
e96107c9a58e
您也可以运行docker stop然后是docker rm,但使用docker rm -f将一次性停止并删除运行中的容器。
您可以看到Docker如何鼓励不可变的容器,以保持状态从镜像中漂移,镜像是应用程序初始环境的“真相来源”。如果您想对镜像进行更改,您不能直接这样做——镜像是不可变的。
更改应该是明确的和有意的,这对于创建和运行可靠的软件很重要。我们如何做到这一点?我们从原始镜像开始,在一种受控和可复制的方式中进行更改(而不是“SSH到服务器并尝试运行这些命令”),然后通过创建一个新的镜像来保存它们。进入强大的Dockerfile。
使用Dockerfile创建镜像
如果您曾经被指派构建一个新的Docker镜像,或者修改一个现有的镜像——可能是您正在开发的Web应用程序——您将大量使用Dockerfile(请参阅官方Dockerfile文档 https://docs.docker.com/engine/reference/builder/)。
大部分新软件已经有官方(或至少是开源的、第三方的)Dockerfile可用。即使您需要在使用这些之前做一些定制,查找示例的好地方是您使用的软件或框架的文档。当打包软件发布重大更新时,这些示例往往不会像您自己的自定义Dockerfile那样容易损坏。
此外,一些框架或开发环境,例如,Spring Boot(Java),可以在构建过程中生成Docker镜像。
所以,尽管您可能永远不需要自己接触Dockerfile,但这是不太可能的,您应该对它们的工作原理有一个基本的了解。
让我们来看一个非常基础的Dockerfile示例,来自开源的HTTP回声服务器项目(https://github.com/hashicorp/http-echo)。这个Dockerfile创建了一个包含Go二进制文件的Docker镜像,该文件充当一个简单的Web服务器:
FROM alpine
ADD "https://curl.haxx.se/ca/cacert.pem" "/etc/ssl/certs/ca-certificates.crt"
ADD "./pkg/linux_amd64/http-echo" "/"
RUN apk add curl
ENTRYPOINT ["/http-echo"]
基本上,通过以下步骤创建一个新的容器镜像:
使用Alpine Linux镜像作为基础。
下载一些证书并添加到镜像层(本质上是添加到结果容器的文件系统中)。
从构建目录复制http-echo二进制文件到容器镜像中。
运行Alpine软件包安装命令以安装curl程序。
定义当从这个镜像启动容器时执行的可执行文件或命令。
这些步骤中的每一个都是由Dockerfile解析器知道如何执行的大写指令调用的。这个特定的Dockerfile只使用了Dockerfile中可用指令的一个子集(FROM, ADD, RUN 和 ENTRYPOINT)。
以下是您在通过Dockerfile创建新的Docker镜像时可以使用的全部指令:
ARG:声明一个构建时参数;基本上是稍后在构建中使用的一个变量。
ENV:在构建期间设置的环境变量,这些变量将在运行的容器环境中持续存在(不仅仅是在构建期间!
)。采用键=值格式。
FROM:基础镜像。
CMD:为容器在启动时运行提供默认命令(或默认ENTRYPOINT参数)。它可以被覆盖,但您的Dockerfile中应该有一个。每个Dockerfile只允许一个——如果有多个CMD,只有最后一个会生效。
ADD:一个灵活的指令,用于复制文件和目录,并将它们添加到镜像的文件系统中。它也可以用来从镜像外部或远程URL(通过HTTP)复制文件,以及执行如展开、解压缩、解归档等复杂操作。在上述示例Dockerfile中,它被用作curl命令的替代品,用于下载CA证书文件。
COPY:只复制文件和目录——比ADD更简单、更少魔法、功能更少。
LABEL:以键=值格式添加镜像元数据。
EXPOSE:告知镜像使用者此容器将监听哪些网络协议和端口。
ENTRYPOINT:告诉容器启动时运行什么命令。使用exec形式(ENTRYPOINT ["可执行文件", "参数1", "参数2"]),以确保容器能够接收并响应来自容器外部进程的信号。
RUN command arg1 arg2:在镜像的shell中运行带有参数arg1和arg2的命令:
RUN ["command", "arg1", "arg2", "argN"]:与上述相同,但有助于避免shell字符串处理。
每个RUN指令在一个新的镜像层中执行(我们这里不深入讨论层,但知道这一点很有帮助)。
RUN --mount 可以在构建期间临时将文件系统挂载到容器中,而不需要将文件本身复制到镜像层。
RUN --network 和 RUN --security 也分别存在,用于管理网络上下文和特权容器。
WORKDIR:为Dockerfile中随后的指令设置工作目录。等同于Unix类操作系统中的cd命令。
SHELL:覆盖在Docker构建期间解释命令的默认shell。命令必须使用exec形式。
STOPSIGNAL:设置容器应解释为退出信号的系统调用信号。默认情况下,这是SIGTERM,就像任何其他Linux进程一样。
VOLUME:定义将从主机挂载的卷。
USER:从这一点开始更改用于构建命令的(容器)用户(在构建期间可以多次使用以切换用户)。
ONBUILD:定义一个指令,当此镜像被用作另一个构建的基础时触发。
HEALTHCHECK:一些健康检查功能,您可能不会使用,因为您的容器调度程序有自己的健康检查功能。
我们将通过一个实际的端到端示例来展示如何将所有这些结合在一起完成一个小项目,但首先让我们更深入地回顾一下我们刚刚使用的命令。
容器命令
现在,让我们更深入地了解一些可能在使用Docker时遇到的更复杂但重要的命令和命令调用。
docker run
让我们看看我们之前使用的docker run命令的一个更复杂的调用:
docker run --rm --name mywebcontainer -p 80:80 -v /tmp:/usr/share/nginx/html:ro -d nginx
--rm:当容器退出时进行清理(移除)。
--name mywebcontainer:给这个容器一个友好的名字——mywebcontainer。
-p 80:80:将主机的80端口映射到容器中的80端口。左边的端口号是“外部”的(运行容器的环境),右边的端口号代表“内部”的(容器)端口。例如,-p 4000:80会将容器的80端口映射到localhost:4000。
-v /tmp:/usr/share/nginx/html:ro:挂载一个卷——宿主环境的/tmp目录将被挂载到容器的/usr/share/nginx/html;:ro确保这将是一个只读挂载(从容器内部无法修改挂载的文件)。
-d:以分离模式(后台)运行容器。
nginx:从这个镜像启动容器。
如果您想在http://localhost:80看到一些HTML,可以将index.html文件添加到您的/tmp目录:
cat <<EOF > /tmp/index.html
<!doctype html>
<h1>Hello World</h1>
<p>This is my container</p>
</html>
EOF
因为我们的/tmp目录映射到了容器的/usr/share/nginx/html目录(nginx将寻找HTML文件的地方),nginx会立即识别并开始提供此文件。
卷是通过无状态容器运行有状态应用程序的机制。
docker image list
要查看您已本地下载的镜像,您可以运行docker images(或者如果您更喜欢,使用docker image list)。
如果您一直在构建和使用许多Docker镜像,列表可能会很长!
$ docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 51086ed63d8c 10 days ago 142MB
vault latest 22fdc6314051 2 months ago 207MB
golang 1.19-alpine d0f5238dcb8b 2 months ago 352MB
docker ps
docker ps有点像Linux中的ps命令。它让您可以看到哪些容器在您的主机上运行,以及它们的一些上下文,比如它们的ID、正在运行的命令、创建时间和运行时间、端口映射等。
运行命令
$ docker ps
会产生这样的输出:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2aca849eef73 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:80->80/tcp mywebcontainer
docker exec
在开发容器镜像期间,通常需要进入容器并运行命令。要在运行中的容器中启动交互式shell,请使用docker exec:
docker exec -it mywebcontainer /bin/bash
对于我们之前启动的nginx容器,这将在容器环境中生成一个Bash shell。您所做的任何状态更改(文件创建、内核设置等)将在容器停止时丢失——下一次docker run将简单地从相同的基础镜像状态启动一个新容器。
docker stop
要停止容器,请运行docker stop $CONTAINERNAME——如果它没有友好的名字,也可以使用容器ID:
docker stop mywebcontainer
如果容器是使用--rm选项启动的,就像我们的nginx容器一样,容器将被删除,其状态(如果该状态与基础镜像不同)将丢失。
如果容器没有使用--rm启动,其状态保留在您的文件系统上,您可以使用docker start $CONTAINERNAME再次启动容器。其状态将被保留。
Docker项目:Python/Flask应用程序容器
我们将容器化一个使用Flask Web框架的小型Python Web服务。这是一个非常常见的模式,Python非常适合容器化,因为在许多Python项目中,打包和依赖管理众所周知是混乱的。您将自己创建所有文件——尝试使用命令行文本编辑器进行练习!
mkdir dockerpy && cd dockerpy
创建一个微型Python Web应用程序。在这个例子中,我使用的是vim,但您可以使用您喜欢的任何编辑器:
vim echo_server.py
将以下文本粘贴到里面:
from flask import Flask, requestimport osapp = Flask(__name__)@app.route('/')def echo(): return { "method": request.method, "headers": dict(request.headers), "args": request.args }@app.route('/health')def health(): return {"status": "healthy"}if __name__ == "__main__": env_port = os.environ.get("PORT", 8080) app.run(host='0.0.0.0', port=env_port)
这是一个完整的Web应用程序——它简单地读取一些传入请求的信息,并使用这些信息将响应推送回客户端。
保存并退出文件(按esc键,然后输入:x)。
创建一个名为requirements.txt的文件,其中只包含以下一行:
Flask>=3.0.0
接下来,创建您的Dockerfile:
vim Dockerfile
输入以下文本:
# 使用官方的Python基础镜像FROM python:3.12-slim# 设置容器内的工作目录WORKDIR /app# 将我们的依赖列表复制到容器中COPY requirements.txt .# 安装Python依赖RUN pip install --no-cache-dir -r requirements.txt# 将脚本复制到容器中COPY echo_server.py .# 设置健康检查,如果容器不在内部端口上监听,则终止容器HEALTHCHECK --interval=30s --timeout=5s \ CMD curl --fail http://localhost:8080/health || exit 1# 暴露应用程序的端口EXPOSE 8080ENV PORT=8080CMD ["python", "echo_server.py"]
目前您只需要这些。您的dockerpy目录现在应该包含三个文件:
Dockerfileecho_server.pyrequirements.txt创建Docker镜像 使用docker build命令构建一个新的Docker镜像。-t用于为容器打上名称的标签:docker build -t dockerpy .
注意结尾的.字符,它告诉Docker使用当前目录作为其构建上下文。
从您的镜像启动容器 您已经在本章前面使用了docker run命令。使用它来启动一个来自您新构建的镜像的容器:docker run --rm -d -p 8080:8080 --name my-dockerpy dockerpy
(该命令将打印出您的新容器的ID) 这里有一些新参数:
--rm告诉Docker在容器退出时删除它。这防止旧容器在您的文件系统上悬挂,正如您在本章前面的示例中看到的那样。 -d告诉Docker将容器作为守护进程运行。这样它就不会附着在您的终端上在前台运行。 -p设置端口映射:冒号左边是容器端口,而右边是它将映射到的宿主端口。如果容器应用程序在端口1234上运行,而您希望将其映射到宿主端口80,这将读取为-p 1234:80。 --name为您的容器打上一个名称,以便您可以在docker ps的输出中轻松找到它。
现在您已经运行了容器化的应用程序,可以在浏览器或命令行中访问它。让我们使用curl命令连接并发送请求到Web服务:
curl localhost:8080
{"args":{},"headers":{"Accept":"/","Host":"localhost:8080","User-Agent":"curl/8.1.2"},"method":"GET"}
对于那些经历过难以重现的依赖性噩梦的人来说(Python、Ruby等语言因此而著名),这应该是一个启示。您以前必须随身携带的所有复杂性——从本地开发环境到CI和测试,再到暂存,最后到生产——现在都浓缩成一个单一的制品,无论您在哪里运行它,都保证包含相同的内容。
我们还没有使用过的一个是docker exec命令,它允许您在运行中的容器内执行命令。如果出于某种原因,您必须检查或修改正在运行的容器,这将非常有用:
docker exec -it my-dockerpy /bin/sh
这将在容器中启动并附加到/bin/sh(大多数生产容器只有一个最小化的shell在/bin/sh,并且不会带有像Bash这样功能齐全的东西)。
让我们使用这里要介绍的最后一个命令docker kill来停止服务器:
docker kill my-dockerpy
这发送一个SIGKILL(信号9)而不是SIGTERM(信号15)给进程,并立即停止它,不给它优雅关闭的机会。
容器与虚拟机 您现在已经体验了创建和使用Docker镜像的工作流程。然而,了解容器和虚拟机之间的底层差异是有益的。这些知识可以在您解决运维问题时发挥作用,并且通常是一个用来衡量您对容器化原理理解程度的面试问题。
虚拟机(VM)允许您在另一个宿主操作系统之上运行完整的操作系统,如Linux、Windows或DragonFly BSD。虚拟机独立于宿主系统运行。实际上,在macOS上运行Docker会透明地使用一个虚拟机来提供Docker所需的Linux操作系统。
因此,虚拟机运行一个完整的操作系统,如Linux,该系统又使用像systemd这样的初始化系统。因此,您管理服务和进程的方式与您的虚拟机是一台物理机器完全一样。在日常使用方面,适用于物理机器的一切也适用于虚拟机。然而,这并不是容器通常的使用方式。
Docker容器通常包含单个应用程序;实际上,它们经常只包含一个单一的进程。如果容器内部存在多个进程,这通常是由于多进程应用程序生成了子进程(Web服务器或命令运行器通常会这样做)。由于广泛认可的最佳实践是容器只运行一个单一的进程,并在该进程退出时立即退出,因此在此处进行任何内部进程的监督和管理都将是浪费。
相反,您会发现通常由操作系统的初始化系统完成的工作已经转移到容器运行时环境之外,转移到管理容器的外部系统,如Kubernetes、Nomad等。
在这个新模式中,容器就是操作系统进程曾经的样子,而容器编排器扮演了各种操作系统和调度器的角色。
在Docker容器中,PID1(在完整的Linux操作系统上是初始化系统)是您的CMD或ENTRYPOINT。通常,这是您正在运行的软件的主进程。通常,容器预期运行一个单一的进程。虽然有些人故意以不同的方式运行他们的容器,但运行一个单一的进程并在进程停止时使容器停止是预期的行为。特别是当简单地将服务容器化以在生产中运行时,应确保遵循这种方法。在运行早于Docker容器普及的软件时,会有此规则的例外,但在这些情况下,您可能会意识到并经常基于为此目的制作的容器。
关于Docker镜像仓库的快速说明 在本章中,我们一直在大量使用nginx镜像。但是,这个镜像究竟来自哪里?默认情况下,Docker尝试从Docker Hub(https://hub.docker.com/)下载镜像,这是公共Docker镜像的中央仓库。Docker Hub的工作方式类似于Linux软件包仓库,其中包含已上传的Docker镜像,供您使用。大多数流行的服务器软件都可以在那里找到,并且可以像您刚刚看到的nginx一样轻松下载和使用。
然而,并非所有应用程序都是公共的,使用私有仓库存储Docker镜像是常见的。Docker镜像仓库提供商的列表不断变化,所以我们不会在这里列出它们,但理解它们都像Docker Hub一样工作就足够了。
痛苦中学到的容器课程 当您开始构建自己的容器时,通过记住Docker官方文档中讨论的最佳实践,您可以避免许多问题:https://docs.docker.com/get-started/09_image_best/。
话虽如此,我们编制了一个简短的列表,列出了我们注意到的最严重的容器化错误以及如何避免它们。这一节是许多不眠之夜、停机和艰难学习的结果。
镜像大小 从像Scratch或Alpine这样的最小镜像开始。对于大多数应用程序的部署,最好尽量避免大型镜像和像Ubuntu这样的发行版。当需要构建依赖时,建议删除这些依赖,或在构建更大/多容器项目时使用中间构建容器。
小型、最小的镜像不仅意味着更快的下载速度和较少的资源使用,而且也使您更容易管理。如果镜像不包含您不需要的软件和库,那么您需要保持更新的内容就少了,犯罪分子攻击的表面积也小了,容器安全扫描器的烦人警告也少了。
C标准库 请注意您正在使用的C标准库(也称为libc)。许多Linux发行版使用glibc;有些,像Alpine Linux,使用musl或其他库。这些库和任何生成的二进制文件可能不兼容。例如,在Alpine上,您可能需要自己编译不太流行的工具。如果您的项目依赖于基础镜像上通过包可用的某些库,您可能会遇到不兼容性问题。当然,升级、降级或完全切换基础镜像也可能引起类似的问题。
然而,由于Alpine和musl的稳步采用,这些问题变得不太可能发生(至少,更易于谷歌搜索!
)。如果您不依赖任何C库,这通常不会是问题。此外,静态编译您的代码可以使您更独立于底层系统,因此也独立于基础镜像。
生产环境不是您的笔记本电脑:外部依赖 不要依赖本地挂载或其他本地容器。部署容器的环境可能与您的笔记本电脑非常不同。仅仅因为您的笔记本电脑上有一个数据库容器紧邻您的Web应用程序容器,并不意味着这些容器会在生产中安排在同一台机器上。
数据卷的情况也是如此——这些容器外的接触点是您需要与您的Ops/DevOps同事进行一些计划的地方。您可能会通过调度程序或其他DevOps工具构造连接到服务发现、健康检查和共享卷。
容器理论:命名空间 如果您想知道这些容器魔法是如何在底层工作的,或者只是担心有一天您将不得不在压力下排除容器环境的故障,熟悉命名空间的概念是有用的。如果您对容器抽象如何在Linux上构建不感兴趣,可以跳过这一节。
命名空间是一个过度使用的术语,在不同的技术领域意味着不同的事情。在Linux容器的上下文中,命名空间的概念最好通过chroot(更改根)来解释。chroot是一个旧的Unix和类Unix操作系统的实用程序,允许用户更改文件系统的根(/路径)。
这个工具的使用非常简单:chroot /some/path将/some/path中的任何内容设置为新的/。除了允许操作系统安装程序更改为正在安装的系统以运行命令外,它还允许基本的命名空间。实际上,各种软件和各种Linux发行版的配置一直在利用这一点来增强安全性,因为使用chroot基本上排除了当前可读范围内的文件系统部分——它使新根之外的任何内容都无法访问。因此,如果攻击者使用漏洞允许在运行在chroot环境中的Web服务器上执行远程代码,系统和此目录之外的任何文件都将不受影响。
用于在Linux和其他操作系统上实现容器的技术原语在过去十年中发生了显著变化,并且可能会继续变化。幸运的是,作为主要使用容器化技术的软件工程师,而不是这项技术的运营商或实现者,低级实现对您并不至关重要。
“容器”抽象依赖于以下底层技术:
文件系统命名空间(例如,使用chroot)。 用户和进程命名空间,使容器外的进程在容器内不可见。换句话说,在容器中,root和PID 5分别映射到外部的非特权用户和另一个进程ID。 资源分组和会计技术,如cgroups。 网络虚拟化/命名空间,以便容器不能直接访问网络接口,也可以处理端口号重叠。例如,您可以运行两个不同的容器,它们都暴露端口8080;不会出现端口已被使用的错误的,因为容器的网络堆栈是相互独立的。
我们如何使用容器进行运维? 虽然这不是一本针对系统管理员或网站可靠性工程师的书,但您应该了解通常运行容器的基本上下文。主要的想法是,容器大多是无状态的“函数”,它们处理输入(Web请求或来自其他服务的HTTP消息)并产生输出(Web响应、副作用和流式传输到STDOUT的日志)。在良好运行的操作环境中,可以将容器视为Linux进程或编程中的函数的类似物。
容器通常由Kubernetes、Nomad等第三方工具层“调度”到主机上。如果容器类似于进程,那么这些工具就充当操作系统调度程序的角色(整个事情是一个分布式系统,而不是单个主机)。
容器的输出通常由相同的工具捕获,并重定向到日志解决方案,如Logstash、Graylog和Datadog。所有运行容器的指标可能会被提取并输入到像Prometheus这样的工具中进行分析和故障排除。
结论 在本章中,您对使用Docker和容器的最重要内容进行了快速了解。尽管个别技术可能会发生变化——哪个容器调度程序正在流行,或者最佳处理日志流式传输的方式——我们试图专注于每个现代软件开发人员都应该具备的核心理论和技能。
我们希望您从本章中获得几个主要的想法。首先,我们希望您对容器化解决的问题有一个直观的理解,主要是通过控制复杂性并将依赖项打包到一个单一的制品中。
同样重要的是要记住镜像和容器之间的区别,并练习从头开始构建您自己的Dockerfile,使用官方文档。
我们希望讨论的一些更高级主题,如虚拟机和容器之间的区别以及命名空间的工作原理,在故障排除或工作面试中会派上用场。我们讨论的最佳实践在那里也会派上用场。
最后,为了巩固您的学习,我们建议您通过容器化您自己的一个应用程序来练习这些技能。您将学到很多,而且趁本章的所有信息还新鲜时开始会更容易。