Docker Cookbook
Table of content:
- About
- 关于 Docker
- Docker 集群管理方案
- Docker 周边
- 原理和思考
- 遇到过的问题和使用场景
- Docker 异常重启问题(todo)
- docker kill hang 住(未查明原因)
- docker build 时, FROM 里面的基础镜像是否会被自动更新
- 磁盘占用问题以及清理方式
- 将 docker image 从 server 之间转移(比如无法访问 docker hub 的情况)
- 限制容器的资源使用(todo)
- 不使用 sudo 执行 docker
- Docker cp 是否支持 wildcard ?
- Docker cp 之后的文件权限是什么样的 ?(todo)
- 各种网络模式之间的差异和性能对比 (todo)
- 什么场景下需要禁用 iptables ?(todo)
- run task in background and foreground
- Docker exec stdout, stderr
- 是否可以在 Dockerfile 中起一些持久的进程
- 获得镜像或者容器的详细信息
- SizeRootFs, SizeRw 的区别是什么
- docker exec 里面包含中文报错
- 容器里面涉及到一些敏感信息问题应该怎么处理
- 物理机和远程容器之间的文件共享方案
- Docker NAT iptables 实现
- Docker on GPU
- Reference & Recommendation
- 更新日志
- 关于头图
About
这是一篇会持续更新并且会很长的文章,致力解决的问题:
- Docker 的架构和原理
- 核心概念
- 我在 Docker 使用过程中遇到过的一些问题,使用场景,以及背后的原理
不包括:
- Docker 的安装使用和常见命令介绍
关于 Docker
核心架构
- Docker daemon, 负责容器的创建,运行和监控,还负责镜像的构建和存储。
- Docker 客户端,通过 HTTP 和 Docker Daemon 通信
- 寄存服务,存储和发布镜像。默认的寄存服务是 Docker Hub
和虚拟机的比较
底层技术
cgroup
管理容器使用的资源,例如 CPU 和内存的使用。
namescpace
负责容器之间的隔离,确保系统的其他部分与容器的文件系统,主机名,用户,网络和进程是分开的。
union file system
联合文件系统,有时也称为联合挂载。联合文件系统允许多个文件系统叠加,并表现为一个单一的文件系统。文件系统的文件可以来自多个文件系统。但如果有两个文件的路径完全相同,最后挂载的文件则会覆盖较早前挂载的文件。
Docker 是支持多种不同的联合文件系统实现,比如 AUFS, Overlay,devicemapper 等, 详见 https://docs.docker.com/storage/storagedriver/select-storage-driver/
可以通过 docker info
里面的 Storage Driver 来查看。
网络模式
网桥 bridge (todo)
Ref:
- Docker源码分析(七):Docker Container网络 (上) https://www.infoq.cn/article/docker-source-code-analysis-part7
主机模式
如果容器以 –net=host 参数启动, 那么它便会共享主机的网络命名空间,还会把自己暴露在公网之上。 这意味着容器与主机必须共用同一个 IP 地址, 不过这就减少了网桥模式中涉及的底层开销,因此速度与常规的主机网络一样快。由于 IP 地址是共享的,需要互相通信的容器必须预先协定使用哪些端口通信,在进行配置的时候必须考虑这一点,而且可能还需要修改程序。
容器模式
未联网模式
核心概念
image 镜像
Docker 的镜像由多个 layer(层) 组成,每一层都是只读的文件系统。Dockerfile 里面每个指令都会创建新的层,这个层会位于前一层之上。
不必要的层会让镜像变得臃肿(并且 AUFS 最多只能有 127 层), 所以很多 Dockerfile 会把多个命令放到一个 RUN 指令中,以减少层的数量。
container 容器
Docker 集群管理方案
swarm
swarm 是原生的 Docker 集群管理工具, 将一组 Docker 主机作为一个虚拟的 Docker 主机来管理。
k8s
mesos
Docker 周边
这部分后续会持续更新
- docker-compose
- docker-ssh
原理和思考
镜像是如何生成的
docker build 需要 Dockerfile 以及构建的上下文(上下文可以为空),上下文可以被 COPY 或 ADD 引用。
以 docker build .
为例, 上下文是 . 即当前目录,在构建镜像的过程中会被打成 tar 文件,然后传给守护进程。如果不想将文件 add 进上下文可以使用 .dockerignore。
Dockerfile 每个指令都会产生一个新的镜像层, 一个新的镜像层的建立,是用上一层的镜像启动容器,然后执行 Dockerfile 的指令后,保存为一个新的镜像。当指令执行成功后,中间使用的容器会被删掉。(可以通过 docker history
来查看组成镜像的所有层)
举一个例子, Dockerfile 如下:
1 | FROM busybox:latest |
构建过程如下,并附上了注释
1 | $ docker build -t docker-image-test . |
context 上下文是一组本地文件和目录,可以被 Dockerfile 的 ADD 或 COPY 指令所引用。
在调试出错的一层的时候,可以将上一层跑起来之后测试。
Docker 卷,在容器访问宿主机文件, 以及文件权限问题
很多场景下都会有想要在容器中访问宿主机文件的需求,比如本地开发方案,本地写代码,容器中调试;访问一个大型的中央式数据库。而这一切是可以通过卷来做到的。
比如 docker run -v /var/db/tables:/var/data1 -it debian bash
表示将宿主机的 /var/db/tables 挂载到容器的 /var/data1 。如果外部和容器的目录都不存在则会创建。
但是挂载可能会遇到这样一些问题:
- 挂载的地址如果已经有文件的情况
结合上面的例子,如果容器中在 /var/data1 下已经有内容了,那之前的内容会消失。
不同系统之间的大小写问题
比如宿主机是 Mac OS,而容器是 Linux。Linux 系统是大小写敏感的,而 Windows 系统和 Mac 系统正好相反,大小写不敏感。所以很可能会出现的情况是物理机上能跑起来的服务,在容器里面会报找不到包之类的。Docker mount 之后的文件权限问题
Linux 内核使用 UID 和 GID 来识别用户,并决定他们的访问权限。容器中的 UID 和 主机上的 UID 是相同的,但是在容器内创建的用户和用户组不会影响宿主机,这个会让访问权限变得很混乱。
Docker 的缓存策略是什么样子
Docker 为了加快构建的速度,会将每个镜像层缓存下来。
但是指令必须要同时满足以下的条件:
- 上一个指令要能在缓存中找到
- 缓存中存在一个镜像层,它的指令和你的指令一模一样,父层也完全相同(即使出现一些无关紧要的空格也会让缓存失效)
还会有的问题是那些每次调用结果都不一样的 RUN 命令,也仍然会被缓存,比如要下载文件, 执行 apt-get update 或者源码,都需要注意。
如果在构建镜像时不想使用缓存,可以加上 –no-cache 的选项。详见官方文档
Dockerfile 的最佳实践
Docker 文档 里面提到了几点,这里做一点总结和延伸:
Create ephemeral containers
像 The Twelve-factor App 里面提到的一样,让创建,销毁,重新构建的配置和启动工作量最小化。Understand build context, Exclude with .dockerignore
在上面的「镜像是如何生成」的部分提到了 context,在构建镜像的日志中,类似Sending build context to Docker daemon xxxkB
这里能看到 context 的大小, 结合 .dockerfile 忽略一些无用的目录来合理控制 context 大小,能控制镜像大小和构建的速度。Minimize the number of layers, Sort multi-line arguments
减不必要的层会让镜像变得臃肿(并且 AUFS 最多只能有 127 层), 所以一个方式是控制层数。
同时将依赖分成多行,按照字母排序,\
前面增加一个空格能帮助 review,同时减少重复。1
2
3
4
5
6RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversionUse multi-stage builds
multi-stage builds 是在 Docker 17.05 支持的,详见 文档, 这个很有意思,举一个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24FROM golang:1.9.2-alpine3.6 AS build
# Install tools required for project
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Install library dependencies
RUN dep ensure -vendor-only
# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY . /go/src/project/
RUN go build -o /bin/project
# This results in a single layer image
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]看起来就是让编译出来的文件单独在一个镜像中,能效果非常明显地减少镜像体积。
如果有安装依赖包,要指定版本
这条是我遇到过的坑,重新构建 Dockerfile 的时候,没有指定版本就会默认装最新稳定版,导致一些版本升级带来的不可预知的问题。
遇到过的问题和使用场景
Docker 异常重启问题(todo)
有时发现某些异常会导致机器重启,并且重启之后,Docker Daemon 无法起起来的情况。还还没开始排查原因。
docker kill hang 住(未查明原因)
经常会遇到一些 JAVA 的容器在回收的时候 hang 住
Docker deamon 的日志是:
1 | time="2018-11-14T22:56:18.614293666+08:00" level=info msg="Container 7dcf4bb3f841 failed to exit within 10 seconds of kill - trying direct SIGKILL" |
而尝试 ssh 到容器中,日志如下:
1 | time="2018-11-14T23:00:55.454937469+08:00" level=error msg="Error running exec in container: rpc error: code = 13 desc = invalid header field value \"oci runtime error: exec failed: container_linux.go:247: starting container process caused \\\"process_linux.go:83: executing setns process caused \\\\\\\"exit status 16\\\\\\\"\\\"\\n\"" |
1 | docker inspect --format {{.State.Pid}} {container_id} # 查看 pid |
尝试直接 kill -9 掉容器中对应的 PID 也是没有成功
在 issue 里面提到重启 Docker 可以解决, 重启之后确实对应的容器就不见了, 问题绕过。但是问题的根本原因还没能查到。
docker build 时, FROM 里面的基础镜像是否会被自动更新
在 docker build 时, docker daemon 会查看 FROM 里面指定的基础镜像,如果本地没有这个镜像,docker 会试图下载它。
但是如果本地已经有这个镜像了,docker 不会去检查是否有更新的版本可用。所以如果你想要升级 FROM 里面指定的基础镜像,还需要显式 docker pull 一下。
磁盘占用问题以及清理方式
在一段时间之后,docker 会占据非常大的磁盘使用,一些清理策略包括:
- 遗留的镜像 dangling images
没有用处的镜像,也没有被其他镜像引用,docker 没有垃圾回收机制,没有用的镜像会一直存在, 可以通过 docker images -f dangling=true
来查看到, 这里的 -f 是指的 filter,
docker image -q
是显示对应的 image id。完整的就是: docker images -q -f dangling=true | xargs --no-run-if-empty docker rmi
- 数据卷
在主机的一个或者多个容器之间共享文件的一种使用方式是创建数据容器。比如,通过docker run --name dbdata postgres echo "data only container"
将 postgres 镜像创建一个容器,并初始化镜像中定义的数据卷,最后执行 echo 并退出。然后通过--volumns-from
使其他容器能使用这个数据卷。docker run -d --volumns-from dbdata --name db1 postgres
数据卷只会在以下集中场景被删除:
- docker rm -v 删除
- docker run 时带有 –rm 选项
Ref:
- Docker does not free up disk space after container, volume and image removal · Issue #32420 · moby/moby https://github.com/moby/moby/issues/32420
- Some way to clean up / identify contents of /var/lib/docker/overlay - General Discussions / General - Docker Forums https://forums.docker.com/t/some-way-to-clean-up-identify-contents-of-var-lib-docker-overlay/30604/2
将 docker image 从 server 之间转移(比如无法访问 docker hub 的情况)
可以通过 docker save 和 docker load 来解决, docker save -o <image path> <image name>
, -o 是保存为的文件
1 | docker save -o image_file_name image_name:latest |
这样的方式也可以用来分发和备用镜像。
限制容器的资源使用(todo)
https://docs.docker.com/config/containers/resource_constraints/
不使用 sudo 执行 docker
Docker 运行时需要特殊权限,所以在执行时前面都要加上 sudo,官方文档里面提供了一种方法, 也就是把用户放到 Docer 组里面
1 | $ sudo usermod -aG docker $USER |
但是这样会带来一些安全隐患: https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface
Docker cp 是否支持 wildcard ?
Docker cp 的基本使用是:
1 | docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH |
不支持使用 wildcard,比如使用会报这样的错:
1 | $ docker cp 80af2e29a69a:/home/*.png . |
虽然在 issue 里面很多人提议, 但是官方没有太多回应,不过有一些 workaround 的方式:
- 使用 volumn, 也就是直接挂载,不用 cp 了。
- docker cp 复制整个目录,比如
docker cp container_id:screenshot/*.png .
换为 copyscreenshot
- 不直接使用 docker cp, 或者挨个 docker cp
从容器复制文件到物理机的场景:
先 docker exec 将对应文件放到对应目录,最后 docker cp 整个目录, link
1 | $ docker exec <id> bash -c "mkdir /extract; mv /path/to/fileset* /extract" |
从宿主机复制文件到容器的场景:
1 | for f in data/*txt; do docker cp $f sandbox_web_1:/usr/src/app/data/; done |
Docker cp 之后的文件权限是什么样的 ?(todo)
各种网络模式之间的差异和性能对比 (todo)
- Overview | Docker Documentation https://docs.docker.com/network/#network-driver-summary
- Networking with standalone containers | Docker Documentation https://docs.docker.com/network/network-tutorial-standalone/#use-user-defined-bridge-networks
- Networking for Docker Containers (a Primer) Part I - Mesosphere https://mesosphere.com/blog/networking-docker-containers/
什么场景下需要禁用 iptables ?(todo)
run task in background and foreground
run command in foreground
1 | docker exec -i CONTAINER_NAME bash <<'EOF' |
run command in the background
1 | $ docker exec -d ubuntu_bash touch /tmp/execWorks |
ref:
Docker exec stdout, stderr
When you tell Docker to set up a TTY, both stdout and stderr are directed to that pseudo-TTY
1 | $docker exec -t 09b24cd7fa69 ls nosuchfile 1>docker.out 2>docker.err |
是否可以在 Dockerfile 中起一些持久的进程
由于镜像的生成策略是每个指令产生一个新的镜像层,中间每层镜像启动的容器在命令执行成功之后都会被删掉,所以命令中任何运行的程序都会被停掉。
也就是你在 RUN 指令中执行的一些持久进程,比如数据库或者 SSH 服务,到了处理下一个指令或者启动容器的时候,他们已经不再运行。
如果需要在容器启动的同时运行一个服务或者进程,可以通过 ENTRYPOINT 或者 CMD。
获得镜像或者容器的详细信息
通过 docker image inspect, example:
1 | docker image inspect {image_id} |
在一些低版本的 Docker 上是不支持的,这里可以用 docker inspect
1 | docker inspect --size {image_id} |
最后再使用 format 取出想要的字段 https://docs.docker.com/config/formatting/ , 一个例子
1 | # 获得镜像大小 |
SizeRootFs, SizeRw 的区别是什么
在 docker inspect {container_id} 之后能拿到的一系列的值,其中包括
SizeRootFs, 容器中所有文件的大小
SizeRw 基于 Base image 变更的文件大小
What is the exact difference between SizeRootFs and SizeRw in Docker containers? - Stack Overflow https://stackoverflow.com/questions/22156563/what-is-the-exact-difference-between-sizerootfs-and-sizerw-in-docker-containers
docker exec 里面包含中文报错
一种解决办法是:
1 | $ docker exec -it fe9bb26bd7aa env LANG=C.UTF-8 /bin/bash |
容器里面涉及到一些敏感信息问题应该怎么处理
比如各种 token, key, 私钥应该怎么更好的处理
- 放在镜像中(这个非常不安全,还是别想了..)
- 通过环境变量,但是环境变量对于子进程,docker inspect 和连接的容器都可见,他们都不具备能看见密钥的充分理由
- 使用数据卷挂载, 这个稍微优雅一点,但唯一的问题是密钥要以文件的形式存在
- 使用 kv 存储,这个是当前最好的方案
比如:
- square/keywhiz: A system for distributing and managing secrets https://github.com/square/keywhiz 将密钥加密放到内存中,提供 REST API 和 cli 访问方式
- hashicorp/vault: A tool for secrets management, encryption as a service, and privileged access management https://github.com/hashicorp/vault 拥有更多的关注度
- Crypt https://xordataexchange.github.io/crypt/
一些其他的思路:
- 在 docker build 时从远程获取,然后 wget -O
- 使用 docker secret, 但是这个的前提是用 docker swarm
物理机和远程容器之间的文件共享方案
一个简单粗暴的思路是先 docker cp 到宿主机,然后从这台机器 scp link
使用 netcat
1
2
3
4
5
6# 假设要将容器里面的 a.txt 复制到 A 物理机
# 在 A 物理机上执行
nc -l -p <port> > a.txt
# 然后在容器中
nc -w 3 A_host_name <port> < a.txt容器里面支持 ssh (todo)
Docker NAT iptables 实现
默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器,容器访问外部实现
容器所有到外部网络的连接,源地址都会被 NAT 成本地系统的 IP 地址(即docker0地址)。这是使用 iptables 的源地址伪装操作实现的
References
Docker on GPU
Reference & Recommendation
- https://coolshell.cn/articles/17010.html
- Docker 开发指南, 主要按照 Docker 的组成和原理来进行的内容架构
- Docker 实践,按照 Docker 的组成,并分成了各种各样的小的案例来介绍。
- Coolshell https://coolshell.cn/articles/17010.html
- 美团容器平台架构及容器技术实践 - 美团技术团队 https://tech.meituan.com/2018/11/15/docker-architecture-and-evolution-practice.html
更新日志
由于这个是一个会长期更新的文章,准备试试加上更新日志。
- 2018-11-24 一些大纲, Dockerfile 最佳实践
- 2018-11-26
docker cp
有 wildcard 场景的一些补充 - 2018-12-01 增加了在 Dockerfile 锁定版本作为一个最佳实践
- 2018-12-13 增加了容器敏感文件的一些解决方案
关于头图
拍摄自纽约