原文:
annas-archive.org/md5/3c34d287e2879a0f121f1884a118ac03译者:飞龙
第四章:创建和管理容器镜像
在上一章中,我们讲解了如何使用 Docker 进行容器化,安装了 Docker 并运行了我们的第一个容器。我们涵盖了一些核心基础知识,包括 Docker 卷、挂载、存储驱动程序和日志驱动程序。我们还介绍了 Docker Compose 作为一种声明性管理容器的方法。
现在,我们将讨论容器的核心构建模块:容器镜像。容器镜像还实现了现代 DevOps 实践的一个核心原则:配置即代码(config as code)。因此,理解容器镜像、它们是如何工作的以及如何有效地构建镜像,对于现代 DevOps 工程师来说非常重要。
在本章中,我们将涵盖以下主要主题:
-
Docker 架构
-
理解 Docker 镜像
-
理解 Dockerfiles、组件和指令
-
构建和管理 Docker 镜像
-
扁平化 Docker 镜像
-
使用无发行版镜像优化容器
-
理解 Docker 仓库
技术要求
对于本章,我们假设你已经在一台运行 Ubuntu 18.04 Bionic LTS 或更高版本的 Linux 机器上安装了 Docker,并且具有 sudo 权限。你可以阅读 第三章,使用 Docker 进行容器化,了解更多关于如何实现这一点的细节。
你还需要克隆一个 GitHub 仓库,用于本章的一些练习,仓库地址是 github.com/PacktPublishing/Modern-DevOps-Practices-2e。此外,大多数活动需要你有一个 Docker Hub 账户。要创建一个,请访问 hub.docker.com/。
Docker 架构
想象一下,你是一位充满热情的厨师,致力于创造令人垂涎的菜肴,以满足饥饿的顾客。在你的厨房里,这个神奇的地方叫做 Docker,你拥有特殊的能力来规划、制作并展示你的烹饪创作。让我们来拆解其中的关键部分:
食材(应用代码和依赖项):想象你的厨房里有一排架子,上面摆满了食材,比如面粉、鸡蛋和香料。这些食材以特定的方式结合在一起,做成一道菜。同样,你的应用代码和依赖项也需要协同工作,才能构建出你的应用程序。
食谱(镜像):每个食谱就像是某道菜的计划。想象一下你有一个巧克力蛋糕或卡邦尼意面的食谱。这些食谱就像是你创作的构建模块。同样,Docker 镜像就是制作 Docker 容器的计划。
食谱卡(Dockerfile):你的烹饪旅程中涉及使用特别的食谱卡,这些卡片被称为 Dockerfiles。这些卡片展示了你需要遵循的重要步骤和食材(命令)。例如,一个巧克力蛋糕的 Dockerfile 可能包括“混合面粉和糖”或“加入鸡蛋和可可粉”这样的步骤。这些 Dockerfiles 引导你的助手(Docker)制作这道菜(容器)。
做好的菜肴(容器):当有人想要一份菜肴时,你用食谱(镜像)来做它。然后,你就有了一道新鲜热乎的菜肴,准备好上桌。这些菜肴是独立的,但它们可以一次又一次地被做出来(多亏了食谱),就像容器一样。
厨房助手(Docker Engine):在你忙碌的厨房里,你的助手(Docker Engine)发挥着重要作用。他们做了繁重的工作,从获取食材到按照食谱做菜并上菜。你给他们指令(Docker 命令),他们就会把事情做成。他们甚至会在做完每道菜后帮你清理。
特制套餐菜单(Docker Compose):有时,你想提供一道包含多种菜肴的特别套餐,它们相互搭配非常好。想象一下,一顿包含前菜、主菜和甜点的餐食。使用 Docker Compose 就像为这个场合制作一个特别的菜单。它列出了每道菜的食谱(镜像)以及它们应该如何搭配。你甚至可以自定义它,只用一个命令就能创造出一整顿饭的体验。
存储区域(Volumes):在厨房里,你需要一个地方来存放食材和餐具。把 Docker 卷想象成特殊的存储区域,你可以在这里保存重要的东西,如数据和文件,多个菜肴(容器)都可以使用这些存储。
通信通道(Networks):你的厨房是个热闹的地方,充满了交谈和互动。在 Docker 中,网络就像是特殊的通信路径,帮助你厨房中的不同部分(容器)相互交流。
所以,Docker 就像你的神奇厨房,你可以使用计划(Dockerfiles)和食材(镜像),在厨房助手(Docker Engine)的帮助下制作菜肴(容器)。你甚至可以提供整套套餐(Docker Compose),并使用特殊的存储区域(Volumes)和通信通道(Networks)来使你的菜肴更加美味。就像大厨通过练习不断进步一样,探索 Docker 将帮助你迅速成为 DevOps 的高手!现在,让我们深入了解 Docker 架构,理解其中的细节!
正如我们已经知道的,Docker 使用 一次构建,到处运行 的概念。Docker 将应用打包成镜像,Docker 镜像形成容器的蓝图,因此容器就是镜像的一个实例。
容器镜像打包了应用程序及其依赖项,因此它们是一个可以在任何运行 Docker 的机器上运行的单一不可变单元。你也可以将它们视为容器的快照。
我们可以在 Docker 注册中心中构建和存储 Docker 镜像,例如 Docker Hub,然后将这些镜像下载到我们希望部署它们的系统中。镜像由多个层组成,这有助于将镜像拆分成多个部分。层通常是可重用的阶段,其他镜像可以在此基础上构建。这也意味着我们在更改镜像时不必传输整个镜像,而只需传输差异部分,这大大节省了网络 I/O。我们将在本章稍后详细讨论分层文件系统。
以下图示展示了 Docker 用于协调以下活动的组件:
图 4.1 – Docker 架构
组件包括:
-
Docker 守护进程:该进程运行在我们希望运行容器的服务器上。它们在 Docker 服务器上部署和运行容器。
-
Docker 仓库:这些用于存储和分发 Docker 镜像。
-
向 Docker 守护进程发送
docker命令。
现在我们了解了 Docker 架构的关键组件,以及 Docker 镜像在其中的重要作用,让我们更详细地理解 Docker 镜像及其组件、指令和仓库。
理解 Docker 镜像
Docker 镜像构成了 Docker 容器的蓝图。就像你需要为运输集装箱设计一个蓝图来确定其大小以及它将包含哪些货物一样,Docker 镜像指定了需要使用的包、源代码、依赖项和库。它还决定了源代码如何运行才能有效。
从技术上讲,它由一系列步骤构成,这些步骤是在基础操作系统镜像上执行的,以便让你的应用程序正常运行。这可能包括安装软件包和依赖项、将源代码复制到正确的文件夹、构建代码生成二进制文件等等。
你可以将 Docker 镜像存储在容器仓库中,这是一个集中存储位置,Docker 主机可以从这里拉取镜像来创建容器。
Docker 镜像使用分层文件系统。我们不再使用一个庞大的、单一的文件系统块作为运行容器的模板,而是有许多层,叠加在一起。那么这意味着什么?解决了什么问题?让我们在下一部分中看看。
分层文件系统
Docker 中的层是中间的 Docker 镜像。其理念是我们对每个 Dockerfile 语句执行时,都会在上一层之上进行更改并构建一个新层。随后的语句会修改当前层,生成下一个层。最终的层会执行 Docker 的 CMD 或 ENTRYPOINT 命令,结果镜像由多个层组成,一个层叠在另一个层之上。让我们通过一个简单的例子来理解这一点。
如果我们拉取上章构建的 Flask 应用程序,我们将看到以下内容:
$ docker pull bharamicrosystems/python-flask-redis
Using default tag: latest
latest: Pulling from bharamicrosystems/python-flask-redis
188c0c94c7c5: Pull complete
a2f4f20ac898: Pull complete
f8a5b284ee96: Pull complete
28e9c106bfa8: Pull complete
8fe1e74827bf: Pull complete
95618753462e: Pull complete
03392bfaa2ba: Pull complete
4de3b61e85ea: Pull complete
266ad40b3bdb: Pull complete
Digest: sha256:bb40a44422b8a7fea483a775fe985d4e05f7e5c59b0806a2
4f6cca50edadb824
Status: Downloaded newer image for bharamicrosystems/python-flask-redis:latest
docker.io/bharamicrosystems/python-flask-redis:latest
如你所见,许多 Pull complete 语句旁边都有随机 ID。这些被称为 层。当前层仅包含与上一层和当前层文件系统之间的差异。一个容器镜像由多个层组成。
容器在镜像层之上包含一个额外的可写文件系统。这是容器修改文件系统以提供预期功能的层。
使用层次结构而不是简单地复制整个容器文件系统有几个优点。由于镜像层是只读的,从一个镜像创建的多个容器共享相同的层次文件系统,从而减少了总体磁盘和网络占用。层次结构还允许你在镜像之间共享文件系统。例如,如果两个镜像来自同一个基础镜像,它们就共享相同的基础层。
下图展示了一个在 Ubuntu 操作系统上运行的 Python 应用程序。从高层次来看,你会看到一个基础层(Ubuntu 操作系统)和安装在其上的 Python。在 Python 上面,我们安装了 Python 应用程序。所有这些组件共同构成了镜像。当我们从镜像创建容器并运行它时,我们得到的是位于最上层的可写文件系统:
图 4.2 – 容器层
所以,你可以从相同的基础镜像创建多个 Python 应用镜像,并根据需要进行定制。
每个从容器镜像创建的容器都有独特的可写文件系统,即使你从相同的镜像创建容器也是如此。
镜像历史
要理解镜像及其层次结构,你可以随时查看镜像历史。
让我们通过运行以下命令检查最后一个 Docker 镜像的历史:
$ docker history bharamicrosystems/python-flask-redis
IMAGE CREATED CREATED BY SIZE COMMENT
6d33489ce4d9 2 years ago /bin/sh -c #(nop) CMD ["flask" "run"] 0B
<missing> 2 years ago /bin/sh -c #(nop) COPY dir:61bb30c35fb351598… 1.2kB
<missing> 2 years ago /bin/sh -c #(nop) EXPOSE 5000 0B
<missing> 2 years ago /bin/sh -c pip install -r requirements.txt 11.2MB
<missing> 2 years ago /bin/sh -c #(nop) COPY file:4346cf08412270cb… 12B
<missing> 2 years ago /bin/sh -c apk add --no-cache gcc musl-dev l… 143MB
<missing> 2 years ago /bin/sh -c #(nop) ENV FLASK_RUN_HOST=0.0.0.0 0B
<missing> 2 years ago /bin/sh -c #(nop) ENV FLASK_APP=app.py 0B
<missing> 2 years ago /bin/sh -c #(nop) CMD ["python3"] 0B
<missing> 2 years ago /bin/sh -c set -ex; wget -O get-pip.py "$P… 7.24MB
<missing> 2 years ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_SHA256… 0B
<missing> 2 years ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_URL=ht… 0B
<missing> 2 years ago /bin/sh -c #(nop) ENV PYTHON_PIP_VERSION=20… 0B
<missing> 2 years ago /bin/sh -c cd /usr/local/bin && ln -s idle3… 32B
<missing> 2 years ago /bin/sh -c set -ex && apk add --no-cache --… 28.3MB
<missing> 2 years ago /bin/sh -c #(nop) ENV PYTHON_VERSION=3.7.9 0B
<missing> 2 years ago /bin/sh -c #(nop) ENV GPG_KEY=0D96DF4D4110E… 0B
<missing> 2 years ago /bin/sh -c set -eux; apk add --no-cache c… 512kB
<missing> 2 years ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 2 years ago /bin/sh -c #(nop) ENV PATH=/usr/local/bin:/… 0B
<missing> 2 years ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 2 years ago /bin/sh -c #(nop) ADD file:f17f65714f703db90… 5.57MB
如你所见,存在多个层次,每个层次都有相应的命令。你还可以看到这些层次何时创建以及每层占用的磁盘空间大小。有些层并不占用磁盘空间,因为它们没有对文件系统做任何更改,比如 CMD 和 EXPOSE 指令。它们执行某些功能,但不会向文件系统写入任何内容。而像 apk add 这样的命令会写入文件系统,你可以看到它们占用了磁盘空间。
每一层都会以某种方式修改旧的层,因此每一层只是文件系统配置的增量。
在下一节中,我们将深入研究 Dockerfile,了解如何构建 Docker 镜像并查看其层次结构是怎样的。
理解 Dockerfile、组件和指令
Dockerfile 是一个简单的文件,它包含了一系列构建 Docker 镜像的步骤。每个步骤称为 指令。有不同类型的指令。让我们通过一个简单的例子来理解它是如何工作的。
我们将从零开始构建一个简单的 NGINX 容器,而不是使用 Docker Hub 上现成的镜像。NGINX 是一种非常流行的 Web 服务器软件,适用于各种应用场景;例如,它可以作为负载均衡器或反向代理服务器。
从创建一个 Dockerfile 开始:
$ vim Dockerfile
FROM ubuntu:bionic
RUN apt update && apt install -y curl
RUN apt update && apt install -y nginx
CMD ["nginx", "-g", "daemon off;"]
让我们逐行分析每个指令,理解这个 Dockerfile 是如何工作的:
-
FROM指令指定了此容器的基础镜像。这意味着我们使用另一个镜像作为基础,并将在其上构建层。我们使用ubuntu:bionic包作为此次构建的基础镜像,因为我们想在 Ubuntu 上运行 NGINX。 -
RUN指令指定了在特定层上需要执行的命令。你可以通过&&分隔多个命令。如果我们想将依赖命令放入同一层中,可以在一行中运行多个命令。每个层应达到特定的目标。在前面的例子中,第一个RUN指令用于安装curl,而下一个RUN指令用于安装nginx。 -
你可能会想知道为什么每次安装之前都要运行
apt update。这是必需的,因为 Docker 构建镜像是通过层来实现的。因此,一个层不应该隐性依赖于前一个层。在这个例子中,如果在安装nginx时省略了apt update,并且我们想在不更改包含apt update指令(即安装curl的那一行)的情况下更新nginx版本,那么当我们运行构建时,apt update不会再运行,导致你的nginx安装可能会失败。 -
CMD指令指定了当构建的镜像作为容器运行时需要执行的一组命令。这是默认执行的命令,它的输出将记录在容器日志中。你的容器可以包含一个或多个CMD指令。对于像 NGINX 这样的长时间运行的进程,最后一个CMD应该包含一些不会将控制权交还给 shell 并且能够持续运行至容器生命周期结束的命令。在这种情况下,我们运行nginx -g daemon off;,这是一种标准的方式来在前台运行 NGINX。
一些指令很容易混淆,比如ENTRYPOINT和CMD,或者CMD和RUN。这些也能考察你对 Docker 基础的掌握程度,所以我们来看看这两者的区别。
我们可以用ENTRYPOINT替代CMD吗?
你可以用ENTRYPOINT替代CMD。虽然它们服务的目的是类似的,但它们是两个完全不同的指令。每个 Docker 容器都有一个默认的ENTRYPOINT——/bin/sh -c。你在CMD中添加的内容会被追加到ENTRYPOINT之后并执行;例如,CMD ["nginx", "-g", "daemon off;"]将被生成为/bin/sh -c nginx -g daemon off;。如果你使用自定义的ENTRYPOINT,那么在启动容器时使用的命令将被追加到它之后。因此,如果你定义了ENTRYPOINT ["nginx", "-g"],并使用docker run nginx daemon off;,你将得到类似的结果。
为了在启动容器时不添加任何CMD参数,您也可以使用ENTRYPOINT ["nginx", "-g", "daemon off;"]来获得类似的效果。
提示
除非有特定的CMD需求,否则应该使用ENTRYPOINT。使用ENTRYPOINT可以确保用户无法更改容器的默认行为,因此它是一种更安全的替代方案。
现在,让我们来看一下 RUN 与 CMD 的区别。
RUN 和 CMD 是一样的吗?
不,RUN 和 CMD 是不同的,它们有不同的用途。RUN 用于构建容器,仅在构建过程中修改文件系统,而 CMD 指令仅在容器运行后在可写容器层中执行。
虽然一个 Dockerfile 中可以有多个 RUN 语句,每个语句修改现有层并生成下一层,但如果 Dockerfile 中包含多个 CMD 指令,除了最后一个,其他都会被忽略。
RUN 指令用于在容器文件系统内执行语句来构建和定制容器镜像,从而修改镜像层。使用 CMD 指令的目的是为容器镜像提供默认的命令,这些命令将在运行时执行。这只会改变可写的容器文件系统。你还可以通过在 docker run 语句中传递自定义命令来覆盖这些命令。
现在,让我们开始构建我们的第一个容器镜像。
构建我们的第一个容器
构建容器镜像非常简单。实际上,它是一个一行命令:docker build -t <image-name>:version <build_context>。虽然我们将在 构建和管理容器镜像 部分详细讨论构建容器镜像,但首先让我们构建 Dockerfile:
$ docker build -t <your_dockerhub_user>/nginx-hello-world .
[+] Building 50.0s (7/7) FINISHED
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 171B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:bionic 2.4s
=> [1/3] FROM docker.io/library/ubuntu:bionic@sha256:152dc042… 2.8s
=> => resolve docker.io/library/ubuntu:bionic@sha256:152dc04… 0.0s
=> => sha256:152dc042… 1.33kB / 1.33kB 0.0s
=> => sha256:dca176c9… 424B / 424B 0.0s
=> => sha256:f9a80a55… 2.30kB / 2.30kB 0.0s
=> => sha256:7c457f21… 25.69MB / 25.69MB 1.0s
=> => extracting sha256:7c457f21… 1.6s
=> [2/3] RUN apt update && apt install -y curl 22.4s
=> [3/3] RUN apt update && apt install -y nginx 21.6s
=> exporting to image 0.6s
=> => exporting layers 0.6s
=> => writing image sha256:9d34cdda… 0.0s
=> => naming to docker.io/<your_dockerhub_user>/nginx-hello-world
你可能已经注意到,容器的名字前面有一个前缀。那是你的 Docker Hub 账户名。镜像的名字结构是 <registry-url>/<account-name>/<container-image-name>:<version>。
这里,我们有以下内容:
-
registry-url:Docker 注册表的 URL,默认为docker.io -
account-name:拥有该镜像的用户或账户 -
container-image-name:容器镜像的名称 -
version:镜像版本
现在,让我们使用以下命令从镜像创建一个容器:
$ docker run -d -p 80:80 <your_dockerhub_user>/nginx-hello-world
092374c4501560e96a13444ce47cb978b961cf8701af311884bfe…
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
092374c45015 <your_dockerhub "nginx -g 28 seconds Up 27 0.0.0.0:80->80/ loving_
_user>/nginx- 'daemon of…" ago seconds tcp, :::80->80/tcp noether
hello-world
在这里,我们可以看到容器已经启动并运行。
如果我们运行 curl localhost,我们会得到默认的 nginx html 响应:
$ curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</body>
</html>
太好了!我们已经使用 Dockerfile 构建了我们的第一个镜像。
如果我们想根据需求自定义镜像怎么办?实际上,没有人会想要一个仅响应默认 Welcome to nginx! 消息的 NGINX 容器,因此让我们创建一个索引页面并使用它代替:
$ vim index.html
Hello World! This is my first docker image!
这个输出一个自定义消息,而不是默认的 NGINX HTML 页面。
我们都知道,默认的 NGINX 目录包含 index.html 文件,路径是 /var/www/html。如果我们可以将 index.html 文件复制到该目录中,应该就能解决我们的问题。
所以,修改 Dockerfile,使其包含以下内容:
$ vim Dockerfile
FROM ubuntu:bionic
RUN apt update && apt install -y curl
RUN apt update && apt install -y nginx
WORKDIR /var/www/html/
ADD index.html ./
CMD ["nginx", "-g", "daemon off;"]
在这里,我们向文件中添加了两个指令:WORKDIR 和 ADD。让我们理解每个指令的作用:
-
WORKDIR:此指令定义当前工作目录,在本例中为/var/www/html。Dockerfile 中的最后一个WORKDIR指令也指定了容器执行时的工作目录。因此,如果你exec进入一个运行中的容器,你将进入最后定义的WORKDIR。WORKDIR可以是绝对路径,也可以是相对于当前工作目录的相对路径。 -
ADD:此指令将本地文件添加到容器文件系统——在此案例中是工作目录。你也可以使用COPY指令代替ADD,虽然ADD提供了一些额外的功能,比如从 URL 下载文件,或使用 TAR 或 ZIP 等存档包。
当我们构建此文件时,我们期望index.html文件被复制到容器文件系统中的/var/www/html目录。让我们来看一下:
$ docker build -t <your_dockerhub_user>/nginx-hello-world .
[+] Building 1.6s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 211B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:bionic 1.4s
=> [1/5] FROM docker.io/library/ubuntu:bionic@sha256:152dc042… 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 81B 0.0s
=> CACHED [2/5] RUN apt update && apt install -y curl 0.0s
=> CACHED [3/5] RUN apt update && apt install -y nginx 0s
=> [4/5] WORKDIR /var/www/html/ 0.0s
=> [5/5] ADD index.html ./ 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:cb2e67bd… 0.0s
=> => naming to docker.io/<your_dockerhub_user>/nginx-hello-world
这次,构建速度更快了!当我们执行 Docker 构建时,它使用了大量来自缓存的层。这就是分层架构的优势之一;你只需要构建变化的部分,其他部分则可以直接使用现有的。
提示
安装完包和依赖后,始终添加源代码。源代码经常变动,而包的内容大致保持不变。这将加快构建速度,并节省大量 CI/CD 时间。
让我们重新运行容器,看看会得到什么。请注意,重新运行前需要先删除旧的容器:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
092374c45015 <your_dockerhub "nginx -g 28 seconds Up 27 0.0.0.0:80->80/ loving_
_user>/nginx- 'daemon of…" ago seconds tcp, :::80->80/tcp noether
hello-world
$ docker rm 092374c45015 -f
092374c45015
此时,我们无法再看到容器了。现在,让我们使用以下命令重新运行容器:
$ docker run -d -p 80:80 <your_dockerhub_user>/nginx-hello-world
cc4fe116a433c505ead816fd64350cb5b25c5f3155bf5eda8cede5a4…
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cc4fe116a433 <your_dockerhub "nginx -g 52 seconds Up 50 0.0.0.0:80->80/ eager_
_user>/nginx- 'daemon of…" ago seconds tcp, :::80->80/tcp gates
hello-world
在这里,我们可以看到容器正在运行。让我们使用curl localhost看看会得到什么结果:
$ curl localhost
Hello World! This is my first docker image!
在这里,我们看到的是一个自定义消息,而不是默认的 NGINX HTML 响应!
现在这看起来已经足够好了,但我将讨论更多的指令,以使这个镜像更加可靠。首先,我们没有明确记录此容器应暴露的端口。虽然这样做也能正常工作,因为我们知道 NGINX 运行在80端口,但如果有人想使用你的镜像却不知道端口怎么办呢?在这种情况下,最好显式地定义端口。我们将使用EXPOSE指令来实现这一点。
提示
始终使用EXPOSE指令为你的镜像提供更多的清晰度和意义。
我们还需要定义如果有人发送docker stop命令时,容器进程应该采取的动作。虽然大多数进程会接受这个信号并杀死进程,但明确指定容器在接收到docker stop命令时应该发送什么STOPSIGNAL是有意义的。我们将使用STOPSIGNAL指令来实现这一点。
现在,虽然 Docker 监控容器进程并保持其运行,除非接收到SIGTERM信号或停止命令,但如果你的容器进程因为某些原因挂起会发生什么呢?当你的应用程序处于挂起状态时,Docker 仍然认为它在运行,因为你的进程依然在运行。因此,通过显式的健康检查来监控应用程序是有意义的。我们将使用HEALTHCHECK指令来实现这一点。
让我们将这些方面结合起来,看看在 Dockerfile 中会得到什么结果:
$ vim Dockerfile
FROM ubuntu:bionic
RUN apt update && apt install -y curl
RUN apt update && apt install -y nginx
WORKDIR /var/www/html/
ADD index.html ./
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
STOPSIGNAL SIGTERM
HEALTHCHECK --interval=60s --timeout=10s --start-period=20s --retries=3 CMD curl -f
localhost
虽然EXPOSE和STOPSIGNAL不言自明,但我们来看看HEALTHCHECK指令。HEALTHCHECK指令会运行一个名为curl -f localhost的命令(因此是CMD)。所以,在curl命令执行成功之前,这个容器会报告为健康状态。
HEALTHCHECK指令还包含以下可选字段:
-
--interval (默认值: 30s):两次健康检查之间的间隔时间。 -
--timeout (默认值: 30s):健康检查探针超时。如果健康检查超时,则表示健康检查失败。 -
--start-period (默认值: 0s):启动容器和第一次健康检查之间的时间间隔。这样可以确保容器在进行健康检查之前已启动。 -
--retries (默认值: 3):探针在宣布容器不健康之前会重试的次数。
现在,让我们构建这个容器:
$ docker build -t <your_dockerhub_user>/nginx-hello-world .
[+] Building 1.3s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 334B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:bionic 1.2s
=> [1/5] FROM docker.io/library/ubuntu:bionic@sha256:152dc0… 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 31B 0.0s
=> CACHED [2/5] RUN apt update && apt install -y curl 0.0s
=> CACHED [3/5] RUN apt update && apt install -y nginx 0s
=> CACHED [4/5] WORKDIR /var/www/html/ 0.0s
=> CACHED [5/5] ADD index.html ./ 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:bba3123d… 0.0s
=> => naming to docker.io/<your_dockerhub_user>/nginx-hello-world
是时候运行它,看看结果如何:
$ docker run -d -p 80:80 <your_dockerhub_user>/nginx-hello-world
94cbf3fdd7ff1765c92c81a4d540df3b4dbe1bd9748c91e2ddf565d8…
现在我们已经成功启动了容器,接下来让我们尝试ps命令,看看会得到什么结果:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
94cbf3fdd7ff <your_dockerhub "nginx -g 5 seconds Up 4 0.0.0.0:80->80/ wonderful_
_user>/nginx- 'daemon of…" ago (health: tcp, :::80->80/tcp hodgkin
hello-world starting)
正如我们所看到的,容器显示health: starting,这意味着健康检查尚未开始,我们正在等待启动时间到期。
让我们等一会儿,然后再试一次docker ps:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
94cbf3fdd7ff <your_dockerhub "nginx -g 2 minutes Up 2 0.0.0.0:80->80/ wonderful_
_user>/nginx- 'daemon of…" ago (healthy) tcp, :::80->80/tcp hodgkin
hello-world
这次,它报告容器为健康状态。因此,我们的容器现在更可靠了,因为任何监控它的人都会知道应用程序的哪一部分是健康的,哪一部分不是。
这个健康检查只报告容器的健康状态,不会执行其他操作。你需要定期监控容器,并编写脚本来处理不健康的容器。
管理这种情况的一种方法是创建一个脚本,用于检查不健康的容器并重新启动它们。你可以在 crontab 中安排这个脚本。你也可以创建一个长时间运行的systemd脚本,不断轮询容器进程并检查健康状态。
提示
尽管使用HEALTHCHECK是一个不错的选择,但你应该避免在 Kubernetes 或类似的容器编排工具中运行容器时使用它。你应该改为使用存活探针和就绪探针。类似地,如果你使用 Docker Compose,也可以在其中定义健康检查,因此应使用它,而不是将健康检查嵌入容器镜像。
现在,让我们继续学习如何构建和管理 Docker 镜像。
构建和管理 Docker 镜像
在上一节中,我们构建了一些 Docker 镜像,到目前为止,你应该知道如何编写 Dockerfile 并从中创建 Docker 镜像。我们还讨论了一些最佳实践,简而言之,内容如下:
-
总是先添加那些不常更改的层,再添加可能频繁更改的层。例如,先安装你的包和依赖,再复制源代码。Docker 会从你更改的部分开始构建 Dockerfile,直到结束,因此,如果你修改了后面的某一行,Docker 会从缓存中获取所有已有的层。将更常变动的部分放在构建的后面,有助于减少构建时间,从而实现更快的 CI/CD 体验。
-
将多个命令合并为尽可能少的层。避免使用多个连续的
RUN指令。相反,使用&&子句将它们合并成一个RUN指令。这将有助于减少容器的总体体积。 -
仅将必需的文件添加到容器中。如果你已经将代码编译成二进制文件,那么在运行容器时,容器不需要沉重的包管理器和 Go 工具包。我们将在接下来的章节中详细讨论如何做到这一点。
Docker 镜像传统上是通过在 Dockerfile 中指定一系列步骤来构建的。但正如我们所知道的,Docker 是符合 DevOps 的,并从一开始就使用配置管理实践。大多数人会在 Dockerfile 中构建他们的代码。因此,我们也需要在构建上下文中包含编程语言库。通过一个简单的顺序 Dockerfile,这些编程语言工具和库最终会出现在容器镜像中。这些被称为单阶段构建,接下来我们将讨论这一点。
单阶段构建
让我们将一个简单的 Go 应用程序容器化,该程序在屏幕上打印 Hello, World!。虽然在这个应用程序中我使用的是 Golang,但这个概念是普遍适用的,无论使用什么编程语言。
本示例的相关文件位于本书 GitHub 仓库中的 ch4/go-hello-world/single-stage 目录下。
首先,让我们看一下 Go 应用程序文件 app.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Dockerfile 如下所示:
FROM golang:1.20.5
WORKDIR /tmp
COPY app.go .
RUN GOOS=linux go build -a -installsuffix cgo -o app . && chmod +x ./app
CMD ["./app"]
这是标准做法。我们使用 golang:1.20.5 基础镜像,声明一个 WORKDIR 为 /tmp,从主机文件系统将 app.go 复制到容器中,然后构建 Go 应用程序以生成一个二进制文件。最后,我们使用 CMD 指令来执行生成的二进制文件,当我们运行容器时,该文件将被执行。
让我们构建 Dockerfile:
$ docker build -t <your_dockerhub_user>/go-hello-world:single_stage .
[+] Building 10.3s (9/9) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 189B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/golang:1.20.5 0.6s
=> [1/4] FROM docker.io/library/golang:1.20.5@sha256:4b1fc02d… 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 27B 0.0s
=> [2/4] WORKDIR /tmp 0.0s
=> [3/4] COPY app.go . 0.0s
=> [4/4] RUN GO111MODULE=off GOOS=linux go build -a -installsuffix cgo -o app . && chmod
+x ./app 9.3s
=> exporting to image 0.3s
=> => exporting layers 0.3s
=> => writing image sha256:3fd3d261… 0.0s
=> => naming to docker.io/<your_dockerhub_user>/go-hello-world:single_stage
现在,让我们运行 Docker 镜像,看看我们能得到什么:
$ docker run <your_dockerhub_user>/go-hello-world:single_stage
Hello, World!
我们得到了预期的响应。现在,让我们运行以下命令来列出镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<your_dockerhub_user>
/go-hello-world single_stage 3fd3d26111a1 3 minutes ago 803MB
这个镜像好大啊!打印 Hello, World! 需要 803 MB。这不是构建 Docker 镜像的最有效方式。
在我们查看解决方案之前,先来了解一下为什么镜像一开始会这么臃肿。我们使用的是 Golang 基础镜像,它包含了完整的 Go 工具包,并生成一个简单的二进制文件。对于这个应用程序的运行,我们并不需要完整的 Go 工具包;它可以在 Alpine Linux 镜像中高效运行。
Docker 通过提供多阶段构建来解决这个问题。你可以将构建过程拆分成多个阶段,在其中一个阶段构建你的代码,然后在第二个阶段将构建好的代码导出到另一个上下文,该上下文从一个更轻的基础镜像开始,只包含运行代码所需的文件和组件。我们将在下一节中详细讨论这个过程。
多阶段构建
让我们按照多阶段构建过程修改 Dockerfile,看看结果如何。
本例的相关文件位于本书 GitHub 仓库中的 ch4/go-hello-world/multi-stage 目录下。
以下是 Dockerfile:
FROM golang:1.20.5 AS build
WORKDIR /tmp
COPY app.go .
RUN GO111MODULE=off GOOS=linux go build -a -installsuffix cgo -o app . && chmod +x ./app
FROM alpine:3.18.0
WORKDIR /tmp
COPY --from=build /tmp/app .
CMD ["./app"]
Dockerfile 包含两个 FROM 指令:FROM golang:1.20.5 AS build 和 FROM alpine:3.18.0。第一个 FROM 指令还包括一个 AS 指令,用于声明阶段并将其命名为 build。在此 FROM 指令之后所做的任何操作都可以通过 build 来访问,直到我们遇到另一个 FROM 指令,这将形成第二个阶段。由于第二个阶段是我们要运行镜像的地方,所以我们没有使用 AS 指令。
在第一阶段,我们使用 golang 基础镜像构建我们的 Golang 代码,生成二进制文件。
在第二阶段,我们使用 Alpine 基础镜像,并将构建阶段中的 /tmp/app 文件复制到当前阶段。这是我们在容器中运行所需的唯一文件。其他文件仅在构建过程中需要,用于在运行时膨胀我们的容器。
让我们构建镜像,看看得到什么:
$ docker build -t <your_dockerhub_user>/go-hello-world:multi_stage
[+] Building 12.9s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 259B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.18.0 2.0s
=> [internal] load metadata for docker.io/library/golang:1.20.5 1.3s
=> [build 1/4] FROM docker.io/library/golang:1.20.5@sha256:4b1fc02d… 0.0s
=> [stage-1 1/3] FROM docker.io/library/alpine:3.18.0@sha256:02bb6f42… 0.1s
=> => resolve docker.io/library/alpine:3.18.0@sha256:02bb6f42… 0.0s
=> => sha256:c0669ef3… 528B / 528B 0.0s
=> => sha256:5e2b554c… 1.47kB / 1.47kB 0.0s
=> => sha256:02bb6f42… 1.64kB / 1.64kB 0.0s
=> CACHED [build 2/4] WORKDIR /tmp 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 108B 0.0s
=> [build 3/4] COPY app.go . 0.0s
=> [build 4/4] RUN GO111MODULE=off GOOS=linux go build -a -installsuffix cgo -o app . &&
chmod +x ./app 10.3s
=> [stage-1 2/3] WORKDIR /tmp 0.1s
=> [stage-1 3/3] COPY --from=build /tmp/app . 0.3s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:e4b793b3… 0.0s
=> => naming to docker.io/<your_dockerhub_user>/go-hello-world:multi_stage
现在,让我们运行容器:
$ docker run <your_dockerhub_user>/go-hello-world:multi_stage .
Hello, World!
我们得到了相同的输出,但这次占用的空间更小。让我们查看镜像以确认这一点:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<your_dockerhub_user>
/go-hello-world multi_stage e4b793b39a8e 5 minutes ago 9.17MB
这个镜像仅占用 9.17 MB,而不是庞大的 803 MB。这是一个巨大的改进!我们已经将镜像大小减少了近 100 倍。
这就是我们在容器镜像中提高效率的方法。构建高效的镜像是运行生产就绪容器的关键,Docker Hub 上的大多数专业镜像都使用多阶段构建来创建高效的镜像。
提示
尽可能使用多阶段构建,以在镜像中包含最小的内容。如果可能,考虑使用 Alpine 基础镜像。
在下一节中,我们将讨论如何管理 Docker 镜像,最佳实践以及一些最常用的命令。
管理 Docker 镜像
在现代 DevOps 实践中,Docker 镜像通常是在开发者机器或 CI/CD 管道上构建的。这些镜像存储在容器注册中心,然后部署到多个预生产环境和生产机器上。这些机器可能运行 Docker 或一个容器编排工具,例如 Kubernetes。
为了高效使用镜像,我们必须理解如何标记它们。
主要情况下,Docker 会在你执行 Docker run 时拉取镜像一次。这意味着,一旦某个版本的镜像已经存在于机器上,Docker 就不会在每次运行时都重新拉取它,除非你显式地执行拉取操作。
要显式拉取镜像,你可以使用 docker pull 命令:
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
f03b40093957: Pull complete
eed12bbd6494: Pull complete
fa7eb8c8eee8: Pull complete
7ff3b2b12318: Pull complete
0f67c7de5f2c: Pull complete
831f51541d38: Pull complete
Digest: sha256:af296b18…
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
现在,如果我们尝试使用这个镜像启动容器,它将立即启动容器,而不拉取镜像:
$ docker run nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform
configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
…
2023/06/10 08:09:07 [notice] 1#1: start worker processes
2023/06/10 08:09:07 [notice] 1#1: start worker process 29
2023/06/10 08:09:07 [notice] 1#1: start worker process 30
因此,在镜像上使用最新标签是一个坏主意,最佳实践是使用语义版本作为标签。主要有两个原因:
-
如果每次都构建最新镜像,像 Docker Compose 和 Kubernetes 这样的编排工具会认为镜像已经在你的机器上,并默认不拉取镜像。在 Kubernetes 上使用像
Always这样的镜像拉取策略,或者用脚本拉取镜像,会浪费网络带宽。还需要注意的是,Docker Hub 限制了你对开源镜像的拉取次数,因此你必须将拉取限制为必要时才进行。 -
Docker 标签允许你快速推出或回滚容器部署。如果你总是使用最新标签,新的构建将覆盖旧的构建,因此你无法将故障容器回滚到最后一个已知的良好版本。在生产环境中使用版本化的镜像也是一个好主意,以确保容器的稳定性。如果由于某些原因,你丢失了本地镜像并决定重新运行容器,可能无法获得与你之前运行的相同版本的软件,因为最新标签经常变化。因此,最好在生产环境中使用特定的容器版本以确保稳定性。
镜像由多个层组成,通常,容器的不同版本在你的服务器上是有关系的。随着时间的推移,新的镜像版本会在你的生产环境中推出,因此通过进行一些清理来移除旧镜像是最好的做法。这将回收容器镜像占用的一些宝贵空间,从而使文件系统更加干净。
要移除特定镜像,你可以使用 docker rmi 命令:
$ docker rmi nginx
Error response from daemon: conflict: unable to remove repository reference "nginx" (must
force) - container d5c84356116f is using its referenced image f9c14fe76d50
哦!我们遇到错误了,为什么呢?因为我们有一个正在运行并使用该镜像的容器。
提示
你不能移除当前正在使用的镜像。
首先,你需要停止并移除容器。然后,你可以使用前面的命令移除镜像。如果你想一气呵成,可以通过使用 -f 标志强制移除,这将停止容器,移除容器,并移除镜像。所以,除非你知道自己在做什么,否则不要使用 -f 标志:
$ docker rmi -f nginx
Untagged: nginx:latest
Untagged: nginx@sha256:af296b18…
Deleted: sha256:f9c14fe7…
我们已经构建了容器多次,但如果我们需要将其推送到 Docker Hub 或其他注册中心该怎么办?但在此之前,我们需要通过以下命令对其进行 Docker Hub 身份验证:
$ docker login
现在,你可以使用以下命令将镜像推送到 Docker Hub:
$ docker push <your_dockerhub_user>/nginx-hello-world:latest
The push refers to repository [docker.io/<your_dockerhub_user>/nginx-hello-world]
2b7de406bdcd: Pushed
5f70bf18a086: Pushed
845348333310: Pushed
96a9e6a097c6: Pushed
548a79621a42: Mounted from library/ubuntu
latest: digest: sha256:11ec56f0… size: 1366
这已经推送了四层,并将其余部分从 Ubuntu 挂载。我们使用了 Ubuntu 作为基础镜像,该镜像已经存在于 Docker Hub 上。
如果你有多个镜像标签,并且希望推送所有标签,那么可以在push命令中使用-a或--all-tags选项。这样会将该镜像的所有标签一起推送:
$ docker push -a <your_dockerhub_user>/go-hello-world
The push refers to repository [docker.io/<your_dockerhub_user>/go-hello-world]
9d61dbd763ce: Pushed
5f70bf18a086: Mounted from <your_dockerhub_user>/nginx-hello-world
bb01bd7e32b5: Mounted from library/alpine
multi_stage: digest: sha256:9e1067ca… size: 945
445ef31efc24: Pushed
d810ccdfdc04: Pushed
5f70bf18a086: Layer already exists
70ef08c04fa6: Mounted from library/golang
41cf9ea1d6fd: Mounted from library/golang
d4ebbc3dd11f: Mounted from library/golang
b4b4f5c5ff9f: Mounted from library/golang
b0df24a95c80: Mounted from library/golang
974e52a24adf: Mounted from library/golang
single_stage: digest: sha256:08b5e52b… size: 2209
当构建因为某种原因失败并且你修改了 Dockerfile 时,旧镜像的层可能会保持悬空。因此,定期清理悬空镜像是最佳实践。你可以使用docker images prune来完成这一操作:
$ docker images prune
REPOSITORY TAG IMAGE ID CREATED SIZE
在下一节中,我们将探讨提高 Docker 镜像效率的另一种方法:扁平化 Docker 镜像。
扁平化 Docker 镜像
Docker 本身使用分层文件系统,我们已经深入讨论过它为什么必要以及如何带来好处。然而,在某些特定的使用场景下,Docker 实践者观察到,拥有更少层的 Docker 镜像表现更好。你可以通过扁平化镜像来减少镜像中的层。然而,这仍然不是最佳实践,只有在你看到性能提升的情况下才应这么做,因为这会导致文件系统开销。
扁平化 Docker 镜像的步骤如下:
-
使用常规镜像运行一个 Docker 容器。
-
对正在运行的容器进行
docker export操作,将其导出为.tar文件。 -
对
.tar文件进行docker import操作,将其导入到另一个镜像中。
让我们使用nginx-hello-world镜像进行扁平化并导出到另一个镜像中;也就是说,<your_dockerhub_user>/nginx-hello-world:flat。
在继续之前,让我们查看最新镜像的历史记录:
$ docker history <your_dockerhub_user>/nginx-hello-world:latest
IMAGE CREATED CREATED BY SIZE COMMENT
bba3123dde01 2 hours ago HEALTHCHECK &
{["CMD-SHELL"
"curl -f localhos… 0B buildkit.dockerfile.v0
<missing> 2 hours ago STOPSIGNAL 0B
SIGTERM 0B buildkit.dockerfile.v0
<missing> 2 hours ago CMD ["nginx"
"-g" "daemon off;"] 0B buildkit.dockerfile.v0
<missing> 2 hours ago EXPOSE map[80/
tcp:{}] 0B buildkit.dockerfile.v0
<missing> 2 hours ago ADD index.html ./ #
buildkit 44B buildkit.dockerfile.v0
<missing> 2 hours ago WORKDIR /var/www/
html/ 0B buildkit.dockerfile.v0
<missing> 2 hours ago RUN /bin/sh -c apt
update && apt 57.2MB buildkit.dockerfile.v0
install -y…
<missing> 2 hours ago RUN /bin/sh -c apt
update && apt 59.8MB buildkit.dockerfile.v0
install -y…
<missing> 10 days ago /bin/sh -c #(nop) 0B
CMD ["/bin/bash"]
<missing> 10 days ago /bin/sh -c #(nop) ADD 63.2MB
file:3c74e7e08cbf9a876…
<missing> 10 days ago /bin/sh -c #(nop) LABEL 0B
org.opencontainers.…
<missing> 10 days ago /bin/sh -c #(nop) LABEL 0B
org.opencontainers.…
<missing> 10 days ago /bin/sh -c #(nop) ARG 0B
LAUNCHPAD_BUILD_ARCH
<missing> 10 days ago /bin/sh -c #(nop) 0B
ARG RELEASE
现在,让我们运行一个最新的 Docker 镜像:
$ docker run -d --name nginx <your_dockerhub_user>/nginx-hello-world:latest
e2d0c4b884556a353817aada13f0c91ecfeb01f5940e91746f168b…
接下来,让我们从正在运行的容器中导出:
$ docker export nginx > nginx-hello-world-flat.tar
将nginx-hello-world-flat.tar导入为一个新镜像;也就是说,<your_dockerhub_user>/nginx-hello-world:flat:
$ cat nginx-hello-world-flat.tar | \
docker import - <your_dockerhub_user>/nginx-hello-world:flat
sha256:57bf5a9ada46191ae1aa16bcf837a4a80e8a19d0bcb9fc…
现在,让我们列出镜像并看看得到的结果:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<your_dockerhub_user>/ flat 57bf5a9ada46 34 seconds 177MB
nginx-hello-world ago
<your_dockerhub_user>/ latest bba3123dde01 2 hours
nginx-hello-world ago 180MB
在这里,我们可以看到扁平化后的镜像,并且它占用的空间比最新镜像少。如果查看它的历史记录,我们应该只看到一个层:
$ docker history <your_dockerhub_user>/nginx-hello-world:flat
IMAGE CREATED CREATED BY SIZE COMMENT
57bf5a9ada46 About a minute ago 177MB Imported from -
它已经将镜像进行了扁平化。但将 Docker 镜像扁平化是最佳实践吗?嗯,这取决于情况。让我们来了解一下什么时候以及如何扁平化 Docker 镜像,以及你需要考虑哪些因素:
-
是否有多个应用程序使用相似的基础镜像?如果是这样,扁平化镜像只会增加磁盘占用,因为你无法利用分层文件系统的优势。
-
考虑使用小型基础镜像(如 Alpine)作为扁平化镜像的替代方案。
-
多阶段构建对于大多数编译语言非常有用,并且可以显著减少镜像大小。
-
你还可以通过将多个步骤合并为单个
RUN指令,使用尽可能少的层来缩小镜像大小。 -
考虑一下扁平化镜像的好处是否超过其缺点,是否会带来显著的性能提升,以及性能是否对你的应用程序需求至关重要。
这些考虑因素将帮助你理解容器镜像的占用空间,并帮助你管理容器镜像。记住,尽管减少镜像的大小是理想的,但将其压缩应作为最后的手段。
到目前为止,我们使用的所有镜像都是从 Linux 发行版派生的,且总是使用某个发行版作为其基础镜像。你也可以在不使用 Linux 发行版作为基础镜像的情况下运行容器,以提高安全性。我们将在下一部分中探讨如何做到这一点。
使用无发行版镜像优化容器
无发行版容器是容器世界中的最新趋势之一。它们很有前景,因为它们考虑了为企业环境优化容器的各个方面。在优化容器时,你应该考虑三个重要因素——性能、安全性和成本。
性能
你不能凭空创建容器。你必须从容器注册表中下载镜像,然后从镜像中运行容器。每个步骤都涉及网络和磁盘 I/O。镜像越大,消耗的资源就越多,性能就越差。因此,更小的 Docker 镜像自然表现更好。
安全性
安全性是当前 IT 环境中最重要的方面之一。公司通常会专注于这一点,并投入大量的时间和资金。由于容器是一项相对较新的技术,它们容易受到黑客攻击,因此,适当保护你的容器至关重要。标准的 Linux 发行版包含了许多可以让黑客访问更多内容的组件,而如果你正确保护容器,它们本来是无法做到的。因此,你必须确保容器内只有必要的内容。
成本
更小的镜像也意味着更低的成本。容器的占用空间越小,你就可以在一台机器中容纳更多的容器,从而减少运行应用程序所需的机器数量。这意味着你可以节省大量随着时间积累的费用。
作为一名现代 DevOps 工程师,你必须确保你的镜像在所有这些方面都得到了优化。无发行版镜像有助于解决这些问题。因此,让我们了解一下什么是无发行版镜像以及如何使用它们。
无发行版镜像是最简化的镜像,仅包含你的应用程序、依赖项以及容器进程运行所需的文件。大多数情况下,你不需要像 apt 这样的包管理器或像 bash 这样的 shell。没有 shell 有其优势。例如,它能帮助你避免任何外部方在容器运行时获得访问权限。你的容器拥有较小的攻击面,因此不会有太多安全漏洞。
Google 在其官方 GCR 注册表中提供了无发行版镜像,这些镜像可以在他们的 GitHub 页面找到,链接为 github.com/GoogleContainerTools/distroless。让我们动手操作,看看我们能用它们做些什么。
本练习所需的资源在本书的 GitHub 仓库中的 ch4/go-hello-world/distroless 文件夹内。
让我们先创建一个 Dockerfile:
FROM golang:1.20.5 AS build
WORKDIR /tmp
COPY app.go .
RUN GO111MODULE=off GOOS=linux go build -a -installsuffix cgo -o app . && chmod +x ./app
FROM gcr.io/distroless/base
WORKDIR /tmp
COPY --from=build /tmp/app .
CMD ["./app"]
这个 Dockerfile 类似于 go-hello-world 容器的多阶段构建 Dockerfile,但它使用 gcr.io/distroless/base 作为基础镜像,而不是 alpine。这个镜像包含一个最简化的 Linux glibc 系统,不带包管理器或 shell。你可以用它来运行用 Go、Rust 或 D 等语言编译的二进制文件。
所以,先使用以下命令构建它:
$ docker build -t <your_dockerhub_user>/go-hello-world:distroless .
[+] Building 7.6s (14/14) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 268B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for gcr.io/distroless/base:latest 3.1s
=> [internal] load metadata for docker.io/library/golang:1.20.5 1.4s
=> [auth] library/golang:pull token for registry-1.docker.io 0.0s
=> [stage-1 1/3] FROM gcr.io/distroless/base@
sha256:73deaaf6a207c1a33850257ba74e0f196bc418636cada9943a03d7abea980d6d 3.2s
=> [build 1/4] FROM docker.io/library/golang:1.20.5@sha256:4b1fc02d 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 108B 0.0s
=> CACHED [build 2/4] WORKDIR /tmp 0.0s
=> CACHED [build 3/4] COPY app.go . 0.0s
=> CACHED [build 4/4] RUN GO111MODULE=off GOOS=linux go build -a -installsuffix cgo -o
app . && chmod +x ./app 0.0s
=> [stage-1 2/3] WORKDIR /tmp 0.9s
=> [stage-1 3/3] COPY --from=build /tmp/app . 0.3s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:51ced401 0.0s
=> => naming to docker.io/<your_dockerhub_user>/go-hello-world:distroless
现在,让我们运行这个镜像,看看结果:
$ docker run <your_dockerhub_user>/go-hello-world:distroless
Hello, World!
它有效!让我们来看一下图像的大小:
$ docker images
REPOSITORY TAG MAGE ID CREATED SIZE
<your_dockerhub_user>/go-hello-world distroless 51ced401d7bf 6 minutes ago 22.3MB
它只有 22.3 MB。是的,比 Alpine 镜像稍大,但它不包含 shell,因此从这个角度看,它更安全。此外,还有适用于解释型编程语言(如 Python 和 Java)的 distroless 镜像,你可以使用这些代替包含工具包的庞大镜像。
Docker 镜像存储在 Docker 注册表中,我们已经使用 Docker Hub 一段时间了。在接下来的章节中,我们将了解它们是什么,以及我们存储镜像的选项。
理解 Docker 注册表
Docker 注册表 是一个无状态、高度可扩展的服务器端应用程序,用于存储和分发 Docker 镜像。该注册表在宽松的 Apache 许可证 下开源。它是一个存储和分发系统,所有 Docker 服务器都可以连接到它,并根据需要上传和下载镜像。它充当你的镜像分发站点。
一个 Docker 注册表包含多个 Docker 仓库。一个 Docker 仓库保存特定镜像的多个版本。例如,所有版本的 nginx 镜像都存储在 Docker Hub 中名为 nginx 的单一仓库内。
默认情况下,Docker 与其公共 Docker 注册表实例 Docker Hub 交互,Docker Hub 帮助你将镜像分发到更广泛的开源社区。
并非所有镜像都可以公开和开源,许多专有活动仍在进行中。Docker 允许你使用私有 Docker 注册表,这是一种可以在你自己的基础设施内托管的场景,称为 Docker Trusted Registry。有多个在线选项可用,包括使用 SaaS 服务,如 GCR,或在 Docker Hub 上创建私有仓库。
虽然 SaaS 选项易于使用且直观,但让我们考虑托管我们自己的私有 Docker 注册表。
托管你的私有 Docker 注册表
Docker 提供了一个镜像,你可以在任何安装了 Docker 的服务器上运行。一旦容器启动并运行,你可以将其用作 Docker 注册表。我们来看一下:
$ docker run -d -p 80:5000 --restart=always --name registry registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
8a49fdb3b6a5: Already exists
58116d8bf569: Pull complete
4cb4a93be51c: Pull complete
cbdeff65a266: Pull complete
6b102b34ed3d: Pull complete
Digest: sha256:20d08472…
Status: Downloaded newer image for registry:2
ae4c4ec9fc7b17733694160b5b3b053bd1a41475dc4282f3eccaa10…
由于我们知道注册表运行在本地主机上并监听端口80,我们现在尝试将镜像推送到这个注册表。首先,让我们标记镜像,指定localhost作为注册表。我们将在 Docker 标签前加上注册表位置,以便 Docker 知道将镜像推送到哪里。我们已经知道 Docker 标签的结构是<registry_url>/<user>/<image_name>:<image_version>。我们将使用docker tag命令给现有镜像另起一个名字,如下所示:
$ docker tag your_dockerhub_user>/nginx-hello-world:latest \
localhost/<your_dockerhub_user>/nginx-hello-world:latest
现在,我们可以继续将镜像推送到本地 Docker 注册表:
$ docker push localhost/<your_dockerhub_user>/nginx-hello-world:latest
The push refers to repository [localhost/your_dockerhub_user/nginx-hello-world]
2b7de406bdcd: Pushed
5f70bf18a086: Pushed
845348333310: Pushed
96a9e6a097c6: Pushed
548a79621a42: Pushed
latest: digest: sha256:6ad07e74… size: 1366
就是这样!简单至极!
还有其他考虑因素,因为这太过简化了。你还需要挂载卷;否则,在重启注册表容器时,你将丢失所有镜像。另外,目前没有身份验证机制,因此任何访问该服务器的人都可以推送或拉取镜像,但我们并不希望如此。此外,通信是不安全的,我们希望在传输过程中对镜像进行加密。
首先,让我们创建将要挂载到容器中的本地目录:
$ sudo mkdir -p /mnt/registry/certs
$ sudo mkdir -p /mnt/registry/auth
$ sudo chmod -R 777 /mnt/registry
现在,让我们生成一个htpasswd文件,为注册表添加身份验证。为此,我们将在新的 Docker 注册表容器内运行htpasswd命令,创建一个文件到我们的本地目录:
$ docker run --entrypoint htpasswd registry:2.7.0 \
-Bbn user pass > /mnt/registry/auth/htpasswd
下一步是生成一些自签名证书,以启用仓库的 TLS。输入服务器名称或 IP 地址时,输入完全限定域名 (FQDN)。你可以将其他字段留空,或者为它们添加合适的值:
$ openssl req -newkey rsa:4096 -nodes -sha256 -keyout \
/mnt/registry/certs/domain.key -x509 -days 365 -out /mnt/registry/certs/domain.crt
在我们继续之前,先删除现有的注册表:
$ docker rm -f registry
registry
现在,我们准备好启动我们的容器,并配置所需的设置:
$ docker run -d -p 443:443 --restart=always \
--name registry \
-v /mnt/registry/certs:/certs \
-v /mnt/registry/auth:/auth \
-v /mnt/registry/registry:/var/lib/registry \
-e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
-e REGISTRY_AUTH=htpasswd \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
registry:2
02bf92c9c4a6d1d9c9f4b75ba80e82834621b1570f5f7c4a74b215960
容器现在已启动并运行。我们这次使用https,但在此之前,我们需要进行docker login到注册表。输入你在创建htpasswd文件时设置的用户名和密码(此例中为user和pass):
$ docker login https://localhost
Username: user
Password:
WARNING! Your password will be stored unencrypted in /root/
.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/
#credentials-store
Login Succeeded
由于登录成功,我们可以继续将镜像推送到注册表:
$ docker push localhost/<your_dockerhub_user>/nginx-hello-world
The push refers to repository [localhost/<your_dockerhub_user>/nginx-hello-world]
2b7de406bdcd: Pushed
5f70bf18a086: Pushed
845348333310: Pushed
96a9e6a097c6: Pushed
548a79621a42: Pushed
latest: digest: sha256:6ad07e7425331456a3b8ea118bce36c82af242ec14072d483b5dcaa3bd607e65
size: 1366
这次,它按我们希望的方式工作。
其他公共注册表
除了在专用的 Docker 服务器上运行注册表外,其他云和本地部署选项也存在。
大多数公共云服务提供商都提供付费的在线注册表和容器托管解决方案,你可以在云端运行时轻松使用它们。以下是一些例子:
-
Amazon Elastic Container Registry (ECR):这是一个流行的 AWS 服务,如果你的基础设施运行在 AWS 上,你可以使用它。它是一个高可用、高性能、完全托管的解决方案。它可以托管公共和私有注册表,你只需为所使用的存储和传输到互联网的数据量付费。最棒的是,它可以与 AWS IAM 集成。
-
Google 容器注册表(GCR):由Google Cloud Storage(GCS)提供支持,如果你的基础设施运行在 GCP 上,GCR 是最好的选择之一。它支持公共和私有仓库,你只需要为 GCS 上的存储付费。
-
Azure 容器注册表(ACR):这是一个完全托管的、地理复制的容器注册表,仅支持私有注册表。如果你在 Azure 上运行基础设施,它是一个不错的选择。除了存储容器镜像外,它还存储 Helm charts 和其他有助于管理容器的工件。
-
Oracle Cloud 基础设施注册表:Oracle Cloud 基础设施注册表是一个高度可用的 Oracle 托管容器注册表。它可以托管公共和私有仓库。
-
CoreOS Quay:此服务支持 OAuth 和 LDAP 身份验证。它提供(付费)私有仓库和(免费)公共仓库,自动安全扫描,并通过与 GitLab、GitHub 和 Bitbucket 的集成进行自动镜像构建。
如果你不想选择云中的托管选项或不希望在本地运行,你还可以使用分发管理软件,如Sonatype Nexus或JFrog Artifactory。这两款工具都开箱即用地支持 Docker 注册表。你可以通过它们的华丽界面创建 Docker 注册表,然后使用docker login连接到注册表。
总结
在这一章中,我们已经覆盖了很多内容。此时,你应该能够从实际操作的角度理解 Docker。我们从 Docker 镜像开始,讲解了如何使用 Dockerfile 构建 Docker 镜像、Dockerfile 的组件和指令,以及如何通过遵循一些最佳实践来创建高效的镜像。我们还讨论了如何通过使用无发行版镜像来简化 Docker 镜像并提高容器安全性。最后,我们讨论了 Docker 注册表、如何在 Docker 服务器上运行私有 Docker 注册表,以及如何使用其他即插即用的解决方案,如 Sonatype Nexus 和 JFrog Artifactory。
以下是一些管理 Docker 容器的最佳实践的快速总结:
-
使用官方镜像:尽可能使用来自可信来源(如 Docker Hub)的官方 Docker 镜像。这些镜像得到了良好的维护,定期更新,通常遵循更好的安全实践。
-
最小化容器:遵循“每个容器一个服务”的原则。每个容器应该承担单一责任,这有助于维护性和可扩展性。
-
优化容器大小:尽可能保持容器轻量化。使用 Alpine Linux 或其他最小化的基础镜像,并删除不必要的文件和依赖。
-
使用环境变量:将配置和敏感数据存储在环境变量中,而不是硬编码到容器中。这可以提高可移植性和安全性。
-
持久化数据:使用 Docker 卷或绑定挂载将应用数据存储在容器外部。这样可以确保即使容器被替换或停止,数据仍然能够持久存在。
-
容器命名:为容器赋予有意义且唯一的名称。这有助于便于识别和故障排除。
-
资源限制:为容器设置资源限制(CPU 和内存),以防一个不正常的容器影响同一主机上的其他容器。
-
容器重启策略:定义重启策略,以决定容器在退出或崩溃时的行为。根据应用程序的需求选择适当的策略。
-
Docker Compose:使用 Docker Compose 来定义和管理多容器应用程序。它简化了复杂设置的部署和协调。
-
网络隔离:使用 Docker 网络来隔离容器并控制它们之间的通信。这增强了安全性和可管理性。
-
健康检查:在容器中实现健康检查,确保它们按预期运行。这有助于自动化监控和恢复。
-
stdout)和标准错误(stderr)流。这使得使用 Docker 的日志机制收集和分析日志变得更容易。 -
安全最佳实践:保持容器的安全补丁更新,避免以 root 身份运行容器,并遵循安全最佳实践以避免漏洞。
-
版本控制 Dockerfile:将 Dockerfile 存储在版本控制系统中(例如 Git),并定期审查和更新它们。
-
容器清理:定期移除未使用的容器、镜像和卷,以释放磁盘空间。考虑使用 Docker 内置的清理命令等工具。
-
编排工具:探索如 Kubernetes 或 Docker Swarm 等容器编排工具,以管理更大且更复杂的容器部署。
-
文档:维护清晰且最新的容器和镜像文档,包括如何运行它们、所需的环境变量以及其他任何配置细节。
-
备份与恢复:建立容器数据和配置的备份与恢复流程,以便在故障发生时迅速恢复。
-
监控与扩展:为容器实现监控和告警,确保它们平稳运行。使用扩展机制来应对增加的负载。
通过遵循这些最佳实践,您可以确保 Docker 容器环境是有序、安全、可维护和可扩展的。
在下一章,我们将深入探讨使用 Kubernetes 进行容器编排。
问题
-
Docker 镜像使用分层模型。(对/错)
-
如果一个容器正在使用某个镜像运行,您仍然可以从服务器上删除该镜像。(对/错)
-
如何从服务器中移除一个正在运行的容器?(选择两项)
A.
dockerrm <container_id>B.
docker rm -``f <container_id>C.
docker stop <container_id> && dockerrm <container_id>D.
docker stop -``f <container_id> -
以下哪个选项是容器构建的最佳实践?(选择四项)
A. 总是将不常更改的层添加到 Dockerfile 的开始部分。
B. 将多个步骤合并为单一指令,以减少层级。
C. 只在容器中使用必要的文件,以保持容器轻量并减少攻击面。
D. 在 Docker 标签中使用语义版本控制,避免使用最新版本。
E. 在容器中包含包管理器和 shell,这有助于调试正在运行的容器。
F. 仅在 Dockerfile 的开头使用
apt update。 -
你应始终将 Docker 镜像压缩成单层。(对/错)
-
不带发行版的容器包含一个 shell。(对/错)
-
改善容器效率的方法有哪些?(选择四项)
A. 尽可能使用较小的基础镜像,例如 Alpine。
B. 仅使用多阶段构建将所需的库和依赖项添加到容器中,省略不必要的重型工具包。
C. 尽可能使用不带发行版的基础镜像。
D. 简化 Docker 镜像。
E. 使用单阶段构建来包含包管理器和 shell,因为这有助于生产环境中的故障排除。
-
定期修剪 Docker 镜像是一种最佳实践。(对/错)
-
健康检查应始终包含在 Docker 镜像中。(对/错)
答案
-
对
-
错误 – 不能删除正在被运行容器使用的镜像。
-
B, C
-
A, B, C, D
-
错误 – 只有在提高性能的情况下,才应简化 Docker 镜像。
-
错误 – 不带发行版的容器不包含 shell。
-
A, B, C, D
-
对
-
错误 – 如果使用 Kubernetes 或 Docker Compose,请使用存活探针或通过 YAML 文件定义健康检查。
第二部分:容器编排与无服务器架构
本部分将基于第一部分,并向您介绍使用容器编排和无服务器技术管理容器。在本部分中,您将学习如何使用先进的工具和技术在本地和云端管理容器。
本部分包括以下章节:
-
第五章,使用 Kubernetes 进行容器编排
-
第六章,管理高级 Kubernetes 资源
-
第七章,容器即服务(CaaS)与无服务器架构
第五章:使用 Kubernetes 进行容器编排
在上一章中,我们介绍了创建和管理容器镜像的内容,讨论了容器镜像、Dockerfile 及其指令和组件。我们还讨论了编写 Dockerfile 的最佳实践,以及如何构建和管理高效的镜像。接着,我们探讨了扁平化 Docker 镜像,并详细研究了无发行版镜像,以提高容器安全性。最后,我们创建了一个私有 Docker 注册中心。
现在,我们将深入探讨容器编排。我们将学习如何使用最流行的容器编排工具——Kubernetes,来调度和运行容器。
在本章中,我们将涵盖以下主要主题:
-
什么是 Kubernetes,为什么我需要它?
-
Kubernetes 架构
-
安装 Kubernetes(Minikube 和 KinD)
-
理解 Kubernetes Pod
技术要求
本章假设您已在具有 sudo 权限的 Linux 机器上安装了 Docker。您可以参考第三章,使用 Docker 容器化,获取更多关于如何操作的细节。
您还需要克隆以下 GitHub 存储库以进行一些练习:github.com/PacktPublishing/Modern-DevOps-Practices-2e。
运行以下命令将存储库克隆到您的主目录,并使用 cd 进入 ch5 目录以访问所需的资源:
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch5
由于该存储库包含带占位符的文件,因此您必须将 <your_dockerhub_user> 字符串替换为您实际的 Docker Hub 用户名。请使用以下命令来替换占位符:
$ grep -rl '<your_dockerhub_user>' . | xargs sed -i -e \
's/<your_dockerhub_user>/<your actual docker hub user>/g'
什么是 Kubernetes,为什么我需要它?
到现在为止,您应该了解容器是什么以及如何使用 Docker 构建和运行容器。然而,我们使用 Docker 运行容器的方式从生产角度来看并不理想。让我给你提供一些考虑事项:
-
由于便携式容器可以在任何 Docker 机器上顺利运行,多个容器还共享服务器资源以优化资源消耗。现在,想象一个由数百个容器组成的微服务应用程序。您将如何选择在哪台机器上运行容器?如果您希望根据资源消耗动态调度容器到另一台机器上呢?
-
容器提供了水平扩展能力,因为您可以创建容器的副本,并在一组容器前面使用负载均衡器。一种方法是提前决定并部署所需数量的容器,但这不是最优的资源利用方式。如果我告诉你,你需要根据流量动态水平扩展容器——换句话说,当流量增加时,创建额外的容器实例来处理额外的负载,而当流量减少时,减少容器实例呢?
-
容器有健康检查报告,显示容器的健康状态。如果容器不健康,并且你想让它自动修复该怎么办?如果整个服务器宕机,你希望将该服务器上的所有容器调度到其他地方,会发生什么?
-
由于容器大多运行在服务器内,并且能够彼此看到,那么我如何确保只有必要的容器能够互相交互,这是我们通常在虚拟机中做的事情?我们不能妥协于安全性。
-
现代云平台允许我们运行自动扩展的虚拟机(VM)。从容器的角度来看,我们如何利用这一点?例如,如果我在夜间只需要一台虚拟机来容纳我的容器,而白天需要五台,我该如何确保在需要时动态分配这些机器?
-
如果多个容器是更广泛服务网格的一部分,你如何管理它们之间的网络连接?
所有这些问题的答案是一个容器编排工具,而最受欢迎且事实上的标准就是 Kubernetes。
Kubernetes 是一个开源的容器编排工具。一群谷歌工程师最初开发了它,然后将其开源并交给了云原生计算基金会(CNCF)。从那时起,Kubernetes 的热度未曾减退,而且这是有充分理由的——Kubernetes 与容器的结合彻底改变了技术思维方式以及我们看待基础设施的方式。Kubernetes 不再将服务器视为专门为某个应用程序服务的机器,或者作为应用程序的一部分,而是允许将服务器可视化为一个已安装容器运行时的实体。当我们将服务器视为标准设置时,我们就能在一群服务器的集群中运行几乎任何东西。因此,你不必为技术栈中的每个应用程序单独规划高可用性(HA)、灾难恢复(DR)和其他运营方面的问题。相反,你可以将所有服务器聚集成一个单位——Kubernetes 集群——并将所有应用程序容器化。然后,你可以将所有容器管理功能交给 Kubernetes 来处理。你可以在裸金属服务器、虚拟机(VM)上运行 Kubernetes,或者通过多种 Kubernetes 作为服务的产品,在云中运行它。
Kubernetes 通过提供开箱即用的高可用性(HA)、可扩展性和零停机时间来解决这些问题。它基本上执行以下功能来提供这些功能:
-
提供集中式控制平面与其交互:API 服务器暴露了一个有用的 API 列表,你可以通过它调用许多 Kubernetes 功能。它还提供了一个名为 kubectl 的 Kubernetes 命令行工具,方便你使用简单的命令与 API 进行交互。拥有一个集中式控制平面确保了你可以无缝地与 Kubernetes 进行交互。
-
与容器运行时交互以调度容器:当我们向kube-apiserver发送请求调度容器时,Kubernetes 会根据各种因素决定将容器调度到哪个服务器,然后通过kubelet组件与服务器的容器运行时进行交互。
-
在键值数据存储中存储期望的配置:Kubernetes 应用集群的预期配置,并将其存储在键值数据存储中——etcd。这样,Kubernetes 会持续确保集群中的容器保持在期望的状态。如果有任何偏离预期状态的情况,Kubernetes 会采取措施将其恢复到期望的配置。通过这种方式,Kubernetes 确保你的容器始终正常运行并保持健康。
-
提供网络抽象层和服务发现:Kubernetes 使用网络抽象层来允许容器之间的通信。因此,每个容器都会分配一个虚拟 IP,Kubernetes 确保一个容器可以从运行在不同服务器上的另一个容器访问。它通过在服务器之间使用覆盖网络提供必要的网络连接。从容器的角度来看,集群中的所有容器就像是在同一台服务器上运行一样。Kubernetes 还使用DNS来通过域名允许容器之间的通信。这样,容器可以通过使用域名而不是 IP 地址来相互交互,从而确保如果容器被重新创建且 IP 地址发生变化时,你不需要更改配置。
-
与云提供商交互:Kubernetes 与云提供商交互,以调度诸如负载均衡器和持久磁盘等对象。因此,如果你告诉 Kubernetes 你的应用程序需要持久化数据并定义了一个卷,Kubernetes 会自动向你的云提供商请求磁盘,并将其挂载到运行容器的地方。你还可以通过向 Kubernetes 请求将应用程序暴露在外部负载均衡器上。Kubernetes 会与云提供商交互,启动负载均衡器并将其指向你的容器。通过这种方式,你可以仅通过与 Kubernetes API 服务器交互来处理所有与容器相关的事务。
Kubernetes 包含多个组件,它们负责处理我们讨论的每个功能。现在,让我们来看看 Kubernetes 的架构,以了解每个组件的作用。
Kubernetes 架构
Kubernetes 是由一组节点组成的集群。在 Kubernetes 中,节点有两种可能的角色——控制平面节点和工作节点。控制平面节点控制 Kubernetes 集群,调度工作负载、监听请求以及其他帮助运行工作负载和使集群运作的方面。它们通常构成集群的大脑。
另一方面,工作节点是 Kubernetes 集群的动力源,为运行容器工作负载提供原始计算能力。
Kubernetes 架构通过 API 服务器遵循客户端-服务器模型。所有的交互,包括组件之间的内部交互,都通过 Kubernetes API 服务器进行。因此,Kubernetes API 服务器被称为 Kubernetes 控制平面的“大脑”。
Kubernetes 还有其他组件,但在深入细节之前,让我们通过下面的图表来了解高层次的 Kubernetes 架构:
图 5.1 – Kubernetes 集群架构
控制平面包含以下组件:
-
API 服务器:如前所述,API 服务器暴露了一组 API,供外部和内部参与者与 Kubernetes 进行交互。所有与 Kubernetes 的交互都通过 API 服务器进行,从前面的图示可以看出。如果将 Kubernetes 集群想象成一艘船,API 服务器就是船长。
-
控制器管理器:控制器管理器是船上的执行官,负责确保船长的命令在集群中得到遵守。从技术角度来看,控制器管理器读取当前状态和目标状态,并采取一切必要的行动将当前状态转变为目标状态。它包含一组控制器,这些控制器根据需要通过 API 服务器与 Kubernetes 组件进行交互。以下是其中的一些:
-
节点控制器:该控制器监控节点何时宕机,并通过与 Kube 调度器 通过 Kube API 服务器 进行交互,将 Pods 调度到健康的节点上。
-
复制控制器:该控制器确保集群中定义的正确数量的容器副本存在。
-
终端控制器:这些控制器帮助通过服务为你的容器提供终端。
-
服务账户和令牌控制器:这些控制器为新的 命名空间 创建默认的 账户 和 令牌。
-
-
云控制器管理器:这是一个可选的控制器管理器,若你在公共云上运行 Kubernetes(例如 AWS、Azure 或 GCP),则需要运行此控制器管理器。云控制器管理器与云提供商的 API 进行交互,来配置你在 Kubernetes 配置中声明的资源,如 持久磁盘 和 负载均衡器。
-
etcd:etcd 是船的日志簿。这里存储着所有关于预期配置的详细信息。从技术角度来看,这是一个键值存储,存储着所有期望的 Kubernetes 配置。控制器管理器会参考这个数据库中的信息来执行集群中的更改。
-
调度器:调度器就像船只的水手长。它们负责监督容器在船上的装卸过程。Kubernetes 调度器会根据资源的可用性、应用程序的高可用性以及其他因素,在合适的工作节点上调度容器。
-
kubelet:kubelet 就像船员一样。它们实际执行容器从船上装卸的操作。从技术角度看,kubelet 与底层的容器运行时交互,根据调度器的指令运行容器。虽然大多数 Kubernetes 组件可以作为容器运行,但 kubelet 是唯一作为 systemd 服务运行的组件。它们通常运行在工作节点上,但如果你计划将控制平面组件作为容器运行,那么 kubelet 也会在控制平面节点上运行。
-
kube-proxy:kube-proxy 在每个工作节点上运行,为容器提供与集群内外网络组件交互的功能。它们是促进 Kubernetes 网络通信的关键组件。
好吧,这涉及很多环节,但好消息是,有现成的工具可以帮助你设置,而部署 Kubernetes 集群非常简单。如果你在公共云上运行,几次点击即可完成,你可以使用云提供商的 Web UI 或 CLI 来快速部署。如果是本地安装,你可以使用kubeadm进行设置。步骤文档完善,易于理解,也不会太麻烦。
对于开发和 CI/CD 环境,你可以使用Minikube或Docker 中的 Kubernetes(KinD)。Minikube 可以直接在你的开发机器上运行单节点 Kubernetes 集群,将机器作为节点使用;它也可以通过将 Kubernetes 节点作为容器来运行多节点集群。另一方面,KinD 仅在单节点和多节点配置中将节点作为容器运行。在这两种情况下,你都需要一个具有必要资源的虚拟机,然后就可以开始了。
在下一部分,我们将使用 Minikube 启动一个单节点 Kubernetes 集群。
安装 Kubernetes(Minikube 和 KinD)
现在,让我们继续进行 Kubernetes 的安装。我们将从 Minikube 开始,帮助你快速入门,然后再了解 KinD。接下来,我们将在本章的其余部分使用 KinD。
安装 Minikube
我们将在与安装 Docker 相同的 Linux 机器上安装 Minikube,参考第三章,使用 Docker 进行容器化。因此,如果你还没有进行该操作,请前往第三章,使用 Docker 进行容器化,并按照提供的说明在你的机器上设置 Docker。
首先,我们将安装 kubectl。如前所述,kubectl 是与 Kubernetes API 服务器交互的命令行工具。在本书中,我们将多次使用 kubectl。
要下载最新版本的 kubectl,请运行以下命令:
$ curl -LO "https://storage.googleapis.com/kubernetes-release/release\
/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)\
/bin/linux/amd64/kubectl"
你也可以下载 kubectl 的特定版本。为此,请使用以下命令:
$ curl -LO https://storage.googleapis.com/kubernetes-release/release\
/v<kubectl_version>/bin/linux/amd64/kubectl
我们将在本章中使用最新版本。现在,让我们继续使二进制文件可执行,然后将其移动到系统的 PATH 中的任何目录:
$ chmod +x ./kubectl
$ sudo mv kubectl /usr/local/bin/
现在,让我们运行以下命令检查 kubectl 是否已成功安装:
$ kubectl version --client
Client Version: version.Info{Major:"1", Minor:"27", GitVersion:"v1.27.3"}
由于 kubectl 已成功安装,接下来你需要下载 minikube 二进制文件,并使用以下命令将其移动到系统路径中:
$ curl -Lo minikube \
https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
$ chmod +x minikube
$ sudo mv minikube /usr/local/bin/
现在,让我们通过运行以下命令来安装 Minikube 正常运行所需的包:
$ sudo apt-get install -y conntrack
最后,我们可以使用以下命令启动一个 Minikube 集群:
$ minikube start --driver=docker
Done! kubectl is now configured to use "minikube" cluster and "default" namespace by
default
由于 Minikube 现在已经启动并运行,我们将使用 kubectl 命令行工具与 Kube API 服务器交互,以管理 Kubernetes 资源。kubectl 命令具有标准结构,并且大多数情况下易于理解。其结构如下:
kubectl <verb> <resource type> <resource name> [--flags]
这里,我们有以下内容:
-
动词:要执行的操作——例如get(获取)、apply(应用)、delete(删除)、list(列出)、patch(修补)、run(运行)等 -
资源类型:要管理的 Kubernetes 资源,例如node(节点)、pod(容器组)、deployment(部署)、service(服务)等 -
资源名称:要管理的资源的名称
现在,让我们使用 kubectl 获取节点并检查我们的集群是否准备好运行容器:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 2m25s v1.26.3
在这里,我们可以看到这是一个运行版本 v1.26.3 的单节点 Kubernetes 集群。Kubernetes 现在已经启动并运行!
这个设置非常适合开发机器,开发人员可以在其上部署并测试他们正在开发的单个组件。
要停止 Minikube 集群并将其从机器中删除,你可以使用以下命令:
$ minikube stop
既然我们已经移除了 Minikube,接下来让我们看看另一个创建多节点 Kubernetes 集群的有趣工具。
安装 KinD
KinD 允许你在运行 Docker 的单个服务器上运行一个多节点的 Kubernetes 集群。我们知道,运行一个多节点的 Kubernetes 集群需要多台机器,但如何在单台服务器上运行一个多节点 Kubernetes 集群呢?答案很简单:KinD 使用 Docker 容器作为 Kubernetes 节点。因此,如果我们需要一个四节点的 Kubernetes 集群,KinD 会启动四个容器,它们表现得就像四个 Kubernetes 节点。就这么简单。
尽管你需要 Docker 来运行 KinD,但 KinD 内部使用 containerd 作为容器运行时,而不是 Docker。Containerd 实现了容器运行时接口,因此 Kubernetes 不需要任何专门的组件,如 dockershim,与其交互。这意味着,KinD 仍然能够与 Kubernetes 配合使用,因为 Docker 不再被支持作为 Kubernetes 的容器运行时。
由于 KinD 支持多节点 Kubernetes 集群,你可以将其用于开发活动,也可以用于 CI/CD 管道。实际上,KinD 重新定义了 CI/CD 管道,因为你不需要一个静态的 Kubernetes 环境来测试你的构建。KinD 启动速度快,这意味着你可以将 KinD 集群的引导过程集成到 CI/CD 管道中,在集群内运行并测试你的容器构建,然后将其销毁。这为开发团队提供了巨大的力量和速度。
重要
永远不要在生产环境中使用 KinD。Docker in Docker 的实现并不安全;因此,KinD 集群不应超出你的开发环境和 CI/CD 管道。
引导 KinD 只需几个命令。首先,我们需要下载 KinD,确保它可执行,然后使用以下命令将其移动到默认的 PATH 目录中:
$ curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
$ chmod +x kind
$ sudo mv kind /usr/local/bin/
要检查是否已安装 KinD,可以运行以下命令:
$ kind version
kind v0.20.0 go1.20.4 linux/amd64
现在,让我们引导一个多节点的 KinD 集群。首先,我们需要创建一个 KinD config 文件。KinD config 文件是一个简单的 YAML 文件,你可以在其中声明每个节点所需的配置。如果我们需要引导一个单控制平面和三个工作节点的集群,可以添加以下配置:
$ vim kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
你还可以使用多个控制平面节点来实现高可用配置,在控制平面角色的节点上使用多个节点项。现在,我们先使用单个控制平面和三个工作节点的配置。
要使用前述配置引导你的 KinD 集群,请运行以下命令:
$ kind create cluster --config kind-config.yaml
这样,我们的 KinD 集群已经启动并运行了。现在,让我们使用以下命令列出节点,确认集群状态:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready control-plane 72s v1.27.3
kind-worker Ready <none> 47s v1.27.3
kind-worker2 Ready <none> 47s v1.27.3
kind-worker3 Ready <none> 47s v1.27.3
在这里,我们可以看到集群中有四个节点——一个控制平面和三个工作节点。现在集群已经准备好,我们将在下一个部分深入了解 Kubernetes,并看看一些最常用的 Kubernetes 资源。
理解 Kubernetes 的 pods
Kubernetes 的 pod 是 Kubernetes 应用程序的基本构建块。一个 pod 包含一个或多个容器,所有容器总是会调度到同一主机上。通常,pod 中只有一个容器,但在某些场景下,你需要在一个 pod 中调度多个容器。
要理解为什么 Kubernetes 最初采用 pod 的概念而不是使用容器,可能需要一些时间,但这是有原因的,随着你对工具的使用经验积累,你会理解其中的深意。现在,让我们来看一个简单的 pod 示例,以及如何在 Kubernetes 中调度它。
运行一个 pod
我们将首先使用简单的命令在 pod 中运行一个 NGINX 容器。然后,我们会看看如何以声明的方式进行操作。
要访问本节的资源,请cd到以下目录:
$ cd ~/modern-devops/ch5/pod/
要运行一个包含单个 NGINX 容器的 pod,请执行以下命令:
$ kubectl run nginx --image=nginx
要检查 pod 是否正在运行,可以运行以下命令:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 26s
就是这样!正如我们所看到的,pod 现在正在运行。
要删除 pod,可以运行以下命令:
$ kubectl delete pod nginx
kubectl run 命令是创建 pod 的命令式方式,但与 Kubernetes 交互的另一种方式是使用声明性清单。docker compose。
提示
在预发布和生产环境中始终使用声明性方法创建 Kubernetes 资源。它们允许您将 Kubernetes 配置存储和版本化在诸如 Git 等源代码管理工具中,并启用 GitOps。在开发过程中,您可以使用命令式方法,因为命令比 YAML 文件具有更快的周转时间。
让我们看一个示例 pod 清单,nginx-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: nginx
resources:
limits:
memory: "200Mi"
cpu: "200m"
requests:
memory: "100Mi"
cpu: "100m"
restartPolicy: Always
让我们首先了解文件。文件包含以下内容:
-
apiVersion: 这定义了我们正在定义的资源版本。在这种情况下,作为 pod 的版本为v1。 -
kind: 这定义了我们要创建的资源类型 – 一个 pod。 -
metadata:metadata部分定义了围绕此资源的名称和标签。它有助于通过标签唯一标识资源并分组多个资源。 -
spec: 这是主要部分,我们在这里定义资源的实际规格。 -
spec.containers: 此部分定义形成 pod 的一个或多个容器。 -
spec.containers.name: 这是容器的名称,在本例中为nginx-container。 -
spec.containers.image: 这是容器镜像,在本例中是nginx。 -
spec.containers.imagePullPolicy: 这可以是Always(始终拉取)、IfNotPresent(仅在节点上未找到镜像时拉取)、或Never(从不尝试从注册表拉取镜像并完全依赖本地镜像)。 -
spec.containers.resources: 这定义了资源的请求和限制。 -
spec.containers.resources.limit: 这定义了资源限制。这是 pod 可以分配的最大资源量,如果资源消耗超出此限制,pod 将被驱逐。 -
spec.containers.resources.limit.memory: 这定义了内存限制。 -
spec.containers.resources.limit.cpu: 这定义了 CPU 限制。 -
spec.containers.resources.requests: 这定义了资源请求。这是在调度期间 pod 需要的最小资源量,如果节点无法分配这些资源,将不会被调度。 -
spec.containers.resources.requests.memory: 这定义了要请求的内存量。 -
spec.containers.resources.requests.cpu: 这定义了要请求的 CPU 核心数量。 -
spec.restartPolicy: 这定义了容器的重启策略 –Always(始终重启)、OnFailure(失败时重启)、或Never(从不重启)。这与 Docker 上的重启策略类似。
在 pod 清单上还有其他设置,但我们将根据进展情况逐步探讨。
重要提示
将 imagePullPolicy 设置为 IfNotPresent,除非你有充分的理由使用 Always 或 Never。这样可以确保你的容器快速启动,并且避免不必要地下载镜像。
在调度 pod 时,请始终使用资源请求和限制。这确保你的 pod 被调度到适当的节点,并且不会耗尽任何现有资源。你还可以在集群级别应用默认的资源策略,以确保如果开发人员由于某些原因忽略了这一部分,也不会造成任何损害。
让我们使用以下命令应用清单:
$ kubectl apply -f nginx-pod.yaml
我们创建的 pod 完全处于主机网络之外。它运行在容器网络内,默认情况下,Kubernetes 不允许任何 pod 暴露给主机网络,除非我们明确要暴露它。
访问 pod 有两种方式——使用 kubectl port-forward 进行端口转发,或者通过 Service 资源暴露 pod。
使用端口转发
在我们进入服务部分之前,让我们考虑一下使用 port-forward 选项。
要通过端口转发暴露 pod,请执行以下命令:
$ kubectl port-forward nginx 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
提示信息停留在这里。这意味着它已打开端口转发会话,并在端口 8080 上监听。它将自动将收到的端口 8080 请求转发到 NGINX 的端口 80。
打开一个重复的终端会话,并在前述地址上执行 curl,查看我们会得到什么:
$ curl 127.0.0.1:8080
...
<title>Welcome to nginx!</title>
...
我们可以看到它正在工作,因为我们得到了默认的 NGINX 响应。
现在,这里有几点需要记住。
当我们使用 HTTP port-forward 时,我们是将请求从运行 kubectl 的客户端机器转发到 pod,类似于下图所示的内容:
图 5.2 – kubectl port-forward
当你运行 kubectl port-forward 时,kubectl 客户端通过 Kube API 服务器打开一个 TCP 隧道,然后 Kube API 服务器将连接转发到正确的 pod。由于 kubectl 客户端和 API 服务器之间的连接是加密的,因此这是一种非常安全的访问 pod 的方式,但在决定使用 kubectl port-forward 将 pod 暴露给外部世界之前,请三思。
有一些特定的使用场景适合使用 kubectl port-forward:
-
用于故障排除任何行为不正常的 pod。
-
用于访问 Kubernetes 内部服务,例如 Kubernetes 仪表盘——也就是说,当你不希望将服务暴露给外部世界,而只允许 Kubernetes 管理员和用户登录仪表盘时。假设只有这些用户可以通过
kubectl访问集群。
对于其他任何情况,你应该使用 Service 资源来暴露你的 pod,无论是内部还是外部。虽然我们将在下一章中讨论 Service 资源,但让我们先看一下可以对 pod 执行的几个操作。
故障排除 pods
类似于我们使用docker logs浏览容器日志的方式,我们可以使用kubectl logs命令浏览 Kubernetes pod 中容器的日志。如果 pod 中运行多个容器,我们可以使用-c标志来指定容器的名称。
要访问容器日志,运行以下命令:
$ kubectl logs nginx -c nginx
...
127.0.0.1 - - [18/Jun/2023:14:08:01 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.47.0" "-"
由于 pod 只运行一个容器,我们无需指定-c标志,因此你可以使用以下命令:
$ kubectl logs nginx
可能有些情况,你可能需要获取运行中容器的 shell 并排查容器内发生了什么。在 Docker 中我们使用docker exec来实现这个操作。同样,在 Kubernetes 中我们可以使用kubectl exec来实现这一点。
运行以下命令以打开与容器的 shell 会话:
$ kubectl exec -it nginx -- /bin/bash
root@nginx:/# cd /etc/nginx/ && ls
conf.d fastcgi_params mime.types modules nginx.conf scgi_params uwsgi_params
root@nginx:/etc/nginx# exit
你甚至可以在不打开 shell 会话的情况下运行特定命令。例如,我们可以通过一行命令来执行前面的操作,类似以下内容:
$ kubectl exec nginx -- ls /etc/nginx
conf.d fastcgi_params mime.types modules nginx.conf scgi_params uwsgi_params
kubectl exec是一个重要命令,有助于我们故障排除容器。
提示
如果你在exec模式下修改容器中的文件或下载包,这些更改会在当前 pod 存活期间持续有效。一旦 pod 被删除,你将失去所有更改。因此,这并不是解决问题的好方法。你应当只使用exec来诊断问题,将正确的更改嵌入到新镜像中,然后重新部署。
在上一章中我们讨论了无发行版容器,它们由于安全原因不允许exec进入容器。对于无发行版容器,提供了调试镜像,可以让你打开一个 shell 会话进行故障排除。
提示
默认情况下,如果在构建镜像时没有在 Dockerfile 中指定用户,容器会以 root 用户运行。如果你想以特定用户运行 pod,可以在 pod 的安全上下文中设置runAsUser属性,但这并不是理想的做法。最佳实践是将用户信息嵌入到容器镜像中。
我们已经讨论了如何故障排除运行中的容器,但如果容器由于某些原因无法启动怎么办?
让我们来看以下示例:
$ kubectl run nginx-1 --image=nginx-1
现在,让我们尝试获取 pod 并亲自查看:
$ kubectl get pod nginx-1
NAME READY STATUS RESTARTS AGE
nginx-1 0/1 ImagePullBackOff 0 25s
哎呀!现在出现了一些错误,状态是ImagePullBackOff。嗯,似乎是镜像出了些问题。虽然我们知道问题出在镜像上,但我们希望了解真正的问题所在,因此,为了进一步了解此问题,我们可以使用以下命令描述 pod:
$ kubectl describe pod nginx-1
现在,这为我们提供了关于 pod 的大量信息,如果你查看events部分,你会找到一行特定信息,告诉我们 pod 出了什么问题:
Warning Failed 60s (x4 over 2m43s) kubelet Failed to pull image "nginx-
1": rpc error: code = Unknown desc = failed to pull and unpack image "docker.io/library/
nginx-1:latest": failed to resolve reference "docker.io/library/nginx-1:latest": pull
access denied, repository does not exist or may require authorization: server message:
insufficient_scope: authorization failed
所以,这条信息告诉我们,要么仓库不存在,要么仓库存在但为私有仓库,因此授权失败。
提示
你可以使用kubectl describe来查看大多数 Kubernetes 资源。它应该是你在故障排除时使用的第一个命令。
由于我们知道该镜像不存在,让我们将镜像更换为有效的镜像。我们必须删除 Pod,并使用正确的镜像重新创建它。
要删除 Pod,请运行以下命令:
$ kubectl delete pod nginx-1
要重新创建 Pod,请运行以下命令:
$ kubectl run nginx-1 --image=nginx
现在,让我们获取 Pod;它应该按如下方式运行:
$ kubectl get pod nginx-1
NAME READY STATUS RESTARTS AGE
nginx-1 1/1 Running 0 42s
由于我们已经解决了镜像问题,Pod 现在已经在运行。
到目前为止,我们已经能够使用 Pod 运行容器,但 Pod 是非常强大的资源,可以帮助你管理容器。Kubernetes Pod 提供了探针来确保应用程序的可靠性。我们将在下一节中详细介绍这一点。
确保 Pod 可靠性
我们在第四章《创建和管理容器镜像》中讨论了健康检查,我还提到过你不应该在 Docker 层面使用健康检查,而应该使用容器编排器提供的健康检查。Kubernetes 提供了三种探针来监控你的 Pod 健康状况——启动探针、存活探针和就绪探针。
以下图示展示了三种探针的图形化表示:
图 5.3 – Kubernetes 探针
让我们逐一查看每种探针,了解如何使用它们以及何时使用它们。
启动探针
Kubernetes 使用启动探针检查应用程序是否已启动。你可以在启动缓慢的应用程序上使用启动探针,或者在你不知道启动可能需要多长时间的情况下使用它。当启动探针处于活动状态时,它会禁用其他探针,以免它们干扰启动探针的操作。由于应用程序在启动探针报告之前并未启动,因此没有必要让其他探针处于活动状态。
就绪探针
就绪探针用来确认容器是否准备好接受请求。它们与启动探针有所不同,因为就绪探针不仅仅检查应用程序是否已启动,它还确保容器可以开始处理请求。当 Pod 中的所有容器都准备好时,Pod 才算是就绪。就绪探针确保在 Pod 没有准备好时不会向其发送流量。因此,它可以提供更好的用户体验。
存活探针
如果将 Pod 的 restartPolicy 字段设置为 Always 或 OnFailure,Kubernetes 会重新启动容器。因此,通过检测死锁并确保容器正在运行而不仅仅是报告运行状态,它提高了服务的可靠性。
现在,让我们通过一个例子来更好地理解探针。
探针实战
让我们改进最后的清单并添加一些探针,以创建以下 nginx-probe.yaml 清单文件:
...
startupProbe:
exec:
command:
- cat
- /usr/share/nginx/html/index.html
failureThreshold: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 3
restartPolicy: Always
该清单文件包含所有三种探针:
-
启动探针检查
/usr/share/nginx/html/index.html文件是否存在。它会以 10 秒的间隔连续检查 30 次,直到其中一次检查成功。一旦检测到文件,启动探针将停止进一步检查。 -
就绪探针检查端口
80上是否有监听器,并以HTTP 2xx – 3xx on path /响应。它最初等待 5 秒,然后每 5 秒检查一次 Pod。如果它收到2xx – 3xx响应,它会报告容器已就绪并接受请求。 -
存活探针检查 Pod 是否在
port80上并且path /路径下响应HTTP 2xx – 3xx。它最初等待 5 秒,然后每 3 秒检查一次容器。假设在一次检查中,它发现 Pod 未响应failureThreshold次(默认为3)。在这种情况下,它将杀死容器,并且 kubelet 将根据 Pod 的restartPolicy字段采取适当的操作。 -
让我们应用 YAML 文件,并使用以下命令查看 Pod 的启动过程:
$ kubectl delete pod nginx && kubectl apply -f nginx-probe.yaml && \
kubectl get pod -w
NAME READY STATUS RESTARTS AGE
nginx 0/1 Running 0 4s
nginx 0/1 Running 0 11s
nginx 1/1 Running 0 12s
正如我们所见,Pod 从运行状态变为就绪状态的过程非常迅速。大约需要 10 秒钟,因为就绪探针在 Pod 启动后的 10 秒开始生效。然后,存活探针继续监控 Pod 的健康状况。
现在,让我们做一些事情来破坏存活检查。假设有人通过 shell 进入容器并删除了一些重要的文件。你认为存活探针会如何反应?我们来看看。
让我们从容器中删除/usr/share/nginx/html/index.html文件,然后使用以下命令检查容器的行为:
$ kubectl exec -it nginx -- rm -rf /usr/share/nginx/html/index.html && \
kubectl get pod nginx -w
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 2m5s
nginx 0/1 Running 1 (2s ago) 2m17s
nginx 1/1 Running 1 (8s ago) 2m22s
因此,当我们观察 Pod 时,初次删除只有在 9 秒后才被检测到。这是因为存活探针。它尝试了 9 秒钟,也就是三次 periodSeconds,因为 failureThreshold 默认为 3,才会宣布 Pod 不健康并杀死容器。容器一被杀死,kubelet 就会重新启动它,因为 Pod 的 restartPolicy 字段设置为 Always。然后,我们看到启动和就绪探针开始生效,很快,Pod 就变为就绪状态。因此,无论发生什么情况,你的 Pod 都是可靠的,即使你的应用程序的某部分出现故障,它仍然能够正常工作。
提示
使用就绪探针和存活探针有助于提供更好的用户体验,因为没有请求会发送到尚未准备好处理任何请求的 Pod。如果你的应用程序没有正确响应,它将替换容器。如果多个 Pod 正在运行以处理请求,那么你的服务具有极强的弹性。
正如我们之前讨论的,Pod 可以包含一个或多个容器。让我们来看一些可能需要多个容器而非一个容器的使用场景。
Pod 多容器设计模式
你可以通过两种方式在 Pod 中运行多个容器——将容器作为初始化容器运行,或者将容器作为主容器的辅助容器运行。我们将在接下来的子章节中探讨这两种方法。
初始化容器
初始化容器在主容器启动之前运行,因此你可以在主容器接管之前使用它们初始化容器环境。以下是一些示例:
-
在使用非 root 用户启动容器之前,某些目录可能需要特定的所有权或权限设置
-
在启动 web 服务器之前,你可能想要克隆一个 Git 仓库
-
你可以添加启动延迟
-
你可以动态生成配置,例如针对那些在构建时不知道但在运行时应该知道的容器,它们可能需要动态连接到其他某个 pod。
提示
仅将 init 容器作为最后手段使用,因为它们会拖慢容器的启动时间。尽量在容器镜像内预先配置或定制它。
现在,让我们看一个示例,了解 init 容器的实际应用。
要访问此部分的资源,cd 进入以下路径:
$ cd ~/modern-devops/ch5/multi-container-pod/init/
让我们通过 nginx web 服务器提供 example.com 网站。在启动 nginx 之前,我们将获取 example.com 网页并将其保存为 index.html 到 nginx 默认的 HTML 目录中。
访问清单文件 nginx-init.yaml,它应包含以下内容:
…
spec:
containers:
- name: nginx-container
image: nginx
volumeMounts:
- mountPath: /usr/share/nginx/html
name: html-volume
initContainers:
- name: init-nginx
image: busybox:1.28
command: ['sh', '-c', 'mkdir -p /usr/share/nginx/html && wget -O /usr/share/nginx/html/index.html http://example.com']
volumeMounts:
- mountPath: /usr/share/nginx/html
name: html-volume
volumes:
- name: html-volume
emptyDir: {}
如果我们查看清单文件的 spec 部分,我们会看到以下内容:
-
containers:此部分定义了一个或多个构成 pod 的容器。 -
containers.name:这是容器的名称,在这种情况下是nginx-container。 -
containers.image:这是容器镜像,在这种情况下是nginx。 -
containers.volumeMounts:这定义了应挂载到容器的卷列表。它类似于我们在 第四章 中学习的内容,创建和管理容器镜像。 -
containers.volumeMounts.mountPath:这定义了挂载卷的路径,在这种情况下是/usr/share/nginx/html。我们将与 init 容器共享这个卷,以便当 init 容器从example.com下载index.html文件时,这个目录中会包含相同的文件。 -
containers.volumeMounts.name:这是卷的名称,在这种情况下是html-volume。 -
initContainers:此部分定义了一个或多个在主容器之前运行的 init 容器。 -
initContainers.name:这是 init 容器的名称,在这种情况下是init-nginx。 -
initContainers.image:这是 init 容器的镜像,在这种情况下是busybox:1.28。 -
initContainers.command:这是 busybox 应执行的命令。在这种情况下,'mkdir -p /usr/share/nginx/html && wget -O /usr/share/nginx/html/index.html [example.com](http://example.com)'将下载example.com的内容到/usr/share/nginx/html目录。 -
initContainers.volumeMounts:我们将在此容器上挂载与nginx-container中定义的相同的卷。因此,我们在该卷中保存的任何内容都会自动出现在nginx-container中。 -
initContainers.volumeMounts.mountPath:这定义了挂载卷的路径,在这种情况下是/usr/share/nginx/html。 -
initContainers.volumeMounts.name:这是卷的名称,在本例中是html-volume。 -
volumes:此部分定义了与 Pod 容器相关联的一个或多个卷。 -
volumes.name:这是卷的名称,在本例中是html-volume。 -
volumes.emptyDir:这定义了一个emptyDir卷。它类似于 Docker 中的tmpfs卷,因此它不是持久的,只在容器的生命周期内存在。
所以,让我们继续应用清单,并使用以下命令观察 Pod 的启动过程:
$ kubectl delete pod nginx && kubectl apply -f nginx-init.yaml && \
kubectl get pod nginx -w
NAME READY STATUS RESTARTS AGE
nginx 0/1 Init:0/1 0 0s
nginx 0/1 PodInitializing 0 1s
nginx 1/1 Running 0 3s
最初,我们可以看到 nginx Pod 显示状态为 Init:0/1。这意味着 1 个初始化容器中有 0 个开始初始化。过了一段时间后,我们可以看到 Pod 报告其状态为 PodInitializing,这意味着初始化容器已经开始运行。初始化容器成功运行后,Pod 报告为运行状态。
现在,一旦 Pod 开始运行,我们可以使用以下命令将容器的端口 80 转发到主机端口 8080:
$ kubectl port-forward nginx 8080:80
打开一个新的终端窗口,尝试通过以下命令使用 curl 访问本地主机的端口 8080:
$ curl localhost:8080
<title>Example Domain</title>
在这里,我们可以看到来自我们 web 服务器的示例域响应。这意味着初始化容器工作正常。
正如你现在可能已经理解的那样,初始化容器的生命周期在主容器启动之前结束,一个 Pod 可以包含一个或多个主容器。接下来,我们来看看我们可以在主容器中使用的一些设计模式。
大使模式
localhost 在任何地方。
现在,你可以采取两种方法:
-
你可以更改应用程序代码,并使用配置映射和机密(稍后会详细介绍)将数据库连接详情注入到环境变量中。
-
你可以继续使用现有代码,并使用第二个容器作为 Redis 数据库的 TCP 代理。该 TCP 代理将与配置映射和机密连接,并包含 Redis 数据库的连接详情。
提示
大使模式帮助开发人员专注于应用程序,而不必担心配置细节。如果你想将应用程序开发与配置管理解耦,可以考虑使用它。
第二种方法解决了我们希望进行完全相同迁移的问题。我们可以使用配置映射来定义特定环境的配置,而无需更改应用程序代码。以下图示展示了这种方法:
图 5.4 – 大使模式
在深入技术细节之前,我们先了解一下配置映射。
配置映射
配置映射 包含键值对,我们可以用于多种目的,例如定义特定环境的属性,或者在容器启动时或运行时注入外部变量。
配置映射的理念是将应用程序与配置解耦,并在 Kubernetes 层次上外部化配置。这类似于使用属性文件,例如,定义特定环境的配置。
以下图表对此进行了很好的解释:
图 5.5 – 配置映射
我们将使用ConfigMap在代理容器内定义外部 Redis 数据库的连接属性。
示例应用程序
我们将使用在第三章中使用的示例应用程序,使用 Docker 进行容器化,以及使用 Docker Compose 部署示例应用程序部分。源代码已复制到以下目录:
$ cd ~/modern-devops/ch5/multi-container-pod/ambassador
你可以查看 Flask 应用程序的app.py文件、requirements.txt文件和 Dockerfile,以了解该应用程序的功能。
现在,让我们使用以下命令构建容器:
$ docker build -t <your_dockerhub_user>/flask-redis .
让我们使用以下命令将其推送到我们的容器注册表:
$ docker push <your_dockerhub_user>/flask-redis
正如你可能注意到的,app.py代码将缓存定义为localhost:6379。我们将在localhost:6379上运行一个代理容器。代理将把连接隧道转发到其他地方运行的redis Pod。
首先,让我们使用以下命令创建redis Pod:
$ kubectl run redis --image=redis
现在,让我们通过Service资源将redis Pod 暴露给集群资源。这将允许集群中的任何 Pod 通过redis主机名与redis Pod 进行通信。我们将在下一章详细讨论 Kubernetes Service资源:
$ kubectl expose pod redis --port 6379
酷!现在 Pod 和Service资源已经启动并运行,让我们来处理代理模式。
我们首先需要定义两个配置映射。第一个描述redis主机和端口信息,第二个定义作为反向代理工作的模板nginx.conf文件。
redis-config-map.yaml文件如下所示:
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
data:
host: "redis"
port: "6379"
上面的 YAML 文件定义了一个名为redis-config的配置映射,其中包含host和port属性。你可以拥有多个配置映射,每个环境一个。
nginx-config-map.yaml文件如下所示:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
nginx.conf: |
...
stream {
server {
listen 6379;
proxy_pass stream_redis_backend;
}
upstream stream_redis_backend {
server REDIS_HOST:REDIS_PORT;
}
}
该配置映射将nginx.conf模板作为配置映射值注入。此模板定义了我们的代理 Pod 配置,使其监听localhost:6379并将连接隧道转发到REDIS_HOST:REDIS_PORT。由于REDIS_HOST和REDIS_PORT值是占位符,我们必须用从redis-config配置映射中获得的正确值填充这些占位符。为此,我们可以将此文件挂载到一个卷上,然后进行操作。我们可以使用initContainer来使用正确的配置初始化代理。
现在,让我们查看 Pod 配置清单flask-ambassador.yaml。该 YAML 文件包含多个部分。首先,让我们看看containers部分:
...
spec:
containers:
- name: flask-app
image: <your_dockerhub_user>/flask-redis
- name: nginx-ambassador
image: nginx
volumeMounts:
- mountPath: /etc/nginx
name: nginx-volume
...
本节包含一个名为flask-app的容器,它使用我们在上一节中构建的<your_dockerhub_user>/flask-redis镜像。第二个容器是nginx-ambassador容器,它将充当代理与redis通信。因此,我们已将/etc/nginx目录挂载到一个卷中。此卷也挂载在初始化容器中,用于在nginx启动之前生成所需的配置。
以下是initContainers部分:
initContainers:
- name: init-nginx
image: busybox:1.28
command: ['sh', '-c', 'cp -L /config/nginx.conf /etc/nginx/nginx.conf && sed -i "s/
REDIS_HOST/${REDIS_HOST}/g" /etc/nginx/nginx.conf']
env:
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: redis-config
key: host
- name: REDIS_PORT
valueFrom:
configMapKeyRef:
name: redis-config
key: port
volumeMounts:
- mountPath: /etc/nginx
name: nginx-volume
- mountPath: /config
name: config
本节定义了一个busybox容器——init-nginx。该容器需要生成与 Redis 通信的nginx-ambassador代理配置;因此,存在两个环境变量。这两个环境变量来自redis-config配置映射。此外,我们还从nginx-config配置映射中挂载了nginx.conf文件。初始化容器中的command部分使用这些环境变量替换nginx.conf文件中的占位符,之后我们就得到了与 Redis 后端的 TCP 代理。
volumes部分将nginx-volume定义为一个emptyDir卷,并将config卷挂载到nginx.conf文件,该文件位于nginx-config配置映射中:
volumes:
- name: nginx-volume
emptyDir: {}
- name: config
configMap:
name: nginx-config
items:
- key: "nginx.conf"
path: "nginx.conf"
现在,让我们开始分步应用 YAML 文件。
使用以下命令应用两个配置映射:
$ kubectl apply -f redis-config-map.yaml
$ kubectl apply -f nginx-config-map.yaml
使用以下命令应用 pod 配置:
$ kubectl apply -f flask-ambassador.yaml
使用以下命令获取 pod 查看配置是否正确:
$ kubectl get pod/flask-ambassador
NAME READY STATUS RESTARTS AGE
flask-ambassador 2/2 Running 0 10s
由于 pod 现在运行正常,让我们通过以下命令将5000端口转发到本地主机以进行一些测试:
$ kubectl port-forward flask-ambassador 5000:5000
现在,打开一个新的终端,并使用以下命令尝试在localhost:5000上进行curl测试:
$ curl localhost:5000
Hi there! This page was last visited on 2023-06-18, 16:52:28.
$ curl localhost:5000
Hi there! This page was last visited on 2023-06-18, 16:52:28.
$ curl localhost:5000
Hi there! This page was last visited on 2023-16-28, 16:52:32.
如我们所见,每次执行curl请求时,都会在屏幕上显示最后一次访问时间。Ambassador 模式正在正常工作。
这是一个简单的 Ambassador 模式示例。你可以进行更高级的配置,以细粒度控制你的应用如何与外界交互。你可以使用 Ambassador 模式来保护从容器传输的流量。它还简化了应用开发过程,开发团队无需担心这些细节。而运维团队可以使用这些容器更好地管理环境,而不必互相干扰。
提示
由于 Ambassador 模式通过代理隧道连接会增加一些开销,因此只有在管理上的好处超过因使用 Ambassador 容器而产生的额外成本时,才应使用此模式。
现在,让我们来看一下另一种多容器 pod 模式——sidecar 模式。
Sidecar 模式
旁车这个名字来源于摩托车的副驾驶厢。旁车并不会改变摩托车的核心功能,而且没有旁车,摩托车也能正常运行。旁车只是增加了一个额外的座位,提供一个功能,帮助你载一个额外的人。类似地,Pod 中的旁车是提供与主容器核心功能无关的辅助容器,增强主容器的功能。例如,日志记录和监控容器。将日志单独放入一个容器,有助于将日志职责与主容器解耦,从而即使主容器因为某些原因宕机时,你也能继续监控你的应用程序。
如果日志代码出现问题,并且不会导致整个应用程序崩溃,而只是影响日志容器,这时使用这种方式会很有帮助。你还可以使用旁车(sidecar)将辅助或相关容器与主容器放在一起,因为我们知道,同一个 Pod 内的容器共享同一台机器。
提示
只有在两个容器功能上有直接关系并且作为一个整体工作时,才使用多容器 Pod。
你还可以使用旁车将应用程序与秘密数据隔离开。例如,如果你正在运行一个需要访问特定密码才能运行的 Web 应用程序,最好将秘密数据挂载到旁车容器上,并通过旁车容器将密码提供给 Web 应用程序。这是因为如果有人访问了你的应用程序容器的文件系统,他们是无法获取到密码的,因为密码是由另一个容器提供的,具体如下面的图所示:
图 5.6 – 旁车模式
让我们实现前面的模式来更好地理解旁车。我们有一个与 Redis 旁车交互的 Flask 应用程序。我们将使用 Kubernetes 的 Secret 资源将一个名为 foobar 的秘密预先填充到 Redis 旁车中。
秘密
使用 base64 编码而不是 plaintext。虽然从安全角度来看,base64 编码并不会提供比 plaintext 更好的保护,但你应当使用秘密存储敏感信息,如密码。因为 Kubernetes 社区会在未来的版本中开发解决方案,加强对秘密数据的安全保护。如果你使用秘密存储,你将直接受益于这些改进。
提示
一般来说,始终使用秘密存储机密数据,如 API 密钥和密码,而配置映射则用于存储非敏感的配置数据。
要访问本节的文件,请前往以下目录:
$ cd ~/modern-devops/ch5/multi-container-pod/sidecar
现在,让我们继续看一下示例 Flask 应用程序。
示例应用程序
Flask 应用程序查询 Redis 旁车以获取密钥并将其作为响应返回。这样做并不理想,因为你不应该将秘密数据作为响应返回,但为了演示的目的,我们就照这样做。
所以,首先,让我们设计我们的旁车容器,让它在启动后预填充容器内的数据。
我们需要创建一个名为 secret 的密钥,其值为 foobar。现在,通过运行以下命令将 Redis 命令进行 base64 编码,将密钥设置到缓存中:
$ echo 'SET secret foobar' | base64
U0VUIHNlY3JldCBmb29iYXIK
现在我们有了 base64 编码的密钥,我们可以创建一个名为 redis-secret.yaml 的清单,并按照以下字符串创建:
apiVersion: v1
kind: Secret
metadata:
name: redis-secret
data:
redis-secret: U0VUIHNlY3JldCBmb29iYXIK
然后,我们需要构建 Redis 容器,以便在启动时创建此密钥。要访问本节的文件,请转到以下目录:
$ cd ~/modern-devops/ch5/multi-container-pod/sidecar/redis/
创建一个 entrypoint.sh 文件,如下所示:
redis-server --daemonize yes && sleep 5
redis-cli < /redis-master/init.redis
redis-cli save
redis-cli shutdown
redis-server
Shell 脚本查找 /redis-master 目录中的文件 init.redis,并在其上运行 redis-cli 命令。这意味着缓存将使用我们的密钥中定义的值进行预填充,前提是我们将密钥作为 /redis-master/init.redis 挂载。
然后,我们必须创建一个 Dockerfile,该文件将使用这个 entrypoint.sh 脚本,如下所示:
FROM redis
COPY entrypoint.sh /tmp/
CMD ["sh", "/tmp/entrypoint.sh"]
现在我们准备好了,可以构建并将代码推送到 Docker Hub:
$ docker build -t <your_dockerhub_user>/redis-secret .
$ docker push <your_dockerhub_user>/redis-secret
现在,我们已经准备好了 Redis 镜像,我们必须构建 Flask 应用程序镜像。要访问本节的文件,请cd进入以下目录:
$ cd ~/modern-devops/ch5/multi-container-pod/sidecar/flask
让我们首先查看 app.py 文件:
...
cache = redis.Redis(host='localhost', port=6379)
def get_secret():
try:
secret = cache.get('secret')
return secret
...
def index():
secret = str(get_secret().decode('utf-8'))
return 'Hi there! The secret is {}.\n'.format(secret)
代码很简单——它从缓存中获取密钥并将其作为响应返回。
我们还创建了与上一节相同的 Dockerfile。
因此,让我们构建并将容器镜像推送到 Docker Hub:
$ docker build -t <your_dockerhub_user>/flask-redis-secret .
$ docker push <your_dockerhub_user>/flask-redis-secret
现在我们的镜像准备好了,让我们看看 Pod 清单,flask-sidecar.yaml,该清单位于 ~/``modern-devops/ch5/multi-container-pod/sidecar/ 目录中:
...
spec:
containers:
- name: flask-app
image: <your_dockerhub_user>/flask-redis-secret
- name: redis-sidecar
image: <your_dockerhub_user>/redis-secret
volumeMounts:
- mountPath: /redis-master
name: secret
volumes:
- name: secret
secret:
secretName: redis-secret
items:
- key: redis-secret
path: init.redis
Pod 定义了两个容器——flask-app 和 redis-sidecar。flask-app 容器运行 Flask 应用程序,该应用程序将与 redis-sidecar 交互以获取密钥。redis-sidecar 容器已将 secret 卷挂载到 /redis-master。Pod 定义还包含一个名为 secret 的单个卷,卷指向 redis-secret 密钥,并将其作为文件 init.redis 挂载。
因此,最后,我们有一个文件 /redis-master/init.redis,正如我们所知,entrypoint.sh 脚本查找此文件并运行 redis-cli 命令,以预填充 Redis 缓存中的秘密数据。
让我们首先使用以下命令应用这个密钥:
$ kubectl apply -f redis-secret.yaml
然后,我们可以使用以下命令应用 flask-sidecar.yaml 文件:
$ kubectl apply -f flask-sidecar.yaml
现在,让我们使用以下命令获取 Pod:
$ kubectl get pod flask-sidecar
NAME READY STATUS RESTARTS AGE
flask-sidecar 2/2 Running 0 11s
由于 Pod 正在运行,现在是时候使用以下命令将其端口转发到主机:
$ kubectl port-forward flask-sidecar 5000:5000
现在,让我们打开一个重复的终端,运行 curl localhost:5000 命令,并看看我们得到了什么:
$ curl localhost:5000
Hi there! The secret is foobar.
正如我们所看到的,我们在响应中得到了密钥 foobar。边车工作正常!
现在,让我们先看另一个流行的多容器 Pod 模式——适配器模式。
适配器模式
正如其名称所示,适配器模式有助于将某些事物转换为符合标准的形式,比如手机和笔记本电脑的适配器,它们将我们的主要电源转换为设备可以使用的形式。适配器模式的一个很好的例子是转换日志文件,使其符合企业标准,并将其传输到你的日志分析解决方案中:
图 5.7 – 适配器模式
当你有一个异构解决方案输出多种格式的日志文件,但却只有一个日志分析解决方案,它只接受特定格式的消息时,这时就会有所帮助。实现这一目标有两种方法:一种是更改输出日志文件的代码以符合标准格式,另一种是使用适配器容器来执行转换。
让我们通过以下场景进一步理解它。
我们有一个应用程序,它不断输出没有日期前缀的日志文件。我们的适配器应该读取日志流,并在每次生成日志行时添加时间戳。
为此,我们将使用以下的 pod 清单文件,app-adapter.yaml:
...
spec:
volumes:
- name: logs
emptyDir: {}
containers:
- name: app-container
image: ubuntu
command: ["/bin/bash"]
args: ["-c", "while true; do echo 'This is a log line' >> /var/log/app.log; sleep
2;done"]
volumeMounts:
- name: logs
mountPath: /var/log
- name: log-adapter
image: ubuntu
command: ["/bin/bash"]
args: ["-c", "apt update -y && apt install -y moreutils && tail -f /var/log/app.log |
ts '[%Y-%m-%d %H:%M:%S]' > /var/log/out.log"]
volumeMounts:
- name: logs
mountPath: /var/log
该 pod 包含两个容器 —— 应用容器,它是一个简单的 Ubuntu 容器,每 2 秒输出一次 This is a log line,以及日志适配器容器,它持续地跟踪 app.log 文件,在每一行的开头添加时间戳,并将结果输出到 /var/log/out.log。这两个容器共享 /var/log 卷,该卷作为 emptyDir 卷挂载在两个容器中。
现在,让我们使用以下命令应用这个清单:
$ kubectl apply -f app-adapter.yaml
让我们等一会儿,通过以下命令检查 pod 是否在运行:
$ kubectl get pod app-adapter
NAME READY STATUS RESTARTS AGE
app-adapter 2/2 Running 0 8s
当 pod 正在运行时,我们现在可以通过以下命令进入日志适配器容器的 shell:
$ kubectl exec -it app-adapter -c log-adapter -- bash
当我们进入 shell 后,可以通过以下命令cd到/var/log目录,并列出其中的内容:
root@app-adapter:/# cd /var/log/ && ls
app.log apt/ dpkg.log out.log
正如我们所看到的,我们得到了app.log和out.log这两个文件。现在,让我们使用cat命令打印它们,看看会得到什么。
首先,使用以下命令cat app.log文件:
root@app-adapter:/var/log# cat app.log
This is a log line
This is a log line
This is a log line
在这里,我们可以看到一系列的日志行正在被打印。
现在,使用以下命令cat out.log文件,看看会得到什么:
root@app-adapter:/var/log# cat out.log
[2023-06-18 16:35:25] This is a log line
[2023-06-18 16:35:27] This is a log line
[2023-06-18 16:35:29] This is a log line
在这里,我们可以看到日志行前面有时间戳。这意味着适配器模式正常工作。然后,你可以将此日志文件导出到你的日志分析工具中。
总结
我们已经完成了这一关键章节的内容。我们已经涵盖了足够的内容,能够帮助你入门 Kubernetes,并理解并重视其最佳实践。
我们从 Kubernetes 及其必要性开始,然后讨论了使用 Minikube 和 KinD 引导 Kubernetes 集群。接着,我们了解了 pod 资源,讨论了如何创建和管理 pod,排除故障,确保应用程序的可靠性(使用探针),以及多容器设计模式,深入理解为什么 Kubernetes 使用 pod 而不是单纯的容器。
在下一章中,我们将深入探讨 Kubernetes 的高级方面,涵盖控制器、服务、入口、管理有状态应用程序以及 Kubernetes 命令行最佳实践。
问题
请回答以下问题,测试你对本章的知识掌握情况:
-
与 Kubernetes 的所有通信通过以下哪一项进行?
A. Kubelet
B. API 服务器
C. Etcd
D. 控制器管理器
E. 调度器
-
以下哪个负责确保集群处于期望状态?
A. Kubelet
B. API 服务器
C. Etcd
D. 控制器管理器
E. 调度器
-
以下哪个负责存储集群的期望状态?
A. Kubelet
B. API 服务器
C. Etcd
D. 控制器管理器
E. 调度器
-
Pod 可以包含多个容器。(正确/错误)
-
你可以使用端口转发来处理以下哪些使用场景?(选择两项)
A. 用于故障排除行为异常的 Pod
B. 用于将服务暴露到互联网
C. 用于访问系统服务,例如 Kubernetes 仪表盘
-
结合使用以下哪两种探针可以帮助确保即使应用程序有一些间歇性问题时,它仍然可靠?(选择两项)
A. 启动探针
B. 存活探针
C. 就绪探针
-
我们可以在生产环境中使用 KinD。(正确/错误)
-
以下哪个多容器模式用于作为前向代理?
A. 使者容器
B. 适配器
C. 边车容器
D. 初始化容器
答案
以下是本章问题的答案:
-
B
-
D
-
C
-
正确
-
A, C
-
B, C
-
错误
-
A
3348

被折叠的 条评论
为什么被折叠?



