现在您已经有了一些与容器和镜像一起工作的经验,我们可以探索一下 Docker 的其他功能。在本章中,我们将继续使用 docker 命令行工具与您配置的运行中的 dockerd 服务器进行交互,同时介绍一些其他基本命令。 Docker 提供了一些额外的命令,使以下功能变得简单易行:
- 打印 Docker 版本
- 查看服务器信息
- 下载镜像更新
- 检查容器
- 进入运行中的容器
- 返回结果
- 查看日志
- 监控统计数据 以及其他更多功能...
让我们来了解这些功能,以及一些扩展 Docker 本身功能的其他社区工具。
打印Docker版本
如果您完成了上一章的内容,您应该已经在 Linux 服务器或虚拟机上设置了一个运行中的 Docker 守护进程,并启动了一个基本容器以确保一切正常工作。如果您尚未完成设置,并且想在本书的后续部分尝试这些步骤,您需要先按照第三章的安装步骤进行设置,然后再继续本章的内容。
在 Docker 中最简单的操作之一是打印各个组件的版本。这听起来可能不算什么,但这是一个非常有用的工具,因为 Docker 是由众多组件构建的,其版本直接决定了您可以使用哪些功能。了解如何显示版本信息还将帮助您解决客户端和服务器之间的某些连接问题。例如,Docker 客户端可能会给您显示有关 API 版本不匹配的晦涩消息,而通过显示 Docker 版本,您可以知道哪个组件需要更新。此命令将与远程 Docker 服务器通信,因此如果客户端由于任何原因无法连接到服务器,客户端将报告错误,并仅打印出客户端的版本信息。如果您发现存在连接问题,您可能需要重新查看上一章的步骤。
由于我们刚刚同时安装了所有 Docker 组件,当我们运行 docker version 命令时,我们应该看到所有版本都是匹配的:
$ docker version
Client:
Cloud integration: v1.0.24
Version: 20.10.17
API version: 1.41
Go version: go1.17.11
Git commit: 100c701
Built: Mon Jun 6 23:04:45 2022
OS/Arch: darwin/amd64
Context: default
Experimental: true
Server: Docker Desktop 4.10.1 (82475)
Engine:
Version: 20.10.17
API version: 1.41 (minimum version 1.12)
Go version: go1.17.11
Git commit: a89b842
Built: Mon Jun 6 23:01:23 2022
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.6.6
GitCommit: 10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
runc:
Version: 1.1.2
GitCommit: v1.1.2-0-ga916309
docker-init:
Version: 0.19.0
GitCommit: de40ad0
请注意,我们有不同的部分代表客户端和服务器。在这种情况下,由于我们刚刚一起安装它们,我们有一个匹配的客户端和服务器。但是需要注意的是,这并不总是如此。希望在生产系统中,您能够确保大多数系统上运行的版本相同。但在开发环境和构建系统中出现略有不同的版本是很常见的。
API 客户端和库通常会跨多个 Docker 版本工作,这取决于它们所需的 API 版本。在服务器部分,我们可以看到当前的 API 版本为 1.41,而它将提供的最低 API 版本为 1.12。当您使用第三方客户端时,这是有用的信息,现在您知道如何验证这些信息了。
查看服务器信息
我们还可以通过Docker客户端了解许多有关Docker服务器的信息。稍后我们将更详细地讨论这些信息的含义,但您可以找出Docker服务器正在运行哪个文件系统后端,运行在哪个内核版本上,运行在哪个操作系统上,安装了哪些插件,使用了哪个运行时,并且当前存储了多少个容器和镜像。docker system info将为您提供类似于下面这样的信息,为了简洁起见,已经进行了缩减:
$ docker system info
Client:
…
Plugins:
buildx: Docker Buildx (Docker Inc., v0.8.2)
compose: Docker Compose (Docker Inc., v2.6.1)
extension: Manages Docker extensions (Docker Inc., v0.2.7)
sbom: View the packaged-based Software Bill Of Materials (SBOM) …
scan: Docker Scan (Docker Inc., v0.17.0)
Server:
Containers: 11
…
Images: 6
Server Version: 20.10.17
Storage Driver: overlay2
…
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries …
…
Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
Default Runtime: runc
…
Kernel Version: 5.10.104-linuxkit
Operating System: Docker Desktop
OSType: linux
Architecture: x86_64
…
根据您的Docker守护程序设置不同,这可能看起来略有不同。不要担心,这只是为了给您提供一个示例。在这里,我们可以看到我们的服务器是运行5.10.104 Linux内核并支持overlay2文件系统驱动的Docker Desktop版本。我们还有一些镜像和容器在服务器上。在刚安装完成后,这个数量应该是零。
这里值得指出的是关于插件的信息。它告诉我们这个Docker安装所支持的所有内容。在刚安装完成后,视情况可能会类似于这样,取决于Docker附带的新插件。Docker本身由许多不同的插件共同组成。这很强大,因为这意味着还可以安装其他社区成员贡献的几个其他插件。即使您只想确保Docker已识别您最近添加的插件,查看已安装的插件也是有用的。
在大多数安装中,/var/lib/docker将是用于存储镜像和容器的默认根目录。如果您需要更改此目录,可以编辑Docker启动脚本以使用--data-root参数指向新的存储位置。要手动测试此操作,您可以运行类似于以下内容的命令:
$ sudo dockerd \
-H unix:///var/run/docker.sock \
--data-root="/data/docker"
稍后我们将详细讨论运行时,但在这里您可以看到我们安装了三个运行时。runc运行时是默认的Docker运行时。当提及Linux容器时,通常是指runc构建的容器类型。在此服务器上,我们还安装了io.containerd.runc.v2和io.containerd.runtime.v1.linux运行时。我们将在第11章中更多地讨论其他一些运行时。
下载镜像更新
接下来的示例中,我们将使用Ubuntu基础镜像。即使您已经获取过ubuntu:latest基础镜像,您也可以再次拉取它,并且它将自动获取自您上次运行它以来发布的任何更新。
这是因为latest是一个标签,根据约定,它应该代表容器的最新构建。然而,latest标签是有争议的,因为它没有永久固定到特定的镜像上,并且在不同的项目中可能具有不同的意义。有些人使用它来指向最新的稳定版本,有些人使用它来指向他们的CI/CD系统生成的最后构建,而其他人则拒绝将任何镜像标记为latest。尽管如此,它仍广泛使用,并且在预生产环境中可以很有用,其中方便性超过了实际版本提供的保障。
调用docker image pull的命令将如下所示:
$ docker image pull ubuntu:latest
latest: Pulling from library/ubuntu
405f018f9d1d: Pull complete
Digest: sha256:b6b83d3c331794420340093eb706a6f152d9c1fa51b262d9bf34594887c2c7ac
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
这个命令只拉取了自上次运行该命令以来发生更改的层。根据您上次拉取镜像的时间、自那时以来推送到仓库的更改以及目标镜像包含的层数,您可能会看到一个更长或更短的列表,甚至是一个空列表。
除了可以使用最新标签或其他版本号标签来引用注册表中的项目外,您还可以使用其内容可寻址标签来引用它们,其格式如下:
sha256:b6b83d3c331794420340093eb706a6f152d9c1fa51b262d9bf34594887c2c7ac
这些内容可寻址标签是通过对镜像内容进行哈希求和生成的,它们是非常精确的标识符。这是目前最安全的引用 Docker 镜像的方式,特别是当您需要确保获取的是您所期望的确切版本,因为这些标签不像版本标签一样可以更改。从注册表中拉取这些标签的语法非常相似,但请注意标签中的 "@" 符号:
$ docker image pull ubuntu@sha256:b6b83d3c331794420340093eb706a6f152d…
与大多数 Docker 命令不同,在 SHA-256 哈希值中您不能缩短哈希值。您必须在此处使用完整的哈希值。
检查容器
一旦您创建了一个容器(无论是正在运行还是停止状态),您可以使用 Docker 查看其配置。这通常在调试时非常有用,并且还包含一些其他信息,有助于识别容器。
对于这个示例,请继续启动一个容器:
$ docker container run --rm -d -t ubuntu /bin/bash
3c4f916619a5dfc420396d823b42e8bd30a2f94ab5b0f42f052357a68a67309b
我们可以使用 "docker container ls" 命令列出所有正在运行的容器,以确保一切都按预期运行,并复制容器的ID:
$ docker container ls
CONTAINER ID IMAGE COMMAND … STATUS … NAMES
3c4f916619a5 ubuntu:latest "/bin/bash" … Up 31 seconds … angry_mestorf
在这种情况下,我们的容器ID是 "3c4f916619a5"。我们也可以使用 "angry_mestorf",这是分配给我们容器的动态名称。然而,许多底层工具需要唯一的容器ID,因此习惯首先查看这个ID是非常有用的。正如我们之前提到的,显示的ID是截断(或简短)版本,但Docker对长版本和短版本都互换使用。就像在许多版本控制系统中一样,该哈希值只是更长哈希的前缀。在内部,内核使用64字节的哈希来标识容器。但对人类来说,使用这个64字节的哈希非常繁琐,因此Docker支持缩短的哈希。
"docker container inspect" 命令的输出非常冗长,因此在以下代码块中,我们将只列出一些值得注意的值。您应该查看完整的输出,以查看您认为有趣的其他信息:
$ docker container inspect 3c4f916619a5
[{ "Id": "3c4f916619a5dfc420396d823b42e8bd30a2f94ab5b0f42f052357a68a67309b", "Created": "2022-07-17T17:26:53.611762541Z", … "Args": [],
…
"Image": "sha256:27941809078cc9b2802deb2b0bb6feed6c…7f200e24653533701ee",
…
"Config": {
"Hostname": "3c4f916619a5",
…
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
…
"Image": "ubuntu",
…
},
…
}]
请注意那个长的 "Id" 字符串。那是该容器的完整唯一标识符。幸运的是,我们可以使用短版本,即使这仍然不是特别方便。我们还可以看到容器创建的精确时间,比 "docker container ls" 给出的时间要精确得多。
这里还显示了一些其他有趣的信息:容器中的顶级命令、创建时传递给它的环境变量、它所基于的镜像以及容器内部的主机名。如果需要,所有这些都可以在容器创建时进行配置。例如,将配置传递给容器的常用方法是通过环境变量。因此,通过 "docker container inspect" 命令查看容器的配置信息在调试时会提供很多有用的信息。
如果您愿意,您可以通过运行类似于 "docker container stop 3c4f916619a5" 的命令来停止当前的容器。
进入运行中的容器
让我们启动一个只有交互式 bash shell 的容器,这样我们就可以进入容器内部查看。我们可以像之前一样运行以下命令来实现:
$ docker container run --rm -it ubuntu:22.04 /bin/bash
这将以 bash shell 作为顶层进程运行一个 Ubuntu 22.04 LTS 容器。通过指定 "22.04" 标签,我们可以确保获取特定版本的镜像。那么,当我们启动该容器时,有哪些进程在运行呢?
root@35fd1ad27228:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 17:45 pts/0 00:00:00 /bin/bash
root 9 1 0 17:47 pts/0 00:00:00 ps -ef
哇,真的不多,不是吗?原来当我们让 Docker 启动 bash 时,除了 bash 之外,没有启动其他任何进程。我们进入了整个 Linux 发行版的镜像,但没有自动为我们启动其他进程。我们只得到了我们所请求的内容。今后使用时,最好记住这一点。
这就是我们在容器中运行 shell 的方法。随意探索并查看容器内的其他有趣内容。您可能只能使用一组有限的命令。不过,您处于基本的 Ubuntu 发行版中,因此可以使用 "apt-get update",然后使用 "apt-get install..." 下载更多软件包。但是这些应用程序只会在此容器的生命周期内存在。您正在修改容器的顶层,而不是基础镜像!容器天然是短暂的,因此您在容器内所做的任何操作都不会超越容器的生命周期。
当您完成在容器内的操作后,请确保退出 shell,这将自然停止容器的运行:
root@35fd1ad27228:/# exit
返回结果
如果每次运行一个命令并获取结果都要启动一个完整的虚拟机,那将会非常低效。通常情况下,您不会这样做,因为这将非常耗时,并且需要启动整个操作系统来执行一个简单的命令。但是 Docker 和 Linux 容器的工作方式与虚拟机不同:容器非常轻量级,不必像操作系统那样启动。在 Linux 容器中运行类似快速后台任务并等待退出代码是正常的用例。您可以将其视为一种远程访问容器化系统的方式,并且可以访问容器内的任何个别命令,可以将数据传送到容器中的命令并从容器中获取数据,并返回退出代码。
这在许多场景下都非常有用:例如,您可以远程运行系统健康检查,或者在一系列使用 Docker 启动的机器上处理工作负载并返回结果。docker 命令行工具会将结果代理到本地机器。如果您以前台模式运行远程命令且没有指定其他操作,docker 将将其 stdin 重定向到远程进程,并将远程进程的 stdout 和 stderr 重定向到您的终端。我们要做的唯一事情是以前台模式运行命令,并且不在远程分配 TTY。这也是默认配置!不需要使用命令行选项。
当我们运行这些命令时,Docker 会创建一个新的容器,在容器的命名空间和控制组中执行我们请求的命令,然后删除容器,并退出,以确保在调用之间不会留下任何正在运行的容器或占用不必要的磁盘空间。以下代码应该给您一些可以做的事情的想法:
$ docker container run --rm ubuntu:22.04 /bin/false
$ echo $?
1
$ docker container run --rm ubuntu:22.04 /bin/true
$ echo $?
0
$ docker container run --rm ubuntu:22.04 /bin/cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
…
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
$ docker container run --rm ubuntu:22.04 /bin/cat /etc/passwd | wc -l
19
在这里,我们在远程服务器上执行了 /bin/false 命令,它总是会以状态码 1 退出。请注意,Docker 将这个结果代理给了我们在本地终端中。为了证明它可以返回其他结果,我们还运行了 /bin/true 命令,它总是会返回状态码 0。就是这样。
然后,我们实际上要求 Docker 在远程容器上运行 cat /etc/passwd 命令。我们得到了一个打印出该容器文件系统内 /etc/passwd 文件内容的输出。因为这只是普通的标准输出,所以我们可以像对待其他任何输出一样将其导入到本地命令中进行处理。
进入正在运行的容器内部
您可以很容易地在一个新容器中运行一个 shell,基于几乎任何镜像,就像我们之前演示的使用 docker container run 的方式。但是,这与在已经在运行您的应用程序的现有容器内获取一个新 shell 是不同的。每次使用 docker container run,您都会得到一个新的容器。但如果您有一个已经在运行应用程序的现有容器,并且您需要从容器内部进行调试,您需要使用其他方法。
使用 docker container exec 是在容器中获取新交互式进程的 Docker 本地方式,但也有一种更 Linux 本地的方法,称为 nsenter。我们将在本节中介绍 docker container exec,并稍后在 "nsenter" 中介绍它。
docker container exec
首先,让我们看一下进入正在运行的容器最简单和最好的方法。dockerd 服务器和 docker 命令行工具支持通过 docker container exec 命令在正在运行的容器内远程执行新的进程。因此,让我们启动一个后台模式的容器,然后使用 docker container exec 进入它,并调用一个 shell。
您执行的命令不一定是一个 shell:您可以使用 docker container exec 在容器内运行单独的命令,并在容器外部查看其结果。但是,如果您想进入容器内部查看情况,shell 是最简单的方式。
为了运行 docker container exec,我们需要知道容器的 ID。在这个演示中,让我们创建一个容器,只运行 sleep 命令,持续 600 秒:
$ docker container run -d --rm ubuntu:22.04 sleep 600
9f09ac4bcaa0f201e31895b15b479d2c82c30387cf2c8a46e487908d9c285eff
这个容器的短ID是 9f09ac4bcaa0。现在我们可以使用它来通过 docker container exec 进入容器。此命令的命令行与 docker container run 的命令行非常相似。我们使用 -i 和 -t 标志请求一个交互式会话和一个伪终端(pseudo-TTY):
$ docker container exec -it 9f09ac4bcaa0 /bin/bash
root@9f09ac4bcaa0:/#
请注意,我们得到了一个命令行回复,告诉我们当前运行容器的ID。这对于跟踪我们所处的位置非常有用。现在,我们可以运行正常的Linux ps命令,查看容器内部还有哪些进程在运行。我们应该能够看到在容器最初启动时创建的sleep进程:
root@9f09ac4bcaa0:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 20:22 ? 00:00:00 sleep 600
root 7 0 0 20:23 pts/0 00:00:00 /bin/bash
root 15 7 0 20:23 pts/0 00:00:00 ps -ef
当你完成后,输入"exit"来退出容器。
docker volume
Docker支持volume子命令,可以列出存储在根目录中的所有卷,并发现有关它们的其他信息,包括在服务器上的物理存储位置。
这些卷不是绑定挂载的,而是特殊的数据容器,为持久化数据提供了一种有用的方法。
如果我们运行一个普通的Docker命令,将目录进行绑定挂载,我们会注意到它不会创建任何Docker卷:
$ docker volume ls
DRIVER VOLUME NAME
$ docker container run --rm -d -v /tmp:/tmp ubuntu:latest sleep 120
6fc97c50fb888054e2d01f0a93ab3b3db172b2cd402fc1cd616858b2b5138857
$ docker volume ls
DRIVER VOLUME NAME
然而,你可以轻松地通过类似以下的命令来创建一个新的卷:
$ docker volume create my-data
如果您然后列出所有的卷,您应该会看到类似这样的内容:
$ docker volume ls
DRIVER VOLUME NAME
local my-data
$ docker volume inspect my-data
[
{
"CreatedAt": "2022-07-31T16:19:42Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/my-data/_data",
"Name": "my-data",
"Options": {},
"Scope": "local"
}
]
现在,您可以通过运行以下命令启动一个容器,并将这个数据卷附加到容器中:
$ docker container run --rm \
--mount source=my-data,target=/app \
ubuntu:latest touch /app/my-persistent-data
那个容器在数据卷中创建了一个文件,然后立即退出了。 如果我们现在将该数据卷挂载到另一个容器,我们将会看到我们的数据仍然存在:
$ docker container run --rm \
--mount source=my-data,target=/app \
fedora:latest ls -lFa /app/my-persistent-data
-rw-r--r-- 1 root root 0 Jul 31 16:24 /app/my-persistent-data
最后,当您完成使用数据卷时,可以通过运行以下命令来删除数据卷:
$ docker volume rm my-data
my-data
这些命令将帮助您详细了解容器。在第11章中,我们将更详细地解释命名空间,从而让您更好地理解所有这些组件是如何相互作用和组合来创建一个容器的。
日志
日志记录是任何生产应用程序的关键部分。当出现问题时,日志可以成为恢复服务的关键工具,因此需要做得很好。在Linux系统上,我们期望以某些常见方式与应用程序日志进行交互,其中一些方式比其他方式更好。如果您在一台机器上运行应用程序进程,您可能期望输出会进入一个本地日志文件,您可以通过它来查阅。或者,您可能期望输出直接被记录到内核缓冲区中,从中可以通过dmesg命令读取。或者,在许多现代Linux发行版中,例如systemd系统中,您可能期望可以从journalctl命令中查看日志。由于容器的限制以及Docker的构建方式,如果没有进行一些配置,这些方式都无法正常工作。但这没关系,因为Docker对日志记录提供了一流的支持。
Docker通过几种关键方式使日志记录更加容易。首先,它捕获并记录了容器中应用程序的所有正常文本输出。容器中发送到stdout或stderr的任何内容都会被Docker守护进程捕获,并流式传输到可配置的日志后端。其次,与Docker的许多其他部分一样,这个日志系统也是可插拔的,您可以选择许多功能强大的插件选项。但我们还是先不要深入研究这些选项。
Docker容器日志
我们将从最简单的Docker用例开始:默认的日志记录机制。这个机制有一些限制,我们一会儿会解释,但对于大多数常见的用例,它表现良好且非常方便。如果您在开发中使用Docker,这可能是您在那里使用的唯一日志记录策略。这种日志记录方法从一开始就存在,并且得到了很好的理解和支持。该机制是json-file方法,docker container logs命令让大多数用户能够使用它。
正如名称所示,当您运行默认的json-file日志记录插件时,Docker守护进程会将您的应用程序日志流式传输到每个容器的一个JSON文件中。这使我们能够随时检索任何容器的日志。
我们可以通过启动一个nginx容器来显示一些日志:
$ docker container run --rm -d --name nginx-test --rm nginx:latest
然后:
$ docker container logs nginx-test
…
2022/07/31 16:36:05 [notice] 1#1: using the "epoll" event method
2022/07/31 16:36:05 [notice] 1#1: nginx/1.23.1
2022/07/31 16:36:05 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
2022/07/31 16:36:05 [notice] 1#1: OS: Linux 5.10.104-linuxkit
…
这很好,因为Docker允许您通过命令行远程获取日志,按需查看。这对于低容量日志记录非常有用。
实际支持此日志记录的文件位于Docker服务器本身,默认情况下在/var/lib/docker/containers/<container_id>/目录下,其中<container_id>会被实际的容器ID替换。如果您查看名为<container_id>-json.log的文件,您会看到它是一个每行表示一个JSON对象的文件。它会类似于以下内容:
{"log":"2022/07/31 16:36:05 [notice] 1#1: using the "epoll" event method\n",
"stream":"stderr","time":"2022-07-31T16:36:05.189234362Z"}
那个"log"字段正是该进程发送到stdout的内容;"stream"字段告诉我们这是stdout而不是stderr,而Docker守护进程接收到该内容的确切时间则提供在"time"字段中。这是一种不常见的日志格式,但它是结构化的而不仅仅是原始流,这在以后处理日志时是有益的。
与日志文件类似,您还可以使用docker container logs -f命令实时追踪Docker日志:
$ docker container logs -f nginx-test
…
2022/07/31 16:36:05 [notice] 1#1: start worker process 35
2022/07/31 16:36:05 [notice] 1#1: start worker process 36
2022/07/31 16:36:05 [notice] 1#1: start worker process 37
2022/07/31 16:36:05 [notice] 1#1: start worker process 38
这看起来与通常的docker container logs命令相同,但客户端将继续等待并显示从服务器接收到的新消息,就像Linux命令行中的tail -f一样。您可以随时键入Ctrl-C来退出日志流:
---
$ docker container stop nginx-test
---
对于单主机日志记录,这个机制相当不错。它的缺点在于日志轮换、一旦日志被轮换后的远程访问,以及高容量日志记录的磁盘空间使用。尽管它是由JSON文件支持的,但这个机制的性能足够好,大多数生产应用程序可以使用这种方式进行日志记录,如果这对您有效的话。但是,如果您有一个更复杂的环境,您可能希望拥有更强大且具有集中式日志记录功能的解决方案。
更高级的日志记录方式
对于那些默认机制不足以满足需求的时候(在大规模情况下,可能并不足够),Docker还支持可配置的日志后端。支持的插件列表不断增加。目前支持的有我们之前介绍过的json-file,以及syslog、fluentd、journald、gelf、awslogs、splunk、gcplogs、local和logentries等插件,这些插件用于将日志发送到各种流行的日志框架和服务中。
这是一个我们刚刚列出的庞大插件列表。目前在大规模运行Docker时,最简单的支持选项是直接从Docker将容器日志发送到syslog。您可以在Docker命令行中使用--log-driver=syslog选项来指定,或者在daemon.json文件中将其设置为所有容器的默认选项。
还有一些第三方插件可供选择。我们从第三方插件中看到了一些不同的结果,主要是因为它们增加了安装和维护Docker的复杂性。然而,您可能会发现有一个第三方实现对于您的系统非常合适,并且值得进行安装和维护。
传统上,大多数Linux系统都有一种syslog接收器,无论是syslog、rsyslog还是其他许多选项。这个协议以其各种形式已经存在很长时间,并且在大多数部署中得到了相当好的支持。当从传统的Linux或Unix环境迁移到Docker时,许多公司已经在使用syslog基础设施,这意味着这通常也是最简单的迁移路径。
尽管syslog是一种传统的解决方案,但它也存在一些问题。Docker syslog驱动程序支持TLS、TCP和UDP连接选项,听起来很不错,但是您应该谨慎地在TCP或TLS上从Docker流式传输日志到远程日志服务器。问题在于它们都在基于连接的TCP会话之上运行,并且Docker会在容器启动时尝试连接到远程日志服务器。如果连接失败,它将阻塞尝试启动容器。如果您将其作为默认的日志记录机制运行,这可能在任何部署的任何时候出现问题。
这对于生产系统来说并不是一个特别可用的状态,因此建议您在使用syslog驱动程序时选择UDP选项进行日志记录。这意味着您的日志不会被加密,并且没有确保交付。对于日志记录,有各种不同的哲学观念,您需要权衡您对日志的需求和系统的可靠性。我们倾向于建议优先考虑可靠性,但如果您在一个安全的审计环境中运行,您可能有不同的优先事项。
关于大多数日志插件还需要注意的最后一个注意事项是:它们默认是阻塞的,这意味着日志反向压力可能会影响您的应用程序。您可以通过设置--log-opt mode=non-blocking来改变此行为,然后将日志的最大缓冲区大小设置为像--log-opt max-buffer-size=4m这样的值。一旦设置了这些参数,当缓冲区填满时,应用程序将不再阻塞。相反,内存中的最旧日志行将被丢弃。同样,您需要权衡可靠性与您的业务对接收所有日志的需求。
监控Docker
对于生产系统来说,其中最重要的要求之一是可观察和可测量性。一个生产系统,如果你对它的行为一无所知,将无法为您提供良好的服务。在现代运维环境中,我们希望监控所有有意义的内容,并报告尽可能多的有用统计数据。Docker支持容器健康检查和一些基本的报告功能,可以通过docker container stats和docker system events命令查看。我们将向您展示这些功能,然后再看看Google提供的一个社区解决方案,可以生成一些漂亮的图形输出,然后我们将查看一个目前还处于实验阶段的Docker特性,它可以将容器指标导出到Prometheus监控系统中。
容器统计信息
让我们从Docker本身附带的CLI工具开始。docker CLI有一个端点用于查看正在运行的容器的重要统计信息。该命令行工具可以从此端点流式传输数据,并且每隔几秒钟报告一次一个或多个列出的容器的基本统计信息,显示正在发生的情况。docker container stats命令类似于Linux的top命令,它接管当前终端并更新屏幕上的相同行以显示当前信息。这很难在文字上展示,因此我们将给出一个例子,但默认情况下它每隔几秒钟更新一次。
命令行统计信息
启动一个正在运行的容器:
$ docker container run --rm -d --name stress \
docker.io/spkane/train-os:latest \
stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128M --timeout 60s
然后运行stats命令查看新的容器:
$ docker container stats stress
CONTAINER ID NAME CPU % MEM USAGE/LIMIT MEM % NET I/O BLOCK I/O PIDS
1a9f52f0855f stress 476.50% 36.09MiB/7.773GiB 0.45% 1.05kB/0B 0B/0B 6
您可以随时键入Ctrl-C来退出stats流。
让我们将这些相当密集的输出拆分为一些可管理的部分。我们有以下信息:
- 容器ID(但不包括名称)。
- 它当前消耗的CPU量。百分之百相当于一个完整的CPU核心。
- 它正在使用的内存量,后面跟着它被允许使用的最大内存量。
- 网络和块I/O统计信息。
- 容器内活动进程的数量。
这些信息中有些对于调试来说比其他信息更有用,所以让我们看看您可以用它们做些什么。
其中一个更有帮助的输出是内存使用百分比与容器设置的限制之间的比较。在运行生产容器时,常见的问题是过于激进的内存限制会导致Linux内核的OOM killer不断地停止容器。stats命令可以帮助您识别和解决这些类型的问题。
关于I/O统计信息,如果您在容器中运行所有应用程序,那么这个摘要可以清楚地显示系统中的I/O去向。在使用容器之前,这要难得多!
容器内活动进程的数量也有助于调试。如果您有一个正在生成子进程而不清理它们的应用程序,这很容易暴露出问题。
docker container stats的一个很棒的功能是它可以在一个单一摘要中显示所有容器,这非常有启示性,即使在您认为您知道它们在做什么的盒子上也是如此。
所有这些信息都很有用且容易理解,因为它们以人类可读的格式提供,并且可以在命令行上查看。但是,Docker API还提供了一个额外的端点,提供的信息比客户端显示的要丰富得多。到目前为止,我们在本书中避免直接使用API,但在这种情况下,API提供的数据比客户端更丰富,我们将使用curl来发出API请求,看看我们的容器正在做什么。虽然阅读起来不太方便,但它提供了更多的细节。
在“stats API端点”的示例是直接调用API的一个很好的入门介绍。
stats API端点
我们将在API上访问的/stats/端点将持续向我们传输统计信息,只要我们保持连接开启。由于作为人类我们不能轻松解析JSON,我们只会请求一行数据,然后使用jq工具来“漂亮地打印”它。要使这个命令正常工作,您需要安装jq(版本2.6或更高版本)。如果您没有安装jq但仍想查看JSON输出,可以跳过管道到jq的部分,但是您将得到普通、丑陋的JSON输出。如果您已经有一个喜欢的JSON漂亮打印工具,可以自由地使用它来代替。
大多数Docker守护进程将只在Unix域套接字上可用API,而不会在TCP上发布。所以我们将使用Docker服务器主机上的curl来调用API。如果您计划在生产中监控这个端点,您需要将Docker API暴露在一个TCP端口上。这不是我们推荐的做法,但Docker文档将引导您完成此操作。
首先,启动一个容器,您可以从中读取统计信息:
$ docker container run --rm -d --name stress \
docker.io/spkane/train-os:latest \
stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128M --timeout 60s
现在容器正在运行,您可以通过类似于curl的命令以JSON格式获取关于容器的持续流式统计信息,命令中使用您容器的名称或哈希值。
$ curl --no-buffer -XGET --unix-socket /var/run/docker.sock \
http://docker/containers/stress/stats
要获取单个统计信息组,我们可以运行类似于以下的命令:
$ curl -s -XGET --unix-socket /var/run/docker.sock \
http://docker/containers/stress/stats | head -n 1 | jq
最后,如果我们有jq或其他能够漂亮地打印JSON的工具,我们可以将输出变得更易读,如下所示:
$ curl -s -XGET --unix-socket /var/run/docker.sock \
http://docker/containers/stress/stats | head -n 1 | jq
{
"read": "2022-07-31T17:41:59.10594836Z",
"preread": "0001-01-01T00:00:00Z",
"pids_stats": {
"current": 6,
"limit": 18446744073709552000
},
"blkio_stats": {
"io_service_bytes_recursive": [
{
"major": 254,
"minor": 0,
"op": "read",
"value": 0
},
…
]
},
"num_procs": 0,
"storage_stats": {},
"cpu_stats": {
"cpu_usage": {
"total_usage": 101883204000,
"usage_in_kernelmode": 43818021000,
"usage_in_usermode": 58065183000
…
},
},
"memory_stats": {
"usage": 183717888,
"stats": {
"active_anon": 0,
"active_file": 0,
…
},
"limit": 8346021888
},
"name": "/stress",
"id": "9be7c9de26864ac97e07fc3d8e3ffb5bb52cc2ba49f569d4ba8d407f8747851f",
"networks": {
"eth0": {
"rx_bytes": 1046,
"rx_packets": 9,
…
}
}
}
其中包含很多信息。为了避免浪费不必要的纸张和电子资源,我们对其进行了简化,但即便如此,需要消化的内容仍然很多。主要目的是让您了解API提供了关于每个容器的大量数据。我们不会花太多时间深入了解细节,但您可以获取相当详细的内存使用信息,以及块I/O和CPU使用信息。
如果您正在进行自己的监控,这也是一个很好的端点。然而,缺点是每个容器一个端点,因此您无法通过单个调用获取有关所有容器的统计信息。
容器健康检查
与任何其他应用程序一样,当您启动一个容器时,它有可能启动并运行,但实际上从未进入可以接收流量的健康状态。生产系统也会发生故障,您的应用程序可能在其生命周期内的某个时候变得不健康,因此您需要能够处理这种情况。
许多生产环境都有标准化的应用程序健康检查方法。不幸的是,在跨组织的情况下,目前没有明确的标准,并且很少有公司以相同的方式执行。因此,构建了监控系统来处理这种复杂性,以便在许多不同的生产系统中工作。这是一个明确需要标准的地方。
为了帮助消除这种复杂性并在通用接口上进行标准化,Docker添加了健康检查机制。遵循货运集装箱的比喻,Linux容器实际上在外部看起来应该是相同的,无论容器内部是什么,因此Docker的健康检查机制不仅标准化了容器的健康检查,而且保持了容器内部和外部的隔离。这意味着来自Docker Hub或其他共享仓库的容器可以实现标准化的健康检查机制,并且将在任何设计用于运行生产容器的Docker环境中运行。
健康检查是构建时的配置项,并在Dockerfile中使用HEALTHCHECK指令创建。此指令告诉Docker守护进程它可以在容器内部运行的命令,以确保容器处于健康状态。只要该命令以零(0)代码退出,Docker将认为容器是健康的。任何其他退出代码将指示Docker容器处于不健康状态,此时调度器或监控系统可以采取适当的措施。
在接下来的几章中,我们将使用以下项目来探索Docker Compose。但是,现在它包含了一个有用的Docker健康检查示例。继续并下载代码的副本,然后进入rocketchat-hubot-demo/mongodb/docker/目录:
$ git clone https://github.com/spkane/rocketchat-hubot-demo.git \
--config core.autocrlf=input
$ cd rocketchat-hubot-demo/mongodb/docker
在这个目录中,您会看到一个Dockerfile和一个名为docker-healthcheck的脚本。如果您查看Dockerfile,您将看到以下内容:
FROM docker.io/bitnami/mongodb:4.4
# Newer Upstream Dockerfile:
# https://github.com/bitnami/containers/blob/
# f9fb3f8a6323fb768fd488c77d4f111b1330bd0e/bitnami/mongodb
# /5.0/debian-11/Dockerfile
COPY docker-healthcheck /usr/local/bin/
# Useful Information:
# https://docs.docker.com/engine/reference/builder/#healthcheck
# https://docs.docker.com/compose/compose-file/#healthcheck
HEALTHCHECK CMD ["docker-healthcheck"]
这个文件非常简短,因为我们基于上游的Mongo镜像进行构建,我们的镜像继承了许多内容,包括入口点、默认命令和要暴露的端口。
EXPOSE 27017
ENTRYPOINT [ "/opt/bitnami/scripts/mongodb/entrypoint.sh" ]
CMD [ "/opt/bitnami/scripts/mongodb/run.sh" ]
因此,在我们的Dockerfile中,我们只添加了一个可以对容器进行健康检查的脚本,并定义了一个健康检查命令来运行该脚本。
您可以像这样构建容器:
$ docker image build -t mongo-with-check:4.4 .
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/bitnami/mongodb:4.4 0.5s
=> [internal] load build context 0.0s
=> => transferring context: 40B 0.0s
=> CACHED [1/2] FROM docker.io/bitnami/mongodb:4.4@sha256:9162…ae209 0.0s
=> [2/2] COPY docker-healthcheck /usr/local/bin/ 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:a6ef…da808 0.0s
=> => naming to docker.io/library/mongo-with-check:4.4 0.0s
然后运行容器并查看docker container ls的输出:
$ docker container run -d --rm --name mongo-hc mongo-with-check:4.4
5a807c892428ab0641232c82bd477fc8d1142c9e15c27d5946b8bfe7056e2695
$ docker container ls
… IMAGE … STATUS PORTS …
… mongo-with-check:4.4 … Up 1 second (health: starting) 27017/tcp …
您会注意到STATUS列现在有一个带括号的健康部分。初始状态下,这将显示为health: starting,因为容器正在启动。您可以使用docker container run的--health-start-period参数来更改Docker等待容器初始化的时间。一旦容器启动并且健康检查成功,状态将变为healthy。可能需要40秒或更长时间,容器才能转换为健康状态。
$ docker container ls
… IMAGE … STATUS PORTS …
… mongo-with-check:4.4 … Up 32 seconds (healthy) 27017/tcp …
您可以直接查询此状态,使用docker container inspect命令:
$ docker container inspect --format='{{.State.Health.Status}}' mongo-hc
healthy
$ docker container inspect --format='{{json .State.Health}}' mongo-hc | jq
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
…
]
}
如果您的容器开始失败健康检查,状态将变为unhealthy,然后您可以确定如何处理这种情况:
$ docker container ls
… IMAGE … STATUS PORTS …
… mongo-with-check:4.4 … Up 9 minutes (unhealthy) 27017/tcp …
此时,您可以通过简单运行docker container stop mongo-hc来停止容器。
这个功能非常有用,强烈建议您在所有容器中使用它。这将帮助您提高环境的可靠性和您对其运行情况的可见性。它还受到许多生产调度器和监控系统的支持,因此实施起来应该很容易。
Docker系统事件
dockerd守护程序在容器的生命周期内部生成一个事件流。这是系统中的各个部分了解其他部分正在发生的情况的方式。您还可以连接到此流来查看在Docker服务器上发生的容器生命周期事件。现在,正如您可能期望的那样,这也被实现为docker CLI工具中的另一个命令行参数。当您运行此命令时,它会阻塞并不断向您流式传输消息。在幕后,这是对Docker API的长时间HTTP请求,会在发生时以JSON块形式返回消息。docker CLI工具对其进行解码并在终端上打印一些数据。
这个事件流在监控场景或触发其他操作时非常有用,例如希望在作业完成时收到警报。对于调试目的,它允许您查看容器何时死亡,即使Docker稍后重新启动它。将来,这也是您可能会直接根据API实现一些工具的地方。
在一个终端中,请继续运行events命令:
$ docker system events
您会注意到什么都没有发生。 在另一个终端中,请继续启动以下短暂存在的容器:
$ docker container run --rm --name sleeper debian:latest sleep 5
在运行events命令的原始终端中,现在您应该会看到类似于这样的内容:
…09:59.606… container create d6… (image=debian:latest, name=sleeper)
…09:59.610… container attach d6… (image=debian:latest, name=sleeper)
…09:59.631… network connect ea… (container=d60b…, name=bridge, type=bridge)
…09:59.827… container start d6… (image=debian:latest, name=sleeper)
…10:04.854… container die d6… (exitCode=0, image=debian:latest, name=sleeper)
…10:04.907… network disconnect ea… (container=d60b…, name=bridge, type=bridge)
…10:04.922… container destroy d6… (image=debian:latest, name=sleeper)
您可以随时键入Ctrl-C来退出事件流。
在这个例子中,我们运行了一个简单的短暂容器,它只是简单地计数了5秒钟然后退出。
容器创建、容器附加、网络连接和容器启动事件都是将容器置于运行状态所需的步骤。当容器退出时,事件流会记录容器死亡、网络断开连接和容器销毁的消息。每个事件标记了完全销毁容器的步骤。Docker还很贴心地告诉我们容器正在运行的镜像的ID。这对于将部署与事件联系起来可能会很有用,因为部署通常涉及新镜像。
如果您的服务器上的容器无法保持运行,docker system events流对于查看发生了什么以及何时发生的情况非常有帮助。但是,如果您在那时没有查看它,Docker会非常贴心地缓存一些事件,您仍然可以在之后的一段时间内获取它们。您可以使用--since选项请求在某个时间之后显示事件,或者使用--until选项请求在某个时间之前显示事件。您也可以同时使用这两个选项将时间窗口限制在您正在调查的问题可能发生的狭窄时间范围内。这两个选项都使用ISO时间格式,例如前面的示例中的格式(例如,2018-02-18T14:03:31-08:00)。
cAdvisor
docker container stats和docker system events是有用的工具,但目前还不能为我们提供图形展示。而在查看趋势时,图形是非常有帮助的。当然,其他人已经填补了这方面的空白。当您开始探索用于监控Docker的选项时,您会发现许多主要的监控工具现在提供了一些功能,帮助您改进对容器性能和运行状态的可见性。
除了Datadog、GroundWork和New Relic等公司提供的商业工具之外,还有许多免费的开源工具可供选择,例如Prometheus或者Nagios。我们将在“Prometheus Monitoring”一节中详细介绍Prometheus。在Docker推出后不久,Google将其内部的容器监控工具作为一个维护良好的开源项目发布在GitHub上,名为cAdvisor。虽然cAdvisor可以在Docker之外运行,但现在您可能不会感到惊讶,最简单的cAdvisor实现方式就是将其作为一个Linux容器运行。
要在大多数Linux系统上安装cAdvisor,您只需要运行以下代码。
$ docker container run \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--publish=8080:8080 \
--detach=true \
--name=cadvisor \
--privileged \
--rm \
--device=/dev/kmsg \
gcr.io/cadvisor/cadvisor:latest
Unable to find image 'cadvisor/cadvisor:latest' locally
Pulling repository cadvisor/cadvisor
f0643dafd7f5: Download complete
…
ba9b663a8908: Download complete
Status: Downloaded newer image for cadvisor/cadvisor:latest
f54e6bc0469f60fd74ddf30770039f1a7aa36a5eda6ef5100cddd9ad5fda350b
完成安装后,您可以在Docker主机上的8080端口上访问cAdvisor的Web界面(例如,http://172.17.42.10:8080/),并查看主机和各个容器的各种详细图表(参见图6-1)。
cAdvisor提供了一个REST API端点,您的监控系统可以轻松查询详细信息:
$ curl http://172.17.42.10:8080/api/v2.1/machine/
您可以在官方文档中找到有关cAdvisor API的详细信息。cAdvisor提供的详细信息应该足够满足您许多图形和监控需求。
Prometheus监控
Prometheus监控系统已成为监控分布式系统的热门解决方案。它主要采用拉模型,在固定时间间隔内从端点获取统计信息。Docker提供了一个专门为Prometheus构建的端点,使将容器的统计数据集成到Prometheus监控系统变得简单。在撰写本文时,该端点仍处于实验阶段,并且默认情况下未在dockerd服务器中启用。我们的简短体验显示它似乎运行良好,并且是一个非常不错的解决方案,我们将向您展示。值得指出的是,这个解决方案用于监控dockerd服务器,与其他解决方案相反,其他解决方案暴露了有关容器的信息。
要将指标导出到Prometheus,我们需要重新配置dockerd服务器以启用实验性特性,并在我们选择的端口上公开指标侦听器。这很好,因为我们不必暴露整个Docker API到TCP侦听器来获取系统的指标数据 - 这是一种安全性考虑,代价是多一点配置。为此,我们可以在命令行上提供--experimental和--metrics-addr=选项,或者将它们放入守护进程用于配置自身的daemon.json文件中。由于许多当前的发行版运行systemd,并且在那里更改配置高度依赖于您的安装情况,我们将使用daemon.json选项,因为它更加通用。我们将在Ubuntu Linux 22.04 LTS上演示此过程。在这个发行版上,通常不会有这个文件。因此,让我们使用您喜欢的编辑器创建一个。
调整或添加以下行到daemon.json文件中:
{
"experimental": true,
"metrics-addr": "0.0.0.0:9323"
}
现在您应该有一个只包含刚刚粘贴内容的文件,没有其他内容。
当我们重启Docker后,现在将在所有地址上监听9323端口。这是Prometheus将连接的地方获取指标数据。但是首先,我们需要重启dockerd服务器。Docker Desktop会自动为您处理重启,但如果您在Linux Docker服务器上,则可以运行类似sudo systemctl restart docker的命令来重启守护进程。重启时不应返回任何错误。如果有错误返回,可能是在daemon.json文件中设置了错误的内容。
现在,您可以使用curl测试指标端点:
$ curl -s http://localhost:9323/metrics | head -15
# HELP builder_builds_failed_total Number of failed image builds
# TYPE builder_builds_failed_total counter
builder_builds_failed_total{reason="build_canceled"} 0
builder_builds_failed_total{reason="build_target_not_reachable_error"} 0
builder_builds_failed_total{reason="command_not_supported_error"} 0
builder_builds_failed_total{reason="dockerfile_empty_error"} 0
builder_builds_failed_total{reason="dockerfile_syntax_error"} 0
builder_builds_failed_total{reason="error_processing_commands_error"} 0
builder_builds_failed_total{reason="missing_onbuild_arguments_error"} 0
builder_builds_failed_total{reason="unknown_instruction_error"} 0
# HELP builder_builds_triggered_total Number of triggered image builds
# TYPE builder_builds_triggered_total counter
builder_builds_triggered_total 0
# HELP engine_daemon_container_actions_seconds The number of seconds it
# takes to process each container action
# TYPE engine_daemon_container_actions_seconds histogram
如果您在本地运行此命令,您应该得到非常相似的输出。可能不完全相同,但只要您得到的不是错误消息,那就没问题。
现在,我们有了一个Prometheus可以获取我们的统计数据的位置。但我们需要在某个地方运行Prometheus,对吧?我们可以通过启动一个容器来轻松实现这一点。但首先,我们需要编写一个简单的配置。我们将其放在/tmp/prometheus/prometheus.yaml中。您可以使用您喜欢的编辑器将以下内容放入文件中:
# Scrape metrics every 5 seconds and name the monitor 'stats-monitor'
global:
scrape_interval: 5s
external_labels:
monitor: 'stats-monitor'
# We're going to name our job 'DockerStats' and we'll connect to the docker0
# bridge address to get the stats. If your docker0 has a different IP address
# then use that instead. 127.0.0.1 and localhost will not work.
scrape_configs:
- job_name: 'DockerStats'
static_configs:
- targets: ['172.17.0.1:9323']
如文件中所指出的,您应该在这里使用您的docker0桥接口的IP地址,或者您的ens3或eth0接口的IP地址,因为localhost和127.0.0.1对于容器来说不可路由。我们在这里使用的地址是docker0的通常默认地址,所以这可能是适合您的正确地址。
现在我们已经编写好配置文件,我们需要使用这个配置启动容器:
$ docker container run --rm -d -p 9090:9090 \
-v /tmp/prometheus/prometheus.yaml:/etc/prometheus.yaml \
prom/prometheus --config.file=/etc/prometheus.yaml
这将运行容器并将我们创建的配置文件挂载到容器中,以便它能找到监视我们的Docker端点所需的设置。如果它成功启动,您现在可以打开浏览器并导航到主机的9090端口。在那里,您将看到一个类似于图6-2的Prometheus窗口。
在下面的图中,您将看到我们选择了其中一个指标,即engine_daemon_events_total,并在短时间内将其绘制成图表。您可以轻松查询下拉列表中的任何其他指标。进一步的工作和探索,您可以根据这些指标定义警报和警报策略。而且监视的范围不仅仅局限于dockerd服务器。您还可以从应用程序中公开指标供Prometheus使用。如果您对此感兴趣,想要查看更高级的内容,您可以查看dockprom,它利用Grafana创建漂亮的仪表板,并查询您的容器指标,就像Docker API /stats端点中的指标一样。
探索
这些内容应该为您开始运行容器提供了基本知识。值得下载一两个容器,从Docker Hub注册表中进行探索,以熟悉我们刚刚学到的命令。Docker还有许多其他功能,包括但不限于以下内容:
- 使用docker container cp命令将文件复制进出容器
- 使用docker image save命令将镜像保存为tar包
- 使用docker image import命令从tar包中加载镜像
Docker拥有庞大的功能集,您可能会随着时间的推移逐渐了解其中的各种功能。每个新版本都会增加更多的功能。我们将在后面对许多其他命令和功能进行详细介绍,但请记住Docker的整个功能集非常庞大。
总结
在下一章中,我们将深入了解Docker的更多技术细节,以及您如何利用这些知识来调试您的容器化应用程序。