All Articles

Docker Notes

1. 前言

云原生概念风生云起,最近算是非常火热,而其最基本的依仗就在于能将任何程序运行在轻量级Cell上的容器Docker。Docker如果仅只是使用的话,是一点都不难的,但作为一个所有应用程序运行其上的基石,对其的细节了解是非常有必要的。

本文会记录从最基本的Docker基础,到日常的工作操作,到比较深入的原理细节,都会有所涉及。

版本信息如下:

$ docker version
Client: Docker Engine - Community
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.8
 Git commit:        6247962
 Built:             Sun Feb 10 04:12:39 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.6
  Git commit:       6247962
  Built:            Sun Feb 10 04:13:06 2019
  OS/Arch:          linux/amd64
  Experimental:     false

此外,docker info比较长,就不贴这里了,放在下面的资料部分。简单看了下应该是不带敏感信息的,这部分应该是做了剔除的。

这里顺道加一个非常重要的文档:Docker Tutorials and Labs。这个文档库里存放的是docker官方的tutorial以及一些架构设计等的资料。对于想要深入的读者来说是非常重要的资料。

2. 安装

MAC下的安装比较简单,直接下载Docker官方的桌面版本即可:Docker Desktop for Mac。文档在:Get started with Docker Desktop for Mac

此外,为了能使用(下载)一些官方(软件公司)发布镜像文件,你需要注册一个docker hub账号(其实也就是docker官方账号):docker hub

3. 镜像仓库

Docker的核心功能点之一就是能将部署这个事情代码化。本来运维的工作中充斥着各种不确定性,版本不兼容、软件包不同、Linux内核不同导致的安装问题,等等。而Docker可以使用Dockerfile将一个镜像的制作(以代码形式)固定下来,保证只要是一份Dockerfile,制作出来的镜像是完全一致的。

此外,只要保证Docker的版本一致,docker镜像的运行状态及结果是可预期的。这就保证了一个软件的研发流程中,只要镜像被制作完成了,后续就可以使用这个镜像文件进行分发了,不再需要每个部署环境都从Dockerfile从头开始制作一份本地的镜像。

而分发这个过程就需要镜像仓库的介入:本地制作镜像 => 上传镜像到仓库 => 部署服务器拉取镜像 => 部署服务器本地运行镜像。

Docker官方有一个开放的镜像仓库,一般知名的第三方软件提供者都会将自己软件的镜像文件发布到这个开放的仓库中,方便第三方使用者下载。当然,私有仓库对于大部分软件公司来说都是必须的。

3.1 创建镜像仓库

制作一个私有的镜像仓库非常简单,直接使用docker命令即可,官方文档在:Docker Registry

运行:

docker run -d -p 5000:5000 --name registry registry:2

这样就在localhost:5000运行了一个镜像仓库。

3.2 login

从镜像仓库获得镜像以及上传镜像都需要有对应的身份认证。使用:

$ docker login --help

Usage:	docker login [OPTIONS] [SERVER]

Log in to a Docker registry

Options:
  -p, --password string   Password
      --password-stdin    Take the password from stdin
  -u, --username string   Username

即便是从官方镜像仓库获取镜像也是需要认证的,所以一开始需要注册一个账号。如果使用的是Docker Desktop的话,可以从UI界面上进行登录。

如果登录的是私有仓库,[SERVER]这块需要输入私有仓库地址。

3.3 push & pull

如果是从私有仓库上进行对应的拉取和推送镜像,需要在镜像名字之前补完仓库地址:

docker push localhost:5000/imagename:major.minor.patch
docker pull localhost:5000/imagename:major.minor.patch

3.4 list tags

docker的命令行工具没有提供查询一个镜像所有tags的命令(ridiculous),可以使用以下方法来查询:

# require tool jq
brew update --verbose
brew install jq --verbose

curl -s https://registry.hub.docker.com/v2/repositories/${repo_name}/tags/?page_size=1024 | jq .
# sample repo_name: elastic/filebeat

4. 日常使用

4.1 docker images

显示所有镜像(本地)

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.10               b977ae81df17        2 weeks ago         73.9MB

4.2 docker rmi

删除镜像,可以同时删除多个,给的名字可以是镜像名也可以是镜像id

$ docker rmi imagename imageid ...

4.3 docker ps -a

可以显示所有的容器(本地),包括不在运行状态的。-a一般来说是必须的,否则无法将停止状态的镜像也列出来。

$ docker ps -a

4.4 docker rm

删除容器,可以同时删除多个,给的名字可以是容器名也可以是容器id

$ docker rm containername containerid ...

4.5 docker build {#ID_DOCKER_BUILD}

从一个Dockerfile构建一个镜像,一般都会进入到Dockerfile同一层目录进行构建:docker build [OPTIONS] .,这里的.就是Dockerfile的路径。全选项参数可以通过:docker build --help来获得,这里就不贴了。

细节相当多,有需要的可以看下官方文档:docker build

此外,docker build命令与Dockerfile息息相关,部分优化相关内容描述在Dockerfile内,请去此处查看:5. Dockerfile

4.5.1 镜像命名及tag

通过-t在构建的时候对镜像进行命名,并打上标签。一般来说这是必须的,方便后续进行镜像查找和仓库内巨量镜像的维护。

$ docker build -t imagename:major.minor.patch .

4.5.2 指定Dockerfile

通过-f指定镜像文件路径。

$ docker build -f yourrepo/Dockerfile .

4.5.3 指定全局变量

在构建镜像的时候为了对内部的应用程序提供一些配置,常用环境变量的方式进行注入,而在镜像构建的时候,可以使用--build-args XXX=...这样的方式进行操作。

$ docker build --build-arg HTTP_PROXY=http://10.20.30.2:1234 --build-arg FTP_PROXY=http://40.50.60.5:4567 .

如果是裸用Docker的话,这样的操作还算是比较容易理解的,但一般来说大型分布系统肯定还使用了K8S这样的容器管理系统或者还甚至用了类似于Istio这样的服务编排系统,就不需要在制作镜像的时候这么弄了。

4.5.4 指定ulimit

通过--ulimit来指定该镜像文件在运行时候的ulimit。

4.5.5 指定父级cgroup

通过--cgroup-parent来指定该镜像文件在运行时候的资源限制群组。关于cgroup这个话题,后面会专门起一个章节来深入。详见:8.3 资源限制

4.5.6 添加/etc/hosts

通过--add-host=domain.com:10.180.0.1来向/etc/hosts里添加匹配。

4.5.7 构建目标环境

通过--target来指定构建的目标环境:

FROM debian AS build-env
...

FROM alpine AS production-env
...
$ docker build -t mybuildimage --target build-env .

4.5.8 其他

  • --no-cache:在构建镜像的时候不使用cache,在频繁更新某个镜像时很有用,防止cache污染
  • --ssh:SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format: default|[=|[,]])

资源限制相关(会在后面的章节深入)

  • --cpu-period:Limit the CPU CFS (Completely Fair Scheduler) period
  • --cpu-quota:Limit the CPU CFS (Completely Fair Scheduler) quota
  • --cpu-shares:CPU shares (relative weight)
  • --cpuset-cpus:CPUs in which to allow execution (0-3, 0,1)
  • --cpuset-mems:MEMs in which to allow execution (0-3, 0,1)
  • --memory:Memory limit
  • --memory-swap:Swap limit equal to memory plus swap: ‘-1’ to enable unlimited swap

详见:8.3 资源限制

4.6 docker run

将镜像运行成容器的命令,虽然是一个很简单的命令,但细节相当多,用起来其实还蛮麻烦的。全选项参数可以通过:docker run --help来获得,这里就不贴了。中文的帖子也有,可以看了参考:Docker run 命令参数及使用

细节相当多,有需要的可以看下官方文档:docker run

命令的基本使用格式:

$ docker run [OPTIONS] IMAGE[:TAG\|@DIGEST] [COMMAND] [ARG...]

4.6.1 后台进程

通过-d来让docker run命令运行起来的容器转为后台常驻进程。

$ docker run -d -p 80:80 my_image service nginx start

4.6.2 指定名称

通过--name来让docker run命名运行起来的容器,后续可以使用这个名字来访问这个启动的容器,不再需要docker ps -a查找这个容器的UUID。在使用同一个镜像启动多个容器的时候特别有用。

$ docker run --name myconname -d ubuntu:18.10

4.6.3 网络设置

网络这块相关的配置内容也非常多,细节的内容建议直接阅读官方的文档:docker run > Network settings

--network=...用来控制网络模式:

  • none:No networking in the container.
  • bridge (default):Connect the container to the bridge via veth interfaces.
  • host:Use the host’s network stack inside the container.
  • container:<name\|id>:Use the network stack of another container, specified via its name or id.
  • NETWORK:Connects the container to a user created network (using docker network create command)

4.6.4 添加/etc/hosts

参数同docker build命令,但很奇怪的是docker官方给出的例子这里倒是没有=

$ docker run -it --add-host domain.com:10.180.0.1 ubuntu cat /etc/hosts

4.6.5 重启策略

通过--restart可以让docker run启动的容器在退出之后按策略重启。官方文档在:docker run > Restart policies (—restart)

  • no:Do not automatically restart the container when it exits. This is the default.
  • on-failure[:max-retries]:Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts.
  • always:Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart the container indefinitely. The container will also always start on daemon startup, regardless of the current state of the container.
  • unless-stopped:Always restart the container regardless of the exit status, including on daemon startup, except if the container was put into a stopped state before the Docker daemon was stopped.

该选项与--rm是冲突的。

4.6.6 退出代码 Exit Status

接上面的重试策略,on-failure会检查退出代码。官方文档在:docker run > Exit Status

4.6.7 退出清理

通过--rm可以让docker run启动的容器在退出之后删除所有的留存信息。如果没有加这个参数的话,在容器退出、停止之后使用docker ps -a可以找到刚才启动的容器。这对于测试来说是很麻烦的,每次停止之后还要使用docker rm命令删除。这时候就可以通过--rm来命令容器退出后自动清理:

# not cleanup
$ docker run -i -t ubuntu:18.10 ps afx
$ docker ps -a

# cleanup
$ docker run -i -t --rm ubuntu:18.10 ps afx
$ docker ps -a

4.6.8 安全选项

参见官方文档:docker run > Security configuration

4.6.9 权限与Linux Capabilities

参见官方文档:docker run > Runtime privilege and Linux capabilities。后续详见:8.3 资源限制

4.6.10 日志输出

通过--log-driver容器可以指定和docker daemon不同的日志输出设备。参见官方文档:Logging drivers (—log-driver)

4.6.11 覆盖Dockerfile设置

主要是以下几项:

  • CMD (Default Command or Options)
  • ENTRYPOINT (Default Command to Execute at Runtime)
  • EXPOSE (Incoming Ports)
  • ENV (Environment Variables)
  • HEALTHCHECK
  • VOLUME (Shared Filesystems)
  • USER
  • WORKDIR

细节参见官方文档:Overriding Dockerfile image defaults

4.6.12 其他

命令测试

使用如下命令可以启动容器之后执行命令进行测试,并在退出之后自动清理刚才生成的容器。

$ docker run --rm -i -t $image_name:$version $command

隔离与共享

  • —pid
  • —uts
  • —ipc

容器之间拥有自己的PID命名空间,并相互隔离。通过--pid=...可以让容器之间共享进程。具体的例子可以直接阅读官方的文档:PID settings (—pid)

简单的例子可以尝试(看下输出的内容有什么不同):

# without pid
$ docker run -i -t --rm ubuntu:18.10 ps afx
...

# with pid
$ docker run -i -t --rm --pid=host ubuntu:18.10 ps afx
...

--uts--ipc也是类似的,可以参考官方文档里的:UTS settings (—uts)IPC settings (—ipc)。中文资料可以看:Docker run参考(5) – UTS(–uts)和IPC (–ipc)设置

资源限制相关(会在后面的章节深入)

参见官方文档:docker run > Runtime constraints on resources。后续详见:8.3 资源限制

4.7 docker attach

4.7.1 attach

将本地的输入输出及错误流附到一个运行中的容器上。简单来说可以理解为:进入到容器的命令行中。

如果需要在一个容器启动之后连上去的话,需要容器在启动(docker run)的时候指定:-i -t参数:

$ docker run -i -t -d --rm ubuntu:18.10

这样就可以在之后使用docker attach命令附到该容器上并打开命令行了:

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS
80172e3d5574        ubuntu:18.10        "/bin/bash"         38 seconds ago      Up 37 seconds

$ docker attach 80172e3d5574
root@80172e3d5574:/#

4.7.2 detach

新手常犯的一个错误就是使用exit命令退出附上的shell,结果就是非但退出了shell还把容器本身给stop了。正确的做法是在shell里ctrl+p然后ctrl+q,这里切记是先按p的组合键,然后按q的组合键,而不是同时ctrl+p+q

$ docker attach f269e680ae14
root@f269e680ae14:/# 
# ctrl+p & ctrl+q
root@f269e680ae14:/# read escape sequence
$ 

资料:

4.8 docker exec

在一个已经处于运行中状态的容器里执行一条命令。

一般来说,如果是有连续工作的话,还是会使用docker attach附上去之后再操作。但如果仅只是一两句命令,或自动化的脚本,就需要使用到docker exec了。这里需要注意,exec只能用在running的容器上,如果没有容器且需要执行命令测试的话,也可以使用docker run命令,直接附带上需要执行的命令即可。

也有有趣的用法:

$ docker exec -it e8c541f9fb33 /bin/bash

这条语句的效果和docker attach是一致的。

在某些不能以/bin/bash作为命令启动的镜像(默认入口已经在Dockerfile里指定了,这种情况还蛮多的,稍微复杂点的镜像一般都会把入口设置掉),比如说Elasticsearch、nginx,等等。在这种情况下,如果需要attach到这个运行中的镜像,就必须使用exec了:

$ docker exec -it e8c541f9fb33 /bin/sh

此外,以Alpine镜像为基础的镜像,默认是没有/bin/bash的,这种时候就需要换成/bin/sh

4.9 tips

  • 做开发的时候可以把镜像构建多包几层,一层层做成镜像存在本地。后面的build可以直接from前面的,节约docker build失败之后的重新测试时间。否则中间一个步骤出错,很多耗时很长的源码编译工作就要从头来一次,太伤
  • node.js docker里的npm安装记得要加上--unsafe-perm,具体可以看:grpc/grpc-node#604

5. Dockerfile {#ID_DOCKERFILE}

Dockerfile是用来进行镜像构建的文本文件,也是docker能将构建整个过程转化为文本固定下来的关键,可以说是docker能如此风靡的核心功能点也不为过。

官方的文档:

下面行文不会过于注重Dockerfile如何编写的语法,因为这东西你看看手册和几个例子也就会了。主要还是关注在几个比较麻烦的概念上。

几点零碎的:

  • Dockerfile里类似COPYEXPOSE之类的,都被称为指令(instruction),这个单词可以了解下
  • .dockerignore file
  • 因为在build命令执行之前,当前指定PATH的context会被发送给docker daemon,因此文件夹越小执行越效率

5.1 Dockerfile指令

5.1.1 RUN {#ID_DOCKERFILE_RUN}

官方文档:RUN

运行指令,有两种语法模式:

  • RUN (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
  • RUN [“executable”, “param1”, “param2”] (exec form)

这里需要注意exec模式并不是在shell里运行的:

Note: Unlike the shell form, the exec form does not invoke a command shell. This means that normal shell processing does not happen. For example, RUN [ “echo”, “$HOME” ] will not do variable substitution on $HOME. If you want shell processing then either use the shell form or execute a shell directly, for example: RUN [ “sh”, “-c”, “echo $HOME” ]. When using the exec form and executing a shell directly, as in the case for the shell form, it is the shell that is doing the environment variable expansion, not docker.

建议是使用exec模式,细节可以看下面一个小节。

5.1.2 CMD & ENTRYPOINT

官方文档:

这两个东西可以放在一起说,因为他们做的事情基本上是类似的。两者都是在容器启动之后执行一个命令,CMD提供的是在镜像启动后会默认执行的一个命令,而ENTRYPOINT则是在镜像启动后提供一个程序入口。

区别在于:

  • CMD:提供的默认执行命令可以在docker run $imagename $command后面接命令进行覆盖,这样操作的话写在Dockerfile CMD里的命令就不会执行了,被替代了;CMD在一个Dockerfile里只能有一个生效,如果编写了多个则只有最后的那个会被执行
  • ENTRYPOINT:提供的是镜像的功能入口,不会被docker run $imagename $command中的命令替换掉,除非指定--entrypoint=...进行替换;当然ENTRYPOINT也只允许有一个

一般来说通常的做法是:ENTRYPOINT提供容器运行之后程序入口,而CMD则提供供给给程序入口的参数,方便后续在docker run的时候进行替换。这两者以这样的方式进行协同工作。

官方文档里也有这部分内容:Understand how CMD and ENTRYPOINT interact

  • Dockerfile should specify at least one of CMD or ENTRYPOINT commands.
  • ENTRYPOINT should be defined when using the container as an executable.
  • CMD should be used as a way of defining default arguments for an ENTRYPOINT command or for executing an ad-hoc command in a container.
  • CMD will be overridden when running the container with alternative arguments.

这两个指令和5.2 RUN一样,都有多种模式可以选择,一般来说最常使用的是直接写命令的shell模式以及使用JSON格式编写的exec模式。最佳实践是:在所有的情况下都使用exec模式。原因如下:

  • shell模式的PID 1进程是/bin/sh,后续的信号不能很好传递到真正执行的命令上
  • shell模式依赖/bin/sh,但某些微型镜像不一定有

Best practices for writing Dockerfiles > ENTRYPOINT

Configure app as PID 1

This script uses the exec Bash command so that the final running application becomes the container’s PID 1. This allows the application to receive any Unix signals sent to the container. For more, see the ENTRYPOINT reference.

CMD指令还多一种模式:CMD ["param1","param2"] (as default parameters to ENTRYPOINT),这也是刚才提到的参数提供者角色的做法。

e.g

FROM ubuntu:trusty
ENTRYPOINT ["/bin/ping","-c","3"]
CMD ["localhost"]

$ docker run ping
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.025 ms
...

$ docker run ping docker.io
PING docker.io (162.242.195.84) 56(84) bytes of data.
64 bytes from 162.242.195.84: icmp_seq=1 ttl=61 time=76.7 ms
...

资料:

有时间的话,官方的ENTRYPOINT文档可以通读一下:Dockerfile > ENTRYPOINT,里面信息量不小。

5.1.3 VOLUME

官方文档:VOLUME

卷加载相关在docker里是一个比较麻烦的概念,这里不作展开,只列出文档里提到的注意点:

  • Changing the volume from within the Dockerfile: If any build steps change the data within the volume after it has been declared, those changes will be discarded.
  • The host directory is declared at container run-time: The host directory (the mountpoint) is, by its nature, host-dependent. This is to preserve image portability, since a given host directory can’t be guaranteed to be available on all hosts. For this reason, you can’t mount a host directory from within the Dockerfile. The VOLUME instruction does not support specifying a host-dir parameter. You must specify the mountpoint when you create or run the container.

更多的深入理解可以查看:9.2 存储

5.1.4 ARG

官方文档:ARG

除了用户定义的变量之外,docker还有一部分预定义的变量,可以通过docker build --build-arg name=value来加入:Predefined ARGs

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

5.1.5 HEALTHCHECK

官方文档:HEALTHCHECK

类似于客户端服务器保持连接的心跳检查的概念,这个指令是用来检查当前的容器其提供的服务是否正常的。

用法:HEALTHCHECK [OPTIONS] CMD command。CMD之前的选项有:

  • —interval=DURATION (default: 30s)
  • —timeout=DURATION (default: 30s)
  • —start-period=DURATION (default: 0s)
  • —retries=N (default: 3)

范例,每5分钟检查一次WEB服务器是否正常工作(3秒内能响应请求):

HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

退出代码:

  • 0: success - the container is healthy and ready for use
  • 1: unhealthy - the container is not working correctly
  • 2: reserved - do not use this exit code

同样细节比较多,可以仔细阅读下官方文档:Dockerfile > HEALTHCHECK

资料:

5.1.6 其他指令

有相当多的指令在本文中并没有展开,可以查看其官方文档:

5.2 最佳实践

5.2.1 缩小镜像构建的Context

官方文档:Understand build context

简单理解就是保证运行docker build命令的根工作文件夹里的内容尽量少,最好只保有构建镜像必须的文件,能显著加速镜像构建的速度(减少Context传送需要花费的时间)。

举个例子:

FROM alpine:3.9.2
ENTRYPOINT ["/bin/sh"]
$ mkdir ~/Downloads/docker && cd ~/Downloads/docker
$ docker build -f Dockerfile -t empty_img:0.0.1 .

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM alpine:3.9.2
 ---> 5cb3aa00f899
Step 2/2 : ENTRYPOINT ["/bin/sh"]
 ---> Running in 6190582d6894
Removing intermediate container 6190582d6894
 ---> bd0e931e4706
Successfully built bd0e931e4706
Successfully tagged empty_img:0.0.1

然后拷贝点垃圾文件到~/Downloads/docker,再执行一次:

$ docker build -f Dockerfile --no-cache -t dummy_img:0.0.1 .

Sending build context to Docker daemon  630.1MB
Step 1/2 : FROM alpine:3.9.2
 ---> 5cb3aa00f899
Step 2/2 : ENTRYPOINT ["/bin/sh"]
 ---> Running in 0b6ebe8953ca
Removing intermediate container 0b6ebe8953ca
 ---> 681e135d49b6
Successfully built 681e135d49b6
Successfully tagged dummy_img:0.0.1

Sending build context to Docker daemon 630.1MB这一步花了很长时间(因为我拷贝过去的是630MB很零碎的小文件)。如果Context再大点,到百千GB级别的话,那影响更大。

对于构建出来的镜像大小是没有任何影响的:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
dummy_img           0.0.1               681e135d49b6        40 seconds ago      5.53MB
empty_img           0.0.1               bd0e931e4706        8 minutes ago       5.53MB
alpine              3.9.2               5cb3aa00f899        3 weeks ago         5.53MB

5.2.2 使用多阶段构建

官方文档:Use multi-stage builds

一般来说总是希望镜像的体积越小越好,有利于传输也有利于减小容器的资源占用。而在镜像构建的时候,总会有很多中间产物。以go语言来举例,go语言最后编译产生的可执行文件是自包含的,对于外部的类库等都是不作要求的,而在go源码构建成可执行文件的过程中,则会有很多SDK等的环境需求。这就对构建生产两个环境做了不同的定义及隔离要求。而多阶段构建这个功能,就是为了这种需求而服务的。

简单来说就是在同一个Dockerfile里有多个``FROM指令,每个FROM指令可以使用as ...这样的语法进行命名,这样的一行语句就定义了一个阶段。后面的阶段可以随意利用之前阶段里产生任何资源。通常的用法就是定义两个FROM即两个阶段,构建阶段和生产阶段。将所有的环境设置等都定义在构建阶段,然后将构建阶段编译产生的二进制文件拷贝到生产环境,这样生产环境就能够做到最小化了。

详细的例子可以直接查看官方文档,里面有一份非常详细的范例,正好就是按go制作的范例。

5.2.3 构建的缓存

官方文档:Leverage build cache

这部分建议通读后理解,因为在docker build的时候,只要不添加--no-cache选项的话,构建就会检查并使用缓存。除非在工作中每次都放弃使用cache(在某些情况下会大大增加构建的时间消耗),否则对于缓存的命中还是有理解的必要。

5.2.4 镜像层的创建

Only the instructions RUN, COPY, ADD create layers. Other instructions create temporary intermediate images, and do not increase the size of the build.

因此对于RUNCOPY以及ADD指令的使用需要非常小心,特别是RUN指令,尽量使用&&将其串起来。

6. Image

镜像文件的很多细节在之前的4.5 docker build以及5. Dockerfile都有提到了,所以讲到这里其实已经没什么很有价值的内容可以讲了。关于Image,有一篇非常不错的文章,只要过了一遍基本上Image本身就没什么神秘的了,可以看下:What’s in a Docker image?

一些额外的资料:

7. Docker Machine

官方文档:Docker Machine Overview

Docker machine是用来辅助Ops对大批量机器进行docker部署时使用的工具。现如今规模化使用容器一般都会使用类似K8S这样的工具,所以这东西了解下就好。

资料:

8. 深入

上面的文章基本上把docker的命令以及一些日常使用的细节都过了一遍,也包含了类似镜像体积等优化内容。后面就会进入一些docker比较底层的东西了,类似于docker的网络模式、docker的磁盘模式等。

8.1 网络

官方文档:docker network overview

docker的网络子系统是可插拔式设计,其中可选的有:

  • bridge:默认的网络驱动。如果不指定一个驱动的话,默认就是bridge模式。bridge模式通常应用在独立运行的容器相互之间需要通讯的应用场景
  • host:移除独立容器和Docker主机之间的网络隔离,并直接使用主机的网络。host模式仅针对高于Docker 17.06版本的swarm services可用
  • overlay:Overlay模式将多个Docker daemon连接起来,并启用swarm service来互通。你也可以使用overlay模式促进swarm service和独立容器之间的沟通,或是两个分别归属于不同Docker daemon的容器。这个策略简化了容器之间的互通,不再依赖于OS级别的路由
  • macvlan:Macvlan模式允许将一个MAC地址交付给一个容器,使得这个容器在你的网络中以一个硬件设备的身份出现。Docker daemon通过MAC地址将通讯路由到容器。macvlan模式通常用来处理需要直接连接到物理网络的遗留应用,而不需要路经Docker主机的网络栈
  • none:将某个容器禁用网络。通常与自定义的网络驱动结合使用。none驱动对swarm service不可用
  • Third-party network plugins:第三方网络插件,可自由安装使用。可以通过Docker Hub来安装,或通过第三方vendor安装

选择建议:

  • User-defined bridge networks:当相同Docker主机内的多个容器需要相互通讯的时候,这是最优解
  • Host networks:当容器的网络不应该与Docker主机隔离,且容器的其他方面应该与Docker主机隔离的情况下,这是最优解
  • Overlay networks:当需要运行在不同的Docker主机上的容器相互之间进行通讯,或当多个容器需要使用swarm service进行协同工作的时候,这是最优解
  • Macvlan networks:当从VM环境迁移到Docker环境或需要让容器看起来像物理主机的时候(每个容器都拥有一个唯一的MAC地址),这是最优解
  • Third-party network plugins:允许你将Docker整合到特殊的网络栈中

通篇过一下之后会发现,基本上常用的场景,只要有bridge驱动就够了。后面会主要看下bridge驱动,其他的可以查看官方文档。

此外,有一篇比较老的文章,用中文举了点例子进行网络模式的说明,可以一读:Docker网络模式

8.1.1 bridge

如果在创建容器的时候不指定网络设置的话,容器会使用默认的bridge驱动。而如果用户进行自定义bridge设置的话,使用的网络驱动就是稍微有点不同:User-defined bridge networks。这里还是有不少区别的:Differences between user-defined bridges and the default bridge

  • User-defined bridges provide better isolation and interoperability between containerized applications.
  • User-defined bridges provide automatic DNS resolution between containers.
  • Containers can be attached and detached from user-defined networks on the fly.
  • Each user-defined network creates a configurable bridge.
  • Linked containers on the default bridge network share environment variables.

自定义bridge驱动使用:

$ docker network create my-net
$ docker network rm my-net

$ docker create --name my-nginx \
  --network my-net \
  --publish 8080:80 \
  nginx:latest

$ docker network connect my-net my-nginx
$ docker network disconnect my-net my-nginx

官方文档里写清楚了,默认的bridge驱动已经属于遗留功能,不建议在生产环境上使用。

The default bridge network is considered a legacy detail of Docker and is not recommended for production use.

见文档:Use the default bridge network

8.1.2 其他

后面几种总的来说应用面都不大,在结合K8S使用的情况下(应该是大部分应用场景),只需要K8S即可。这里都可以略过。

8.1.3 实践经验

如果只是照范例抄启动命令的话,一般来说总会有几个知识点理解不是很透彻,这个章节就仔细看下这些知识点。

容器IP

每个容器在启动之后,都有其在设定的--network=xxx下的固定IP地址(e.g 172.17.0.35)。因此不要在让容器上运行的应用程序监听Listener=127.0.0.1:xxx这样的地址,否则其他服务将无法找到该应用。这和在本地运行应用程序是不一样的。

在本地开发和运行应用的时候,实际上所有的应用程序是部署在同一台物理机上的,因此回环地址可以正确找到应用。但使用容器的时候一般一个容器内只会有一个应用程序,会启动不同的几个容器让他们相互之间通讯,这时候回环地址就不会起效果了。

这是一个理解上的盲点,需要注意。

容器名

上面举的例子中,如果多个容器之间的应用程序需要相互访问,最好的监听地址配置方法是使用容器名作为监听的host。e.g Listener=node1:xxx。容器只要启动在同一个network下,相互之间是可以通过容器名查找到的。

expose vs publish

两者之间的区别很简单:

  • expose仅将端口暴露给同一个network下的其他容器
  • publish会将端口暴露给其他不同的网络或docker host

如果你的应用全部运行在同一个network下,那么就用expose就够了,如果有多个docker network,相互之间的访问需要publish port。

参见:

8.2 存储 {#ID_PRINCIPLE_STORAGE}

官方文档:Manage data in Docker

任何在容器内创建的文件都会存储在容器的可写层内。这意味着:

  • 当容器消失的时候这些数据会丢失,且当其他进程需要共享这些数据的时候很难获取到这些数据
  • 容器的可写层是和物理主机紧密关联的,很难轻易移动到其他机器上
  • 向容器的可写层写入数据要求一个存储驱动来管理文件系统。存储驱动是一个洋葱式的文件系统,使用Linux内核。这额外的抽象会降低性能,和直接使用data volumn相比,后者直接向主机的文件系统写入数据

Docker提供了选项来让容器在物理主机上存储文件,这样才能保证容器在停止之后文件仍旧存在:

  • volumes
  • bind mounts
  • tmpfs mount(仅限Linux)

8.2.1 几种类型的mount

  • Volumes:会将数据写入到主机磁盘上的Docker指定地点(/var/lib/docker/volumes/ on Linux),且这些文件是由Docker进程进行管理的,其他任何进程都不应该直接修改这些文件。Volumes是在Docker内存储数据的最佳选择
  • Bind mounts:可将数据写入到主机磁盘上的任何地点,甚至可以是重要的系统文件,或文件夹。非Docker进程及Docker容器都可以在任何时候修改这些文件
  • tmpfs mounts:仅将数据保存在内存中,并永远不会存储到磁盘上

更进一步的各种mount类型细节可以查阅官方文档:More details about mount types。这里做一下整理:

Volumns

  • 一个volume可以被多个容器共享
  • volume在容器都停止之后不会被删除,除非手动操作:docker volume prune
  • volume是可命名的,如果没有被命名,则会被自动附加一个唯一串作为名字
  • volume还可以加载volume drivers,为其提供更丰富的存储功能

Bind mounts

  • 使用bind mounts的时候,一个主机上的文件或文件夹被加载到容器中
  • 文件或文件夹需要完整路径
  • 文件或文件夹不需要已经存在
  • docker cli命令无法直接操作bind mounts资料
  • 官方更推荐命名化的volumes,而不是使用bind mounts

官方还给了最佳使用范例:

几点tips:

  • 如果把一个空的volume(使用docker create创建出来的)绑定到运行中容器的某个已经存在的文件夹上,会导致已经存在文件夹内的资料被拷贝到volume内
  • 如果把一个bind mount或已经有内容的volume绑定到某个运行中容器的某个已经存在的文件夹上,则容器内重合文件夹内的资料会被屏蔽(不会被删除),volume内的资料则可访问

在我看来:

  • volumes应该是最先选择,如果没有特殊需求的话,类似于web服务器这种无状态可以很快横向扩展的就很适合使用volumes
  • bind mounts可以用在其他进程有直接访问文件需求的场景。volumes是docker进程自组织的,对外就是一个block文件,无法看到细节,其实不太友好,在docker之外的进程也需要对文件进行访问的时候就很不方便了,这种场景就需要bind mounts

8.2.2 日常使用命令

$ docker volume create my-vol
$ docker volume ls
$ docker volume inspect my-vol
$ docker volume rm my-vol # delete volume
$ docker volume prune # delete all

8.2.3 Volumes

官方文档:Use volumes

优势:

  • Volumes are easier to back up or migrate than bind mounts.
  • You can manage volumes using Docker CLI commands or the Docker API.
  • Volumes work on both Linux and Windows containers.
  • Volumes can be more safely shared among multiple containers.
  • Volume drivers let you store volumes on remote hosts or cloud providers, to encrypt the contents of volumes, or to add other functionality.
  • New volumes can have their content pre-populated by a container.

可以使用--volume--mount来指定容器运行时需要mount的卷,使用上有点些微的不同:

$ docker run --volume \
    $volume_name:$path_mounted_in_container:$option1,$option2,...

$ docker run --mount \
    type=volume,source|src=$volume_name,destination|dst|target=$path_mounted_in_container,readonly,volume-opt=$key1=$val1,volume-opt=$key2=$val2,...

几点不常用但可能用得到的点:

8.2.4 Bind mounts

官方文档:Use bind mounts

可以使用--volume--mount来指定容器运行时需要mount的卷,使用上有点些微的不同:

$ docker run --volume \
    $path_on_host_tobe_bound:$path_mounted_in_container:$option1,$option2,...

$ docker run --mount \
    type=bind,source|src=$path_on_host_tobe_bound,destination|dst|target=$path_mounted_in_container,readonly,bind-propagation=rprivate|private|rshared|shared|rslave|slave

对bind mounts来说--volume--mount在使用上是有区别的:

  • --volume:指定的主机位置不存在的话,会主动创建出来(总是创建成文件夹)
  • --mount:指定的主机位置不存在的话,不会主动创建,而是报错

举个简单的例子:

$ cd /Users/XXX && mkdir ./docker && cd ./docker && touch ./dummy.txt && echo dummy > ./dummy.txt
$ docker run -it -d --rm -v /Users/XXX/Downloads/docker/:/dirinc --name bind_mount_test ubuntu:18.10
$ docker attach 8d11c4aad023

root@8d11c4aad023:/# ll /dirinc
...
-rw-r--r-- 1 root root    8 Apr  3 02:23 dummy.txt
root@8d11c4aad023:/# echo changed > /dirinc/dummy.txt

$ cat /Users/XXX/docker/dummy.txt
changed

几点不常用但可能用得到的点:

8.2.5 Storage drivers

官方文档:Docker storage drivers

这部分里会讲到的storage drivers和之前的volumes是不同的东西,所谓的storage drivers是指docker从镜像生成容器之后,在容器的最上面创建出来的那一层可写入层里,使用的文件处理策略(驱动)。

这部分不会太过展开,主要是因为:

  • 会在容器的可写层里大量产生数据的应用场景一般不会存在,如果存在那一般也是有问题的
  • 在真的需要容器对存储设备进行大量写入操作的情况,需要考虑是否使用volumes,而不是直接在容器的可写入层里进行写操作(两者在性能上也有差距)

因此实际上这块的实用性并不高。后面主要以认识storage drivers为主。

Docker支持的storage drivers:

  • overlay2:首选的驱动,对现行的所有Linux发行版本可用,且无需额外的配置
  • aufs:对 Docker 18.06 及更老旧的版本来说是首选驱动,或 Ubuntu 14.04 kernel 3.13 不支持 overlay2 的环境
  • devicemapper:受到支持,但在生产环境需要direct-lvm,是因为loopback-lvm虽然无需配置,但性能不是很好
  • The btrfs and zfs storage drivers are used if they are the backing filesystem (the filesystem of the host on which Docker is installed). These filesystems allow for advanced options, such as creating “snapshots”, but require more maintenance and setup. Each of these relies on the backing filesystem being configured correctly.
  • The vfs storage driver is intended for testing purposes, and for situations where no copy-on-write filesystem can be used. Performance of this storage driver is poor, and is not generally recommended for production use.

要查看各Linux发行版本支持的驱动,可以查看:Supported storage drivers per Linux distribution

要查看各文件系统支持的驱动,可以查看:Supported backing filesystems

后面还有很多各种驱动的细节,这里就不展开了,给出一些资料:

8.3 资源限制 {#ID_PRINCIPLE_RESOURCE}

这块的话题有点大,本质上来说,Docker的资源限制利用的还是Linux本身的机制。即便撇开Docker,这些知识点也是值得一看的,但鉴于主题,这里就不多展开了。后续这个章节的主要目标是将Docker的一些限制手段以及后面的Linux原理相关的思路整理出来,并附上资料,不会过于深入。

实际上在真实场景使用的时候一般也会使用类似K8S这样的系统来进行集群管理,不太会直接在Docker这一层工具上对资源限制这块过多设置。

8.3.1 Linux

Linux中的namespace、cgroup、capabilities等核心概念,可以看下面几篇:

8.3.2 Docker

Docker官方对于资源限制也有不小的篇幅进行解说,在使用上有需要的时候可以查看:

8.3.3 其他

其他资料:

8.4 监控 & Metrics

8.4.1 Official Guide

Docker官方的监控,这块和上面的内容类似也比较鸡肋,一般来说不会直接裸用Docker,如果有K8S之类的,那监控也不会从Docker来了。所以这里就给点资料即可,有需要的可以深入下官方文档:

8.4.2 cAdvisor

在以容器为主的系统架构中,除了主机监控之外还需要监控容器,因为应用程序都是运行在容器中的。而且也有可能某些主机运行了多个容器,那么这些容器(应用程序)占用了多少资源就需要进行监控了。Google出品的cAdvisor基本上是最优解:

注意cAdvisor不仅仅只服务于Docker,也可以用在其他容器运行时上。

8.5 安全

官方文档:Docker security

老话重提,里面的知识点仍旧是上面提到过的Linux cgroup、capabilities等几个知识点。

此外,用户管理也是一个需要关注的点,Docker默认的用户是root,在某些情况下使用者有必要进行更换。参见:Docker creates files as root in mounted volume [duplicate]

8.7 其他

9. OS镜像选择

9.1 Alpine

技术过硬的话,Alpine应该说是制作生产环境产品镜像的首选了,毕竟体积摆在那边,只有几兆的基本镜像可真的没几个好选的。

一些资料:

DNS issue

DNS问题算是Alpine被诟病得比较多的一点,可以看一个issue:DNS Issue #255。官方在github的文档上也给了说明,可以看下:caveats.md >> DNS,当然具体是否有这个情况以及如何解决,就要看实践了。

10. 实践

列出所有本机镜像

$ docker images

列出本机所有容器

$ docker ps -a

列出本机磁盘使用情况

$ docker system df -v

本机空间清理

$ docker system prune

几个概念:

  • 已使用镜像(used image): 指所有已被容器(包括已停止的)关联的镜像。即 docker ps -a 看到的所有容器使用的镜像
  • 未引用镜像(unreferenced image):没有被分配或使用在容器中的镜像,但它有 Tag 信息
  • 悬空镜像(dangling image):未配置任何 Tag (也就无法被引用)的镜像,所以悬空。这通常是由于镜像 build 的时候没有指定 -t 参数配置 Tag 导致的
  • 挂起的卷(dangling Volume):类似的,dangling=true 的 Volume 表示没有被任何容器引用的卷

docker system prune 自动清理说明:

该指令默认会清除所有如下资源:

  • 已停止的容器(container)
  • 未被任何容器所使用的卷(volume)
  • 未被任何容器所关联的网络(network)
  • 所有悬空镜像(image)
  • 该指令默认只会清除悬空镜像,未被使用的镜像不会被删除
  • 添加 -a 或 —all 参数后,可以一并清除所有未使用的镜像和悬空镜像
  • 可以添加 -f 或 —force 参数用以忽略相关告警确认信息
  • 指令结尾处会显示总计清理释放的空间大小

参见:

本机镜像清理

# 删除所有悬空镜像,但不会删除未使用镜像:
$ docker rmi $(docker images -f "dangling=true" -q)

# 删除所有未使用镜像和悬空镜像。
# 【说明】:轮询到还在被使用的镜像时,会有类似"image is being used by xxx container"的告警信息,所以相关镜像不会被删除,忽略即可。
$ docker rmi $(docker images -q)

本机卷清理

如果通过 docker system df 分析,是卷占用了过高空间。则可以根据业务情况,评估相关卷的使用情况。对于未被任何容器调用的卷(-v 结果信息中,“LINKS” 显示为 0),可以使用如下指令手工清理:

# 删除所有未被任何容器关联引用的卷:
$ docker volume rm $(docker volume ls -f "dangling=true" -q)

# 也可以直接使用如下指令,删除所有未被任何容器关联引用的卷(但建议使用上面的方式)
# 【说明】轮询到还在使用的卷时,会有类似"volume is in use"的告警信息,所以相关卷不会被删除,忽略即可。
$ docker volume rm $(docker volume ls -q)

日志驱动及容量设置

见过好几个日志文件膨胀到把主机磁盘吃光的情况,所以这方面还是要小心处理。

官方文档:

在docker run命令中修改日志输出driver,以及限定最大日志空间占用:

$ docker run \
      --log-driver json-file --log-opt max-size=10m \
      alpine echo hello world

更多的可以查看官方文档。

查看网络内容器列表

注意下面的例子中双花括号的转译符需要自行去除:

# 只获取名字
$ docker network inspect -f '\{\{ range $key, $value := .Containers \}\}\{\{ printf "%s\n" $value.Name \}\}\{\{ end \}\}' ${网络名}

# 观察打印出来的结构中的 Containers 部分,可以获得更详细的信息
$ docker network inspect ${网络名}

查看容器所属网络

注意下面的例子中双花括号的转译符需要自行去除:

# 只获取名字
$ docker inspect -f '\{\{ range $key, $value := .NetworkSettings.Networks \}\}\{\{ printf "%s\n" $key \}\}\{\{ end \}\}' ${容器名}

# 观察打印出来的结构中的 Networks 部分,可以获得更详细的信息
$ docker inspect ${容器名}

查看容器运行状态

注意下面的例子中双花括号的转译符需要自行去除:

$ docker stats # 默认一直刷新,类似top命令
$ docker stats --no-stream # 不刷新,只获取一次
$ docker stats --no-stream ${容器名} # 获取指定的容器状态
$ docker stats --format "table \{\{.Name\}\}\t\{\{.CPUPerc\}\}\t\{\{.MemUsage\}\}" # 格式化输出
# JSON格式输出
$ docker stats --no-stream --format \
      "{\"container\":\"\{\{ .Container \}\}\",\"memory\":{\"raw\":\"\{\{ .MemUsage \}\}\",\"percent\":\"\{\{ .MemPerc \}\}\"},\"cpu\":\"\{\{ .CPUPerc \}\}\"}"
  • .Container:根据用户指定的名称显示容器的名称或 ID
  • .Name:容器名称
  • .ID:容器 ID
  • .CPUPerc:CPU 使用率
  • .MemUsage:内存使用量
  • .NetIO:网络 I/O
  • .BlockIO:磁盘 I/O
  • .MemPerc:内存使用率
  • .PIDs:PID 号

参见:

11. Swarm集群解决方案

11.1 Overview

如今在集群解决方案方面执牛耳的无疑是Kubernetes以及Mesh解决方案Istio,但docker自身也具备集群管理和部署的能力。在Docker中这被称为Swarm,具体使用上来说:

  • docker-machine:在远程主机上安装docker engine守护进程,保证远程主机作为docker环境可用,并将远程机器记录在操作者机器上,形成记录,以便后续远程控制
  • docker swarm:系列命令可以用来构建一个swarm集群,其中有一台作为master机,所有的集群操作都必须在master机上执行
  • docker stack:集群部署相关命令,使用compose配置文件,swarm版的docker compose命令
  • docker service:单独服务管理命令,swarm版的docker run命令

相关官方文档:

11.2 Network

Swarm集群使用的网络不再是简单的bridge类型的driver,而是overlay类型的driver。如果仅只是在单一一台物理机上部署docker container,并能够让他们相互感知并进行通讯的话,只需要使用bridge类型的driver network,而这样的网络类型是不能让两台相互物理隔离的服务器上的container相互通讯的。要做到在swarm集群内各容器相互之间能够进行通讯,则必须使用overlay类型的driver的network。

11.3 中文系列博文

有作者做了一个系列的博文,虽然细节上来说并不算特别好,但看看还行了:Docker Swarm 系列教程。最主要是三篇:

11.4 实际操作经验

我这里也做了点实践进行试验:dist-system-practice/experiment/swarm/

下面一步步进行解释。

11.4.1 创建远程docker环境

虚拟机

docker-machine可以使用远程物理host进行docker环境部署和操控,也可以在本地进行虚拟机的制作,并进行控制和部署。虚拟机需要单独下载并安装virtualbox

MAC下安装:

$ brew cask install virtualbox

此外需要下载virtualbox的镜像,并放到对应的文件夹下,下载releases:boot2docker/boot2docker。下载完成后放到:~/.docker/machine/cache

虚拟机的创建,driver类型为virtualbox

$ docker-machine create -d virtualbox --help # 帮助选项查看

$ docker-machine create -d virtualbox \
    --virtualbox-boot2docker-url ~/.docker/machine/cache/boot2docker.iso \
    --virtualbox-hostonly-cidr "192.168.99.1/24" \
    --engine-opt dns=192.168.99.1 \
    host1
Running pre-create checks...
(host1) Boot2Docker URL was explicitly set to "/Users/xxx/.docker/machine/cache/boot2docker.iso" at create time, so Docker Machine cannot upgrade this machine to the latest version.
Creating machine...
(host1) Boot2Docker URL was explicitly set to "/Users/xxx/.docker/machine/cache/boot2docker.iso" at create time, so Docker Machine cannot upgrade this machine to the latest version.
(host1) Downloading /Users/xxx/.docker/machine/cache/boot2docker.iso from /Users/xxx/.docker/machine/cache/boot2docker.iso...
(host1) Creating VirtualBox VM...
(host1) Creating SSH key...
(host1) Starting the VM...
(host1) Check network to re-create if needed...
(host1) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env host1

$ docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER     ERRORS
host1     -        virtualbox   Running   tcp://192.168.99.100:2376           v18.09.2
host2     -        virtualbox   Running   tcp://192.168.99.101:2376           v18.09.2

远程host

远程物理host创建,driver类型为generic(文档:Generic)。如果要创建某些受官方支持的云服务的远程环境,可以参考:Machine drivers。如果创建的只是普通的远程host实例,则选择driver为generic即可。

$ docker-machine create -d generic --help # 帮助选项查看

$ docker-machine create -d generic \
    --generic-ip-address=x.x.x.x \
    --generic-ssh-port 22 \
    --generic-ssh-key ~/.ssh/id_rsa \
    --generic-ssh-user root \
    vultr
Running pre-create checks...
Creating machine...
(vultr) No SSH key specified. Assuming an existing key at the default location.
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with ubuntu(systemd)...
Installing Docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Error creating machine: Error checking the host: Error checking and/or regenerating the certs: There was an error validating certificates for host "x.x.x.x:2376": tls: DialWithDialer timed out
You can attempt to regenerate them using 'docker-machine regenerate-certs [name]'.
Be advised that this will trigger a Docker daemon restart which might stop running containers.

$ docker-machine regenerate-certs vultr
Regenerate TLS machine certs?  Warning: this is irreversible. (y/n): y
Regenerating TLS certificates
Waiting for SSH to be available...
Detecting the provisioner...
Installing Docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...

$ docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                       SWARM   DOCKER     ERRORS
vultr     -        generic      Running   tcp://x.x.x.x:2376                v18.09.6

11.4.2 抹除远程docker环境

其实主要是在本地控制记录中将对象删除:

$ docker-machine rm vultr
About to remove vultr
WARNING: This action will delete both local reference and remote instance.
Are you sure? (y/n): y
Successfully removed vultr

11.4.3 远程命令执行

当远程实例创建完成之后,在管理者机器上形成记录,并能够远程进行命令的执行。远程命令执行有两种方法:

ssh

使用docker-machine ssh $name "$command"的方式可以执行远程命令,如果使用docker-machine ssh $name则可以直接登录到远程机器环境。

env

ssh方法可以做很多事情,但打命令会比较繁琐,毕竟每条命令都需要带上docker-machine前缀。另一种方法则是使用eval "$(docker-machine env $name)"命令,来将当前会话窗口的执行环境切换到$name机器,即当上述命令执行完毕后,当前会话内的所有命令都是在$name机器上执行的,而不是管理者机器。

如果要退出远程机器的会话,则需要执行:eval "$(docker-machine env -u)"

11.4.4 Swarm集群管理

Swarm集群由一个Manager节点,多个备用Manager节点,以及大量Worker节点组成。官方文档:docker swarm

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
3fff9l59dvn5jvot2s27gf9n2 *   ManagerX            Ready               Active              Leader
4byjmtcm1ag8qffxjwnhwsf4l     WorkerA             Ready               Active
sks1qb0zqlaetmpsqfj5tfx56     WorkerB             Ready               Active

Manager状态正常为Leader,备选为Reachable。Worker节点正常状态为Active

创建Swarm集群
记住,所有的Swarm集群操作都必须要到远程机器上执行,而不是在管理者机器上执行。也就是说必须使用docker-machine ssh ...eval "$(docker-machine env ...)"来操作。

如果要创建一个Swarm集群,需要到被选为Manager的节点上:

$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (lvfnka9mz92699ke3j189dznd) is now a manager.

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

    docker swarm join --token SWMTKN-1-4zc49exg8u9vh9nsasua10xzu2a5qaxahmgblxkejltw6v6mt0-aadqrk196sgi97z4nkn6eagjl 192.168.99.100:2377

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

后面的IP地址为被选为Manager的机器的自机IP。

添加Worker节点

要向一个只有Manager的Swarm集群添加Worker节点:

$ docker swarm join --token SWMTKN-1-4zc49exg8u9vh9nsasua10xzu2a5qaxahmgblxkejltw6v6mt0-aadqrk196sgi97z4nkn6eagjl 192.168.99.100:2377
This node joined a swarm as a worker.

这句命令在刚才Manager创建Swarm集群的时候已经被打印出来了,照做即可。不要忘记该命令必须到被要求加入Swarm集群的Worker节点上执行。

离开集群
如果需要将某个节点剔除出集群,则需要到该节点上执行:

$ docker swarm leave --force
Node left the swarm.

Manager节点必须要带上--forceoption,Worker节点则不需要。

11.4.5 Service

在Swarm集群中,一个容器被称为服务 Service,实际上还是个容器,只不过叫法不一样而已。官方文档:docker service

创建服务

$ docker service create \
    --name memcached \
    --network dist_net \
    --replicas 1 \
    -p 11211:11211 \
    --constraint node.hostname==host1 \
    memcached:1.5.14-alpine \
    -l 0.0.0.0 \
    -p 11211 \
    -m 64
uezlg3xa9ku412l4la8i8tue7
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged

大部分的option和使用docker run的时候没有差别,这里需要注意的是:

  • replicas:在整个集群中,指定的服务需要启动多少个,如果是类似于web server这样的无状态角色,就很容易理解,一般和constraint结合使用,指定部署的节点范围
  • constraint:限制部署的对象节点,范例中node.hostname指定的是被命令的单个节点,该服务只允许部署到这个节点上

停止服务

$ docker service rm memcached
memcached

其他命令
其他服务相关命令可以查看:

$ docker service --help

11.4.6 Stack

类似于docker service就是docker run的Swarm版本,stack可以理解为Swarm版本的compose。一样需要指定一个compose.yaml配置文件,然后开始stack的部署。这里需要注意,一般compose的部分选项在Swarm的stack中是不可用的,当然大部分都是通用的。

启动stack

$ docker stack deploy ./cluster.yaml dist
Creating network dist_net
Creating service dist_memcache_admin
Creating service dist_memcached

停止stack

$ docker stack rm dist
Removing service dist_memcache_admin
Removing service dist_memcached
Removing network dist_net

11.4.7 状态查看命令

查看所有远程主机

该命令需要在管理者机器上执行,不是远程主机,注意。

$ docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER     ERRORS
host1     -        virtualbox   Running   tcp://192.168.99.100:2376           v18.09.2
host2     -        virtualbox   Running   tcp://192.168.99.101:2376           v18.09.2

查看Swarm集群所有节点

需要到Swarm集群Manager节点上执行命令。

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
z70e57nrkfpbcs33j5pyzzaqi *   host1               Ready               Active              Leader              18.09.2
x2f6tereq1btochykl18lrrtv     host2               Ready               Active                                  18.09.2

查看Swarm单个节点细节

需要到对应节点上执行命令。

$ docker node inspect host1 --pretty
ID:                 un1mkdvkh814oehap89wna4zw
Hostname:           host1
Joined at:          2019-06-24 08:04:35.836543206 +0000 utc
Status:
 State:             Ready
 Availability:      Active
 Address:           192.168.99.116
Manager Status:
 Address:           192.168.99.116:2377
 Raft Status:       Reachable
 Leader:            Yes
Platform:
 Operating System:  linux
 Architecture:      x86_64
Resources:
 CPUs:              1
 Memory:            989.4MiB
Plugins:
 Log:               awslogs, fluentd, gcplogs, gelf, journald, json-file, local, logentries, splunk, syslog
 Network:           bridge, host, macvlan, null, overlay
 Volume:            local
Engine Version:     18.09.2

查看Swarm集群所有服务

需要到Swarm集群Manager节点上执行命令。

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                                PORTS
b3ozvdgrgudv        memcache_admin      replicated          1/1                 plopix/docker-memcacheadmin:latest   *:9083->9083/tcp
yujhl39733lg        memcached           replicated          1/1                 memcached:1.5.14-alpine              *:11211->11211/tcp

查看Swarm集群单个服务

需要到Swarm集群Manager节点上执行命令。

$ docker service ps memcached
ID                  NAME                IMAGE                     NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
oe41a6ioom9r        memcached.1         memcached:1.5.14-alpine   host1               Running             Running about a minute ago

$ docker service ps memcache_admin
ID                  NAME                IMAGE                                NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
w9y6zhukra0w        memcache_admin.1    plopix/docker-memcacheadmin:latest   host2               Running             

查看Swarm集群Stack细节

需要到Swarm集群Manager节点上执行命令。

$ docker stack ps dist
ID                  NAME                    IMAGE                                NODE                DESIRED STATE       CURRENT STATE            ERROR                        PORTS
kscj4niatj9n        dist_memcached.1        memcached:1.5.14-alpine              host1               Running             Running 1 second ago
4hcwv6ni3elv        dist_memcache_admin.1   plopix/docker-memcacheadmin:latest   host2               Running             Running 31 seconds ago

查看Swarm单个服务的日志

需要到Swarm集群Manager节点上执行命令。服务日志其实就是容器日志。

$ docker service logs $name

11.5 Issues

从compose.yaml进行docker stack deploy出来的服务总是会遇到:getaddrinfo(): name does not resolve,启动的服务无法解析域名,一直都没有查到问题。

一开始以为是dns问题:

然后根据stackoverflow的讨论进行machine create的options设定,发现--engine-opt dns=8.8.8.8会被docker-machine create忽略:

cat ~/.docker/machine/machines/cache/config.json
    "HostOptions": {
        "Driver": "",
        "Memory": 0,
        "Disk": 0,
        "EngineOptions": {
            "ArbitraryFlags": [
                "dns=192.168.99.1"
            ],
            "Dns": null,
            ...

Dns选项为null,而要求的内容则出现在了EngineOptions > ArbitraryFlags里。

最后抛开DNS不管,直接不使用compose配置文件,也不使用docker stack deploy命令,直接使用docker service create命令,和compose配置文件中一模一样的option却发现启动成功。估计是stack命令内部实现有点问题。

Swarm因为与K8S竞争失败,最近已经很久听不到消息了,官方的资料以及社区的讨论都是非常老的资料,有问题什么都查不到。此外,很简单的一个容器服务,在Swarm集群中以Service启动非常非常慢,好几十秒才把很简单的一个memcached启动起来。实际使用如果也是这种性能的话是完全不能接受的。

总之,Swarm的现状来看,完全不适合使用在生产环境。应用风险非常之高。

12. 其他

12.1 什么是docker host

在很多docker的文档里都提到了docker host这个概念。至于什么是docker host,可以参见链接:Clarify what is the host

I think you pretty much found out the answer by yourself. In that diagram you mention a ‘docker host’ is any machine that is running the Docker daemon. This means that you’re either running Docker natively in your operating system, or a virtual machine that has Docker installed.

Docker machine is basically the second option. When you do a docker-machine create it creates a new virtual machine with Docker already installed. This means that to access any containers running on this machine you’ll have to use the IP address of the virtual machine. Also, to give Docker access to a file you’ll either have to copy it to the VM that’s running Docker, or share a directory between your OS and the VM that’s running Docker.

12.2 com.docker.hyperkit X00% cpu consumption MAC

在MAC上使用docker,如果放一些有真实负载的container,一般都会遇到这个问题(甚至是没什么负载,只是搭建环境都会)。

官方论坛有一个issue:High CPU Utilization of Hyperkit in Mac #1759,到现在还是open状态,且看起来解决遥遥无期。

issue中似乎有用的只有改大max open file选项这条意见,具体操作可以参见:Maximum limits (macOS etc.)

不确定是否一定能起效果,应该是对部分情况有效的。但对我来说貌似是没用,我的ulimit已经是:

$ launchctl limit maxfiles
# maxfiles    65536          65536

资料

Docker官方

Docker命令

镜像相关

其他

docker info {#ID_APP_DOCKER_INFO}

docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 18.09.2
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 9754871865f7fe2f4e74d43e2fc7ccd237edcbce
runc version: 09c8266bf2fcf9519a651b04ae54c967b9ab86ec
init version: fec3683
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.9.125-linuxkit
Operating System: Docker for Mac
OSType: linux
Architecture: x86_64
CPUs: 4
Total Memory: 1.952GiB
Name: linuxkit-025000000001
ID: CNVU:5KZS:A2M7:WY5W:NUEW:KPW3:WXOA:IH2Q:EBAN:LP7C:3EQR:36U4
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
 File Descriptors: 24
 Goroutines: 50
 System Time: 2019-03-27T06:19:11.1044801Z
 EventsListeners: 2
HTTP Proxy: gateway.docker.internal:3128
HTTPS Proxy: gateway.docker.internal:3129
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false
Product License: Community Engine

EOF

Published 2019/1/17

Some tech & personal blog posts