Kun

Kun

IT学徒、技术民工、斜杠青年,机器人爱好者、摄影爱好 PS、PR、LR、达芬奇潜在学习者


共 279 篇文章


  Docker容器部署

安装

原理

由于 Docker 的容器隔离依赖于 Linux 内核中的相关支持,所以使用 Docker 首先需要确保安装机器的 Linux kernel中包含 Docker 所需要使用的特性。以目前 Docker 官方主要维护的版本为例,我们需要使用基于 Linux kernel 3.10 以上版本的 Linux 系统,也就是centos7、debian7、ubuntu14以上的系统版本来安装 Docker.

WindowsmacOS 中没有 Docker能够利用的 Linux 环境,那么我们生造一个 Linux 环境就行啦!Docker for WindowsDocker for Mac 正是这么实现的…

由于虚拟化在云计算时代的广泛使用,WindowsMacOS 也将虚拟化引入到了系统本身的实现中,这其中就包含了之前我们所提到的通过 Hypervisor实现虚拟化的功能。在 Windows 中,我们可以通过 Hyper-V 实现虚拟化,而在 macOS 中,我们可以通过 HyperKit 实现虚拟化

Docker for WindowsDocker for Mac 这里利用了这两个操作系统提供的功能来搭建一个虚拟 Linux 系统,并在其之上安装和运行 docker daemon

底层原理

镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发额软件,它包含某个软件所需要的所有内容,包括代码、运行时库、环境变量和配置文件

UnionFS

UnionFS(联合文件系统):Union 文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下 (unite several directories into a single virtual filesystem)。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。 特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录

下载 docker 镜像时一层一层的其实就是联合文件系统的体现

Docker镜像加载原理

docker 的镜像实际上由一层一层的文件系统组成,这种层级的文件系统 UnionFS。 bootfs (boot file system) 主要包含 bootloader 和 kernel, bootloader 主要是引导加载 kernel, Linux 刚启动时会加载 bootfs 文件系统,在 Docker 镜像的最底层是 bootfs。这一层与我们典型的 Linux/Unix 系统是一样的,包含 boot 加载器和内核。当 boot 加载完成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs 转交给内核,此时系统也会卸载 bootfs。

rootfs (root file system) ,在 bootfs 之上。包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。rootfs 就是各种不同的操作系统发行版,比如 Ubuntu,Centos 等等。

平时我们安装虚拟机的 CentOs 都是好几个 G,为什么 docker 才几百 M

对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了,因为底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。由此可见对于不同的 linux 发行版,bootfs 基本是一致的,rootfs 会有差别,因此不同的发行版可以公用 bootfs。

分层的理解

下载 docker 镜像时一层一层的其实就是分层最直观的体现

分层最大的一个好处就是 - 共享资源

比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。

这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改? 答案:不会!因为修改会被限制在单个容器内。

Mac os

使用homebrew安装

brew cask install docker

手动安装

下载stable版本的dmg文件,

检查是否安装成功

docker --version

查看docker配置

docker info

Linux

移除可能有旧的Docker版本

yum erase -y docker docker-common docker-engine

安装工具包和依赖,设置仓库源

yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager \ 
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

用yum安装docker-ce

##最新稳定版
yum install docker-ce docker-ce-cli containerd.io
##指定版本
yum install docker-ce-18.09.6 docker-ce-cli-18.09.6 containerd.io

启动docker服务

systemctl start docker

检查docker的信息和版本

docker version
docker info

验证docker,通过下载镜像和创建容器来看看Docker是否可以运转起来。可以使用下面的命令从Docker的镜像仓库下载名为hello-world的镜像文件。

docker pull hello-world

docker安装容器

docker pull gitlab/gitlab-ce

如果docker拉取较慢更换docker源,docker默认为docker国内镜像,可以腾讯源、中科大源或者dcloud

启动容器

基本概念

Docker 体系里,有四个对象 (Object) 是我们不得不进行介绍的,因为几乎所有 Docker 以及周边生态的功能,都是围绕着它们所展开的。它们分别是:镜像 ( Image )、容器 ( Container )、网络 ( Network )、数据卷 ( Volume )

所谓镜像,可以理解为一个只读的文件包,其中包含了虚拟环境运行最原始文件系统的内容

容器就是用来隔离虚拟环境的基础设施,而在 Docker 里,它也被引申为隔离出来的虚拟环境。

如果把镜像理解为编程中的类,那么容器就可以理解为类的实例。镜像内存放的是不可变化的东西,当以它们为基础的容器启动后,容器内也就成为了一个“活”的空间

一个容器由三个部分组成:一个docker镜像、一个程序运行环境、一个指令集合

网络:在 Docker 中,实现了强大的网络功能,我们不但能够十分轻松的对每个容器的网络进行配置,还能在容器间建立虚拟网络,将数个容器包裹其中,同时与其他网络环境隔离

在以往的虚拟机中,我们通常直接采用虚拟机的文件系统作为应用数据等文件的存储位置。然而这种方式其实并非完全安全的,当虚拟机或者容器出现问题导致文件系统无法使用时,虽然我们可以很快的通过镜像重置文件系统使得应用快速恢复运行,但是之前存放的数据也就消失了。

为了保证数据的独立性,我们通常会单独挂载一个文件系统来存放数据。这种操作在虚拟机中是繁琐的,因为我们不但要搞定挂载在不同宿主机中实现的方法,还要考虑挂载文件系统兼容性,虚拟操作系统配置等问题。值得庆幸的是,这些在 Docker 里都已经为我们轻松的实现了,我们只需要简单的一两个命令或参数,就能完成文件系统目录的挂载。

能够这么简单的实现挂载,主要还是得益于 Docker 底层的 Union File System 技术。在 UnionFS 的加持下,除了能够从宿主操作系统中挂载目录外,还能够建立独立的目录持久存放数据,或者在容器间共享。

Docker 中,通过这几种方式进行数据共享或持久化的文件或目录,我们都称为数据卷 ( Volume )…

报错处理

rate limit

docker拉取镜像时报错

Error response from daemon:toomanyrequests: You have reached your pull rate limit

dockerhub从2020年11月2日起限制非付费用户的拉取频率:

匿名用户每6小时允许pull100次,

已登录用户每6小时允许pull200次

方法:修改拉取的镜像源

vim /etc/docker/daemon.json

添加中科大的镜像源

{
  "registry-mirrors":["https://ustc-edu-cn.mirror.aliyuncs.com"]
}

重启docker服务

systemctl daemon-reload && systemctl restart docker

Dockerfile

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。通过 Dockerfile 我们可以更加清晰、明确的给定 Docker 镜像的制作过程,而由于其仅是简单、小体积的文件,在网络等其他介质中传递的速度极快,能够更快的帮助我们实现容器迁移和集群部署

Dockerfile 的指令简单分为五大类:

  • 基础指令:用于定义新镜像的基础和性质。
  • 控制指令:是指导镜像构建的核心部分,用于描述镜像在构建过程中需要执行的命令。
  • 引入指令:用于将外部文件直接引入到构建镜像内部。
  • 执行指令:能够为基于镜像所创建的容器,指定在启动时需要执行的脚本或命令。
  • 配置指令:对镜像以及基于镜像所创建的容器,可以通过配置指令对其网络、用户等内容进行配置

通常来说,我们不会从零开始搭建一个镜像,而是会选择一个已经存在的镜像作为我们新镜像的基础,这种方式能够大幅减少我们的时间。

Dockerfile 里,我们可以通过 FROM 指令指定一个基础镜像,接下来所有的指令都是基于这个镜像所展开的。在镜像构建的过程中,Docker 也会先获取到这个给出的基础镜像,再从这个镜像上进行构建操作。

实例

FROM nginx
RUN echo '这是一个本地构建的nginx镜像' > /usr/share/nginx/html/index.html

RUN:用于执行后面跟着的命令行命令。有以下俩种格式:

执行命令 shell格式

RUN <命令行命令>

执行可执行文件 exec格式

RUN ["可执行文件", "参数1", "参数2"]

特别注意,Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。

通过 EXPOSE 指令就可以为镜像指定要暴露的端口。

VOLUME:定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

作用:避免重要的数据因容器重启而丢失,避免容器不断变大。

CMD:类似于RUN指令。RUN指令在docker build时运行,CMD是在docker run时运行。

在制作新的镜像的时候,我们可能需要将一些软件配置、程序代码、执行脚本等直接导入到镜像内的文件系统里,使用 COPYADD 指令能够帮助我们直接从宿主机的文件系统里拷贝内容到镜像里的文件系统中。

构建镜像

docker build -t nginx:test .

最后的.点是上下文路径,上下文路径是指 docker 在构建镜像,有时候想要使用到本机的文件(比如复制),docker build 命令得知这个路径后,会将路径下的所有内容打包。

​ 优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。

​ 缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定。

使用技巧

Dockerfile 里,我们可以用 ARG 指令来建立一个参数变量,我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile里使用它。

例如,我们希望通过参数变量控制 Dockerfile 中某个程序的版本,在构建时安装我们指定版本的软件,我们可以通过 ARG 定义的参数作为占位符,替换版本定义的部分

环境变量也是用来定义参数的东西,与 ARG指令相类似,环境变量的定义是通过 ENV 这个指令来完成的

构建高质量镜像

构建目标:

docker 希望我们每个账号做到 单一职责, 本质是希望我们运行一个前台进程, 这样就可以保证容器生命周期和进程一致. 简单列一下 dockerfile 的目标:

  • 单一职责
  • 不能臃肿
  • 构建速度快
  • Dockerfile 文件清晰简洁易于维护

构建基础:

docker 镜像是基于 Union FS 做的, 是一种树形结构一层层继承下去. dockerfile 大多命令都会产生一层 layer.

.dockerignore 文件

这一点虽然很基础, 但很容易被忽视. 有时还会看到某些镜像里面还有 .git 文件夹, 就觉得这点确实容易被忽视.

看到这个后缀就知道, 是控制 docker 构建命令 COPYADD 忽略哪些文件, 更重要的是会影响到 docker build 的上下文. 假如我们指定上下文为当前目录, docker build 时会将目录中的所有文件压缩打包后发送给 Docker daemon. 可想而知一个 node 项目有没有 ignore node_modules 文件夹时的差别.

可能有人要问, 为什么不继承 .gitignore 文件呢? 因为大多数情况我们会使用 Git 做版本管理. 因为镜像是做部署的, 复制进去最后运行的往往是构建编译后的产物, 而这些恰好不是源码仓库需要的, 所以不适合直接使用 .gitignore 文件.

基础镜像选择:

上面说到镜像构建简单来理解是层层叠加的, 所以我们保证自己代码和依赖库大小之后, 镜像大小很大程度上依赖于基础镜像的大小.

例如, node 官方镜像提供了基于 Debian Linux 的和基于 alpine Linux 的版本, 可以看到镜像大小差别非常大: debian 345.07 MB 而 alpine 只有 38.79 MB. 可以看出基础镜像对于我们上层镜像大小的影响.

那么是不是无论何时都要选择 alpine 版本呢? 其实也不是, Linux 不同发行版本软件包差别比较大. 假如你的应用依赖很多库, 那么在 alpine 寻找这些依赖可能很费功夫, 所以我们需要根据场景选择基础镜像版本.

还有一点值得注意, 有些时候是需要线上 debug 排查一些问题, 所以可以额外安装一些工具, 比如: curl, telnet, vim 之类的. 也可以做两个版本的基镜像, 一个是 prod 的什么都没有的, 另一个是 dev 的含有这些工具的, 想要 debug 时部署一个基于 dev 镜像的容器. 个人感觉没必要追求极限, 多安装一点工具也没有什么问题.

镜像复用

有时我们上层应用会依赖一些 Linux 库, 比如图片处理. 在构建镜像时往往会做编译操作, 这些操作一般都很费时间, 而且三方 ci 构建一般不会持久化, 用不了 docker build 缓存, 这会导致每次构建都很费时. 回过头来想我们是否需要每次都编译这部分软件呢? 答案是根本不需要, 那么我们为什么不把这些操作打包然后发布成自己的镜像呢? 这样我们应用层镜像基于它, 就省去了编译的时间和消耗.

这一点也可以推广到更一般的情况, 也就是我们要评估镜像构建中哪些耗时步骤更新频率不需要那么频繁, 这部分都可以使用基础镜像的形式减少构建时间.

假如你在本地构建, 本地总会有构建缓存, 那你可能没必要这么做, 不过需要注意的是: 尽量要把变动不频繁的东西放在上面, 才能更好地利用构建缓存.

多层构建

我们从静态语言来说多层构建. 例如, go 语言, 编译成二进制文件运行, 根本就不需要 go 语言编译器和标准库这些文件, 使用纯 alpine 基镜像构建镜像会非常小, 基本只是二进制文件大小 + 4M 左右. 那么我们可否在本机编译好二进制文件 COPY 进镜像呢? 答案是可以的, 但是这样二进制文件就和编译它的机器有关联了, 也会引入一些之前的痛点, 比如一些全局库版本, 或者是 go 语言版本差异都会使得二进制文件有差异. 所以我们需要多层构建.

多层构建一般来说是在 A 镜像编译好二进制产物, 然后 COPY 到我们最终的运行镜像中. 例如 go 程序经常会这样操作:

FROM golang:1.14 AS build
WORKDIR /mnt
COPY . .
RUN CGO_ENABLED=0 go build -o ./bin/main ./main.go

FROM alpine:3.12
WORKDIR /opt
COPY --from=build /mnt/bin/main /usr/bin/
ENTRYPOINT ["main"]

最终镜像其实是基于 alpine:3.12 的, 所以大小不会是问题, 而且我们编译也是在容器 golang:1.14 进行的, 也就与宿主机没有关系了.

同理不光是静态语言, 甚至一个纯前端项目, 构建时需要 node 环境, 并且安装一堆依赖包, 其实我们部署一般就只需要构建后的 dist 资源, 这时我们使用多层构建, 使用 nginx 容器作为运行容器, 将 node 环境构建好的 dist 文件 COPY 过去就好了.

再例如, 假如你的前端代码不想开源, 只想提供镜像, 那么这种 COPY dist 的方式也很简单就控制了镜像中的文件, 不需要构建后加上很多删除源文件的操作了.

当然在使用多层构建时, 还可以使用 docker build --target [name] 指定只 build 到那一层就停止, 也就是可以使用中间任何一层作为 build 终点, 就可以使用中间镜像了.

参数化构建

镜像复用那里也说了, 如果有一些耗时但不常更新的操作, 往往建议我们将它构建成自己的基础镜像, 减少上层构建时间.

那么基础镜像升级了, 我们的 dockerfile 也要升级, 更常见的是我们会同时使用多个基础镜像版本. 例如: 我们基础镜像同时使用基于 node11, node12, node14 的, 难道在构建时, 需要手动更改 FROM node:[version] 吗?

其实更简单的方法就是 build arg 结合镜像构建 ARG 关键字.

ARG 声明的变量可以通过 docker build --build-arg NODE_VERSION=xxx 在构建时传入, 所以我们 dockerfile 这样写:

ARG NODE_VERSION

FROM node:${NODE_VERSION}

就可以不用修改文件改变镜像 tag 了.

使用entrypoint

docker 镜像控制镜像默认运行行为的关键字有两个: CMDENTRYPOINT . 区别在哪里?

cmd 相当于默认命令, 用户自定义命令时会直接覆盖掉;

而 entrypoint 则会将用户自定义的命令当做参数, 除非使用 --entrypoint 来替换.

有些时候使用 entrypoint 可以提升使用简便性, 例如 curl 镜像指定 ENTRYPOINT ["curl"] 使用者就可以直接 docker run curl httpbin.org 如果换做 CMD 的话, 就要 docker run curl curl httpbin.org.

还有一些场景是在启动前做一些事情, 比如 mysql 镜像会根据环境变量帮我们建数据库, 还会帮我们导入使用 volume 挂在到相应文件夹下的 sql 文件, 这些都是通过 entrypoint 实现的. 还有一些容器承载了多种功能, 会使用 entrypoint 实现不同命令使用不同 user 执行.

提升用户体验

docker 镜像构建出来很多情况是共享给他人使用的, 所以我们应该注意点用户体验, dockerfile 除了允许注释外, 还有一些声明式关键字, 例如: LABEL, EXPOSE

LABEL 允许我们添加一些元信息, 例如 maintainer 信息, 或者留个邮箱之类的.

EXPOSE 允许我们声明我们服务对外暴露的是什么端口, 它对于容器运行没有任何影响, 只是方便使用者知道该把什么端口映射出去. 这点很重要, 很多时候使用别人镜像还要去在源码里面找寻这些信息.

生产自动构建相关

虽然 LABEL 可以增加一些元信息, 但是查看起来不太方便, 而且假如生产 k8s 运行一个镜像, 让你查看这些信息也会很麻烦. 所以我们就要在 tag 上面下点功夫.

首先强烈建议镜像 tag 和 git commit 或者 git tag 能够对应, 不然出了问题半天都不知道镜像对应的代码对应哪次提交.

经过我们生产实践, 基本 tag 需要包含以下几个信息:

  1. git commit hash 和 git tag, 主要用来定位和源码对应关系
  2. 构建日期, 可以快速定位构建记录, 或者定位发版记录
  3. 应用名, 这个没有太大意义, 主要是看到 tag 就知道是哪个服务的

只用 tag 版本号例如 1.1.0 之类的会有哪些缺点呢? 有两点, 如果你的 tag 版本号和 git release tag 能够对应上, 那还好, 不然就是上面说的找不到对应关系. 第二点, 就是不同服务版本会有重叠, 容易发错版本, 例如 B 应用要发版 1.1.0 但是缺误操作到 A 应用上面去了, 结果有可能导致一个线上 2.0.0 的 A 应用版本回退到 1.1.0. 如果使用我们上面的 tag 的话, 会变成一个找不到镜像的错误, 不会对生产应用产生影响.

构建速度优化

优化原则:

  1. 最小化处理变更文件,仅变更下一步所需的文件,以尽可能减少构建过程中的缓存失效。
  2. 对于处理文件变更的 ADD 命令、COPY 命令,尽量延迟执行。

构建体积优化

在保证速度的前提下,体积优化也是我们需要去考虑的。这里我们需要考虑的有三点:

  1. Docker 是以层为单位上传镜像仓库的,这样也能最大化的利用缓存的能力。因此,执行结果很少变化的命令需要抽出来单独成层,如上面提到的 npm install的例子里,也用到了这方面的思想。
  2. 如果镜像层数越少,总上传体积就越小。因此,在命令处于执行链尾部,即不会对其他层缓存产生影响的情况下,尽量合并命令,从而减少缓存体积。例如,设置环境变量和清理无用文件的指令,它们的输出都是不会被使用的,因此可以将这些命令合并为一行 RUN 命令。

    bash
    
    体验AI代码助手
    代码解读
    复制代码RUN set ENV=prod && rm -rf ./trash
  3. Docker cache 的下载也是通过层缓存的方式,因此为了减少镜像的传输下载时间,我们最好使用固定的物理机器来进行构建。例如在流水线中指定专用宿主机,能是的镜像的准备时间大大减少。

当然,时间和空间的优化从来就没有两全其美的办法,这一点需要我们在设计 Dockerfile 时,对 Docker Layer 层数做出权衡。例如为了时间优化,需要我们拆分文件的复制等操作,而这一点会导致层数增多,略微增加空间。

避免使用进程守护

我们编写传统的后台服务时,总是会使用例如 pm2、forever 等等进程守护程序,以保证服务在意外崩溃时能被监测到并自动重启。但这一点在 Docker 下非但没有益处,还带来了额外的不稳定因素。

首先,Docker 本身就是一个流程管理器,因此,进程守护程序提供的崩溃重启,日志记录等等工作 Docker 本身或是基于 Docker 的编排程序(如 kubernetes)就能提供了,无需使用额外应用实现。除此之外,由于守护进程的特性,将不可避免的对于以下的情况产生影响:

  1. 增加进程守护程序会使得占用的内存增多,镜像体积也会相应增大。
  2. 由于守护进程一直能正常运行,服务发生故障时,Docker 自身的重启策略将不会生效,Docker 日志里将不会记录崩溃信息,排障溯源困难。
  3. 由于多了个进程的加入,Docker 提供的 CPU、内存等监控指标将变得不准确。

因此,尽管 pm2 这样的进程守护程序提供了能够适配 Docker 的版本: pm2-runtime,但我仍然不推荐大家使用进程守护程序。

其实这一点其实是源自于我们的固有思想而犯下的错误。在服务上云的过程中,难点其实不仅仅在于写法与架构上的调整,开发思路的转变才是最重要的,我们会在上云的过程中更加深刻体会到这一点。

日志的持久化存储

无论是为了排障还是审计的需要,后台服务总是需要日志能力。按照以往的思路,我们将日志分好类后,统一写入某个目录下的日志文件即可。但是在 Docker 中,任何本地文件都不是持久化的,会随着容器的生命周期结束而销毁。因此,我们需要将日志的存储跳出容器之外。

最简单的做法是利用 DockerManagerVolume,这个特性能绕过容器自身的文件系统,直接将数据写到宿主物理机器上。具体用法如下:

运行 docker 时,通过-v 参数为容器绑定 volumes,将宿主机上的 /app/log 目录(如果没有会自动创建)挂载到容器的 /usr/share/log 中。这样服务在将日志写入该文件夹时,就能持久化存储在宿主机上,不随着 docker 的销毁而丢失了。

当然,当部署集群变多后,物理宿主机上的日志也会变得难以管理。此时就需要一个服务编排系统来统一管理了。从单纯管理日志的角度出发,我们可以进行网络上报,给到云日志服务(如腾讯云 CLS)托管。或者干脆将容器进行批量管理,例如 Kubernetes这样的容器编排系统,这样日志作为其中的一个模块自然也能得到妥善保管了。

k8s服务控制器的选择

镜像优化之外,服务编排以及控制部署的负载形式对性能的影响也很大。这里以最流行的 Kubernetes的两种控制器(Controller): DeploymentStatefulSet 为例,简要比较一下这两类组织形式,帮助选择出最适合服务的 Controller。

StatefulSet是 K8S 在 1.5 版本后引入的 Controller,主要特点为:能够实现 pod 间的有序部署、更新和销毁。

Deployment 用于部署无状态服务,StatefulSet 用来部署有状态服务。

StatefulSet的特点可以从如下几个步骤进行理解:

  1. StatefulSet管理的多个 pod 之间进行部署,更新,删除操作时能够按照固定顺序依次进行。适用于多服务之间有依赖的情况,如先启动数据库服务再开启查询服务。
  2. 由于 pod 之间有依赖关系,因此每个 pod 提供的服务必定不同,所以 StatefulSet 管理的 pod 之间没有负载均衡的能力。
  3. 又因为 pod 提供的服务不同,所以每个 pod 都会有自己独立的存储空间,pod 间不共享。
  4. 为了保证 pod 部署更新时顺序,必须固定 pod 的名称,因此不像 Deployment 那样生成的 pod 名称后会带一串随机数。
  5. 而由于 pod 名称固定,因此跟 StatefulSet 对接的 Service 中可以直接以 pod 名称作为访问域名,而不需要提供 ClusterIP,因此跟 StatefulSet 对接的 Service 被称为 HeadlessService

容器

Docker 容器的生命周期里分为五种状态,其分别代表着

  • Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
  • Running:容器正在运行,也就是容器中的应用正在运行。
  • Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
  • Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
  • Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除…

创建容器

sudo docker create --name nginx nginx:1.12

--name后面跟的是指定容器的名称

启动容器

sudo docker start nginx

nginx为之前创建容器时指定的名称,启动之后为running状态,只要应用程序还在运行,那么容器的状态就会是 Running,除非进行一些修改容器的操作。

创建容器和启动容器也可以合并为一步run命令,

sudo docker run --name nginx -d nginx:1.12

通过 docker run 创建的容器,在创建完成之后会直接启动起来,不需要我们再使用 docker start 去启动了。

管理容器

容器创建和启动后,除了关注应用程序是否功能正常外,我们也会关注容器的状态等内容.使用docker ps罗列出 Docker 中的容器

docker ps

默认情况下,docker ps 列出的容器是处于运行中的容器,如果要列出所有状态的容器,需要增加 -a--all选项

docker logs 显示容器的标准输出

停止和删除容器

docker stop 停止正在运行的容器

正在运行中的容器默认情况下是不能被删除的,我们可以通过增加 -f--force 选项来让 docker rm 强制停止并删除容器,不过不建议这样做。

进入容器

很多时间,我们需要的操作并不仅仅是按镜像所给出的命令启动容器而已,我们还会希望进一步操作容器,这时候最佳的方式就是让我们进入到容器了

Docker 为我们提供了一个命令 docker exec 来让容器运行我们所给出的命令

sudo docker exec nginx more /etc/hostname

容器网络

容器网络模型为容器引擎提供了一套标准的网络对接范式,而在 Docker 中,实现这套范式的是 Docker 所封装的 libnetwork 模块。

Docker 官方为我们提供了五种 Docker网络驱动,分别是:Bridge DriverHost DriverOverlay DriverMacLan Driverone Driver

Bridge DriverOverlay Driver 在开发中使用频率较高。Bridge 网络是 Docker 容器的默认网络驱动,简而言之其就是通过网桥来实现网络通讯 ( 网桥网络的实现可以基于硬件,也可以基于软件 )。

Overlay 网络是借助 Docker 集群模块 Docker Swarm来搭建的跨 Docker Daemon 网络,我们可以通过它搭建跨物理主机的虚拟网络,进而让不同物理机中运行的容器感知不到多个物理机的存在。

创建网络

Docker 里,我们也能够创建网络,形成自己定义虚拟子网的目的。

docker CLI 里与网络相关的命令都以 network 开头,创建网络的命令是 docker network create

sudo docker network create -d bridge individual

通过 -d 选项我们可以为新的网络指定驱动的类型,其值可以是刚才我们所提及的 bridgehostoverlaymaclannone,也可以是其他网络驱动插件所定义的类型。这里我们使用的是 Bridge Driver ( 当我们不指定网络驱动时,Docker也会默认采用 Bridge Driver作为网络驱动 )

查看网络

通过 docker network ls 或是 docker network list 可以查看 Docker 中已经存在的网络

查看网络详情

我们通过 docker inspect 观察一下此时的容器网络

端口映射

有一个非常常见的需求,就是我们需要在容器外通过网络访问容器中的应用。最简单的一个例子,我们提供了 Web 服务,那么我们就需要提供一种方式访问运行在容器中的 Web 应用。

通过 Docker 端口映射功能,我们可以把容器的端口映射到宿主操作系统的端口上,当我们从外部访问宿主操作系统的端口时,数据请求就会自动发送给与之关联的容器端口

要映射端口,我们可以在创建容器时使用 -p 或者是 --publish选项

sudo docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12

使用端口映射选项的格式是 -p <ip>:<host-port>:<container-port>,其中 ip 是宿主操作系统的监听 ip,可以用来控制监听的网卡,默认为 0.0.0.0,也就是监听所有网卡。host-portcontainer-port 分别表示映射到宿主操作系统的端口和容器的端口,这两者是可以不一样的,我们可以将容器的 80 端口映射到宿主操作系统的 8080 端口,传入 -p 8080:80 即可。

Docker for WindowsDocker for 中,这个端口映射的操作程序会自动帮助我们完成,所以我们不需要做任何额外的事情,就能够直接使用 WindowsmacOS 的端口访问容器端口了。

而当我们使用 Docker Toolbox 时,由于其自动化能力比较差,所以需要我们在 VirtualBox 里单独配置这个操作系统端口到 Linux 端口的映射关系。

容器互联

镜像

镜像基本操作

镜像是由 Docker 进行管理的,所以它们的存储位置和存储方式等我们并不需要过多的关心,我们只需要利用 Docker 所提供的一些接口或命令对它们进行控制即可

要查看当前连接的 docker daemon 中存放和管理了哪些镜像,我们可以使用 docker images这个命令

docker images

可以输出镜像的iD、构建时间(created time)、大小(size)等数据

username: 主要用于识别上传镜像的不同用户,与 GitHub 中的用户空间类似。

repository:主要用于识别进行的内容,形成对镜像的表意描述。

tag:主要用户表示镜像的版本,方便区分进行内容的不同细节

查看镜像的详细信息

docker inspect redis:3.2

拉取镜像

docker pull ubuntu

Docker 会开始从镜像仓库中拉取我们所指定的镜像了,在控制台中,我们可以看到镜像拉取的进度。下载进度会分为几行,其实每一行代表的就是一个镜像层。Docker 首先会拉取镜像所基于的所有镜像层,之后再单独拉取每一个镜像层并组合成这个镜像。当然,如果在本地已经存在相同的镜像层 ( 共享于其他的镜像 ),那么 Docker 就直接略过这个镜像层的拉取而直接采用本地的内容。

当我们没有提供镜像的标签时,Docker 会默认使用 latest 这个标签

删除镜像

虽然 Docker 镜像占用的空间比较小,但日渐冗杂的镜像和凌乱的镜像版本会让管理越来越困难,所以有时候我们需要清理一些无用的镜像,将它们从本地的 Docker Engine 中移除

sudo docker rmi ubuntu:latest

保存和共享镜像

Docker 镜像的本质是多个基于 UnionFS 的镜像层依次挂载的结果,而容器的文件系统则是在以只读方式挂载镜像后增加的一个可读可写的沙盒环境。

基于这样的结构,Docker 中为我们提供了将容器中的这个可读可写的沙盒环境持久化为一个镜像层的方法。更浅显的说,就是我们能够很轻松的在 Docker 里将容器内的修改记录下来,保存为一个新的镜像。

将容器修改的内容保存为镜像的命令是 docker commit,由于镜像的结构很像代码仓库里的修改记录,而记录容器修改的过程又像是在提交代码,所以这里我们更形象的称之为提交容器的更改

sudo docker commit -m "Configured" webapp

我们发现提交容器更新后产生的镜像并没 REPOSITORYTAG 的内容,也就是说,这个新的镜像还没有名字。

共享卷

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github