docker学习

        Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。

docker的安装(必须是linux操作系统):

       wget -qO- https://get.docker.com/ | sh

安装完成后,添加用户:

      sudo usermod -aG docker your_name

检查是否添加成功:

       cat /etc/group | grep docker

检查docker是否安装成功:

       docker --version

        Docker 的三个基本概念:镜像,容器,仓库。

docker 镜像,它实际上就是一个linux的文件系统,可以直接在linux的内核上运行。它的作用就是将我们开发的应用软件打包,这个打包,不仅仅是打包了我们开发的应用软件,还包含了运行这个应用所需要的Linux运行环境。这样一来它的移植性就会非常好了。不会说,在我们开发的环境可以正常运行,但是一旦放到其他人的环境,就出问题,这样就等于大大的减少了服务部署的调试时间。有很多帖子,将docker镜像也比喻成python中的类,或者模板,倒也没错,因为docker的镜像本身是不能运行我们所打包的那些服务的,只有运行镜像,然后生成docker容器,才可以运行相应的服务。所以就有人比喻镜像就像是类对象,容器就是这个类对象的实例化对象。或者镜像相当于模板,容器相当于根据模板生成自己的作品。docker仓库,就更好理解了,仓库存储的就是镜像呗,我们可以通过命令,docker pull xxx 就可以将镜像拉到本地了。

每个 Docker 容器都有一个本地存储空间,用于保存层叠的镜像层(Image Layer)以及挂载的容器文件系统。默认情况下,容器的所有读写操作都发生在其镜像层上或挂载的文件系统中,所以存储是每个容器的性能和稳定性不可或缺的一个环节。存储驱动的选择是节点级别的。这意味着每个 Docker 主机只能选择一种存储驱动,而不能为每个容器选择不同的存储驱动。在 Linux 上,读者可以通过修改 /etc/docker/daemon.json 文件来修改存储引擎配置,修改完成之后需要重启 Docker 才能够生效。下面的代码片段展示了如何将存储驱动设置为 overlay2。

{ "storage-driver": "overlay2" }

如果读者修改了正在运行 Docker 主机的存储引擎类型,则现有的镜像和容器在重启之后将不可用,这是因为每种存储驱动在主机上存储镜像层的位置是不同的(通常在 /var/lib/docker/ /... 目录下)。修改了存储驱动的类型,Docker 就无法找到原有的镜像和容器了。切换到原来的存储驱动,之前的镜像和容器就可以继续使用了。如果希望在切换存储引擎之后还能够继续使用之前的镜像和容器,需要将镜像保存为 Docker 格式,上传到某个镜像仓库,修改本地 Docker 存储引擎并重启,之后从镜像仓库将镜像拉取到本地,最后重启容器。所以,总的来说,选择存储驱动并正确地配置在 Docker 环境中是一件重要的事情,特别是在生产环境中。

下面的清单可以作为一个参考指南,帮助我们选择合适的存储驱动。同时还可以参阅 Docker 官网上由 Linux 发行商提供的最新文档来做出选择。

  • Red Hat Enterprise Linux:4.x版本内核或更高版本 + Docker 17.06 版本或更高版本,建议使用 Overlay2。
  • Red Hat Enterprise Linux:低版本内核或低版本的 Docker,建议使用 Device Mapper。
  • Ubuntu Linux:4.x 版本内核或更高版本,建议使用 Overlay2。
  • Ubuntu Linux:更早的版本建议使用 AUFS。
  • SUSE Linux Enterprise Server:Btrfs。

当我们安装 Docker 的时候,会涉及两个主要组件:Docker 客户端和 Docker daemon(有时也被称为“服务端”或者“引擎”)。

daemon 实现了 Docker 引擎的 API。

使用 Linux 默认安装时,客户端与 daemon 之间的通信是通过本地 IPC/UNIX Socket 完成的(/var/run/docker.sock);在 Windows 上是通过名为 npipe:./pipe/docker_engine 的管道(pipe)完成的。

在容器内部运行 ps -elf  命令查看当前正在运行的全部进程。

按 Ctrl-PQ 组合键,可以在退出容器的同时还保持容器运行。这样 Shell 就会返回到 Docker 主机终端。可以通过查看 ps -elf 提示符来确认。

注意:

命令:docker run image_id     这个命令是将静止的镜像运行起来,运行状态的镜像就是容器,所                                                  以这个命令的意义是启用容器。

命令:docker exec -it container_id /bin/bash   这个命令是用来连接容器的,连接容器进入容器,                                                                            进行相关操作

每个仓库中,或者说每个容器应用中,都包含一个名为 Dockerfile 的文件。(可参考下面的创建镜像的命令,加以理解)Dockerfile 是一个纯文本文件,其中描述了如何将应用构建到 Docker 镜像当中。Dockerfile 的 每一行都代表一个用于构建镜像的指令。

Docker 引擎是用来运行和管理容器的核心软件。其实这并不难理解,容器是啥?容器就是动态的镜像,那么镜像从静态如何变为动态的,就是通过docker引擎来实现的,所以说docker引擎是运行容器和管理容器的核心软件。同时,基于开放容器计划(OCI)相关标准的要求,Docker 引擎采用了模块化的设计原则,其组件是可替换的。Docker 引擎就像汽车引擎——二者都是模块化的,并且由许多可交换的部件组成。汽车引擎由许多专用的部件协同工作,从而使汽车可以行驶,例如进气管、节气门、气缸、火花塞、排气管等。Docker 引擎由许多专用的工具协同工作,从而可以创建和运行容器,例如 API、执行驱动、运行时、shim 进程等。Docker 引擎由如下主要的组件构成:Docker 客户端(Docker Client)、Docker 守护进程(Docker daemon)、containerd 以及 runc。它们共同负责容器的创建和运行。 docker学习_第1张图片

 上述模型,是docker容器的早期模型,属于大而全,非小而精,在后来的实践中,docker的性能变得越来越差。其实,任何一个软件的开发都应遵循在 UNIX 中得以实践并验证过的一种软件哲学:小而专的工具可以组装为大型工具。所以,后来docker也对其模型做出了调整,将其进行拆解,完成了向小而精的转换。

docker学习_第2张图片

runc:

runc 实质上是一个轻量级的、针对 Libcontainer 进行了包装的命令行交互工具,runc 生来只有一个作用——创建容器,这一点它非常拿手,速度很快!不过它是一个 CLI 包装器,实质上就是一个独立的容器运行时工具。

containerd:

在对 Docker daemon 的功能进行拆解后,所有的容器执行逻辑被重构到一个新的名为 containerd的工具中。它的主要任务是容器的生命周期管理——start | stop | pause | rm....

进一步来说:

常用的启动容器的方法就是使用 Docker 命令行工具。下面的docker container run命令会基于 alpine:latest 镜像启动一个新容器。

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

当使用 Docker 命令行工具执行如上命令时,Docker 客户端会将其转换为合适的 API 格式,并发送到正确的 API 端点。
API 是在 daemon 中实现的。这套功能丰富、基于版本的 REST API 已经成为 Docker 的标志,并且被行业接受成为事实上的容器 API。
一旦 daemon 接收到创建新容器的命令,它就会向 containerd 发出调用。daemon 已经不再包含任何创建容器的代码了!
daemon 使用一种 CRUD 风格的 API,通过 gRPC 与 containerd 进行通信。
虽然名叫 containerd,但是它并不负责创建容器,而是指挥 runc 去做。
containerd 将 Docker 镜像转换为 OCI bundle,并让 runc 基于此创建一个新的容器。
然后,runc 与操作系统内核接口进行通信,基于所有必要的工具(Namespace、CGroup等)来创建容器。容器进程作为 runc 的子进程启动,启动完毕后,runc 将会退出。

docker学习_第3张图片

该模型的显著优势

将所有的用于启动、管理容器的逻辑和代码从 daemon 中移除,意味着容器运行时与 Docker daemon 是解耦的,有时称之为“无守护进程的容器(daemonless container)”,如此,对 Docker daemon 的维护和升级工作不会影响到运行中的容器。
在旧模型中,所有容器运行时的逻辑都在 daemon 中实现,启动和停止 daemon 会导致宿主机上所有运行中的容器被杀掉。

shim

shim 是实现无 daemon 的容器(用于将运行中的容器与 daemon 解耦,以便进行 daemon 升级等操作)不可或缺的工具。
前面提到,containerd 指挥 runc 来创建新容器。事实上,每次创建容器时它都会 fork 一个新的 runc 实例。
不过,一旦容器创建完毕,对应的 runc 进程就会退出。因此,即使运行上百个容器,也无须保持上百个运行中的 runc 实例。
一旦容器进程的父进程 runc 退出,相关联的 containerd-shim 进程就会成为容器的父进程。作为容器的父进程,shim 的部分职责如下。

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

daemon 的作用

当所有的执行逻辑和运行时代码都从 daemon 中剥离出来之后,daemon 的主要功能包括镜像管理、镜像构建、REST API、身份验证、安全、核心网络以及编排。

前面已经描述过了容器和镜像的关系,这里再做一些补充

首先,由于一个镜像可以创建多个容器,所以,容器和镜像之前是存在联系的,有点本体和影子的意思,所以,再所有通过某一镜像创建出的容器没有全部停止之前,是无法删除镜像的。

其次,容器追求快速和小巧,这意味着构建镜像的时候通常需要裁剪掉不必要的部分,保持较小的体积。

最后,镜像中是不包含内核的,容器都是共享所在 Docker 主机的内核。所以有时会说容器仅包含必要的操作系统(通常只有操作系统文件和文件系统对象)。这也印证了上面开始中所提到的,docker镜像就是,一个linux的文件系统。

在虚拟机模型中,首先要开启物理机并启动 Hypervisor 引导程序。一旦 Hypervisor 启动,就会占有机器上的全部物理资源,如 CPU、RAM、存储和 NIC。
Hypervisor 接下来就会将这些物理资源划分为虚拟资源,并且看起来与真实物理资源完全一致。
然后 Hypervisor 会将这些资源打包进一个叫作虚拟机(VM)的软件结构当中。这样用户就可以使用这些虚拟机,并在其中安装操作系统和应用。

docker学习_第4张图片而容器模型则略有不同。
服务器启动之后,所选择的操作系统会启动。在 Docker 世界中可以选择 Linux,或者内核支持内核中的容器原语的新版本 Windows。
与虚拟机模型相同,OS 也占用了全部硬件资源。在 OS 层之上,需要安装容器引擎(如 Docker)。
容器引擎可以获取系统资源,比如进程树、文件系统以及网络栈,接着将资源分割为安全的互相隔离的资源结构,称之为容器。
每个容器看起来就像一个真实的操作系统,在其内部可以运行应用。

docker学习_第5张图片

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

注意,容器并不是完整的操作系统,由于容器讲究小巧,灵活,所以通常情况,不会搞一个完整的操作系统,基本上属于够在容器内构建的app用就行。也正因此,所以其启动要远比虚拟机快。

在容器内部并不需要内核,也就没有定位、解压以及初始化的过程——更不用提在内核启动过程中对硬件的遍历和初始化了。
这些在容器启动的过程中统统都不需要!唯一需要的是位于下层操作系统的共享内核是启动了的!最终结果就是,容器可以在 1s 内启动。唯一对容器启动时间有影响的就是容器内应用启动所花费的时间。
这就是容器模型要比虚拟机模型简洁并且高效的原因了。使用容器可以在更少的资源上运行更多的应用,启动更快,并且支付更少的授权和管理费用,同时面对未知攻击的风险也更小。

docker port  容器id       可以查看该容器的端口映射情况

docker logs  容器id       可以查看容器的运行日志,

                                     通常会加 -f 参数用来跟踪日志输出,

                                     --since用来显示某个开始时间的所有日志

                                     -t 添加时间戳

docker top   容器id        查看容器中正在运行的进程

docker inspect  容器id   查看容器底层信息(容器的配置和状态),返回一个json文件,记录相关 

                                      信息

docker stop 容器id       停止容器

docker start 容器id       启动容器

docker rm   容器id        删除容器

docker rm -f 容器id       删除正在运行中的容器。但是,删除容器的最佳方式还是分两步,先停止                                       容器然后删除。这样可以给容器中运行的应用/进程一个停止运行并清理残                                       留数据的机会。

docker system info       查看安装的docker的系统信息

docker build . -t test:latest(镜像名称)     构建镜像命令,其中的 . 表示当前目录,此目录应                                                                              包含应用的代码,以及dockerfile

docker build . -t test:latest(镜像名称)  --squash  构建镜像,同时将所有镜像层合并为一层

注意,通过build构建的镜像实际是在根据dockerfile在构建镜像,所以,如果执行这个命令的目录下没有dockerfile肯定是会出错的,因此,也可选择不使用 . 做为参数,而选择使用 -f dockerfile_path的方式来构建镜像。

docker image pull <镜像名称>:<标签>             拉取镜像,由于同一个镜像可能存在多个版本,所                                                                           以会添加标签,以区分同一镜像的不同版本,当然                                                                             也可以在该命令中不提供标签,那么默认会拉取标                                                                             签为 latest 的镜像,但是,标签是可以设置的,所                                                                             以就出现了标签为 latest 但是,实际却并非最新的                                                                             镜像的情况,而且标签是唯一的,所以有时候也会                                                                             出现,新推送的某一标签,在老的版本已经存在,                                                                             所以,这种情况下,老版本的标签就会自动消失,                                                                             以维持标签唯一性的特点。那些没有标签的镜像被                                                                             称为悬虚镜像,在列表中展示为:

docker image prune        删除悬虚镜像,如果添加 -a 参数,就同时删除没有用来启动容器的镜像

docker image ls --filter dangling=true             查看本地镜像(显示根据条件过滤后的内容)

                     Docker 目前支持如下的过滤器。

                     dangling:可以指定 true 或者 false,仅返回悬虚镜像(true),或者非悬虚镜像                                           (false)。

                     before:需要镜像名称或者 ID 作为参数,返回在之前被创建的全部镜像。

                     since:与 before 类似,不过返回的是指定镜像之后创建的全部镜像。

                     label:根据标注(label)的名称或者值,对镜像进行过滤。docker image ls命令输                                    出中不显示标注内容。

                     其他的过滤方式可以使用 reference,模糊匹配

                     eg: docker image ls --filter=reference="*:latest"

docker search name                   命令允许通过 CLI 的方式搜索 Docker Hub。可以通过“NAME”字                                                      段的内容进行匹配,并且基于返回内容中任意列的值进行过滤。

                                                    注意:通过这个命令所获得的镜像中既有官方的也有非官方的,                                                      如果只要显示官方的,可以加过滤: --filter "is-official=true",                                                          docker search 需要注意的最后一点是,默认情况下,Docker 只                                                      返回 25 行结果。但是,可以通过指定 --limit 参数来增加返回内                                                        容行数,最多为 100 行。

docker image rm image_id         删除镜像,但是实际上我更习惯用 docker rmi image_id 来进行同                                                     样的操作

docker exec -it 容器_id /bin/bash      链接一个容器,-it 参数其中 i 是指标准输入输出,t 是指                                                                    terminal终端,综合来说,-it 参数可以将当前终端连接到容器                                                            的 Shell 终端之上

ps -elf                                           进入容器后执行该命令可以查看容器中正在运行的进程

docker container ls -a                  该命令可以查看所有容器,包括被停止的容器

docker container run -d --name c1 \
-p 80:8080  镜像_id                     该命令是用来启动容器的,-d 参数的作用是让应用程序以守护线                                                      程的方式在后台运行。 -p 80:8080 参数的作用是将主机的80端                                                        口与容器内的8080端口进行映射。         

docker tag ymengkai/machineforge:latest  aaa:latest   可以将前者标签改为后者,完成后,

                                                                                      docker images查看,会看到有1个镜像id,

                                                                                       对应有2个名称                       

Docker的核心思想就是如何将应用整合到容器中,并且能在容器中实际运行。

容器是为应用而生的,具体来说,容器能够简化应用的构建、部署和运行过程。

完整的应用容器化过程主要分为以下几个步骤。

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

docker学习_第6张图片

在代码目录当中,有个名称为 Dockerfile 的文件。这个文件包含了对当前应用的描述,并且能指导 Docker 完成镜像的构建。通常将 Dockerfile 放到构建上下文的根目录下。另外很重要的一点是,文件开头字母是大写 D,这里是一个单词。像“dockerfile”或者“Docker file”这种写法都是不允许的。

Dockerfile 主要包括两个用途:

  • 对当前应用的描述。
  • 指导 Docker 完成应用的容器化(创建一个包含当前应用的镜像)。

Dockerfile 能实现开发和部署两个过程的无缝切换。

每个 Dockerfile 文件第一行都是 FROM 指令。

FROM 指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。基于 Linux 操作系统,所以在 FROM 指令当中所引用的也是一个 Linux 基础镜像。

docker学习_第7张图片

Dockerfile 中通过标签(LABLE)方式指定了当前镜像的维护者

eg: LABEL maintainer="[email protected]"

每个标签其实是一个键值对(Key-Value),在一个镜像当中可以通过增加标签的方式来为镜像添加自定义元数据。
备注维护者信息有助于为该镜像的潜在使用者提供沟通途径,这是一种值得提倡的做法。
RUN apk add --update nodejs nodejs-npm 指令使用 alpine 的 apk 包管理器将 nodejs 和 nodejs-npm 安装到当前镜像之中。

 RUN 指令会在 FROM 指定的 alpine 基础镜像之上,新建一个镜像层来存储这些安装内容。当前镜像的结构如下图所示。

docker学习_第8张图片

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

docker学习_第9张图片

 Dockerfile 通过 WORKDIR 指令,为 Dockerfile 中尚未执行的指令设置工作目录。该目录与镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。
然后,RUN npm install 指令会根据 package.json 中的配置信息,使用 npm 来安装当前应用的相关依赖包。
npm 命令会在前文设置的工作目录中执行,并且在镜像中新建镜像层来保存相应的依赖文件。

目前镜像一共包含 4 层,如下图所示。

docker学习_第10张图片

因为当前应用需要通过 TCP 端口 8080 对外提供一个 Web 服务,所以在 Dockerfile 中通过 EXPOSE 8080 指令来完成相应端口的设置。
这个配置信息会作为镜像的元数据被保存下来,并不会产生新的镜像层。
最终,通过 ENTRYPOINT 指令来指定当前镜像的入口程序。ENTRYPOINT 指定的配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。

总结一下:

  1. Dockerfile 中的注释行,都是以#开头的。
  2. 除注释之外,每一行都是一条指令(Instruction)。                                                              指令的格式为:INSTRUCTION argument,指令是不区分大小写的,但是通常都采用大写的方式。这样 Dockerfile 的可读性会高一些。
  3. Docker image build命令会按行来解析 Dockerfile 中的指令并顺序执行。
  4. 部分指令会在镜像中创建新的镜像层,其他指令只会增加或修改镜像的元数据信息。
  5. 关于如何区分命令是否会新建镜像层,一个基本的原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果只是告诉 Docker 如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。
  6. 使用 FROM 指令引用官方基础镜像是一个很好的习惯,这是因为官方的镜像通常会遵循一些最佳实践,并且能帮助使用者规避一些已知的问题。
    除此之外,使用 FROM 的时候选择一个相对较小的镜像文件通常也能避免一些潜在的问题。

对于 Docker 镜像来说,过大的体积并不好!
越大则越慢,这就意味着更难使用,而且可能更加脆弱,更容易遭受攻击。
鉴于此,Docker 镜像应该尽量小。对于生产环境镜像来说,目标是将其缩小到仅包含运行应用所必需的内容即可。问题在于,生成较小的镜像并非易事。
不同的 Dockerfile 写法就会对镜像的大小产生显著影响。
常见的例子是,每一个 RUN 指令会新增一个镜像层。因此,通过使用 && 连接多个命令以及使用反斜杠(\)换行的方法,将多个命令包含在一个 RUN 指令中,通常来说是一种值得提倡的方式。

多阶段构建方式使用一个 Dockerfile,其中包含多个 FROM 指令。每一个 FROM 指令都是一个新的构建阶段(Build Stage),并且可以方便地复制之前阶段的构件。

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 AS production
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 叫作 appserver。
  • 阶段 2 叫作 production。

storefront 阶段拉取了大小超过 600MB 的 node:latest 镜像,然后设置了工作目录,复制一些应用代码进去,然后使用 2 个 RUN 指令来执行 npm 操作。
这会生成 3 个镜像层并显著增加镜像大小。指令执行结束后会得到一个比原镜像大得多的镜像,其中包含许多构建工具和少量应用程序代码。
appserver 阶段拉取了大小超过 700MB 的 maven:latest 镜像。然后通过 2 个 COPY 指令和 2 个 RUN 指令生成了 4 个镜像层。
这个阶段同样会构建出一个非常大的包含许多构建工具和非常少量应用程序代码的镜像。
production 阶段拉取 java:8-jdk-alpine 镜像,这个镜像大约 150MB,明显小于前两个构建阶段用到的 node 和 maven 镜像。
这个阶段会创建一个用户,设置工作目录,从 storefront 阶段生成的镜像中复制一些应用代码过来。
之后,设置一个不同的工作目录,然后从 appserver 阶段生成的镜像中复制应用相关的代码。最后,production 设置当前应用程序为容器启动时的主程序。
重点在于 COPY --from 指令,它从之前的阶段构建的镜像中仅复制生产环境相关的应用代码,而不会复制生产环境不需要的构件。
还有一点也很重要,多阶段构建这种方式仅用到了一个 Dockerfile,并且 docker image build 命令不需要增加额外参数。

执行构建(这可能会花费几分钟)。

$ docker image build -t multi:stage .

Sending build context to Docker daemon 3.658MB
Step 1/19 : FROM node:latest AS storefront
latest: Pulling from library/node
aa18ad1a0d33: Pull complete
15a33158a136: Pull complete

Step 19/19 : CMD --spring.profiles.active=postgres
---> Running in b4df9850f7ed
---> 3dc0d5e6223e
Removing intermediate container b4df9850f7ed
Successfully built 3dc0d5e6223e
Successfully tagged multi:stage

$ docker image ls

REPO TAG IMAGE ID CREATED SIZE
node latest 9ea1c3e33a0b 4 days ago 673MB
6598db3cefaf 3 mins ago 816MB
maven latest cbf114925530 2 weeks ago 750MB
d5b619b83d9e 1 min ago 891MB
java 8-jdk-alpine 3fd9dd82815c 7 months ago 145MB
multi stage 3dc0d5e6223e 1 min ago 210MB

输出内容的第一行显示了在 storefront 阶段拉取的 node:latest 镜像,下一行内容为该阶段生成的镜像(通过添加代码,执行 npm 安装和构建操作生成该镜像)。
这两个都包含许多的构建工具,因此镜像体积非常大。
第 3~4 行是在 appserver 阶段拉取和生成的镜像,它们也都因为包含许多构建工具而导致体积较大。
最后一行是 Dockerfile 中的最后一个构建阶段(stage2/production)生成的 multi:stage 镜像。
可见它明显比之前阶段拉取和生成的镜像要小。这是因为该镜像是基于相对精简的 java:8-jdk-alpine 镜像构建的,并且仅添加了用于生产环境的应用程序文件。
最终,无须额外的脚本,仅对一个单独的 Dockerfile 执行 docker image build 命令,就创建了一个精简的生产环境镜像。
多阶段构建是随 Docker 17.05 版本新增的一个特性,用于构建精简的生产环境镜像。

对于上述方法(COPY --from),我个人认为,可能不太好,因为前两个app的工程代码在运行时,可能会需要一些组件,所以,在构建他们各自镜像的时候,是有添加许多组件的,但是使用COPY --from 这种方式,在第三个镜像中,应该只是将工程代码给弄过来了,那么就会产生一个问题,这样会不会导致前两个app在第三个镜像里由于缺乏组件,而无法运行呢。当然,也有另一种理解,就是,前两个镜像会生成一些东西,而这些东西刚好会对第三个容器有用,所以直接通过这种方式,去将前两个镜像中生成的对第三个镜像有用的东西,给弄到第三个镜像中。(大概率是这样的)

最佳实践

下面介绍一些最佳实践。

1) 利用构建缓存

Docker 的构建过程利用了缓存机制。观察缓存效果的一个方法,就是在一个干净的 Docker 主机上构建一个新的镜像,然后再重复同样的构建。
第一次构建会拉取基础镜像,并构建镜像层,构建过程需要花费一定时间;第二次构建几乎能够立即完成。
这就是因为第一次构建的内容(如镜像层)能够被缓存下来,并被后续的构建过程复用。
docker image build 命令会从顶层开始解析 Dockerfile 中的指令并逐行执行。而对每一条指令,Docker 都会检查缓存中是否已经有与该指令对应的镜像层。
如果有,即为缓存命中(Cache Hit),并且会使用这个镜像层;如果没有,则是缓存未命中(Cache Miss),Docker 会基于该指令构建新的镜像层。
缓存命中能够显著加快构建过程。
下面通过实例演示其效果。
示例用的 Dockerfile 如下。

FROM alpine
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]

第一条指令告诉 Docker 使用 alpine:latest 作为基础镜像。
如果主机中已经存在这个镜像,那么构建时会直接跳到下一条指令;如果镜像不存在,则会从 Docker Hub(docker.io)拉取。
下一条指令(RUN apk...)对镜像执行一条命令。
此时,Docker 会检查构建缓存中是否存在基于同一基础镜像,并且执行了相同指令的镜像层。
在此例中,Docker 会检查缓存中是否存在一个基于 alpine:latest 镜像且执行了 RUN apk add --update nodejs nodejs-npm 指令构建得到的镜像层。
如果找到该镜像层,Docker 会跳过这条指令,并链接到这个已经存在的镜像层,然后继续构建;如果无法找到符合要求的镜像层,则设置缓存无效并构建该镜像层。
此处“设置缓存无效”作用于本次构建的后续部分。也就是说 Dockerfile 中接下来的指令将全部执行而不会再尝试查找构建缓存。
假设 Docker 已经在缓存中找到了该指令对应的镜像层(缓存命中),并且假设这个镜像层的 ID 是 AAA。
下一条指令会复制一些代码到镜像中(COPY . /src)。因为上一条指令命中了缓存,Docker 会继续查找是否有一个缓存的镜像层也是基于 AAA 层并执行了 COPY . /src 命令。
如果有,Docker 会链接到这个缓存的镜像层并继续执行后续指令;如果没有,则构建镜像层,并对后续的构建操作设置缓存无效。
假设 Docker 已经有一个对应该指令的缓存镜像层(缓存命中),并且假设这个镜像层的 ID 是 BBB。
那么 Docker 将继续执行 Dockerfile 中剩余的指令。
理解以下几点很重要。
首先,一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。
在编写 Dockerfile 时须特别注意这一点,尽量将易于发生变化的指令置于 Dockerfile 文件的后方执行。
这意味着缓存未命中的情况将直到构建的后期才会出现,从而构建过程能够尽量从缓存中获益。
通过对 docker image build 命令加入 --nocache=true 参数可以强制忽略对缓存的使用。
还有一点也很重要,那就是 COPY 和 ADD 指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化。
例如,有可能 Dockerfile 中的 COPY . /src 指令没有发生变化,但是被复制的目录中的内容已经发生变化了。
为了应对这一问题,Docker 会计算每一个被复制文件的 Checksum 值,并与缓存镜像层中同一文件的 checksum 进行对比。如果不匹配,那么就认为缓存无效并构建新的镜像层。

也就是说,想要制作出只含有我们所需文件的镜像是比较困难的。因为一不小心就会整出许多的镜像层出来,从而使整个镜像变的很大。

2)合并镜像

当镜像中层数太多时,合并是一个不错的优化方式。例如,当创建一个新的基础镜像,以便基于它来构建其他镜像的时候,这个基础镜像就最好被合并为一层。简单来说就是现有有一个镜像,然后还需要创建一个镜像,新的镜像需要老的镜像作为基础镜像,那么这个时候,最好将老的镜像合并成一个整体,来作为新镜像的基础镜像。缺点是,合并的镜像将无法共享镜像层。这会导致存储空间的低效利用。

具体的做法为:执行 docker image build命令时,可以通过增加 --squash 参数来创建一个合并的镜像。

下图阐释了合并镜像层带来的存储空间低效利用的问题。
docker学习_第11张图片

简单来说,对于没合并的镜像,就是哪变了你提提交时就提交哪,而对于后者,甭管哪变了,全部都得提交,就小石子和大板砖的差异吧。

3) 使用 no-install-recommends

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

4)不要安装 MSI 包(Windows)

在构建 Windows 镜像时,尽量避免使用 MSI 包管理器。因其对空间的利用率不高,会大幅增加镜像的体积。

Dockerfile:

使用  Docker 中的 docker image build 命令会读取 Dockerfile,并将应用程序容器化。

简单的来说,Dockerfile 就是用来指导构建镜像的。它的命令是不分大小写的,只不过为了有更好的可读性,会将命令大写,同时,# 代表注释,这点跟python一样,基本上它的每一行,都是一条命令。

使用 -t 参数为镜像打标签,使用 -f 参数指定 Dockerfile 的路径和名称,使用 -f 参数可以指定位于任意路径下的任意名称的 Dockerfile。

Dockerfile 中的 FROM 指令用于指定要构建的镜像的基础镜像。它通常是 Dockerfile 中的第一条指令。

Dockerfile 中的 RUN 指令用于在镜像中执行命令,这会创建新的镜像层。每个 RUN 指令创建一个新的镜像层。

Dockerfile 中的 COPY 指令用于将文件作为一个新的层添加到镜像中。通常使用 COPY 指令将应用代码赋值到镜像中。

Dockerfile 中的 EXPOSE 指令用于记录应用所使用的网络端口。
Dockerfile 中的 ENTRYPOINT 指令用于指定镜像以容器方式启动后默认运行的程序。
其他的 Dockerfile 指令还有 LABEL、ENV、ONBUILD、HEALTHCHECK、CMD 等。

Docker Compose:

如果说,Dockerfile文件是用来定义docker镜像的,那么docker-compose.yml文件就是用来部署和管理docker服务的。

内部实现上,Docker Compose会解析 YAML 文件,并通过 Docker API 进行应用的部署和管理。

docker-compose.yml:

docker Compose 使用 YAML 文件来定义多服务的应用。YAML 是 JSON 的一个子集,因此也可以使用 JSON。
Docker Compose 默认使用文件名 docker-compose.yml。当然,也可以使用 -f 参数指定具体文件。

version: "2.3"
services:
  buildapi-ob:
    image: ${USER}/buildapi
    build: ${BUILDAPI_DIR}
    labels:
        - "buildapi-ob=${BUILDSYSTEM_DEV_TEST_ENV_ID}"
    environment:
      USER: ${USER}
      PYTHONPATH: /service:/build/apps/lib
      DATABASE_CHOICE: "official"
    volumes:
      - ${APPS_PATH}:/build/apps
      - ${BUILDAPI_DIR}:/service/buildapi
    working_dir: /service/buildapi
    restart: on-failure
    tty: true
    entrypoint:
      - python3.8
      - /service/buildapi/manage.py
      - runserver
      - 0.0.0.0:80
    networks:
      "build_net":
        aliases:
          - ${USER}-ob-buildapi.eng.vmware.com
  buildapi-sb:
    image: ${USER}/buildapi
    build: ${BUILDAPI_DIR}
    labels:
        - "buildapi-sb=${BUILDSYSTEM_DEV_TEST_ENV_ID}"
    environment:
      USER: ${USER}
      PYTHONPATH: /service:/build/apps/lib
      DATABASE_CHOICE: "sandbox"
    volumes:
      - ${APPS_PATH}:/build/apps
      - ${BUILDAPI_DIR}:/service/buildapi
    working_dir: /service/buildapi
    restart: on-failure
    tty: true
    entrypoint:
      - python3.8
      - /service/buildapi/manage.py
      - runserver
      - 0.0.0.0:80
    networks:
      "build_net":
        aliases:
          - ${USER}-sb-buildapi.eng.vmware.com
  buildapi:
    image: build-docker-main.artifactory.eng.vmware.com/nginx:1.15.3
    labels:
        - "buildapi=${BUILDSYSTEM_DEV_TEST_ENV_ID}"
    restart: on-failure
    volumes:
        - ${BUILDAPI_DIR}/dev_nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - ${BUILDAPI_HOST_PORT}:80
    networks:
      "build_net":
        aliases:
          - ${USER}-buildapi.eng.vmware.com

networks:
  "build_net":
    name: ${USER}_build_net
    driver: bridge

volumes:
  counter-vol:

大致上看,一级标签有4个:

version、services、networks、volumes

首先,version 是必须指定的,而且总是位于文件的第一行。它定义了 Compose 文件格式的版本。(version 并非定义 Docker Compose 或 Docker 引擎的版本号。)

然后,services 用于定义不同的应用服务。Docker Compose 会将每个服务部署在各自的容器中。需要明确的是,它会将每个服务部署为一个容器,并且会使用 key 作为容器名字的一部分。

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

这里我要插一嘴:按道理来说,同一个机子上的不同容器之间网络上应该是通的才对,但是看我上面发的一个docker-compose.yml文件,我是给不同的服务添加了别名,然后就可以正常访问了,要不然,会出现无法访问的情况。

1) build

指定 Docker 基于当前目录(.)下 Dockerfile 中定义的指令来构建一个新镜像。该镜像会被用于启动该服务的容器。

2) command

python app.py 指定 Docker 在容器中执行名为 app.py 的 Python 脚本作为主程序。
因此镜像中必须包含 app.py 文件以及 Python,这一点在 Dockerfile 中可以得到满足。

3) ports

指定 Docker 将容器内(-target)的 5000 端口映射到主机(published)的 5000 端口。
这意味着发送到 Docker 主机 5000 端口的流量会被转发到容器的 5000 端口。容器中的应用监听端口 5000。

4) networks

使得 Docker 可以将服务连接到指定的网络上。这个网络应该是已经存在的,或者是在 networks 一级 key 中定义的网络。
对于 Overlay 网络来说,它还需要定义一个 attachable 标志,这样独立的容器才可以连接上它(这时 Docker Compose 会部署独立的容器而不是 Docker 服务)。

最后,volumes 用于指引 Docker 来创建新的卷。卷这个东西,有点像挂载,就是把容器外的磁盘内容,挂载到容器内部。这样容器内部,也就可以使用一些在容器外部才能使用的东西。这个主要是,我们创建一个应用的时候,往往是在容器外部创建的,它可能会对一些文件产生依赖,但是这些依赖本身又不是应用本身的东西,比如:工具链,运行应用需要依赖工具链,但是应用本身的构成,是不需要工具链的,因此,构建镜像文件时,会将应用打包,但是对于一些依赖,往往会以卷的形式放到容器里。语法如上图:- 外部文件路径:容器文件路径

文件部署应用:

命令:docker-compose up

默认情况下,docker-compose up 会查找名为 docker-compose.yml 或 docker-compose.yaml 的 Compose 文件,如果 Compose 文件是其他文件名,则需要通过 -f 参数来指定。

例如: docker-compose -f prod-equus-bass.yml up

也可以加 -d 参数,后台执行。

例如: docker-compose -f prod-equus-bass.yml up -d

补充一句,这条命令通常是在工程目录下执行,同时,一般情况下,Dockerfile,docker-compose.yml 这2个文件通常也都是在工程目录下。

上面介绍了服务的部署和启动,接下来看下停止:

命令:docker-compose down

注意,执行该命令后,它会停止容器,删除服务,删除网络,退出docker-compose up的进程,但是它不会删除卷,不会删除镜像。卷应该是用于数据的长期持久化存储的。因此,卷的生命周期是与相应的容器完全解耦的,写到卷上的所有数据都会保存下来。还有,应用的源码也不会被删除。

命令:docker-compose stop

该命令,只是停止服务,不删东西。

命令:docker-compose rm

该命令必须在stop之后执行,作用等于 down

命令: docker-compose restart

该命令作用与 stop 相反

命令: docker-compose ps

输出中会显示容器名称、其中运行的 Command、当前状态以及其监听的网络端口。

命令: docker-compose top

该命令列出各个服务(容器)内运行的进程。其中 PID 编号是在 Docker 主机上(而不是容器内)的进程 ID。

命令: docker volume inspect xxx

该命令可以查看卷位于 Docker 主机的什么位置。

命令: cp xxx    xxxx

将容器外的文件,复制到容器里

例如:cp ~/counterapp/app.py \
/var/lib/docker/volumes/counterapp_counter-vol/_data/app.py

Swarm 来创建安全的集群

docker 是可以通过创建集群,然后实现集群化管理。

Swarm 是 Docker 官方提供的一款集群管理工具,其主要作用是把若干台 Docker 主机抽象为一个整体,并且通过一个入口统一管理这些 Docker 主机上的各种 Docker 资源。
Swarm 和 Kubernetes 比较类似,但是更加轻,具有的功能也较 kubernetes 更少一些。

集群讲究的是节点(node),节点可以是主机,可以是容器,等...

节点必须具备以下特点:

每个节点都需要安装 Docker,并且能够与 Swarm 的其他节点通信。
如果配置有域名解析就更好了,这样在命令的输出中更容易识别出节点,也更有利于排除故障。
在网络方面,需要在路由器和防火墙中开放如下端口。

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

创建集群的好处就是,可以统一的管理许多docker节点,比如,现在有一个服务,它被部署到了许多容器里,那么现在这个服务需要进行升级,那么总不至于我们跑到每一个容器上,然后来一场重复的升级动作吧,所以这时候,就显现处集群的好处了,完全可以在管理节点下发升级命令,然后自动的完成所有节点的升级动作。通常这种升级动作是滚动升级的。

ok,现在聊聊咋弄集群服务.....

1、通常情况只需在某一个节点上执行命令:

     docker swarm init --advertise-addr xxx:xx --listen-addr xxx:xx    该命令是将当下节点集群化,指定了ip地址和端口号,同时监听这个ip 和 port ,同时将这个节点设置成管理节点。

2、要知道,在管理节点上添加其他的节点需要用到 token 手令,所以通常情况,我们需要先获取手令,然后将其保存起来,后面添加新的节点的时候要用,手令只有两种,管理节点手令,和工作节点手令,获取手令的命令为:

   docker swarm join-token worker             获取工作节点手令

   docker swarm join-token manager          获取管理节点手令

3、添加节点,这里注意,对于管理节点,最好不要搞成偶数个,这是因为如果弄成偶数个,然后在网络上发生分区的时候,就有可能出现,2个区都拥有相同个数的节点的情况,这种情形下,就不存在主次之分,就会造成脑裂的现象。所以需要将其搞成奇数个,这样节点就一定不会出现均分的情况,就必然有了主次,那么就已然可以管理整个集群(少数服从多数的原则),同时,管理节点不宜多,以 3 or 5 为最佳,最多7个,不要再多了,因为过多的管理节点回影响效率,就好比3个人决定去哪吃饭,远便利与33个人做决定。对了,如果有多个管理节点的情况下,通常是只生效一个管理节点,其他的节点是静默的(一个人话事,其他人闭嘴),但是如果谁给处于静默状态的管理节点发消息,那么该节点会自动将消息转发为处于激活状态的节点。

添加的命令为:

   docker swarm join  --token xxx  --advertise-addr ip:port  --listen-addr ip:port  

至于添加的是工作节点还是管理节点,就完全取决于这里的 token 是给的啥了。

4、节点的查看命令: docker node ls

5、重启一个旧的管理节点或进行备份恢复仍有可能对集群造成影响。一个旧的管理节点重新接入 Swarm 会自动解密并获得 Raft 数据库中长时间序列的访问权,这会带来安全隐患。进行备份恢复可能会抹掉最新的 Swarm 配置。为了规避以上问题,Docker 提供了自动锁机制来锁定 Swarm,这会强制要求重启的管理节点在提供一个集群解锁码之后才有权从新接入集群。所以,基本上之前在创建管理节点的时候,我们是没有给锁的,那么这东西也可以后面添加。

命令为: docker swarm init --autolock=true    或者   docker swarm update --autolock=true

$ 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-5+ICW2kRxPxZrVyBDWzBkzZdSd0Yc7Cl2o4Uuf9NPU4

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

 使用锁命令后,就会出现一个 key,这就是锁的钥匙,这是固定的,需要保存好,如果后面重启节点就需要输入这个钥匙了(个人估计添加新的节点也需要锁的钥匙了)。

这时,节点执行命令:  service docker restart   将不会被纳入集群。

$ docker swarm unlock
Please enter unlock key: 

需要解锁命令,才可以将节点重新纳入集群。

6、docker 1.12版本之后,本身就可以基于swarm创建服务,前面说了通过swarm创建集群,那么现在就可以通过docker自己的服务模块或者功能,将我们的服务部署到节点上。这里有两种方式,一种是副本服务模式,一种是全局服务模式。

副本服务模式下,docker service 会将服务均匀的分布到整个集群,而全局服务模式,则会使每一个节点只运行一个服务,比如,有一个节点是主机,这个主机本身就是一个节点,但是这个主机上安装了好几个虚拟机,每个虚拟机又又许多容器,那么等于说,全局服务模式就只在主机上部署一个服务。

服务的部署命令为: docker service create --name xxx -p port:port --replicas 5  镜像

它的意思是, 创建一个服务,--name 为xxx, 映射端口为服务端口:节点端口,--replicas 5 副本个数为 5个,最后是镜像,也就是这5个服务都是运行这个镜像。全局模式添加参数 --mode global 即可。

7、查看服务命令: docker service ls

8、查看服务在哪些节点运行: docker service ps 服务name

9、查看详细信息: docker service inspect 服务name   (这个就过于详细了)

                                docker service inspect --pretty 服务name   (只显示常规信息)

10、服务的扩容和缩容:docker service scale 服务name=num

      由于之前创建副本时,是给了副本个数的,现在重新设定副本个数,多了就是扩,少了就是缩

11、删除服务:docker service rm 服务name

12、创建覆盖网络:docker network create -d overlay xxx   

此方法创建出来的是一个覆盖网络,覆盖网络是一个二层网络,容器可以接入该网络,并且所有接入的容器均可互相通信。即使这些容器所在的 Docker 主机位于不同的底层网络上,该覆盖网络依然是相通的。本质上说,覆盖网络是创建于底层异构网络之上的一个新的二层容器网络。docker学习_第12张图片

13、查看网络:docker network ls

14、这时候就可以在创建服务的时候,指定网络了,从而所有服务就都是通的,可以互相访问。

        命令为: docker service create --name xx -p port:port  --replicas 5 --network xx  镜像

15、滚动升级(比如升级镜像):docker service update --image nigelpoulton/tu-demo:v2 \
                                                      --update-parallelism 2  --update-delay 20s 服务name

        就是说给服务xxx进行镜像升级,--update-parallelism 2 每次升2个副本,--update-delay 20s   每次升级间隔20s

16、服务日志:docker service logs  

       可以使用 --follow 进行跟踪、使用 --tail 显示最近的日志,并使用 --details 获取额外细节

Docker镜像(image)详解

你可能感兴趣的:(docker,容器,运维)