深入浅出Docker学习笔记——第二部分

  |   0 评论   |   0 浏览

第二部分 Docker 技术

第 5 章 Docker 引擎

5.1 Docker 引擎——简介

Docker 引擎是用来运行和管理容器的核心软件。通常人们会简单地将其代指为 Docker 或者 Docker 平台,可以类别为 VMWare 的 ESXI 的角色。

基于开放容器计划(OCI)相关标准,Docker 引擎采用了模块化的设计原则,其组件是可以替换的。从多个角度来看,Docker 引擎就像汽车引擎——二者都是模块化的,并且由许多可交换的部件组成。

在今天,Docker 引擎由以下主要的组件构成:Docker 客户端(Docker Client)、Docker 守护进程(Docker daemon)、containerd 以及 runc。他们共同负责容器的创建和运行。总体逻辑图如下:

docker2.png

本书中,当提到 runc 和 containerd 时,一律使用小写的“r”和“c”。

5.2 Docker 引擎——详解

Docker 首次发布时,Docker 引擎由两个核心组件组成:LXC 和 Docker daemon。

Docker daemon 是单一的二进制文件,包含诸如 Docker 客户端、Docker API、容器运行时、镜像构建等等。LXC 提供了对诸如命名空间(Namespace)和控制组(CGroup)等基础工具的操作能力,它们是基于 Linux 内核的容器化技术。

5.2.1 摆脱 LXC

对于 LXC 的依赖始终是个问题。首先,LXC 是基于 Linux 的,而 Docker 立志要做成跨平台的。其次,核心组件依赖与外部工具,这会给项目带来巨大的风险。

基于上面的缘由,Docker 公司开发了名为 Libcontainer 的自研工具,用于代替 LXC。Libcontainer 的目标是称为与平台无关的工具,可基于同的内核为 Docker 上层提供必要的容器交互功能。在 Docker 0.9 版本中,Libcontainer 取代了 LXC 成为默认的执行驱动。

5.2.2 摒弃大而全的 Docker daemon

早期的 DOcker daemon 是个大而全的组件,这带来了很多问题,例如:难以变更、运行越来越慢以及这并非生态所期望的。

所以,拆分这个大而全的 Docker daemon 进程,并将其模块化就非常重要了。这个拆解任务的目标是尽可能拆解出其中的功能特性,并用小而专的工具来实现它。

目前,这个拆解工作仍在进行中。不过,所有容器的执行和容器运行时的代码已经完全从 daemon 中移除,并重构为小而专的工具。

目前 Docker 引擎的架构示意图如下图所示,图中由简要的描述:

docker3.png

5.2.3 开放容器计划(OCI)的影响

当 Docker 公司正在进行 Docker daemon 进程的拆解和重构的时候,OCI 也正在着手定义两个容器相关的标准:

  • 镜像规范
  • 容器运行时规范

Docker 引擎尽可能实现 OCI 的规范。例如,Docker daemon 不再包含任何容器运行时的代码——所有容器的运行代码在一个独立的 OCI 兼容层中实现。默认情况下,Docker 使用 runc 来实现这一点。runc 时 OCI 容器运行时标准的参考实现。runc 项目的目标之一就是与 OCI 规范保持一致。目前 OCI 规范均为 1.0 版本,我们不希望它们频繁地迭代,毕竟稳定胜于一切。

5.2.4 runc

runc 实质上是一个轻量级的、针对 Libcontainer 进行了包装的命令行交互工具。runc 生来只有一个作用——创建容器,这一点它非常拿手,速度很快!不过它是一个 CLI 包装器,实际上就是一个独立的容器运行时工具。因此直接下载它或者基于源码编译二进制文件,即可拥有一个全功能的 runc。但它只是一个基础工具,并不提供类似 Docker 引擎所拥有的丰富功能。

5.2.5 containerd

在对 Docker daemon 的功能进行拆解后,所有的容器执行逻辑被重构到一个新的名为 containerd 的工具中。它的主要任务是容器生命周期管理(start|stop|rm...)。containerd 在 Linux 和 Windows 中以 daemon 的方式运行。DOcker 引擎技术栈中,containerd 位于 daemon 和 runc 所在的 OCI 层之间。K8S 也可以通过 cri-containerd 使用 containerd。

如前所述,containerd 最初被设计为轻量级的小型工具,仅用于容器的生命周期管理。然而,随着时间的推移,它被赋予了更多的功能,比如镜像管理。

其原因之一在于,这样便于在其他项目中使用它。比如,在 k8s 中,containerd 就是一个很受欢迎的容器运行时。然而在 k8s 这样的项目中,如果 containerd 能够完成一些诸如 push 和 pull 镜像这样的操作就更好了。因此,如今的 containerd 还能够完成一些除容器生命周期管理之外的操作。不过,所有的这些额外功能都是模块化的、可选的,便于自行选择所需的功能。所以,k8s 这样的项目在使用 containerd 时,可以仅包含所需的功能。

containerd 时由 Docker 公司开发的,并捐献给了云原生计算基金会(CNCF),并且已经在 19 年 2 月 28 日毕业。

5.2.6 启动一个新的容器(示例)

下面我们使用 docker container run 命令来启动一个新的容器:

docker container run --name ctr1 -it alpine:latest sh

当使用 Docker 命令行工具执行如上命令时,Docker 客户端会将其转换为合适的 API 格式,并发送到正确的 API 端点。一旦 daemon 接收到创建新容器的命令,它就会向 containerd 发出调用。daemon 已经不再包含任何创建容器的代码了。daemon 使用一种 CRUD 风格的 API,通过 gRPC 与 contained 进行通信。

虽然名叫 containerd,但是它并不负责创建容器,而是指挥 runc 去做。containerd 将 Docker 镜像转换为 OCI bundle,并让 runc 基于此创建一个新的容器。

然后,runc 与操作系统内核接口进行通信,基于所有必要的工具(Namespece,CGroup 等)来创建容器,容器进程作为 runc 的子进程启动,启动完毕后,runc 将会退出。下图是新容器启动过程的模型图解:

docker4.png

5.2.7 该模型的显著优势

将所有的用于启动、管理容器的逻辑从 daemon 中移除,意味着容器运行时与 Docker daemon 是解耦的,有时称之为“无守护进程的容器”,如此,对 Docker daemon 的维护和升级工作不会影响到运行中的容器。

在旧模型中,所有容器运行时的逻辑都是在 daemon 中实现,启动和停止 daemon 会导致宿主机上所有运行中的容器被杀掉。这在生产环境是一个大问题——想一下新版 Docker 的发布频率吧!每次 daemon 的升级都会杀掉宿主机上所有的容器,这太糟了。

5.2.8 shim

shim 是实现无 daemon 的容器不可或缺的工具。前面提到,containerd 指挥 runc 来创建新容器。事实上,每次创建容器时它都会 fork 一个新的 runc 实例。不过,一旦容器创建完毕,对应的 runc 进程就会退出。因此,即使运行上百个容器,也无需保持上百个运行中的 runc 示例。

一旦容器进程的父进程 runc 退出,相关联的 containerd-shim 进程就会称为容器的父进程。作为容器的父进程,shim 的部分职责如下:

  • 保持所有的 STDIN 和 STDOUT 流是开启状态,从而当 daemon 重启的时候,容器不会因为管道的关闭而终止。
  • 将容器的退出状态反馈给 daemon

5.2.9 在 Linux 中的实现

在 Linux 系统中,前面谈到的组件由单独的二进制来实现,具体包括 docker(docker daemon)、docker-containerd(containerd)、docker-containerd-shim(shim)和 docker-runc(runc)。
在宿主机上使用 ps 命令,你能看到以上组件的进程。当然,有些进程只有运行容器的时候才可见。

5.2.10 daemon 的作用

当所有的执行逻辑和运行时代码都从 daemon 中剥离出来后,问题出现了——daemon 中还剩什么?

显然,随着越来越多的功能从 daemon 中拆解出来并被模块化,这一问题的答案页会发生变化。不过,目前来说,daemon 的主要功能包括镜像管理、镜像构建、RESTAPI、身份验证、安全、核心网络以及编排。

5.3 本章小结

基于 OCI 的开放标准,Docker 引擎目前采用模块化设计。
Docker daemon 实现了 Docker API,该 API 是一套功能丰富、基于版本的 HTTP API,并且随着其他 DOcker 项目的开发而演化。

对容器的操作由 containerd 完成。它可以被看作是负责容器生命周期的容器管理器。它小巧而轻量,可被其他项目或第三方工具使用。例如,它已成为 k8s 中默认的、常见的容器运行时。

containerd 需要指挥与 OCI 兼容的容器运行时来创建容器。默认情况下,Docker 使用 runc 作为默认的容器运行时。runc 已经是 OCI 容器运行时规范的事实上的实现了,它使用与 OCI 兼容的 bundle 来启动容器。containerd 调用 runc,并确保 Docker 镜像以 OCI bundle 的格式交给 runc。

runc 可以作为独立的 CLI 工具来创建容器,它基于 Libcontainer,也可以被其他项目或者第三方工具使用。

仍然由许多的功能是在 Docker daemon 中实现的,其中多数的功能可能随着时间的推移被拆解掉。目前 DOcker daemon 中依然存在的功能包括但不限于 API、镜像管理、身份认证、安全特性、核心网络以及卷。

Docker 引擎的模块化工作仍在进行中。

第 6 章 Docker 镜像

6.1 Docker 镜像——简介

对于运维人员来说,Docker 镜像可以理解为 VM 模板,VM 模板就像停止运行的 VM,而 Docker 镜像就像停止运行的容器;对于开发人员,可以把镜像理解为类(Class)。

读者需要从镜像仓库中拉取镜像。常见的镜像仓库服务是 Docker Hub,但是也存在其他镜像仓库服务。拉取操作会将镜像下载到本地 Docker 主机,读者可以使用该镜像启动一个或者多个容器。

镜像由多层组成,每层叠加之后,从外部看来就如一个独立的对象。镜像内部是一个精简的操作系统,同时还包含应用运行所必须的文件和依赖包。因为容器的设计初衷就是快速和小巧,所以镜像通常都比较小。

6.2 Docker 镜像——详解

前面多次提到镜像就像是停止运行的容器(类)。实际上,读者可以停止某个容器的运行,并从中创建新的镜像。在该前提下,镜像可以理解为一种构建时结构,而容器可以理解为一种运行时结构。

6.2.1 镜像和容器

我们通常使用 docker container run 和 docker service create 命令从某个镜像启动一个或多个容器。一旦容器从镜像启动后,二者之间就变成了相互依赖的关系,并且在镜像上启动的容器全部停止之前,镜像是没法被删除的。

容器的目的就是运行应用或者服务,这意味这容器的镜像中必须包含应用/服务运行所必须的操作系统和应用文件。但是,容器由追求小巧和快速,这意味这构建镜像的时候通常需要裁剪掉不必要的部分,保持较小的体积。

例如,Docker 镜像不会像一个完整的 Linux 那样提供多个 Shell 让读者选择——通常 DOcker 镜像中只有一个精简的 Shell,甚至没有 Shell。此外,镜像中还不包含内核——容器都是共享所在 Docker 主机的内核的。所以有时会说容器仅包含必要的操作系统(通常只有操作系统文件和文件系统对象)。Docker 官方镜像 Alpine Linux 大约只有 4MB,Ubuntu 官方 Docker 镜像大小大概为 110MB,这都远远小于完整的 Linux OS 镜像。

6.2.2 拉取镜像

Docker 主机安装之后,本地并没有镜像。Linux Docker 主机本地镜像仓库通常位于 /var/lib/docker/<storage-driver>,我们可以使用下面的命令来查看 Docker 主机的本地仓库中包含了哪些镜像

docker image ls

将镜像拉取到本地 Docker 主机的操作是拉取,如果你要使用最新版本的 Ubuntu 镜像,那就需要拉取它。可以通过下面的命令去拉取:

docker image pull ubuntu:latest

我的 Dokcer 主机在前面已经拉取了几个镜像,我们来看下:

[pangcm@docker01 ~]$ docker image ls 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              775349758637        2 weeks ago         64.2MB
nginx               latest              540a289bab6c        3 weeks ago         126MB
alpine              latest              965ea09ff2eb        3 weeks ago         5.55MB
hello-world         latest              fce289e99eb9        10 months ago       1.84kB

6.2.3 镜像仓库服务

Docker 镜像存储在镜像仓库服务(Image Registry)当中。Docker 客户端的镜像仓库服务是可以配置的,默认使用 Docker Hub。Docker Hub 可以分为官方仓库和非官方仓库,通常我们更加建议使用官方仓库的镜像,因为这会更加安全可靠;非官方仓库虽然也有不少的优秀镜像,但是使用之前要仔细确认,以免造成不必要的损失。

6.2.4 镜像命名和标签

只要给出镜像的名称和标签,就能在官方仓库中定位一个镜像(采用“:”分隔)。从官方参考拉取镜像时,使用 docker image pull 命令。默认情况下,我们拉取的是带有标签 "latest"的镜像,如果要拉取不同的镜像,需要指定特有的标签,如:

docker image pull ubuntu:18.04

这里要注意标签为 latest 的镜像不保证这是最新的镜像,比如 Apline 的最新镜像的标签通过是 edge。如果要从非官方镜像中拉取镜像,还需要在镜像名称中加入 "/" 分割符。

docker image pull nigelpoulton/tu-demo:v2

如果你要从第三方仓库下载镜像,那还有在前面加上第三方镜像仓库的 DNS 名称,在拉取镜像之前还需要完成登录。

一个镜像可以有多个标签,比如我们刚刚下载的 ubuntu:18.04 镜像,以及前面现在的 ubuntu:latest 实际上是同一个镜像来的,我们使用 docker image ls Ubuntu 来查看,可以看到这两个镜像的 ID 是一样的。

[pangcm@qcloud01 ~]$ docker image ls ubuntu
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.04               775349758637        2 weeks ago         64.2MB
ubuntu              latest              775349758637        2 weeks ago         64.2MB

6.2.5 使用 --filter 参数

Docker 提供了--filter 参数来过滤 docker image ls 命令返回的镜像列表内容。下面的示例指挥返回悬虚(dangling)镜像。

[pangcm@qcloud01 ~]$ docker image ls --filter dangling=true
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
b3log/solo          <none>              ff78fbb88bec        2 weeks ago         151MB
b3log/solo          <none>              f5d60038de5c        3 weeks ago         151MB

悬虚镜像就是那些没有标签的镜像,在列表中展示为:。出现这种情况通常是因为构建了一个新镜像的时候,打上了一个已存在的标签。这时候,Docker 会移除旧镜像上的标签,然后把这个标签打在新的镜像上。这样下来,旧的镜像旧变成了悬虚镜像。

使用 docker image prune 命令可以移除全部的悬虚镜像,如果加上 -a 参数,Docker 会额外移除没有被使用的镜像(那些没有被任何容器使用的镜像)。

Docker image 目前支持如下的过滤器:

  • dangling:可以指定 true 或者 false,仅返回悬虚镜像或者非悬虚镜像。
  • before: 需要镜像名称或者 ID 作为参数,返回之前被创建的全部镜像
  • label:根据标注(label)的名称或者值,对镜像进行过滤。docker image ls 命令中不显示标注内容。

我们除了在 docker image ls 中使用 filter 参数之外,通常在 docker search 命令中也会用到。docker search 命令允许通过 CLI 的方式搜索 Docker Hub,比如我要搜索 Ubuntu 的镜像:

[pangcm@qcloud01 ~]$ docker search ubuntu
NAME                                                      DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
ubuntu                                                    Ubuntu is a Debian-based Linux operating sys…   10169               [OK]                
dorowu/ubuntu-desktop-lxde-vnc                            Docker image to provide HTML5 VNC interface …   362                                     [OK]
rastasheep/ubuntu-sshd                                    Dockerized SSH service, built on top of offi…   235                                     [OK]
consol/ubuntu-xfce-vnc                                    Ubuntu container with "headless" VNC session…   194                                     [OK]
ubuntu-upstart                                            Upstart is an event-based replacement for th…   101                 [OK]                
ansible/ubuntu14.04-ansible                               Ubuntu 14.04 LTS with ansible                   98                                      [OK]
......

要进一步过滤查询结果,我们可以使用 filter 参数,必要我只要官方的镜像:

[pangcm@qcloud01 ~]$ docker search ubuntu --filter is-official=true
NAME                 DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
ubuntu               Ubuntu is a Debian-based Linux operating sys…   10169               [OK]                
ubuntu-upstart       Upstart is an event-based replacement for th…   101                 [OK]                
ubuntu-debootstrap   debootstrap --variant=minbase --components=m…   40                  [OK] 

有一点要注意的是,默认情况下,Docker 只返回 25 行结果,要显示更多结果请使用 --limit 参数,最多为 100 行。

6.2.6 镜像和分层

Docker 镜像有一些松耦合的只读镜像层组成,如下图所示:

docker5.png

Docker 负责堆叠这些镜像层,并且将它们表示为单个统一对象。在你使用 docker image pull Ubuntu 拉取镜像的时候,你会发现拉取多个镜像层的过程。此外,我们可以使用 docker image inspect 命令来查看镜像的分层情况:

[pangcm@qcloud01 ~]$ docker image inspect ubuntu
...
"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:cc967c529ced563b7746b663d98248bc571afdb3c012019d7f54d6c092793b8b",
                "sha256:2c6ac8e5063e35e91ab79dfb7330c6154b82f3a7e4724fb1b4475c0a95dfdd33",
                "sha256:6c01b5a53aac53c66f02ea711295c7586061cbe083b110d54dafbeb6cf7636bf",
                "sha256:e0b3afb09dc386786d49d6443bdfb20bc74d77dcf68e152db7e5bb36b1cca638"
            ]
        },
...

可以看到 Ubuntu 这个镜像一个包含了 4 个镜像层,这里使用了 SHA256 散列值来表示镜像层。

所有的 Docker 镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上,创建新的镜像层。举个例子,加入基于 ubuntu:18.04 创建了一个新的镜像层,这就是新镜像层的第一层;如果在该镜像中添加 python 包,就会在这个基础上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。

Docker 通过存储引擎(新版本采用快照机制)的方式来实现镜像层堆栈,并保证多个镜像层对外展示为一个统一的文件。Linux 上可用的存储引擎有 AUFS、Overlay2、Device Maooer 等等,每种存储引擎都基于 Linux 对应的文件系统或者块设备技术,并且每种存储引擎都有独特的性能特点。

Docker 通过共享镜像层来节省空间并且提高性能。我们使用 docker image pull 命令的时候,Docker 会识别出要拉取的镜像中,哪几层已经在本地存在的了。这样只需要拉取不存在的镜像层即可。

6.2.7 删除镜像

当不需要某个镜像的时候,可以通过 docker image rm 命令从 Docker 主机删除该镜像,其中,rm 是 remove 的缩写。

删除操作会在当前主机上删除该镜像以及相关的镜像层。这意味着无法通过 docker image ls 命令看到删除后的镜像,并且对应的包含镜像层数据的目录也会被删除。但是,如果某个镜像层被多个镜像共享,那只有当全部依赖该镜像层的镜像都被删除后,该镜像层才会被删除。

下面示例使用镜像 ID 来删除镜像

[pangcm@qcloud01 ~]$ docker image rm 9b915a241e29
Untagged: nigelpoulton/tu-demo:latest
Untagged: nigelpoulton/tu-demo@sha256:42e34e546cee61adb100144aed000d90e6dc403a0c5b53f324a9e1c1aae451e9
Untagged: nigelpoulton/tu-demo@sha256:9ccc0c67e5c5eaae4bebeeed9b22e0e22f8a35624c1d5c80f2c9623cbcc9b59a
Deleted: sha256:9b915a241e29dc2767980445e3109412b1905b6f1617aea7098e7ac1e5837ae2
Deleted: sha256:27eb08aec7b41dbfa2fd49bc2b3fad9b020fe40b0bc8289af7f53770f0843e7d

如果被删除的镜像上存在运行状态的容器,那么该删除操作不会被允许。再次执行删除镜像命令之前,需要停止并删除该镜像相关的全部容器。

要快速删除 Dokcer 主机上的全部镜像,可以使用 docker image ls -q 来传入镜像 ID。如下:

docker image rm $(docker image ls -q) -f

6.3 镜像——命令

  • docker image pull 是下载镜像的命令。
  • docker image ls 列出了本地 Docker 主机上存储的镜像。
  • docker image inspect 命令可以展示镜像的细节,包括镜像层和元数据。
  • docker image rm 用于删除镜像。

6.4 本章小结

在本章中,读者学习了 Docker 镜像的相关内容,包括镜像和虚拟机模板很类似,可用于启动容器;镜像由一个或者多个只读镜像层构成,当多个镜像层堆叠在一起,就构成了一个完整镜像。

本书使用 Docker image pull 命令拉取到 Docker 主机本地仓库。此外,本章还涵盖了镜像命名、官方和非官方仓库、镜像分层、镜像层共享以及加密 ID 等等。本章还极少了 DOcker 是如何支持多架构和多平台镜像的,并且在本章最后重新梳理了常见的镜像操作命令。

第 7 章 Docker 容器

7.1 Docker 容器——简介

容器是镜像的运行时实例。正如从虚拟机模板启动 VM 一样,用户也同样从单个镜像上启动一个或多个容器。虚拟机和容器最大的区别是容器更快并且更加轻量级——与虚拟机运行在完整的操作系统之上相比,容器会共享其所在主机的操作系统/内核。

启动容器的简便方式是使用 docker container run 命令。该命令可以携带很多参数,在其基础的格式 docker container run 中,指定启动所需的镜像以及要运行的应用。docker container run -it Ubuntu /bin/bash 则会启动某个 Ubuntu Linux 容器,并允许 Bash Shell 作为其应用。

-it 参数可以将当前终端连接到容器的 shell 终端之上。容器随着其中运行的应用的退出而终止。在上面的示例中,Linux 容器会在 bash shell 退出后终止。

使用 docker container stop 命令可以手动停止容器运行,并且使用 docker container start 再次启动该容器。如果不需要使用该容器了,则使用 docker container rm 命令来删除容器。

7.2 Dcker 容器——详解

7.2.1 容器 VS 虚拟机

容器和虚拟机都依赖于宿主机才能运行,下面示例假设宿主机是一台需要运行 4 个业务应用的物理服务器。

docker6.png

在虚拟机的模型中我们通常在宿主机上运行 4 个 VM 并且在上面安装 4 个操作系统;但是在容器模型中,容器引擎直接获取系统资源,包括进程数、文件系统以及网络栈等等。对比下,容器的额外开销要小得多。

从更高层面上来讲,Hypervisor 是硬件虚拟化(Hardware Virtualization)——Hypervisor 将硬件物理资源划分为虚拟资源;另外,容器时操作系统虚拟机(OS Virtualization)——容器将系统资源划分为虚拟资源。

虚拟机模型将底层硬件资源划分到虚拟机当中,每个虚拟机都包含了虚拟的 CPU、内存以及磁盘资源。因此,每个虚拟机都需要自己的操作系统来声明、初始化并管理这些虚拟机资源。而操作系统本身时有额外的开销的:例如每个操作系统需要 CPU、内存的资源,都需要一个独立的许可证,并且都需要打补丁升级,每个操作系统也都面临被攻击的风险。

容器模型具有在宿主机操作系统中运行的单个内核。在一台主机上运行数十个甚至上百个容器都是可能的——容器共享一个操作系统/内核。这意味着只有一个操作系统消耗 CPU、内存等资源,只有一个系统需要授权,只有一个系统需要升级和打补丁。同时,只有一个操作系统面临被攻击的风险。简言之,就是只有一份 OS 损耗。

量变引起质变,上面示例只有 4 个业务应用的场景,如果是成百上千个应用....

此外,容器的启动时间远远小于虚拟机。因为容器不是完整的操作系统,容器内部并不需要内核,也就没有定位、解压以及初始化的过程——更不用提在内核启动过程中对硬件的遍历和初始化了。这些在容器的启动过程中统统不需要!唯一需要的就是位于下层操作系统的共享内核启动了。最终的结果就是,容器可以在 1S 内启动,唯一对容器启动时间有影响的就是容器内应用启动所花费的时间。

7.2.2 启动容器

通常登录 Docker 主机后第一件事情就是检查 Docker 是否正在运行。在这里我们可以使用 docker version 命令来查看

[pangcm@docker01 ~]$ docker version
Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfc
 Built:             Thu Aug 29 05:28:55 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.2
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.8
  Git commit:       6a30dfc
  Built:            Thu Aug 29 05:27:34 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.6
  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb
 runc:
  Version:          1.0.0-rc8
  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

这个命令会显示 Docker 的版本信息,以及 client 和 server 的内容。

使用 docker container run 命令来启动容器,例如我们上面多次举例的启动一个 Ubuntu 容器:

docker container run -it ubuntu /bin/bash

当敲击回车键之后,Docker 客户端选择合适的 API 来调用 Docker daemon。Docker daemon 接受到命令并搜索 Docker 本地缓存,观察是否有命令请求的镜像。上面的示例中,我们的本地有缓存的镜像,如果没有缓存,那就会到 Docker Hub 上查找对应的镜像,找到后拉取到本地,然后创建容器。

7.2.3 容器进程

上面创建的容器,一旦我们退出了 bash shell,该容器也会退出(终止)。原因是容器如果不允许任何进程则无法存在,退出了 bash shell 也就是等于杀死了容器。我们使用 Crtl-PQ 组合键来退出容器但不会终止容器运行,然后我们使用 docker container ls 命令来观察当前系统中正在运行的容器列表。

[pangcm@docker01 ~]$ docker container ls 
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
cdea83d65909        ubuntu              "/bin/bash"         6 minutes ago       Up 6 minutes                            adoring_banach

可以看到该容器还在运行中,使用 docker container exec 命令可以重新连接到 Docker 终端。

[pangcm@docker01 ~]$ docker container exec -it cde bash
root@cdea83d65909:/#

这时候容器运行了两个 bash 进程,我们这时候退出 bash 容器并不会终止,原因是还有一个 bash shell 在运行着。退出后,通过命令 docker container ps 可以确认容器是否在运行中。

我们使用 docker container stop 和 docker container rm 命令来停止和删除容器。

docker stop cde
docker rm cde

7.2.4 容器的生命周期

坊间流传容器不能持久化数据,其实容器是可以做到的。人们认为容器不擅长持久化工作或者持久化数据,很大成都是因为容器在非持久化方面表现得太出色。我们来做一个演示,首先我们先启动一个 Ubuntu 的容器。

#1.启动一个ubuntu容器,并且命名为test1
docker container run --name test1 -it ubuntu /bin/bash

#2.在容器中写入部分数据到tmp目录下
echo "I will stay here" >/tmp/test.log

#3.使用Crtl-PQ命令退出容器,然后停止容器
docker container stop test1

#4. 确认容器是不是exited的状态了
docker container ls -a 

#5.重新启动test1容器
docker container start test1

#6.连接到容器中,然后查看tmp目录下的那个文件是否还存在
docker container exec -it test1 bash
cat /tmp/test.log

可以看到在一开始创建的人家还是保留在容器里面的,这就证明了容器时可以持久化数据的。但是说到之旧话,卷(volume)才是容器中存储持久化数据的首选方式。

我们在演示这个例子的同时也看到了容器的生命周期是怎样的。可以根据需要多次启动、停止容器,这些执行都很快。容器及其数据是安全的,直到删除容器前,容器都不会丢失其中的数据。

7.2.5 优雅地停止容器

前面我们使用 docker container stop 来停止容器,该命令会给容器内进程发送将要停止的告警信息,给进程机会来有序处理停止前要做的事情。然后就可以使用 docker container rm 命令来删除容器了。但是如果我们在删除容器的命令上加上 -f 参数,那就要暴力多了。这会直接销毁运行中的容器,不会发出任何警告。

这背后的原理可以通过 Linux 的信号来解析,docker container stop 命令向容器内的 PID 1 进程发送了 SIGTERM 这样的信号。就像前文提到的一样,会为进程预留一个清理并优雅停止的机会。如果 10s 内进程没有终止,那么就会收到 SIGKILL 信号。这是致命一击,但是进程起码有 10s 的时间来“解决” 自己。而 docker container rm -f 就是直接发出 SIGKILL 的信号,直接杀掉容器。

7.2.6 利用重启策略进行容器的自我修复

通常建议在运行容器时配置好重启策略。这是容器的一种自我修复能力,可以在指定事件或者错误后重启来完成自我修复。重启策略应用于每个容器,可以作为参数被强制传入 docker container run 命令中,或者在 Compose 文件中声明。

重启策略包括 always、unless-stoped 和 on-failed 三种。

always 策略时最简单的方式,除非容器被明确停止,比如通过 docker container stop 命令,否则该策略会一直尝试重启处于停止状态的容器。一种简单的证明方式是启动一个新的交互式容器,并在后面指定 --restart always 策略,同时在命令中指定运行的 Shell 进程。当容器退出交互式容器的时候,容器会被杀死。但是因为指定了 --restart always 策略,所以容器会自动重启。如果运行 docker container ls 命令,就会看到容器的启动时间小于创建时间。

[pangcm@docker01 ~]$ docker container run -it  --restart always ubuntu /bin/bash
root@881a478cae9a:/# exit 
exit
[pangcm@docker01 ~]$ docker container ls 
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
881a478cae9a        ubuntu              "/bin/bash"         22 seconds ago      Up 12 seconds                           cool_mestorf

可以看到容器是在 22 秒之前创建的,但是却在 12 秒前才启动。这是因为在容器中输入退出命令的时候,容器被杀死,然后 Docker 又重新启动了该容器。

--restart always 策略有个比较有趣的特征,就是当 daemon 重启的时候,停止的容器也会被重启。

always 和 unless-stopped 的最大区别,就是那些指定了 --restart unless-stopped 并处于 Stopped(Exited)状态的容器,不会在 Docker daemon 重启的时候被重启。

on-failure 策略会在退出容器并且返回值不是 0 的时候,重启容器。就算容器处于 stopped 状态,Docker daemon 重启的时候,容器也会被重启。

7.2.7 Web 服务器示例

上面已经介绍了如何启动一个简单的容器,并与其交互。同时也知道了如何停止、重启以及删除一个容器。现在来看一个 Linux Web 服务器示例。该示例会在 80 端口上启动一个简单的 Web 服务。

docker container run -d --name webserver -p 80:80 nginx

该命令会在你的 Dokcer 主机上启动一个 nginx 容器,因为使用了 -d 参数,所以并不会进入容器的 shell 里面去。 -d 表示后台模式,告知容器在后台运行。此外,这里还指定了 -p 参数,p 参数将 Dokcer 主机的短裤映射到容器内。可以想下你访问 Web 服务的顺序是先到 Docker 然后再转到 Docker 容器中的。所以前面的那个端口是 Dokcer 主机的端口。

现在容器已经在运行中了,你可以通过命令查看,或者用浏览器打开这个 Docker 主机的 80 端口查看。

[pangcm@docker01 ~]$ docker container ls 
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
6134f2808424        nginx               "nginx -g 'daemon of…"   5 minutes ago       Up 5 minutes        0.0.0.0:80->80/tcp   webserver

7.2.8 查看容器详情

前面我们一直使用 docker container ls 命令去查看容器的运行情况,这样显示的信息非常少。下面我们使用 docker image inspect 来查看容器运行的详情。

docker image inspect nginx
...
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "80/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "NGINX_VERSION=1.17.5",
                "NJS_VERSION=0.3.6",
                "PKG_RELEASE=1~buster"
            ],
            "Cmd": [
                "nginx",
                "-g",
                "daemon off;"
            ]
...

这里截取了其中的一部分,其中 Cmd 那部分展示了容器将会执行的命令或应用。在构建镜像的时候指定默认命令是一种很普遍的做法,因为这样可以简化容器的启动。这也为镜像指定了默认的行为,并且从侧面阐述了镜像的用途——可以通过 inspect 镜像的方式来了解所要运行的镜像。

此外,还有 docker container inspect 的命令,会展示非常多的信息,有兴趣可以查看下。

7.3 容器——命令

  • docker container run 是启动新容器的命令
  • docker container ls 用于列出所有在运行状态的容器,如果加上 -a 还可以看到处于停止状态的容器
  • docker container exec 运行用户正在运行状态的容器中,启动一个新的进程。
  • docker container stop 命令会停止运行中的容器,并将其状态置为 Exited(0)。
  • docker container start 会重启处于停止状态的容器,可以使用容器的名称或者 ID
  • docker container rm 会删除停止运行的容器。
  • docker container inspect 命令会显示容器的配置细节和运行时信息。

7.4 本章小结

在本章中,对容器和虚拟机两种模型进行了比较。其中重点关注了虚拟机模型的缺点,并且分析了虚拟机模型相对于物理机模型的巨大优势,以及容器模型如果能带来更加显著的提升。

本章还介绍了如何使用 dockerscontainer run 命令启动一组简单的容器,并且对比了前台和后台运行容器在交互方面的差异性。

此外还了解了杀死容器中 PID 为 1 的进程会杀死容器。同时还了解了如何启动、停止以及删除容器。

在本章最后介绍了 docker container inspect 命令,该命令可以查看容器元数据的细节信息。

第 8 章 应用的容器化

Docker 的核心思想就是如何将应用整合到容器中,并且能在容器中实际运行。将应用整合到容器中并且运行起来的这个过程,称为“容器化”(Containerizing),有时也叫做“Docker 化”(Dockerizing)。

8.1 应用的容器化——简介

容器就是为应用而生!具体来说,容器能够简化应用的构建、部署和运行过程。
完整的应用容器化过程主要分为以下几个步骤:

  1. 编写应用代码
  2. 创建一个 Dockerfile,其中包括当前应用的描述、依赖以及该如何运行这个应用。
  3. 对该 Dockerfile 执行 docker image build 命令。
  4. 等待 Docker 将应用程序构建到 Docker 镜像中。

一旦应用容器化完成,就能以镜像的形式交付并以容器的方式运行了。

8.2 应用的容器化——详解

8.2.1 单体应用容器化

下面演示如何将一个简单的单节点 Node.js Web 应用容器化,一共有 8 个步骤:

获取应用代码——> 分析 Dockerfile——> 构建应用镜像——> 运行该应用——> 测试应用——> 容器应用化细节——> 生产环境中多阶段构建——> 最佳实践

  1. 获取应用代码
## 使用git 去下载github上的代码
git clone https://github.com/nigelpoulton/psweb.git
  1. 分析 Dockerfile

在代码目录中,有个名称为 Dockerfile 的文件。这个文件包含了对当前应用的描述,并且能知道 Docker 完成镜像的构建。在 Docker 当中,包含应用文件的目录通常被称为构建上下文(Build Context)。通常将 Dockerfile 放到构建上下文的根目录下。

另外,文件开头字母是大写的 D,这里是一个单词。像 dockerfile 或者 Docker file 这种写法是不允许的。下面我们看下 Dockerfile 里面的文件内容吧。

FROM alpine
LABEL maintainer="nigelpoulton@hotmail.com"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN  npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]

Dockerfile 主要有两个用途:1 是对当前应用的描述,2 是知道 Docker 完成应用的容器化。Dockerfile 文件非常重要,它能实现开发和部署两个过程的无缝切合。同时 Dockerfile 还能帮助新手快速熟悉这个项目,所以必须要理解 Dockerfile。下面是该 Dockerfile 文件的一些步骤描述。

首先使用 alpine 镜像作为基础镜像,指定维护者为 "nigelpoulton@hotmail.com" 。然后安装 node.js 和 NPM,将应用从当前目录复制到镜像当中,并且配置工作目录为 /src 。最后安装依赖包,记录应用的网络端口,然后把 app.js 设置为默认运行的应用。

下面对每一步进行一个详解。

每个 Dockerfile 文件的第一行都是 FROM 指令。FROM 指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。这里我们使用 alpine 作为一个基础的镜像。

接下来,Dockerfile 中通过标签(LABEL)方式指定当前镜像的维护者是谁。每个标签其实是一个键值对,在一个镜像当中可以通过增加标签的方式为镜像添加自定义的元数据。

RUN apk add --update nodejs nodejs-npm 指定使用 alpine 的 apk 包管理器将 nodejs 和 nodejs-npm 安装到当前镜像中。RUN 指令会在 FROM 指定 alpine 基础镜像之上,新建一个镜像层来存储这些安装内容。这时候一共有了两个镜像层。

COPY . /src 指令将应用相关文件从构建上下文复制到当前镜像中,并且新建一个镜像层来存储。COPY 执行结束后,当前镜像有 3 层了。

下一步,Dockerfile 通过 WORKDIR 指令,为 Dockerfile 中尚未执行的指令设置工作目录。该目录也镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。

然后 RUN npm install 指令会根据 package.json 中的配置信息,使用 npm 来安装当前应用的相关依赖包。npm 命令会在前文设置的工作目录下执行,并且在镜像中新建镜像层来保存相应的依赖文件。目前镜像一共包含了 4 层。

因为当前应用需要通过 8080 端口对外提供一个 Web 服务,所以 Dockerfile 中通过 EXPOSE 8080 指令来完成相应端口的设置。这个配置信息会作为镜像的元数据被保存下来,并不会产生新的镜像层。

最后,通过 ENTRYPOINT 指令来指定当前应用程序的入口程序。ENTRYPOINT 指定的二配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。

  1. 容器化当前应用/构建具体的镜像

我们使用 Docker image build 来构建一个镜像,下面的命令表示使用当前目录作为构建上下文去构建一个名为 web:latest 的镜像。

docker image build -t web:latest .

命令执行完之后,我们可以通过 docker image ls 去查看刚刚构建的镜像,或者可以通过 docker image inspect Web;latest 来确认刚刚构建的镜像。

  1. 推送镜像到仓库

镜像构建好之后,这时候镜像只存在你的本地 Docker 主机上,接下来要推送到镜像仓库上。比如,我们可以推送到 Docker Hub 上,首先你要有 Docker Hub 的账号,然后在 Docker 主机上登录该账号。

docker login

这里要注意的是在推送之前想要给自己的镜像打标签,原因是我们并没有 docker.io 的访问权限,只能推送到我们自己的二级命名空间中(比如我的 pangcm)。我们使用 docker image tag 来打标签。

docker image tag web:latest pangcm/web:latest

打好标签之后我们就可以把镜像推送到 Docker Hub 中去了

docker image push pangcm/web:latest
  1. 运行应用程序

这里我们启动这个镜像,命名为 c1,并且使用 Docker 主机的 80 端口进行映射。

docker container run -d --name c1 -p 80:8080 web:latest

6.APP 测试

打开浏览器,输入 Docker 主机的 IP 地址就可以访问到这个应用程序了。

  1. 详述

我们可以使用 docker image history 来查看在构建镜像的过程中都执行了哪些指令。

[pangcm@docker01 psweb]$ docker image history web:latest 
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
d36f8bbd5ccf        10 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["node" "./app…   0B                  
ee60c0c4e5e7        10 minutes ago      /bin/sh -c #(nop)  EXPOSE 8080                  0B                  
e51298cd61da        10 minutes ago      /bin/sh -c npm install                          20.6MB              
163ad5f277a7        11 minutes ago      /bin/sh -c #(nop) WORKDIR /src                  0B                  
4a6bd98a85bc        11 minutes ago      /bin/sh -c #(nop) COPY dir:270a2b97bd349a826…   22kB                
e578b1e719d7        11 minutes ago      /bin/sh -c apk add --update nodejs nodejs-npm   45.3MB              
370ef133d092        19 minutes ago      /bin/sh -c #(nop)  LABEL maintainer=nigelpou…   0B                  
961769676411        3 months ago        /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
<missing>           3 months ago        /bin/sh -c #(nop) ADD file:fe64057fbb83dccb9…   5.58MB  

显示的顺序应该由下而上去查看,这里看到一共构建了 4 层镜像,我们可以通过 docker image inspect 去确认。

8.2.2 生产环境中的多阶段构建

对于 Docker 镜像来说,过大的体积并不好。越大则越慢,这意味着更难使用,而且可能更加脆弱,更容易遭受攻击。
因此,Docker 镜像应该尽量小。对于生产环境的镜像来说,目标是将其缩小到仅包含运行应用所必需的内容即可。问题在于,生成较小的镜像并非易事。

不同 Dockerfile 写法会对镜像的大小产生不同的影响。例如,每个 RUN 指令都会新增一个镜像层。因此,通过使用 && 连接多个命令以及使用反斜杠换行的方法,将多个命令包含在一个 RUN 指令中,这是一种值得提倡的做法。此外,在执行完 RUn 指令之后,应该要清理一些临时文件以及工具。这些中间文件都不应该出现在生产环境中的。

有多种办法可以改善上面提到的问题,比如使用建造者模式。使用建造者模式需要至少两个 Dockerfile,一个用于开发环境一个用于生产环境,构建难度比较大。或者可以使用多阶段构建的办法,只需要一个 Dockerfile,使用起来更加简单。

下面是使用多阶段构建方式的一个示例,只有一个 Dockerfile,其中有多个 FROM 指令。

下载该示例项目

git clone https://github.com/nigelpoulton/atsea-sample-shop-app.git

##进入到app目录下,Dockerfile文件在这里
cd atsea-sample-shop-app/app

查看 Dockerfile 文件的内容

FROM node:latest AS storefront
WORKDIR /usr/src/atsea/app/react-app
COPY react-app .
RUN npm install
RUN npm run build

FROM maven:latest AS appserver
WORKDIR /usr/src/atsea
COPY pom.xml .
RUN mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency:resolve
COPY . .
RUN mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests

FROM java:8-jdk-alpine
RUN adduser -Dh /home/gordon gordon
WORKDIR /static
COPY --from=storefront /usr/src/atsea/app/react-app/build/ .
WORKDIR /app
COPY --from=appserver /usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "/app/AtSea-0.0.1-SNAPSHOT.jar"]
CMD ["--spring.profiles.active=postgres"]

首先注意到,Dockerfile 中共有 3 个 FROM 指令。每一个 FROM 指令构成了一个单独的构建截断。各个构建阶段在内部从 0 开始编号。不过,示例中针对每个截断都定义了便于理解的名字。

  • 阶段 0 叫做 storefront
  • 阶段 1 叫做 appsserver
  • 阶段 2 叫做 production

storefront 截断拉取了大小超过 600MB 的 node:latest 镜像,然后设置了工作目录,复制了一些应用代码进去,然后使用 2 个 RUN 指令来执行 npm 操作。这会生成 3 个镜像层并显著增加镜像大小。指令执行结束后会得到一个比原镜像大得多的镜像,其中包含许多构建工具和少量应用程序代码。

appserver 阶段拉取一个大小超过 700MB 的 maven:latest 镜像。然后通过 2 个 COPY 指令和两个 RUN 生成了 4 个镜像层。这个阶段同样会构建出一个非常大的包含许多构建工具和非常少量应用程序代码的镜像。

production 阶段拉取了 java:8-jdk-alpine 镜像,这个镜像大约 150MB,明显小于前两个构建阶段用到的 node 和 maven 镜像。这个阶段会创建一个用户,设置工作目录,从 storefront 阶段生成的镜像中复制一些应用程序代码过来。之后,设置一个不同的工作目录,然后从 appserver 阶段生成的镜像复制应用相关的代码。最后,production 设置当前应用程序为容器启动时的主程序。

重点在于 COPY --from 指令,它从之前的阶段构建的镜像中仅复制生产环境相关的应用代码,而不会复制生产环境不需要的构件。

还有一点也很重要,多阶段构建这种方式仅用到一个 Dockerfile,并且 docker image build 命令不需要增加额外参数。

接下来,我们进入 app 目录,然后执行构建容器的命令吧。

cd atsea-sample-shop-app/app
docker image build -t multi:stage

构建完成后可以通过 docker image ls 来查看构建命令拉取和生成的镜像。

[pangcm@docker01 ~]$ docker image ls 
REPOSITORY                                       TAG                 IMAGE ID            CREATED             SIZE
multi                                            stage               ff8bd7176247        5 weeks ago         210MB
node                                             latest              a8d7efbae951        6 weeks ago         908MB
maven                                            latest              e941463218b9        2 months ago        616MB
java                                             8-jdk-alpine        3fd9dd82815c        2 years ago         145MB

由于我这里清除了一些悬虚镜像,所以显示的镜像会少了点,只有 3 个下载的镜像和最后构建的镜像。实际上还应该有 3 个中间的镜像的,也就是我们中途构建使用到的镜像。

多阶段构建是随着 Docker 17.05 版本新增的一个特性,用于构建精简的生产环境镜像。

8.2.3 最佳实践

  1. 利用构建缓存

Docker 的构建过程利用了缓存机制。验证的办法很简单,重新构建一次上面构建的镜像,肯定是瞬间完成。在做构建的时候,Docker 会检查缓存的情况,如果缓存有需要下载的镜像那就不下载了,有了已经构建好的镜像层,那也可以跳过这一步,直接到与缓存不一样的构建命令上。

所以,为了提高构建的效率,我们通常把经常变化的部分放到了构建最后面。如果你不想使用缓存可以在构建命令后面加入 --nocache=true。

  1. 合并镜像

当要构建镜像的层次很多的时候,我们可以考虑把这些镜像层给合并。但是合并的镜像不能被共享,这会导致了存储空间的浪费,因为你是没办法使用这个大的镜像层的。

要合并镜像,可以在 docker image build 的时候加上参数 --squash 即可。

  1. 使用 no-install-recommends

在构建 Linux 镜像时,若使用的是 APT 包管理器,则应该在执行 apt-get install 命令时增加 no-install-recomments 参数。这样能确保 APT 仅安装核心包,而不是推荐和建议包。这样能够显著减少不必要包的下载数量。

8.3 应用的容器化——命令

  • docker image build 命令会读取 Dockerfile,并将应用程序容器化。使用 -t 参数给镜像打标签,使用 -f 参数指定 Dockerfile 的路径和名称。构建上下文是指文件存放的位置,可能是本地 Docker 主机上的一个目录或者远程的 Git 库。
  • Dockerfile 中的 FROM 指令用于指定要构建的镜像的基础镜像。
  • Dockerfile 中的 RUN 指令用于在镜像中执行命令,这会构建新的镜像层。
  • Dockerfile 中的 COPY 指令用于将文件作为一个新的层添加到镜像中。
  • Dockerfile 中的 EXPOSE 指令用于记录应用所使用的网络端口。
  • Dockerfile 中国的 ENTRYPOINT 指令用于指定镜像以容器方式启动后默认运行的程序。
  • 其他的 Dockerfile 指令还有 LABEL、ENV、ONBUILD、HEALTHCHECK、CMD 等

8.4 本章小结

本章介绍了如何容器化一个应用。首先从远程 Git 库拉取一些应用代码,库中除了应用代码,还包括 Dockerfile,后者包括了一系列指令,用于定义如何将应用构建为一个镜像。然后介绍了 Dockerfile 基本的工作机制,并用 docker image build 命令创建了一个新的镜像。

镜像创建后,基于该镜像启动了容器,并借助 Web 浏览器对其进行了测试。接下来,读者可以了解多阶段构建提供了一个简单的方式,能够构建更加精简的生产环境镜像。

读者从本章中还可以了解到 Dockerfile 是一个将应用程序文档化的有力工具。正因如此,他能够帮助新加入的开发人员迅速进入状态,能够为开发人员和运维人员弥合分歧。处于这种考虑,请将其视为代码,并用源控制系统进行管理。

第 9 章

9.1 使用 Docker Compose 部署应用——简介

多数的现代应用通过多个更小的服务相互协同来组成一个完整可用的应用。比如一个简单的示例应用可能由如下 4 个服务组成。

  • Web 前端
  • 订单管理
  • 品类管理
  • 后台数据库

将以上服务组织在一起,那就是一个可用的应用。部署和管理繁多的服务是困难的。而这正式 Docker Compose 要解决的问题。

Docker Compose 并不是通过脚本和各种冗长的 docker 命令来将应用组件组织起来,二十通过一个声明式的配置文件描述整个应用,从而使用一条命令完成部署。

应用部署成功后,还可以通过一系列的命令实现对其完整声明周期的管理。甚至,配置文件还可以至于版本控制系统中进行存储和管理。这是显著的进步。

9.2 使用 Docker Compose 部署应用——详解

9.2.1

Docker Compose 的前身是 Fig,这是一家由 Orchard 公司开发的强有力的工具,用于进行多容器管理。在 14 年,该公司被 Docker 公司收购,Fig 也改名为 Docker Compose,该工具称为了绑定在 Docker 引擎之上的外部工具。

Docker Compose 是一个需要在 Docker 主机上进行安装的外部 Python 工具。使用它时,首先编写定义多容器(多服务)应用的 YAML 文件,然后将其交由给 docker-compose 命令处理,DOcker Compose 就会基于 Docker 引擎 API 完成应用的部署。

9.2.2 安装 Docker Compose

我们可以直接下载 Docker Compose 的二进制包,然后修改下文件权限即可使用 Docker Compose.

##下载二进制包,目前最新版本时1.24.1
sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

##修改文件权限
sudo chmod +x /usr/local/bin/docker-compose

##查看docker-compose查看版本信息
docker-compose --version

9.2.3 Compose 文件

Docker Compose 使用 YAML 文件来定义多服务的应用。YAML 时 JSON 的一个子集,因此也可以使用 JSON。不过本章的例子全部采用 YAML。Dockers Compose 默认使用的文件名为 docker-compose.yml。当然,用户也可以使用 -f 参数指定具体文件。

下面是一个简单的 Compose 文件的示例,它定义了一个包含两个服务(web-fe 和 Redis)的小型 Flask 应用。这是一个能够对访问者进行计数并将其保存到 Redis 的简单的 Web 服务。我们可以到 GitHub 上获取代码。

git clone https://github.com/nigelpoulton/counter-app.git

进入到拉取的代码目录,查看 docker-compose.yml 文件

version: "3.5"
services:
  web-fe:
    build: .
    command: python app.py
    ports:
      - target: 5000
        published: 5000
    networks:
      - counter-net
    volumes:
      - type: volume
        source: counter-vol
        target: /code
  redis:
    image: "redis:alpine"
    networks:
      counter-net:

networks:
  counter-net:

volumes:
  counter-vol:

首先我们可以看到这个文件的基本结构,这里包含了 4 个一级 key:version、services、networks、volumes。

version 时必须指定的,而且总是位于文件的第一行。它定义了 Compose 文件格式(主要时 API)的版本。建议使用最新版。注意,version 并非定义 Dockers Compose 或者 Docker 引擎的版本号。这里我们使用的 Compose 文件都使用 3 及以上的版本。

services 用于定义不同的应用服务。上面定义了两个服务:一个名为 web-fe 的 Web 前端服务以及一个名为 Redis 的内存数据库服务。Docker Compose 会将每个服务部署在各自的容器中。

networks 用于指引 Docker 主机创建新的网络。默认情况下,Docer Compose 会创建 bridge 网络。这是一种单主机网络,只能够实现同一主机上容器的连接。当然,也可以使用 driver 属性来指定不同类型的网络。

volumes 用于指引 Docker 来创建新的卷。

这里分析下上面的 Compose 文件。

上面的例子中 Compose 文件使用的是 v3.5 版本的格式,定义了两个服务,一个名为 counter-net 的网络和一个名为 counter-vol 的卷。更多的信息实在 services 中,我们着重分析下。

在 services 部分定义了两个二级 key:web-fe 和 Redis。他们各自定义了一个应用程序服务。需要明确的是,Docker Compose 会将每个服务部署为一个容器,并且会使用 key 作为容器名字的一部分。本例中定义了两个 key,因此 Docker Compose 会部署两个容器。

其中 web-fe 的服务定义中,包含了以下指令。

  • build: .指定 Docker 基于当前目录下 Dockrfile 中定义的指令去构建一个新的镜像,该镜像会被用于启动该服务的容器。
  • command: python app.py 指定 Docker 在容器中执行名为 app.py 的 python 脚本作为主程序。因此镜像中必须包含 app.py 文件以及 python,这一点在 Dockerfile 中可以得到满足。
  • ports:指定 Docker 将容器内(-target)的 5000 端口映射到主机(published)的 5000 端口。这意味着发送到 Docker 主机 5000 端口的流量会被转发到容器的 5000 端口。
  • networks:使得 Docker 可以将服务连接到指定的网络上。这个网络就是我们下面定义的。
  • volumes:指定 Docker 将 counter-vol 卷挂载到容器内的 /code.这个卷也是我们下面定义的。

综上,DOcker Compose 会调用 Docker 来为 web-fe 服务部署一个独立的容器。该容器基于于 Compose 文件同一目录的 Docker 构建的镜像。基于该镜像启动的容器会运行 app.py 作为其主程序,将 5000 端口暴露给宿主机,连接到 counter-net 网络上,并且挂载一个卷到 /code。

事实上我们在 Docker Compose 中不用定义 command 命令了,因为镜像的 Dockerfile 已经定义了。

Redis 服务的定义相对来说就比较简单了。

  • image:redis:alpine 使得 Docker 可以基于 redis:alpine 镜像启动一个独立的名为 Redis 的容器,这个容器会从 Docker Hub 上拉取下来。
  • networks:配置 Redis 容器连接到 counter-net 网络。

由于两个服务都连接到 counter-net 网络,因此他们可以通过名称解析到对方的地址。了解这一点很重要,本例中上层应用被配置为通过名称与 Redis 服务通信。

9.2.4 使用 Dockers Compose 部署应用

上面我们已经把源码从 GitHub 上面下载下来了,进入目录我们大概看下里面的那几个文件:

  • app.py 是应用程序代码
  • docker-compose.yml 是 Compose 文件,其中定义了 Docker 如何部署应用
  • Dockerfile 定义了如何构建 web-fe 服务所使用的镜像。
  • requirements.txt 列出了应用所依赖的 python 包

下面我们使用 docker-compose 来把应用给部署然后启动起来。

cd counter-app
docker-compose up &

常用的启动一个 Compose 应用的方式就是 docker-compose up 命令。它会构建所需的镜像,创建网络和卷,并启动容器。默认情况下,docker-compose up 会查找名为 docker-compose.yml 或者 docker-compose.yaml 的 Compose 文件。如果使用其他的文件名,请使用 -f 参数去指定。要后台启动更加合理的方式是使用 -d 参数,使用 & 的话不大正规。

最后,这个命令会创建 3 个镜像(有个 python 基础镜像是 Dockerfile 拉取的),并且启动其中的两个(web-fe 和 Redis).

[pangcm@docker01 ~]$ docker container ls 
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                    NAMES
8f43b3727185        redis:alpine         "docker-entrypoint.s…"   22 seconds ago      Up 17 seconds       6379/tcp                 counter-app_redis_1
3e9d124ae4a5        counter-app_web-fe   "python app.py"          22 seconds ago      Up 17 seconds       0.0.0.0:5000->5000/tcp   counter-app_web-fe_1

到这里,多容器的应用已经借助 Docker Compose 成功部署了。你可以是用浏览器打开 Docker 主机的 5000 端口查看成果。

9.2.5 使用 Docker Compose 管理应用

本节会介绍如何使用 Docker Compose 启动、停止和删除应用,以及获取应用状态。还会演示如何使用挂载的卷来实现对 Web 前端的更新。

前面我们启动了应用,接下来我们使用 docker-compose down 来关闭应用。

[pangcm@docker01 counter-app]$ docker-compose down 
Stopping counter-app_redis_1  ... done
Stopping counter-app_web-fe_1 ... done
Removing counter-app_redis_1  ... done
Removing counter-app_web-fe_1 ... done
Removing network counter-app_counter-net

这里可以看到关闭的过程,停止并删除容器,然后删除网络。可以看到我们的卷并没有被删除掉,因为卷是用来做持久化存储的。如果你使用的是 docker-compose up & 来启动过的话,还可以看到更多的过程。

然后我们再次来启动,使用 docker-compose ps 观察应用的状态。

[pangcm@docker01 counter-app]$ docker-compose up -d 
Creating network "counter-app_counter-net" with the default driver
Creating counter-app_web-fe_1 ... done
Creating counter-app_redis_1  ... done
[pangcm@docker01 counter-app]$ docker-compose ps 
        Name                      Command               State           Ports         
--------------------------------------------------------------------------------------
counter-app_redis_1    docker-entrypoint.sh redis ...   Up      6379/tcp              
counter-app_web-fe_1   python app.py                    Up      0.0.0.0:5000->5000/tcp

输出的内容会显示容器的名称,其中运行的 Command、当前状态以及其侦听的网络端口。
使用 docker-compose top 命令可以列出各个服务内运行的进程。

[pangcm@docker01 counter-app]$ docker-compose top
counter-app_redis_1
  UID      PID    PPID    C   STIME   TTY     TIME         CMD     
-------------------------------------------------------------------
polkitd   10501   10468   0   18:01   ?     00:00:00   redis-server

counter-app_web-fe_1
UID     PID    PPID    C   STIME   TTY     TIME                    CMD                
--------------------------------------------------------------------------------------
root   10514   10492   0   18:01   ?     00:00:00   python app.py                     
root   10632   10514   0   18:01   ?     00:00:00   /usr/local/bin/python /code/app.py

这里需要注意的是 PID 编号是在 Docker 主机上的进程 ID,而不是容器里面的。

使用 docker-compose stop 可以停止应用,但是不会删除资源。

[pangcm@docker01 counter-app]$ docker-compose stop
Stopping counter-app_redis_1  ... done
Stopping counter-app_web-fe_1 ... done
[pangcm@docker01 counter-app]$ docker-compose ps 
        Name                      Command               State    Ports
----------------------------------------------------------------------
counter-app_redis_1    docker-entrypoint.sh redis ...   Exit 0        
counter-app_web-fe_1   python app.py                    Exit 0 

已经停止的应用可以使用 docker-compose rm 命令去删除资源,这里也一样不会删除卷和镜像的。使用 docker-compose restart 可以重启应用。

docker-compose restart 

上面多处提到卷不会被删除,那么我们来看下卷的情况。查看卷可以使用 docker volome ls 命令查看。

[pangcm@docker01 counter-app]$ docker volume ls 
DRIVER              VOLUME NAME
local               counter-app_counter-vol

这个卷我们把它挂载到容器的 /code 目录下,/code 目录是应用的工作目录,我们的项目文件就是存放在这个目录下,从 Dockerfile 文件可以得到确定。

FROM python:3.4-alpine
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

这就是说我们的项目文件实际上持久化在对应的卷上的了,如果我们在 Docker 主机对卷中的文件做修改,会不会马上反应到应用中呢?下面验证下。

首先我们要找到我们这个卷所在的位置,使用 inspect 可以查看到。

[pangcm@docker01 counter-app]$ docker volume inspect counter-app_counter-vol |grep Mountpoint
        "Mountpoint": "/var/lib/docker/volumes/counter-app_counter-vol/_data",

可以看到这个卷实际上在 Docker 主机的 /var/lib/docker/volumes/目录下,我们进入到该目录,然后修改里面的 app.py 文件,最后在页面上验证下结果即可。

##修改这行的内容即可
return "What's up PCM1 Docker Deep Divers! You've visited me {} times.\n".format(count)

在生成环境中我们不会这么做,但是在开发环境下能节省很多时间。这里可以看到,Docker Compose 可以用来部署和管理复杂得多的应用。

9.3 使用 Docker Compose 部署应用——命令

  • docker-compose up 命令用于部署一个 Compose 应用。
  • docker-compose stop 命令会停止 Compose 应用相关的所有容器,但不会删除他们。
  • docker-compose rm 命令用于删除停止的 Compose 应用。
  • docker-compose restart 命令会重启已停止的 Compose 应用。
  • docker-compose ps 命令用于列出 Compose 应用中的各个容器。输出内容包括当前状态、容器运行的命令以及网络端口。
  • docker-compose down 会停止并删除运行中的 Compose 应用。它会删除容器和网络,但是不会删除卷和镜像。

9.4 本章小结

本章介绍了如何使用 Docker Compose 部署和管理一个多容器的应用。Docker Compose 是一个基于 Docker Engine 进行安装的 Python 工具。该工具使得用户可以在一个声明式的配置文件中定义一个多容器的应用,并通过一个简单的命令完成部署。

Compose 文件可以是 YAML 或者 JSON 格式,其中定义了所有的容器、网络、卷以及应用所需的密码。docker-compose 命令行工具会解析该文件,并调用 Docker 来执行部署。

一旦应用完成部署,用户就可以使用不同的 docker-compose 子命令来管理应用的整个生命周期。

本章还介绍了如何使用挂载卷来修改容器内的文件。Docker Compose 在开发者中得到广泛使用,而且对应用来说,Compose 文件也是一种非常不错的文档——其中定义了组成应用的所有服务,他们使用的镜像、网络和卷,暴露的端口,以及更多信息。基于此,我们可以弥合开发与运维之间的隔阂。Compose 文件应该被当作代码,因此应该将其保存在源控制库中。

第 10 章 Docker Swarm

上面已经介绍了如何安装 Docker、拉取镜像以及使用容器,接下来需要探讨的话题将是关于规模(Scale)方面的,这时候轮到 Docker Swarm 登场了。

10.1 Dockers Swarm——简介

Docker Swarm 包含两方面:一个企业级的 Docker 安全集群,以及一个微服务应用编排引擎。

集群方面,Swarm 将一个或多个 Docker 节点组织起来,使得用户能够以集群方式管理它们。Swarm 默认内置有加密的分布式集群存储、加密网络、公用 TLS、安全集群介入令牌以及一套简化数字证书管理的 PKI。用户可以自如地添加或删除节点,这非常棒。

编排方面,Swarm 提供了一套丰富的 API 使得部署和管理复杂的微服务应用变得易如反掌。通过将应用定义在声明式配置文件中,就可以使用原生的 Docker 命令完成部署。此外,甚至还可以执行滚动升级、回滚以及扩缩容操作、同样基于简单的命令即可完成。

以往,Docker Swarm 是一个基于 Docker 引擎之上的独立产品。自 Docker 1.12 版本之后,它已经完全集成在 Docker 引擎中,执行一条命令即可启用。到 2018 年,除了原生 Swarm 应用,他还可以部署和管理 k8S 应用。即便在本书撰写时,对 k8s 应用的支持也是新特性。

10.2 Dockers Swarm——详解

10.2.1 Swarm 的初步介绍

从集群角度来说,一个 Swarm 由一个或多个 Docker 节点组成。节点会被配置为管理节点(Manager)或者工作节点(Worker)。管理节点负责集群控制面(Control Plane),进行诸如监控集群状态、分发任务至工作节点等操作。工作节点接收来自管理节点的任务并执行。

Swarm 的配置和状态信息保存在一套位于所有管理节点上的分布式 etcd 数据库中。该数据库运行于内存中,并保持数据的最新状态。关于该数据库最棒的是,他几乎不需要任何配置——作为 Swarm 的一部分被安装、无需管理。

关于集群管理,最大的挑战在于保证其安全性。搭建 Swarm 集群时将不可避免地使用 TLS,因为它被 Swarm 紧密集成。在安全意识日盛的今天,这样的工具值得大力推广。Swarm 使用 TLS 进行通信通信、节点认证和角色授权。自动密钥轮换更是锦上添花!其在后台默默进行,用户甚至感知不到这一功能的存在。

关于应用编排,Swarm 中的最小调度单元是服务。它是随 Swarm 引入的,在 API 中是一个新的对象元素,它基于容器封装了一些高级特征,是一个更高层次的概念。

当容器被封装在一个服务中时,我们称之为一个任务或者一个副本,服务中增加了诸如扩缩容、滚动升级以及简单回滚等特征。

10.2.2 搭建安全的 Swarm 集群

书本的示例一共有 6 个节点,包括 3 个管理节点和 3 个工作节点。我这里没有那么多的主机资源,一共 3 个节点,分别包括 1 个管理节点和 2 个工作节点。(实际上你可以使用 docker machine 轻松搭建多套 docker 的运行环境)

这里的每个节点都要安装 Docker,并且能够于 Swarm 的其他节点通信。因为都是同一个局域网内,所以我放行了内网的所有防火墙规则。运行 Swarm 会开启 3 个端口,分别是:

  • 2377/tcp:用于客户端于 Swarm 进行安全通信
  • 7946/tcp 与 7946/udp:用于控制面 gossip 分发
  • 4789/udp:用于基于 VXLAN 的覆盖网络。

下面我们开始搭建 Swarm 吧。搭建 Swarm 的大体流程如下:初始化第一个管理节点——> 加入额外的管理节点——> 加入工作节点——> 完成。

  1. 初始化一个全新的 Swarm

不包含在任何 Swarm 中国的 Docker 节点,称为运行于单引擎(Single-Engine)模式。一旦被加入 Swarm 集群,则切换为 Swarm 模式。

在单引擎模式下的 Docker 主机上运行 docker swarm init 会将其切换到 Swarm 模式,并创建一个新的 Swarm,将自身设置为 Swarm 的第一个管理节点。然后其他的节点就可以加入进来了,新加入的节点也会切换为 swarm 模式。

节点 192.168.113.70 上初始化:

[pangcm@docker01 ~]$ docker swarm init --advertise-addr 192.168.113.70:2377 --listen-addr 192.168.113.70:2377 
Swarm initialized: current node (liokj5021pc9ls40fj3tcebxq) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-59xcha9v3tw180c64c0zg0ic2418qw69ns6ics4t6cj2t0wl6x-b5mlt9leakzj4gpo9qa9souc1 192.168.113.70:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

这条命令解析如下:

  • docker swarm init 会通知 Docker 来初始化一个新的 Swarm,并将自身设置为第一个管理节点。同时也会使得该节点开启 Swarm 模式。
  • 后面的两个和 IP 相关的参数是可选的,在多 IP 的 Docker 主机上需要配置。这里还是建议在执行命令的时候加上着两个参数。
  1. 列出 Swarm 中的节点,docker node ls
[pangcm@docker01 ~]$ docker node ls 
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
liokj5021pc9ls40fj3tcebxq *   docker01            Ready               Active              Leader              19.03.5

这里可以看到目前只有一个节点,该节点是一个 leader 的角色。

  1. 在 192.168.113.71 和 192.168.113.72 节点上把这两个节点加入到上面创建的 swarm。
##这个命令在我们初始化swarm的时候系统生成的,同理添加管理节点的命令也在初始化的时候给出了
docker swarm join --token SWMTKN-1-59xcha9v3tw180c64c0zg0ic2418qw69ns6ics4t6cj2t0wl6x-b5mlt9leakzj4gpo9qa9souc1 192.168.113.70:2377
  1. 这时候我们回到管理节点上查看 swarm 节点的情况。
[pangcm@docker01 ~]$ docker node ls 
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
liokj5021pc9ls40fj3tcebxq *   docker01            Ready               Active              Leader              19.03.5
qg63e8vx18v1rhnv9hjmwml16     docker02            Ready               Active                                  19.03.4
yle6i7nhhxglc2qva49i91x1v     docker03            Ready               Active                                  19.03.5

就这样,Swarm 集群我们就创建好了。其中包含了 1 个管理节点和 2 个工作节点,每个节点的 Docker 引擎都被切换到 Swarm 模式下,并且自动启动了 TLS 安全。如果你有多个管理节点,那么有的节点会是 Reachable状态,有一个是 Leader状态。Leader 那个就是主节点了,后面我们会介绍到。

10.2.3 Swarm 管理器的高可用(HA)。

虽然我这里只用到一个管理节点,但是官方是建议 3 个的,为什么是 3 个呢?了解集群系统的人都知道,集群工作的时候 3 或 5 个节点都是一个比较好的选择。

Swarm 的管理节点内置有对 HA 的支持。这意味着,即使有一个或者多个节点发生了故障,剩余管理节点也会继续保证 Swarm 的运转。

从技术上来说,Swarm 实现了一种主从方式的多管理节点的 HA。也就是说,你可能有多个管理节点,但是只有一个处于活动状态。通常处于活动状态的节点被称为主节点,而主节点也是唯一一个会对 Swarm 发送控制命令的节点。也就是说,只有主节点才会变更配置,或发送任务到工作节点。如果一个备用管理节点接收到了 Swarm 命令,则它会将其转发给主节点。

Swarm 使用 Raft 算法来实现支持管理节点的 HA,Raft 协议在选举 Leader 的时候遵循少数服从多数的原则。所以使用奇数个节点的好处就是避免脑裂现象的出现。而不建议部署太多管理节点的原因是过多的节点数据同步的效率太低了,并且选举耗时会更长,所以一般建议 3 个或者 5 个。

10.2.4 Swarm 的安全

Swarm 集群内置了众多的安全机制,并提供了开箱即用的合理默认配置——如 CA 设置、接入 Token、公用 TLS、加密集群存储、加密网络、加密节点 ID 等等。更多内容会在第 15 章介绍。

尽管内置有如此多的原生安全机制,重启一个旧的管理节点或者进行备份恢复仍有可能对集群造成影响。一个旧的管理节点重新接入 Swarm 会自动解密并获得 Raft 数据库中长时间序列的访问权,这回带来安装隐患。进行本非恢复可能会抹掉最新的 Swarm 配置。

为了避免以上问题,Docker 提供了自动锁机制来锁定 Swarm,这会强制要求重启的管理节点在提供一个集群解锁码之后才有权重新接入集群。

在创建 Swarm 集群的时候可以直接加上 --autolock 参数启用锁。我们前面并没有这么操作,那就需要使用 docker swarm update 命令来启动锁。

在管理节点上

[pangcm@docker01 ~]$ docker swarm update --autolock=true
Swarm updated.
To unlock a swarm manager after it restarts, run the `docker swarm unlock`
command and provide the following key:

    SWMKEY-1-8bDcdp+PQ1IYuind8XanEVCxcdsne0OJ6rIYoqQRiPI

Please remember to store this key in a password manager, since without it you
will not be able to restart the manager.

这时候重启其他的管理节点,管理节点不会自动重新接入集群。要接入集群那就要先解锁,使用 docker swarm unlock 命令进行解锁。这里看到,这个 key 非常重要,请妥善保管。因为我的是测试环境,所以我会把这个锁机制给关闭,以免我自己忘了这个 key.

docker swarm update --autolock=false

10.2.5 Swarm 服务

本节介绍的内容可以使用 Docker Stack(第 14 章)进一步改进。然而,本章的概念对于准备第 14 章的学习是非常重要的。

就像在 Swarm 初步介绍中提到的,服务是自 Docker 1.12 后新引进的概念,并且仅适用于 Swarm 模式。使用服务仍能够配置大多数熟悉的容器熟悉,比如容器名、端口映射、接入网络和镜像。此外还增加了额外的特性,比如可以声明应用服务的期望状态,将其告知 Docker 后,Docker 会负责进行服务的部署和管理。

举例说明,加入某个应用的 Web 前端服务,这个服务经过测试我们使用 5 个实例能够应对正常的流量。那么我们就可以把这个需求转换为一个服务,该服务声明了容器使用的镜像,并且服务应该总是有 5 个运行中的副本。

下面,我们来看下怎么去创建一个新的服务吧。

##镜像比较大,拉取需要一定的时间
docker service create --name web-fe -p 8080:8080 --replicas 5 nigelpoulton/pluralsight-docker-ci

该命令的参数于 docker container run 命令大致相同,这里我们使用 --name 和 -p 定义服务的方式,与单机启动容器的定义方式一样的。

使用 docker service create 命令告知 Docker 正在声明一个新服务,并传递--name 参数将其命名为 web-fe。将每个节点上的 8080 端口映射到服务副本内部的 8080 端口。接下来,使用 --replicas 参数告诉 Docker 应该总是有 5 个此服务的副本。最后,告知 Docker 哪个镜像用于副本——重要的是,要了解所有的服务副本使用相同的镜像和配置。

这还没有结束,所有的服务都会被 Swarm 持续监控——Swarm 会在后台进行轮询检查,来次序比较服务的实际状态和期望状态是否一致。如果不一致,Swarm 会使其一致。比如,web-fe 还有某个节点宕机了,副本数量从 5 下降到 4 个,Docker 会马上启动一个新的 web-fe 副本来使实际状态和期望状态保持一致。这一功能非常强大,使得服务在面对节点宕机等问题的时候具有自愈的能力。

  1. 查看服务

使用 docker service ls 命令可以查看 Swarm 中的所有运行中的服务。

[pangcm@docker01 ~]$ docker service ls 
ID                  NAME                MODE                REPLICAS            IMAGE                                       PORTS
kiyro8y0wbj7        web-fe              replicated          5/5                 nigelpoulton/pluralsight-docker-ci:latest   *:8080->8080/tcp

使用 docker service ps 可以查看服务副本列表及各副本的状态,

[pangcm@docker01 ~]$ docker service ps web-fe
ID                  NAME                IMAGE                                       NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
rncmk0pcakm2        web-fe.1            nigelpoulton/pluralsight-docker-ci:latest   docker02            Running             Running 30 minutes ago                       
mchv3wizclfa        web-fe.2            nigelpoulton/pluralsight-docker-ci:latest   docker03            Running             Running 18 minutes ago                       
xaeqa2lehef2        web-fe.3            nigelpoulton/pluralsight-docker-ci:latest   docker02            Running             Running 30 minutes ago                       
slq06rv4xuac        web-fe.4            nigelpoulton/pluralsight-docker-ci:latest   docker03            Running             Running 18 minutes ago                       
j3xtrie3s8jb        web-fe.5            nigelpoulton/pluralsight-docker-ci:latest   docker01            Running             Running 30 minutes ago  

这里可以看到各个副本分别运行在 Swarm 的哪个节点上,以及期望的状态和实际状态。

要查看更加详细的服务信息,可以使用 docker service inspect 命令查看。

[pangcm@docker01 ~]$ docker service inspect web-fe  --pretty

ID:		kiyro8y0wbj7lxbtzmf3iudrc
Name:		web-fe
Service Mode:	Replicated
 Replicas:	5
Placement:
UpdateConfig:
 Parallelism:	1
 On failure:	pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Update order:      stop-first
RollbackConfig:
 Parallelism:	1
 On failure:	pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Rollback order:    stop-first
ContainerSpec:
 Image:		nigelpoulton/pluralsight-docker-ci:latest@sha256:7a6b0125fe7893e70dc63b2c42ad779e5866c6d2779ceb9b12a28e2c38bd8d3d
 Init:		false
Resources:
Endpoint Mode:	vip
Ports:
 PublishedPort = 8080
  Protocol = tcp
  TargetPort = 8080
  PublishMode = ingress 

使用 --pretty 只显示我们感兴趣的信息,便于阅读。强烈建议能通读 docker inspect 命令的信息,其中包含了大量的信息,是我们了解底层运行机制的一个途径。

  1. 副本服务 VS 全局服务

服务的默认复制方式是副本模式,这种模式会部署期望数量的服务副本,并尽可能均匀地将各个副本分布在整个集群中。另外一种模式是全局模式,这种模式下,每个节点仅运行一个副本。

可以通过 docker service create 命令传递 --mode global 参数来部署一个全局服务。

  1. 服务的扩缩容

服务的另外一个强大的特征就是能够方便地进行扩缩容。在业务发展的时候,我们会增加副本的数量。

docker service scale web-fe=10

在服务流量低的时候,我们可以降低副本的数量。

docker service scale web-fe=3
  1. 删除服务

删除一个服务的操作非常简单, 正是由于太简单了,请慎用,删除服务的时候不会进行确认的。

docker service rm web-fe
  1. 滚动升级

拥有多个副本的服务可以轻松实现滚动升级,这对运维和开发人员太友好了。滚动升级非常简单,只需要执行 docker service update 命令即可。

这里只是为了示例怎么使用命令滚动升级,由于书本的例子很长,这里我就使用 Tomcat 的镜像直接对上面我们创建的服务进行滚动升级。(书本的例子涉及到覆盖网络的创建,后面我们会有介绍)

docker service update --image tomcat --update-parallelism 2 --update-delay 20s  web-fe

这里说明下这两个参数 --update-parallelism 和 --update-delay 声明名词使用新镜像更新 2 个副本,期间有 20s 的延迟。升级的过程可以通过 docker service ps web-fe 去查看。

滚动升级完成后,可以使用浏览器检查下结果,这时候页面应该是 Tomcat 的欢迎页面。此外,如果你使用 docker service inspect 命令,还可以看到我们前面配置的两个参数也保存在里面了。

10.2.7 故障排除

日志是我们排错的好助手,docker service logs 可以查看 Swarm 的服务日志。然而并非所有的日志驱动都支持该命令。

Docker 节点默认的配置是,服务使用 json-file 日志驱动,其他的驱动还有 journald(Linux 特有)、syslog、splunk 和 gelf。

10.3 Dockers Swarm——命令

  • docker swarm init 命令用户创建一个新的 Swarm。
  • docker swarm join-token 命令用于查询加入管理节点和工作节点到现有 Swarm 时使用的命令和 Token.
  • docker node ls 命令用于列出 Swarm 中所有节点及相关信息。
  • docker service create 命令用于创建一个新的服务
  • docker service ls 命令用于列出 Swarm 中运行的服务,以及诸如服务状态、服务副本等基本信息。
  • docker service ps 命令会给出更多关于某个服务副本的信息
  • docker service scale 命令用于对服务副本个数进行增减
  • docker service update 命令用于对运行中国的服务的属性进行变更
  • docker service logs 命令用于查看服务的日志
  • docker service rm 用于删除从 Swarm 中删除某服务,这会直接删除所有副本,不做确认,慎用。

10.4 本章小结

Docker Swarm 是使 Docker 规模化的关键方案。Docker Swarm 的核心包含一个安全集群组件和一个编排组件。

安全集群管理组件是一个企业级的安全套件,提供了丰富的安全机制以及 HA 特性,这些都是自动配置好的,并且非常容易调整。

编排组件允许用户以一种简单的声明式的方式来部署和管理微服务应用。它不仅支持原生的 Docker Swarm 应用,还支持 K8s 应用。

第 14 章会对如何使用声明式的方式部署微服务展开更加深入的讨论。

第 11 章 Docker 网络

网络无处不在,每当基础设施出现问题,被抱怨的通常是网络。很大一部分原因是,网络负责连接一切——无网络,无 APP。

11.1 Docker 网络——简介

Docker 在容器内部运行应用,这些应用之间的交互依赖于大量不同的网络,这意味着 Docker 需要强大的网络功能。

幸运的是,Docker 对于容器之间、容器与外部网络和 VLAN 之间的连接均有相应的解决方案。后者对于那些需要跟外部系统(如虚拟机和物理机)的服务打交道的容器化应用来说至关重要。

Docker 网络架构源自一种叫做容器的网络模型(CNM)的方案,该方案是开源的并且支持插式连接。Libnetwork 式 Docker 对 CNM 的一种实现,提供了 Docker 和兴网络架构的全部功能。不同的驱动可以通过插拔的方式接入 Libnerwork 来提供定制化的网络拓扑。

为了实现开箱即用的效果,Docker 封装了一系列本地驱动、覆盖了大部分常见的网络需求。其中包括单机桥接网络、多级覆盖网络,并且支持接入现有的 VLAN。Docker 生态系统中的合作伙伴通过提供驱动的方式,进一步拓展了 Docker 的网络功能。

最后要说的是,Libnetwork 提供了本地服务发现和基础的容器负载均衡的解决方案。

11.2 Docker 网络——详解

11.2.1 基础理论

在顶层设计中,Docker 网络架构由 3 个主要部分构成:CNM、Libnetwork 和驱动。

其中 CNM 是涉及标准,在 CNM 中,规定了 Docker 网络架构的基础组成要素;Libnetwork 是 CNM 的具体实现,并且被 Docker 采用。Libnetwork 通过 Go 语言编写,并实现了 CNM 中列举的核心组件。驱动通过实现特定网络拓扑的方式来拓展该模型的能力。

1.CNM

CNM 定义了 3 个基本要素:沙盒(Sandbox)、终端(Endpoint)、和网络(Network)。

沙盒是一个独立的网络栈。其中包括以太网接口、端口、路由表以及 DNS 配置;终端就是虚拟网络接口。就像是普通的网络接口一样,终端主要职责是负责创建连接。在 CNM 中,终端负责将沙盒连接到网络;网络就是 802.1d 网桥(交换机)的软件实现。因此,网络就是需要交互的终端的集合,并且终端之间相互独立。

Docker 环境中最小的调度单位就是容器,而 CNM 也恰如其名,负责为容器提供网络功能。下图展示了 CNM 组件时如何与容器进行关联的——沙盒放置在容器内部,为容器提供给网络连接。

docker7.png

上图可以看到,容器 A 只有一个接口(终端)并连接到网络 A。容器 B 有两个接口(终端)并且分别接入了网络 A 和 B。容器 A 和 B 之间时可以相互通信的,因为都接入了网络 A。但是,如果没有三层路由器的支持,容器 B 的两个终端之间时不能通信的。

这时候可以把容器类比为我们现实中的 PC,一个 PC 可以有多个网卡接口(终端),所以可以接入到不同网络中。接入到同一网络中的两个容器可以相互通信,接入不同网络的容器必须依赖三层路由才能实现通信。

  1. Libnetwork

CNM 是设计规范文档,Libnetwork 是标准的实现。在早期,网络部分的代码都存在 daemon 中,daemon 显得非常臃肿。后来 Docker 将该网络部分从 daemon 中拆分,并重构了一个叫做 Libnetwork 的外部类库。现在,Docker 核心网络架构代码都在 Libnetwork 当中。

Libnetwork 除了实现 CNM 中定义的三个组件外,还实现了本地服务发现、基于 Ingress 的容器负载均衡以及网络控制层和管理层功能。

  1. 驱动

如果说 Libnetwork 实现了控制层和管理层功能,那么驱动就负责实现数据层。比如网络连通性和隔离性就是由驱动来处理的,驱动层实际创建网络对象也是如从。

Docker 封装了若干内置驱动,通常被叫做原生驱动或者本地驱动。在 Linux 中包括 Bridge、overlay 以及 Macvlan,在 Windows 上包括 NAT、Overlay、Transport 以及 L2 Bridge。当然也有第三方的一些驱动。

每个驱动都负责其上所有网络资源的创建和管理。为了满足复杂且不固定的环境需求,Libnetwork 支持同时激活多个网络驱动。这意味着 Docker 环境可以支持一个庞大的异构网络。

11.2.2 单机桥接网络

最简单的 Docker 网络就是单机桥接网络了。从名称中可以看到两点。

  • 单机以为着该网络只能在单个 Docker 主机上运行,并且只能与所在 Docker 主机上的容器进行连接。
  • 桥接意味着这是 802.1d 桥接的一种实现(二层交换机)。

Linux Docker 创建单机桥接网络采用内置的桥接驱动,而 Windows Docker 创建时使用内置的 NAT 驱动。实际上,着两种驱动工作起来毫无差异。

每个 Docker 主机都有一个默认的单机桥接网络。在 Linux 上网络名称为 bridge,在 Windows 上叫做 nat。除非读者通过命令创建容器的时候指定参数 --network ,否则默认情况下,新创建的容器都会连接到该网络上。

使用 docker network ls 可以查看 Docker 主机的网络情况,一开始只有一个默认的网络。

[pangcm@docker01 ~]$ docker network ls 
NETWORK ID          NAME                DRIVER              SCOPE
7cb75625ceeb        bridge              bridge              local

使用 docker network inspect 命令可以查看网络的更多内容。

在 Linux 主机中,Docker 网络由 Bridge 驱动创建,而 Bridge 底层是基于 Linux 内核中久经考验达 15 年之久的 Linux Bridge 技术。这意味着 Bridge 是高性能并且非常稳定的。同时这还表示可以通过标准的 Linux 去查看这些网络,比如 ip link show 。

[pangcm@docker01 ~]$ ip link show docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default 
    link/ether 02:42:01:12:07:d9 brd ff:ff:ff:ff:ff:ff

在 Linux Docker 主机之上,默认的 bridge 网络被映射到内核中为 docker0 的 Linux 网桥。所以,我们也可以通过 dokcer network inspect 命令观察到上面输出的内容。

我们除了可以使用默认的 bridge 网络之外,我们可以自己新建一个单机桥接的网络。使用 docker network create 命令创建。

docker network create -d bridge localnet

就这样,新网络创建成功,名为 localnet,我们可以使用 docker network ls 去查看。和默认的 bridge 网络一样,新创建的网络一样在我们 Docker 主机上新建一个 Linux 网桥。通过使用 brctl show 命令即可查看。

[pangcm@docker01 ~]$ brctl show 
bridge name	bridge id		STP enabled	interfaces
br-e6bca1080f16		8000.0242126f947e	no		
docker0		8000.0242011207d9	no	

目前这两个网络都没有任何设备的接入,STP 也没有开启。下面我们新建一个容器,并加入到新建的桥接网络中去。

docker container run -d --name test2 --network localnet alpine sleep 1d

就这样,新创建的容器加入了我们上面创建的网络中去了。可以通过 docker network inspect 命令去确认。

[pangcm@docker01 ~]$ docker network inspect localnet format
...
 "Containers": {
            "e1e87148a8a3b06cf3315991a0502b82314721877c425a52657e4b1ec72e74ed": {
                "Name": "test2",
                "EndpointID": "e8903620447d66e987ea36dc332c5f65ec7ea8bdb922dfd8e7d6565dde892dbf",
                "MacAddress": "02:42:ac:13:00:02",
                "IPv4Address": "172.19.0.2/16",
                "IPv6Address": ""
            }
        },
...

可以看到容器 test2 已经加入到桥接网络 localnet 上了,我们再次使用 brctl 查看下。

[pangcm@docker01 ~]$ brctl show 
bridge name	bridge id		STP enabled	interfaces
br-e6bca1080f16		8000.0242126f947e	no		vethbff0b42
docker0		8000.0242011207d9	no	

前面我们有提到加入同一个桥接网络的不同容器是可以相互通信的(同一个局域网),事实上在我们自定义的网络中除了可以使用 IP 来进行通信之外,还可以使用容器名称去进行通信(默认的 bridge 不行)。原因是新容器都会注册到指定的 Docker DNS 服务中,所以相同网络的容器可以解析其他容器的名称。下面我们来测试下吧。

##新建一个 test3 容器,加入localnet
docker container run -it --name test3 --network localnet alpine sh

##进入到容器里面,我们直接ping test2
ping test2

可以验证 ping 命令是生效的。

在上面,我们验证了同一个 Docker 主机同一个桥接网络之间的容器可以相互通信的。那么不同 Docker 主机上的桥接网络容器可以相互通信么?答案是可以的,怎么做呢。

这个我们前面已经验证过很多次了,就是用端口映射。把桥接网络中的容器端口映射到 Docker 主机上,然后访问 Docker 主机的端口流量就会转发到 Docker 的端口上了。例如我要把一个 nginx 容器的 80 端口映射到 Docker 主机的 8888 端口上。

docker container run -d --name test4 --network localnet -p 8888:80 nginx

在其他网络上访问 Docker 主机的 8888 端口就能访问到 Docker 容器的 80 端口了。

11.2.3 多机覆盖网络

后面会有专门的介绍,有点复杂。

11.2.4 接入现有网络

能够将容器化应用连接到外部系统以及物理网络的能力是非常必要的。常见的例子是部分容器化的应用——应用中已容器化的部分需要与那些运行在物理网络和 VLAN 上未容器化部分进行通信。

Docker 内置的 Macvlan 驱动(Windows 上是 Transparent)就是为此场景而生。通过为容器提供 Mac 和 IP 地址,让容器在物理网络上成为“一等公民”。(就是和物理网络同一个网段)

Macvlan 的优点是性能优异,因为无须端口映射或者额外桥接,可以直接通过主机接口(或子接口)访问容器接口。但是,Macvlan 的缺点是需要将主机网卡设置为混杂模式(Promiscuous Mode),这在大部分的公有云平台上是不允许的。所以 Macvlan 对于公司内部数据中心网络来说很好,但是在共有用上却不可行。

macvlan 的实验这里不做介绍了,因为用得不多。

11.2.5 服务发现

作为核心网络架构,Libnetwork 还提供了一些重要的网络服务。服务发现运行容器和 Swarm 服务通过名称相互定位。唯一的要求就是需要处于统一网络当中。

其底层实现是利用了 Docker 内置的 DNS 服务器,为每个容器提供 DNS 解析功能。这一点我们上面有提到过,DNS 解析的过程和真实的 DNS 解析过程类似,本地解析器找不到域名的话会向 Docker 的 DNS 服务发起一个递归查询。

每个启动时使用了 --name 参数的 Swarm 服务或者独立容器,都会将自己的名称和 IP 地址注册到 Docker DNS 服务。这意味着容器和服务副本可以通过 Docker DNS 服务相互发现。但是,服务发现时受网络限制的。这意味着名称解析只对同一个网络中的容器和服务生效。如果两个容器在不同的网络,那么就不能相互解析了。

用户可以为 Swarm 服务和独立容器进行自定义的 DNS 配置。举个例子, --dns 参数允许读者指定自定义的 DNS 服务列表,以防出现内置的 Docker DNS 服务器解析失败的情况。此外也可以使用 --dns-search 参数指定自定义查询时所使用的域名(例如当查询名称并非完整域名的时候)。

在 Linux 上,上述工作都是通过容器内部 /et/resolve.conf 文件内部增加条目来实现的。下面的例子会启动一个新的容器,并添加声名狼藉的 8.8.8.8 Google DNS 服务器,同时指定 dockercerts.com 作为域名添加到非完整查询当中。

docker container run -it --name test5 --dns=8.8.8.8 \
--dns-search=dockercerts.com alpine sh

11.2.6 Ingress 网格

Swarm 支持两种服务发布模式,两种模式均保证服务从集群外可访问。分别是:Ingress 模式(默认)和 Host 模式。

通过 Ingress 模式发布的服务,可以保证从 Swarm 集群内任一节点(即使没有允许服务的副本)都能访问该服务;以 Host 模式发布的服务只能通过运行服务副本的节点来访问。

Ingress 模式是默认方式,如果要使用 Host 模式发布服务,读者需要使用 --publish 参数的完整格式,并添加 mode=host。下面来看下怎么做。

docker service create -d --name svc1 \
--publish published=5000,target=80,mode=host nginx

上面就是完整的 --publish 参数示例。

在底层,Ingress 模式采用名为 Service Mesh 或者 Swarm Mode Service Mesh 的四层路由网络来实现。书本在这里有个示例,这里不做演示,后面在第14章中有对应的实验。这里只给出书中的两个结论。

一是使用Ingress模式的服务,访问任意一个节点(即使没有运行对应的容器),Docker都能把流量转到实际运行容器的节点上,;二是并且如果存在多个运行中的副本,流量会均衡到每个副本上。

11.3 Docker 网络——命令

  • docker network ls 用于列出运行在本地 Docker 主机上的全部网络
  • docker network create 创新的 Docker 网络。可以使用 -d 来指定网络的类型,如:docker network create -d overlay overnet
  • docker network inspect 提供 Docker 网络的详细配置信息。
  • docker network prune 删除 Docker 主机上全部未使用的网络
  • docker network rm 删除 Docker 主机上指定的网络

11.4 本章小结

容器网络模型(CNM)是 Docker 网络架构的主要设计文档,它定义了 Docker 网络中用到的 3 个主要结构——沙盒、终端以及网络。

Libnetwork 是开源库,采用 Go 编写,实现了 CNM。Docker 使用了该库,并且 Docker 网络架构的核心代码都在该库中。Libnetwork 同时还提供了 Docker 网络控制层和管理层的功能。

驱动通过实现特定网络类型的方式拓展了 Docker 网络栈,例如桥接网络和覆盖网络。Docker 内置了几种网络驱动,同时也支持第三方驱动。

单机桥接网络是基本的 Docker 网络类型,对于本地开发和小型应用来说也十分适用。单机桥接网络不可拓展,并且对外发布服务依赖于端口映射。Linux Docker 使用内置的 Bridge 驱动实现了单机桥接网络。

覆盖网络是当下流行的方式,并且是一种出色的多机容器网络方案。下一章会介绍。

Macvlan 启动允许容器接入现存物理网络以及 VLAN。通过赋予容器 Mac 和 IP 地址的方式,让容器称为网络中的一等公民。不过,该驱动需要主机的 NIC 支持混杂模式,这意味着该驱动在公有云上没法使用。

Docker 使用 Libnetwork 实现了基础服务发现功能,同时还实现了服务网格,支持对入站流量实现容器级别的负载均衡。

第 12 章 Docker 覆盖网络

在大部分与容器网络相关的场景中,覆盖网络都处于核心地位。在本章中会介绍原生 Docker 覆盖网络的基本要素,以及覆盖网络在 Docker Swarm 集群中的实现。

12.1 Docker 覆盖网络——简介

在现实世界中,容器间通信的可靠性和安全性相当重要,即使容器分属于不同网络中不同主机。这也是覆盖网络大展拳脚的地方,它允许读者创建扁平的、安全的二层网络来连接多个主机,容器可以连接到覆盖网络并且直接相互通信。

Docker 提供了原生覆盖网络的支持,易于配置并且非常安全。其背后是基于 Libnetwork 以及相应的驱动来构建的。

  • Libnetwork
  • 驱动

Libnetwork 是 CNM 的典型实现,从而可以通过插拔驱动的方式来实现不同的网络技术和拓扑结构。DOcker 提供了一些诸如 Overlay 的原生驱动,同时第三方也可以提供驱动。

12.2 Docker 覆盖网络——详解

在 2015 年 3 月,Docker 公司收购了一个叫做 Socket Plane 的网络初创企业。收购的原因有二,首先是因为这会给 Docker 带来真正意义的网络架构,其次是容器间联网变得非常简单,以至于开发人员都可以配置它。

12.2.1 在 Swarm 模式下构建并测试 Docker 覆盖网络

其实我们在前面已经配置使用过覆盖网络了,因为我们在部署 Swarm 的时候,创建的服务默认会把所有的节点加入到名为 ingress 的覆盖网络。正因为如此,所以 Swarm 下所有节点的容器才能够轻松地实现互通。下面我们示例如何创建一个覆盖网络,并且把创建的服务加入到创建的网络上。

实验的流程分为几步:构建 Swarm——> 创建新的覆盖网络——> 将服务连接到覆盖网络——> 测试覆盖网络。本实验要求至少有两台 Docker 主机。

  1. 构建 Swarm 这一步略掉,前面章节已有介绍。
  2. 创建新的覆盖网络

在管理节点上执行 docker network create 命令,创建一个名为 uber-net 的网络。

docker network create -d overlay uber-net

通过 docker network ls 我们可以看到刚刚创建的网络。

[pangcm@docker01 ~]$ docker network ls 
NETWORK ID          NAME                DRIVER              SCOPE
7cb75625ceeb        bridge              bridge              local
u8vvkh4r4io7        uber-net            overlay             swarm
  1. 将服务连接到覆盖网络

下面我们创建一个 2 个副本的名为 test 的服务。

docker service create --name test --network uber-net \
--replicas 2 alpine sleep 1d

上面我们创建了 test 服务,并连接到 uber-net 网络上。在这个示例中,我们使用 sleep 命令来保持容器运行。

使用 docker service ps test 查看下刚刚创建的服务。

[pangcm@docker01 ~]$ docker service ps test
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
9v7muoa0h4kd        test.1              alpine:latest       docker01            Running             Running 26 seconds ago                       
wabd836cry3j        test.2              alpine:latest       docker03            Running             Running 26 seconds ago 
  1. 测试覆盖网络

首先,我们要先知道这两个容器获取到的 IP 地址是什么先,分别在两个节点上使用 docker network inspect 命令查看网络,获取 IP 地址。

[pangcm@docker01 ~]$ docker network inspect uber-net
...
 "Containers": {
            "409062f55feb0371e7548f7b1e5a88067313b9f2bfaf7958eb3251a045b49cfe": {
                "Name": "test.1.9v7muoa0h4kdoqf1zzgp399hz",
                "EndpointID": "3b273aa02c46ae8b477db2726933a460497d695b33553d0723e0dde33143e055",
                "MacAddress": "02:42:0a:00:02:8b",
                "IPv4Address": "10.0.2.139/24",
                "IPv6Address": ""
            },
...
[pangcm@docker03 ~]$ docker network inspect uber-net
...
 "Containers": {
            "3c535be36493c5181e3768762cd0c304c3aa807dfae7a28c5c9ba4d60c58ed3d": {
                "Name": "test.2.wabd836cry3jnyz3cz9gattbj",
                "EndpointID": "ac1bba42f6303d3080b239c5559c9895226153be85990dba6c1b234c7f9b83c5",
                "MacAddress": "02:42:0a:00:02:8c",
                "IPv4Address": "10.0.2.140/24",
                "IPv6Address": ""
            },
...

然后我们登录到其中的某一个容器上,去 ping 一下另外一个容器的 IP 即可。

12.2.2 工作原理

  1. VXLAN 入门

Docker 使用 VXLAN 隧道技术创建了虚拟二层覆盖网络。在 VXLAN 的设计中,允许用户基于已经存在的三层网络结构创建虚拟的二层网络。在前面的示例中创建一个子网掩码为 10.0.0.0/24 的二层网络,该网络是基于一个三层 IP 网络实现的,三层网络 IP 由两个 Docker 主机去构成。

docker8.png

VXLAN 的美妙之处在于它是一种封装技术,能使现存的路由器和网络架构看起来就像是普通的 IP/UDP 包一样,并且处理起来毫无问题。

为了创建二层覆盖网络,VXLAN 基于现有的三层 IP 网络创建了隧道。读者可能听过基础网络(Underlay Network
)这个术语,它用于指代三层之下的基础部分。

VXLAN 隧道两端都是 VXLAN 隧道终端(VXLAN Tunnel Endpoint,VTEP)。VTEP 完成了封装和解压的步骤,以及一些功能实现所必需的操作。

  1. 两个容器通信的示例

在前面我们在两台 Docker 主机上分别运行一个容器,为该容器创建了一个 VXLAN 覆盖网络。为了实现上述场景,在每台主机上都新建了一个 Sandbox(网络命名空间)。Sandbox 就像是一个容器,但是其中运行的不是应用,而是当前主机上独立的网络栈。

在 Sandbox 内部创建了一个名为 Br0 的虚拟交换机。同时 Sandbox 内部还创建了一个 VETP,其中一端接入到名为 Br0 的虚拟交换机当中,另一端接入主机网络栈(VETP)。在主机网络栈中的终端从主机所连接的基础网络中获取到 IP 地址,并以 UDP Socket 的方式绑定到 4789 端口。不同主机上的两个 VTEP 通过 VXLAN 隧道创建了一个新的覆盖网络,如下图所示。

docker13.png

这是 VXLAN 上层网络创建和使用必须的。接下来每个容器都会有自己的虚拟以太网(veth)适配器,并接入本地 Br0 虚拟交换机。目前的拓扑结构如下图所示,虽然是在主机所属网络相互独立的情况下,但这样更容易看出分别位于两个不同主机上的容器之间时如何通过 VXLAN 上层网络进行通信的。

docker14.png

12.3 Docker 覆盖网络——命令

  • docker network create 是创建新网络所使用的命令,-d 参数允许用户指定所用驱动,常见的驱动是 Overlay。
  • docker network ls 用于列出 Docker 主机上全部可见的容器网络。Swarm 模式下的 Docker 主机只能看到已经接入允许中的容器的网络。这种方式保证了 Gossip 开销最小化。
  • docker network inspect 用于查看特定容器网络的详情。其中包括范围、驱动、IPV6、子网配置、VXLAN 网络 ID 以及加密状态。
  • docker network rm 删除指定网络。

12.4 本章小结

本章首先介绍了通过 docker network create 命令创建新的 Docker 覆盖网络有多简单。接下来介绍了 Docker 如何利用 VXLAN 技术来实现网络间的连接。

第 13 章 卷和持久化数据

13.1 卷和持久化数据——简介

数据主要分为两类:持久化的与非持久化的。持久化数据就是要保存的数据。例如客户信息、财务、预定、审计日志以及某些应用日志数据。非持久化数据就是不需要保存的那些数据。

两者都很重要,并且 Docker 均有对应的支持方式。

每个 Docker 容器都有自己的非持久化存储。非持久化存储自动创建,从属于容器,生命周期与容器相同。这意味着删除容器也会删除全部持久化数据。

如果希望自己的容器数据保留下来(持久化),则需要将数据存储在卷上。卷与容器时解耦的,从而可以独立地创建并管理卷,并且卷并未与任意容器生命周期绑定。最终效果即用户可以删除一个关联了卷的容器,但是卷并不会被删除。

13.2 卷和持久化数据——详解

对于微服务设计模式来说,容器是个不错的选择。通常和微服务挂钩的词有暂时以及无状态。所以,微服务就是无状态的、临时的工作负载,同时容器即微服务。因此,我们经常会轻易下结论,认为容器就是用于临时场景。

但这种说法时错误的,而且大错特错。

13.2.1 容器与非持久数据

毫无疑问,容器擅长无状态和非持久化事务。

每个容器都会自动分配了本地存储。默认情况下,这是容器全部文件和文件系统保存的地方。非持久存储属于容器的一部分,并且与容器的生命周期一致————容器创建时会创建非持久化存储,同时该存储也会随着容器的删除而被删除。在 Linux 系统中,该存储的目录在 /var/lib/docker/<storage-driver>/之下,是容器的一部分。

默认情况下,容器的所有存储都使用本地存储。所以默认情况下容器全部目录都是用该存储。如果容器不产生持久化数据,那么本地存储即可满足需求并且能够正常使用。但是如果容器确实需要持久化数据,那就需要使用卷了。

13.2.2 容器与持久化数据

在容器中持久化数据的方式推荐采用卷。总体来说,用户创建卷,然后创建容器,接着将卷挂载到容器上。卷会挂在到容器文件系统的某个目录下, 任何写到该目录下的内容都会写到卷中。即使容器被删除,卷与其上面的数据仍然存在。

在上面章节中,我们曾经把卷挂载到 /code 目录下。下面我们完整地再做一次的示例:

  1. 创建和管理容器卷

Docker 中卷属于一等公民,可以直接使用 API 进行创建,拥有独立的 docker volume 子命令。下面的命令会创建一个名为 myvol 的新卷。

docker  volume create  myvol

默认情况下,Docker 创建新卷时采用内置的 local 驱动。恰如其名,本地卷只能被所在节点的容器使用。可以使用 -d 参数指定不同的驱动。第三方驱动可以通过插件方式接入,这些驱动提供了一些高级特性,并为 Docker 继承了外部存储系统。常见的卷插件类型包括有块存储、文件存储和对象存储等。

上面我们创建了卷,接下来我们可以使用 docker volume ls 命令去查看,要查看更多信息可以使用 docker volume inspect 命令。

[pangcm@docker01 ~]$ docker volume ls 
DRIVER              VOLUME NAME
local               myvol

上面的输出中可以看到驱动的类型。接下来我们可以再 Docker 服务或者容器中使用 myvol 卷了。例如,可以在 docker container run 后面增加参数 --flag 将卷挂载到新建容器中。

有两种方式删除 docker

docker volume prune
docker volume rm

其中第一个命令会删除所有没有使用的所有卷,所以请谨慎使用。第二个命令会删除指定卷。这两个删除的命令都不能删除正在被容器或者服务使用的卷。

到目前为止,我们已经知道了卷怎么去创建、查看以及删除了。此外,还可以通过在 Dockerfile 中使用 VOLUME 指令的方式部署卷。具体的格式为 VOLUME <container-mount-point>。但是,在 Docker 中没法指定主机目录。这是因为主机目录通常情况下是相对于主机的一个目录,这意味这这个目录在不同的主机间很可能是不同的,并且可能会构建失败。如果通过 Dockerfile 指定,那么每次部署时都需要指定主机目录。

  1. 演示卷在容器和服务中的使用

下面我们演示下在容器和服务中怎么使用卷。

我们使用下面的命令创建一个新的独立的容器,并且挂载一个名为 bizvol 的卷。

docker container run -itd --name voltainer --mount source=bizvol,target=/vol alpine

系统中没有叫做 bizvol 的卷,但是命令也能够运行成功的。这是因为如果指定了已存在的卷,Docker 会使用该卷;否则,Docker 会创建一个新的卷。

我们尝试删除下这个卷,由于正在使用,应该会删除失败。

[pangcm@docker01 ~]$ docker volume rm bizvol
Error response from daemon: remove bizvol: volume is in use - [5eb4a6db5328cd77040b7267a41f82304d3600d6ffe19643edab92b9c6180119]

然后我们试下把容器删除掉,看下卷是否还存在。

[pangcm@docker01 ~]$ docker container rm voltainer -f 
voltainer
[pangcm@docker01 ~]$ docker volume ls
DRIVER              VOLUME NAME
local               bizvol

可以看到卷还是存在的,如果你的卷原本有数据的,那还是会继续保存在卷中的。

我们试下把 bizvol 挂载到一个新的服务中看下,下面会创建一个名为 hellcat 的新的 Docker 服务,并且将 bizvol 挂载到该服务副本的 /vol 目录。

docker service create --name hellcat --mount source=bizvol,target=/vol alpine sleep 1d

当然,服务创建的前提是你的 Docker 主机是 swarm 模式下,并且在管理节点下。上面我们没有指定 --replicas 参数,所以服务只会部署一份副本。

下面我查看下这个服务的情况。

[pangcm@docker01 ~]$ docker service ps hellcat
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
h612lnqim6tx        hellcat.1           alpine:latest       docker01            Running             Running 2 minutes ago 

我们找到运行容器的节点,然后该该节点上看下。

[pangcm@docker01 ~]$ docker container ls 
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
a098cb4dd5f2        alpine:latest       "sleep 1d"          3 minutes ago       Up 3 minutes                            hellcat.1.h612lnqim6txuhui7mdbw1swn

上面就是关于卷的使用的介绍。

13.2.3 在集群节点间共享存储

Docker 能够集成外部存储系统,使得集群间护额短板共享外部存储数据变得简单。例如,独立存储 LUN 或者 NFS 共享可以应用到多个 Docker 主机,因此无论容器或者服务副本运行在哪个节点上,都可以共享存储。

这种的配置主要关注的点在与数据损害。由于是多节点共享一个存储,那就有可能有多个节点同时更新数据产生冲突的情况。例如,A 节点更新了部分数据,但是为了快速返回,数据实际写入了本地缓存而不是卷中,但是 A 认为已经更新了。在 A 更新数据之前,B 节点已经更新了相同部分的数据,但是值不一样,B 采用的是直接写入卷中。这时候,两个节点都认为自己更新成功了,事实上,B 更新的数据被 A 给覆盖了。但是这种情况,B 一无所知。

为了避免这种情况,需要在应用程序中进行控制。

13.3 卷和持久化数据——命令

  • docker volume create 命令用于创建新卷,新卷创建使用 local 驱动,但是可以通过 -d 参数来指定不同的驱动。
  • docker volume ls 会列出本地 Docker 主机上的全部卷
  • docker volume inspect 用于查看卷的详细信息。可以使用该命令查看卷在 Docker 主机文件系统中的具体位置。
  • docker volume prune 会删除未被容器或者副本使用的全部卷。谨慎使用。
  • docker volume rm 删除未被使用的指定卷

13.4 本章小结

数据主要分为两类:持久化数据和非持久化数据。持久化数据需要保存,而非持久化数据不需要。默认情况下,所有容器都有与自身生命周期相同的非持久化存储——本地存储,它非常适合用于非持久化数据。但是,如果容器需要创建长期保存的数据,最好将数据存储到 Docker 卷中。

Docker 卷是 Docker API 的一等公民,并使用 docker volume 子命令独立管理。这意味着删除容器并不会删除容器所使用的卷。

在 Docker 环境中,推荐使用卷来保存持久化数据。

第 14 章 使用 Docker Stack 部署应用

大规模场景下的多服务部署和管理是一件很难的事情。幸运的是,Docker Stack 为解决该问题而生。Docker Stack 通过提供期望状态、滚动升级、简单易用、扩缩容、健康检查等特性简化了应用的管理。这些功能都封装在一个完美的声明式模型当中。

14.1 使用 Docker Stack 部署应用——简介

在笔记本上测试和部署应用很简单。但是这只能算是业余选手。在真实的生产环境进行多服务的应用部署和管理,这才是专业选手的水平。

幸运的是,Stack 正为此而生!Stack 能够在单个声明文件中定义复杂的多服务应用。Stack 还提供了简单的方式来部署应用并管理完整的生命周期:初始化部署—> 健康检查—> 扩容—> 更新—> 回滚,以及其他功能。

步骤很简单。在 Compose 文件中定义应用,然后通过 docker stack deploy 命令完成部署和管理。Compose 文件中包含了构成应用所需的完整服务栈,此外还包括了卷、网络、安全以及应用所需的其他架构。然后基于该文件使用 docker stack deploy 命令来部署应用。

Stack 是基于 Docker Swarm 之上来完成应用的部署。因此诸如安全等高级特性,其实都是来自 Swarm。简而言之,Docker 适用于开发和测试。Docker Stack 则适用于大规模场景和生产环境。

14.2 使用 Docker Stack 部署应用——详解

从体系结构上来讲,Stack 位于 Docker 应用层级的最顶端。Stack 基于服务进行构建,而服务又基于容器,如下图所示。

docker9.png

14.2.1 简单应用

本章的示例使用的应用是 AtSea Shop,这个应用我们在前面曾经下载过了(多阶段构建 Docker)。该应用是个多服务应用,并且利用了认证和安全相关的技术。应用的架构图如下:

docker10.png

如果还没有下载这个项目,请使用 Git 来下载。

git clone https://github.com/nigelpoulton/atsea-sample-shop-app.git

该应用代码由若干的目录和源码文件组成,我们重点关注 docker-stack.yml 这个文件。该文件通常被称为 Stack 文件,在该文件中定义了应用及其依赖。

在该文件整体结构中,定义了 4 种关键字。其中包括 version、services、networks、secrets。与我们之前在 Swarm 章节介绍的一样,前 3 个关键字是同样的含义。secrets 定义的是应用用到的密钥。

我们展开顶级菜单,就会看到 5 个服务,3 个网络和 4 个密钥。

version: "3.2"

services:
  reverse_proxy:
  database:
  appserver:
  visualizer:
  payment_gateway:

networks:
  front-tier:
  back-tier:
  payment:

secrets:
  postgres_password:
  staging_token:
  revprox_key:
  revprox_cert:

14.2.2 深入分析 Stack 文件

Stack 文件就是 Docker Compose 文件。唯一的要求就是 version:一项需要是 3.0 或者更高的值。在 Docker 根据某个 Stack 文件部署应用的时候,首先会检查并创建 networks:关键字对应网络。如果网络不存在,Docker 会进行创建。下面我们详细看下这几个模块。

  1. 网络
networks:
  front-tier:
  back-tier:
  payment:
    driver: overlay
    driver_opts:
      encrypted: 'yes'

这里定义了 3 个网络,默认情况下网络都是使用 overlay 驱动,新建对应的覆盖类型的网络。但是 payment 网络比较特殊,需要对数据层加密。

默认情况下,覆盖网络的所有控制层都是加密的。如果需要加密数据层,有两种选择。

  • 在 docker network create 命令中指定 -o encrypted 参数。
  • 在 stack 文件中的 driver_opts 之下指定 encrypted:'yes'

数据层加密会导致额外开销,而影响额外开销大小的因素有很多,比如流量的类型和流量的多少。但是,通常额外开销会在 10% 的范围之内。

正如前面提到的,全部的 3 个网络均会先于密钥和服务被创建。

  1. 密钥

密钥数据顶级对象,在当前 Stack 文件中定义了 4 个。

secrets:
  postgres_password:
    external: true
  staging_token:
    external: true
  revprox_key:
    external: true
  revprox_cert:
    external: true

注意,4 个密钥都被定义为 external。这意味着在 Stack 部署之前,这些密钥必须存在。当然在应用部署时按需创建密钥也是可以的,只需要将 file: <filename> 替换为 external:true>。但该方式生效的前提是,需要在主机文件系统对应路径下有一个文本文件,其中包含密钥所需的值,并且是未加密的。这种方式存在明显的安全隐患。稍后会介绍如何创建密钥。

  1. 服务

部署中主要的操作都在服务这个环节。每个服务都是一个 JSON 集合,其中包含了一系列关键字。下面分别介绍着 5 个服务。

(1) reverse_proxy 服务

reverse_proxy:
    image: dockersamples/atseasampleshopapp_reverse_proxy
    ports:
      - "80:80"
      - "443:443"
    secrets:
      - source: revprox_cert
        target: revprox_cert
      - source: revprox_key
        target: revprox_key
    networks:
      - front-tier

reverse_proxy 服务定义了镜像、端口、密钥和网络。

image 关键字是服务对象中唯一的必填项,这个关键字定义了将要用于构建服务副本的 Docker 镜像。默认情况下会从 Docker Hub 拉取镜像,如果要从第三方服务中拉取,则需要自己添加对应的第三方镜像仓库服务 API 的 DNS 名称。

Docker Stack 和 Docker Compose 的一个区别是,Stack 不支持构建。这意味着在部署 Stack 之前,所有镜像都必须提前构建完成。

ports 关键字定义了两个关键字。默认情况下,所有端口映射都采用 Ingress 模式。这意味着 Swarm 集群中每个节点的对应端口都会映射并且是可以访问的,即使那些没有允许副本的节点。另一种方式是 Host 模式,端口只映射了允许副本的 Swarm 节点上。但是 Host 模式需要使用完整的格式去配置,我们在前面曾经介绍过。

secrets 关键字定义了两个密钥,这两个密钥必须在顶级关键字 secerts 下定义,并且必须在系统中已经存在。密钥以普通文件的形式被挂载到服务副本当中。文件的名称就是 stack 文件中定义的 target 属性的值,其在 Linux 下的路径为 /run/secrets,Linux 将 /run/secrets 作为内存文件系统挂载。

networks 关键字确保服务所有副本都会连接到 front-tier 网络。网络相关定义必须位于顶级关键字 networks 之下,如果定义的网络不存在,Docker 会以 Overlay 网络方式新建一个网络。

(2)database 服务

数据库服务除了定义上述的内容之外,还应用了环境变量和部署约束。

  database:
    image: dockersamples/atsea_db
    environment:
      POSTGRES_USER: gordonuser
      POSTGRES_DB_PASSWORD_FILE: /run/secrets/postgres_password
      POSTGRES_DB: atsea
    networks:
      - back-tier
    secrets:
      - postgres_password
    deploy:
      placement:
        constraints:
          - 'node.role == worker'

environment 关键字允许在服务副本中注入环境变量。在该服务中,使用了 3 个环境变量来定义数据库用户、数据库密码的位置(挂载到每个服务副本的密钥)以及数据库服务的名称。

该服务还在 deploy 关键字下定义了部署约束。这样保证了当前服务只会运行在 Swarm 集群的 worker 节点之上。部署约束是一种拓扑感知定时任务,是一种很好的优化调度选择的方式。Swarm 目前允许通过如下几种方式进行调度。

  • 节点 ID,如 node.id == qwertyuasdads
  • 节点名称,如 node.hostname==wrk-12
  • 节点角色,如 node.role != manager
  • 节点引擎标签,如 engine.labels.operatingsystem==ubuntu16.04
  • 节点自定义标签,如 node.labels.zone==prod1

(3) appserver 服务

  appserver:
    image: dockersamples/atsea_app
    networks:
      - front-tier
      - back-tier
      - payment
    deploy:
      replicas: 2
      update_config:
        parallelism: 2
        failure_action: rollback
      placement:
        constraints:
          - 'node.role == worker'
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    secrets:
      - postgres_password

appserver 服务使用了一个镜像,连接到 3 个网络,并且挂载了一个密钥。此外,appserver 服务还在 deploy 关键字下引入了一些额外的特性。

接下来我们进一步了解 deploy 关键字新增的内容。

  • replicas: 2 设置了期望服务的副本数量为 2,默认为 1.如果服务正在运行,需要调整副本数。可以调整 stack 文件中的 replicas 的数值,然后重新部署 stack。重新部署 stack 并不会影响那些没有改动的服务。
  • update_config 定义了服务在滚动升级的时候应该如何操作。在这里是每次更新两个副本,升级失败之后会自动回滚。回滚会基于之前的服务定义启动新的副本。failure_action 的默认操作时 pause,会在服务升级失败后阻止其他副本的升级。failure_action 还支持 continue。
  • restart_policy 定义了 Swarm 针对容器异常退出的重启策略。当前服务的重启策略是:如果某个副本以非 0 返回值退出,会立即重启当前副本。重启最多尝试 3 次,每次都是等待之多 120s 来检测是否成功。每次重启的间隔是 5s。

(4) visualizer 服务

  visualizer:
    image: dockersamples/visualizer:stable
    ports:
      - "8001:8080"
    stop_grace_period: 1m30s
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    deploy:
      update_config:
        failure_action: rollback
      placement:
        constraints:
          - 'node.role == manager'

除了指定镜像、定义端口映射、更新配置以及部署约束之外,这里还挂载了一个指定卷,并且定义了容器的优雅停止方式。

  • stop_grace_period 属性调整了默认为 10s 的优雅停止时长。
  • volumes 关键字用于挂载提前创建的卷或者主机目录到某个服务副本中。在这里挂载 Docker 主机的 /var/run/docker.sock 目录到每个副本的 /var/run/docker.sock 路径。这意味着在服务副本中任何对 /var/run/docker.sock 的读写操作都会指向 Docker 主机的对应目录。

docker.sock 文件时 Docker 提供的套接字,Docker daemon 通过该套接字对其他进程暴露其 API 终端。这意味着如果给某个容器访问该文件的权限,就是允许该容器接受全部的 API 终端,即等级与给予了容器查询和管理 Docker daemon 的能力。在大部分场景下这是不允许的,但是这是个测试环境的示例应用。

该服务需要 Docker 套接字访问权限的原因时需要以图形化的方式展示当前 Swarm 中的服务。为了实现这个目标,当前服务需要能访问管理节点 Docker daemon ,当前服务通过部署约束的方式,强制服务服务只能部署在管理节点之上,同时将 Docker 套接字绑定到每个服务副本中。

(5) payment_gateway 服务

  payment_gateway:
    image: dockersamples/atseasampleshopapp_payment_gateway
    secrets:
      - source: staging_token
        target: payment_token
    networks:
      - payment
    deploy:
      update_config:
        failure_action: rollback
      placement:
        constraints:
          - 'node.role == worker'
          - 'node.labels.pcidss == yes'

在这里,payment_gateway 服务被要求只能允许在符合 PCI DSS(支付卡行业标准)标准的节点之上。为了能使其生效,读者可以将某个自定义节点标签应用到 Swarm 集群中符合要求的节点之上。本书会在搭建应用部署实验环境的时候完成这个操作。

因为这里定义了两个部署约束,也就是只有同时满足 pcidss=yes 并且时 worker 的节点才会被部署。

14.2.3 部署应用

在部署应用之前,有几个前置处理需要完成。分别是:

  • Swarm 模式:应用将采用 Docker Stack 部署,而 Stack 依赖 Swarm 模式。
  • 标签: 某个 Swarm worker 节点需要自定义标签
  • 密钥: 应用所需的密钥需要在部署前创建完成。
  1. 搭建应用实验环境

同 Swarm 的实验一样,这里我们使用三个 Dokcer 主机来搭建 Swarm 集群,其中包括 1 个管理节点和 2 个工作节点。初始化并添加工作节点的操作这里不重复操作了,请看前面的章节。

实验的节点情况如下:

[pangcm@docker01 ~]$ docker node ls 
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
g6kx0krb978aqlexv6nd2i66x *   docker01            Ready               Active              Leader              19.03.5
px24lw8vvy3kh0ge61oge9cjo     docker02            Ready               Active                                  19.03.4
zha2x49djdhuqch35f6gm4efh     docker03            Ready               Active                                  19.03.5

(1) 添加节点标签(pcidss)

在前面我们分析过了 payment_gateway 服务配置了部署约束,限制了只有运行在 pcidss=yes 标签的工作节点之上。这里我们在 docker02 节点上增加该标签。

docker node update --label-add pcidss=yes docker02

可以使用 docker node inspect docker02 命令去确认标签

[pangcm@docker01 ~]$ docker node inspect docker02|grep pcidss
                "pcidss": "yes"

(2) 创建密钥

密钥中有 3 个是需要加密的 key 的,所以我们要先创建加密的 key,然后把加密 key 放到 Docker 密钥文件当中。

创建键值对

## 不断enter 即可
openssl req -newkey rsa:4096 -nodes -sha256  -keyout domain.key -x509 -days 365 -out domain.crt

创建 revprox_cert、revprox_key、postgres_password 的密钥

docker secret create revprox_cert domain.crt
docker secret create revprox_key domain.key
docker secret create postgres_password domain.key

创建 stage_token 密钥

echo staging |docker secret create staging_token -

列出所有的密钥:

[pangcm@docker01 ~]$ docker secret ls 
ID                          NAME                DRIVER              CREATED             UPDATED
zp591zcfknxprzhbn06r3rrwh   postgres_password                       31 minutes ago      31 minutes ago
tzfh3qgajlnf6yh9s82yw77ws   revprox_cert                            35 minutes ago      35 minutes ago
f109x6mtz1ueerxwxoy772ujv   revprox_key                             34 minutes ago      34 minutes ago
3bmwnmddfgyy8ij98csmfldjm   staging_token                           34 minutes ago      34 minutes ago

这里我们就完成了全部的前置准备,下面开始部署。

  1. 部署示例应用
##进入到项目目录下
cd atsea-sample-shop-app
##使用docker stack deploy 命令部署
docker stack deploy -c docker-stack.yml seastack

这里我们指定了 stack 文件,并把 stack 命名为 seastack。我们可以运行 docker network ls 以及 docker service ls 命令来擦看应用的网络和服务情况。下面是命令输出需要注意的地方。

网络是先于服务创建的。这是因为服务依赖于网络,所以网络需要在服务启动前创建。Docker 将 Stack 名称附加到由他创建的任何资源名称前作为前缀。在本例中,Stack 名为 seastack,所以所有资源名称的格式都如: seastack_<resource>。例如,payment 网络的名称是 seastack_payment。而在部署之前创建的资源则不会被重命名,比如密钥。

另外一个需要注意的是查看网络状态的时候会看到多了一个 seastack_default 的网络。该网络我们没有定义,为什么会创建呢?要知道每个服务都要连接到网络的,我们的 visualizer 服务没有指定具体的网络。于是 Docker 就创建了这样的一个网络,并且把 visualizer 连接到该网络。

我们可以通过 docker stack ls 和 docker stack ps 去查看 stack 的更多信息。

## docker stack ls 
[pangcm@docker01 atsea-sample-shop-app]$ docker stack ls 
NAME                SERVICES            ORCHESTRATOR
seastack            5                   Swarm

## docker stack ps 
[pangcm@docker01 atsea-sample-shop-app]$ docker stack ps seastack 
ID                  NAME                         IMAGE                                   NODE                DESIRED STATE       CURRENT STATE            ERROR 
aioybx1kz4vv        seastack_appserver.1         dockersamples/atsea_app:latest          docker03            Running             Preparing 10 hours ago                                      
i5k69i7gekyc        seastack_database.1          dockersamples/atsea_db:latest           docker02            Running             Running 10 hours ago                                        
8ulzn7q95axq        seastack_appserver.1         dockersamples/atsea_app:latest          docker02            Shutdown            Failed 10 hours ago      "task: n
...

可以看到 appserver 在 docker02 节点启动失败过,通过 docker stack ps 可以看到 Stack 中每个服务的概况,包括服务副本所在的节点、当前状态、期望状态和异常细腻些。

如果要查看具体某个服务的详细信息,可以使用 docker service logs 命令。

在服务都起来之后,我们使用浏览器访问 https://xxxx 即可访问这个应用的首页。

14.2.4 管理应用

Stack 是一组相关联的服务和基础设施,需要进行统一的部署和管理。这意味着 Stack 是由普通的 Docker 资源构建而来:网络、卷、密钥、服务等。我们可以通过普通的 Docker 命令对其进行查看和重新配置,如:docker netwoerk、docker volume、docker secret、docker service 等。

在这样的一个前提下,通过 docker service 命令来管理 Stack 中的某个服务是可行的。一个简单的例子是通过 docker service scale 命令来扩充 appserver 的服务副本数,但我们不推荐这么操作。

推荐的方式是通过声明式方式修改,即将 Stack 文件作为配置的唯一声明。这样,所有 Stack 相关的改动都需要体现在 Stack 文件中,然后重新部署应用所需的 Stack 文件。

举例,我们把 appserver 的副本数目从 2 修改为 3,把 visuaizer 服务的优雅停止时间增加到 2 分钟,我们修改 stack 文件。

  appserver:
    deploy:
      replicas: 3
      update_config:
        parallelism: 2
        failure_action: rollback

  visualizer:
    ports:
      - "8001:8080"
    stop_grace_period: 2m

然后重新部署

docker stack deploy -c docker-stack.yml seastack

如果要删除某个 Stack,使用 docker stack rm 命令。一定要谨慎,删除 Stack 不会进行二次确认。该命令会把网络和服务都删除,但是密钥不会被删除,原因是密钥是我们在部署前就创建并存在的。当然,卷也是不会被删除的。

14.3 使用 Docker Stack 部署应用——命令

  • docker stack deploy 用于根据 Stack 文件部署和更新 Stack 服务的命令。
  • docker stack ls 会列出 Swarm 集群中全部的 Stack,包括每个 Stack 拥有多少服务。
  • docker stack ps 列出某个已经部署的 Stack 相关详情。该命令支持 Stack 名称作为其主要参数,列举了服务副本在节点的分布情况,以及期望状态和当前状态。
  • docker stack rm 命令用于从 Swarm 集群中移除 Stack。移除操作执行前并不会进行二次确认。

14.4 本章小结

Stack 是 Docker 原生的部署和管理多服务应用的解决方案。Stack 默认集成在 Docker 引擎中,并且提供了简单的声明式接口对应用进行部署和全生命周期管理。

在本章开始提供了应用代码以及一些基础设施需求,比如网络、端口、卷和密钥。接下来的内容完成了应用的容器化,并且将全部应用服务和基础设施需求集成到一个声明式的 Stack 文件当中。在 Stack 文件中设置了服务副本数、滚动升级以及重启策略。然后通过 docker stack deploy 命令基于 Stack 文件完成了应用的部署。

对于已部署应用的更新操作,应该通过修改 Stack 文件完成。首先需要从源码管理系统中检出 Stack 文件,更新该文件,然后重新部署应用,最后将改动后的 Stack 文件重新提交到源码控制系统中。

因为 Stack 文件中定义了像服务副本数这样的内容,所以读者需要自己维护多个 Stack 文件以用于不同的环境,比如 dev、test 以及 prod。

第 15 章 Docker 安全

好的安全性是基于分层隔离的,而 Docker 恰好有很多分层。Docker 支持所有主流 Linux 安全机制,同时 Docker 自身还提供了很多简单的并且易于配置的安全技术。

15.1 Docker 安全——简介

安全的本质就是分层!通俗地讲,拥有更多的安全层,就能拥有更多的安全性。而 Docker 提供了很多安全层,可以看下图。

docker11.png

Linux Dokcer 利用了大部分 Linux 通用的安全技术。这些技术包括命名空间、控制组、系统权限、强制访问控制、安全计算。对于上述的每种技术,Docker 都设置合理的默认值,实现了流畅的并且适度安全的开箱即用体验,同时,Docker 也允许用户根据特定需求自定义调整每项安全配置。

Docker 平台本书也提供了一些非常棒的原生安全技术。并且重要的是,这些技术使用起来都非常简单。

  • Docker Swarm 模式:默认是开启安全功能的。无需任何配置,就可以获得加密节点 ID、双向认证、自动化 CA 配置、自动证书更新、加密集群存储、加密网络等安全功能。
  • Docker 内容新荣:允许用户对镜像签名,并且对拉取镜像的完整度和发布者进行验证。
  • Docker 密钥:使安全称为 Docker 生态系统中重要的一环。Docker 密钥存储在加密集群存储中,在容器传输过程中实时解密,使用时保存在内存文件系统,并运行了一个最小权限模型。

15.2 Docker 安全——详解

大家都知道安全是非常重要的。同时安全又很复杂并且枯燥无味。Docker 平台提供的绝大部分安全功能使用起来都非常简单。并且大部分的安全设置都配置了默认值,意味着用户无需任何配置,就能得到一个相当安全的平台。当然,默认配置不一定是最合适的,但是至少在最开始能够保障一定的安全性。如果默认配置与用户需求不符,那么用户也可以进行自定义配置。

15.2.1 Linux 安全技术

这里会介绍容器用到的主要 Linux 技术进行简介。

  1. Namespace

内核命名空间属于容器中非常核心的一部分!该技术能够将操作系统进行拆分,使一个操作系统看起来像多个相互独立的操作系统一样。这种技术可以用来做一些非常酷的事情,比如在相同的 OS 上运行多个 Web 服务,同时还不存端口冲突的问题。该技术还允许多个应用运行在相同 OS 上不存在竞争,同时还能共享配置文件以及类库。

举例说明:用户可以在相同的 OS 上运行多个 Web 服务,每个端口都是 443.为了实现该目的,可以将两个 Web 服务分别运行在自己的网络命名空间中。这样可以生效的原因是每个网络命名空间都拥有自己的 IP 地址和对应的全部端口。也可能需要将每个 IP 映射到 Docker 主机的不同端口上,但是使用 IP 上的哪个端口无须其他额外配置。

Linux Docker 现在利用了下列内核命名空间:进程 ID、网络、文件系统/挂载、进程内通信、用户、UTS 等。

Docker 容器是由各种命名空间组合而成的,Docker 容器本质就是命名空间的组织集合。

  1. Control Group

如果说命名空间用于隔离,那么控制组就是用于限额。假设容器就是酒店中的房间。每个容器间都是相互独立的,但是每个房间共享一部分公共资源,比如水电、游泳池、健身房、餐厅等等。CGroup 允许用户设置一些限制来保证不会存在单一容器占用全部的公共资源,如用光全部水或者吃光餐厅的全部早餐。

抛开酒店的例子,在 Docker 的世界中,容器之间是相互隔离的,但却共享 OS 资源,比如 CPU、RAM 以及硬盘 IO。CGroup 允许用户设置限制,这样单个容器就不能占用主机全部的资源了。

  1. Capability

以 root 身份运行容器可以不是什么好主意,root 拥有全部的权限,因此很危险。到那时,如果以非 root 身份在后台运行容器的话,非 root 用户缺少权限,处处受限。所以用户需要一种技术,能选择容器运行所需的 root 用户权限。了解下 Capability。

Docker 采用 Capability 机制来实现用户以 root 身份运行容器的同时,还能移除非必须的 root 能力。如果容器运行只需要 root 的绑定系统网络号的能力,则用户可以在启动容器的同时移除全部 root 能力,然后再将 CAP_NET_BIND_SERVICE 能力添加回来。

  1. Mac

Docker 采用主流 Linux Mac 技术,例如 AppArmor 以及 SELinux。基于用户的 Linux 发行版本,Docker 对新容器增加了默认的 AppArmor 配置文件。根据 Docker 文档的描述,默认配置文件提供了“适度的保护,同时还能兼容大部分应用”。

  1. Seccomp

Docker 使用过滤模式下的 Seccomp 来限制容器对宿主机内核发起的系统调用。按照 Docker 的安全理念,每个新容器都会设置默认的 Seccomp 配置,文件中设置了合理的默认值。这样做是为了在不影响兼容性的前提下,提供适度的安全保障。

用户同样可以自定义 Seccomp 配置,同时也可以通过向 Docker 传递指定参数,使 Docker 启动时不设置任何 Seccomp 配置。

  1. Linux 安全技术总结

Docker 基本支持所有的 Linux 重要安全技术,同时对其进行封装并赋予合理的默认值,这在保证了安全的同时也避免了过多的限制。

docker12.png

15.2.2 Docker 平台安全技术

  1. Swarm 模式

Swarm 模式时 Docker 未来的趋势。Swarm 模式支持用户集群化管理多个 Docker 主机,同时还能通过声明式的方式部署应用。每个 Swarm 都有管理者和工作者节点构成,系欸但可以是 Linux 或者 Windows。管理者节点构成了集群中的控制层,并负责集群配置以及工作负载的分配。工作者节点就是运行应用代码的容器。

正如所预期的,Swarm 模式包括很多开箱即用的安全特性,同时还设置了合理的默认值。这些安全特性包括以下几点:加密节点 ID、基于 TLS 的认证机制、安全准入令牌、支持周期性证书自动更新的 CA 配置、加密集群存储、加密网络。

  1. Docker 安全扫描

快速发现代码缺陷的能力至关重要。Docker 安全扫描功能使得对 Docker 镜像中已知缺陷的检测工作变得简单。Docker 安全扫描对 Docker 镜像进行二进制代码级别的扫描,对其中的软件根据已知缺陷数据库(CVE 数据库)进行检查。在扫描执行完成后,会生成一份详细报告。

登录 Docker Hub,我们可以查看官方仓库镜像的安全扫描报告。

Docker 可信镜像仓库服务(Docker Trusted Registry,DTR),属于 Docker 企业版中本地化镜像仓库服务的一部分内容,提供了相同的 Capability,同时还允许用户自行控制其镜像扫描实际以及扫描方式。

  1. Docker 内容信任

Docker 内容信任(Docker Content Tuest,DCT)使得用户很容易确认所下载镜像的完整性以及其发布者。在不可信任的网络环境中下载镜像时,这一点很重要。

从更高层面来看,DCT 允许开发者对发布到 Docker Hub 或者 Docker 可信服务的镜像进行签名。当这些镜像被拉取的时候,会自动确认签名状态。

DCT 还以提供关键上下文,如镜像是否已被签名从而可用于生产环境,镜像是否被新版本取代而过时等。

  1. Docker 密钥

很多应用都需要密钥。比如密码、TLS 证书、SSH key 等。在 Docker 1.13 版本之前,没有一种标准且安全的方式能让密钥在应用间实现共享。场景的方式时开发人员将密钥以文本的方式写入环境变量。这与理想状态差距甚远。

Docker 1.13 引入了 Docker 密钥,将密钥变成 Docker 生态系统中的一等公民。例如,增加了一个新的子命令 docker secret 来管理密钥。在 Docker 的 UCP 界面中,也有专门的地方来创建和管理密钥。在后台,密钥在创建后以及传输中都是加密的,使用时被挂载到内存文件系统,并且支队那些已经被授权了的服务开放访问。这确实时一种综合性的端到端解决方案。

用户可以通过 docker secret 子命令来管理密钥,可以通过在运行 docker service create 命令时附件 --secret,从而为某个服务指定密钥。

15.3 本章小结

Docker 可以通过配置变得特别安全。Docker 支持全部的 Linux 主流安全技术,包括 Nmaespace、Control Group、Capability、Mac 以及 SECcomp。Docker 为这些安全技术设定了合理的默认值,但是用户也可以自行修改配置,或者禁用这些安全技术。

在通用的 Linux 安全技术之上,Docker 平台还引入了大量自有安全技术。Swarm 模式基于 TLS 构建,并且配置上及其简单灵活。安全扫描对镜像进行二进制源码级别扫描,并提供已知缺陷的详细报告。Docker 内容信任允许用户对内容进行签名和认证,密钥目前也是 Docker 中的一等公民。

最终结论就是,无论用户希望 Docker 环境有多安全,Docker 都可以实现。这一切都取决于用户如何配置 Docker。


标题:深入浅出Docker学习笔记——第二部分
作者:pangcm
地址:http://pangcm.club/articles/2019/12/03/1575342122308.html