docker
docker#
Docker 架构详解#
Docker 的核心组件包括:
- Docker 客户端 - Client
- Docker 服务器 - Docker daemon
- Docker 镜像 - Image
- Registry
- Docker 容器 - Container
镜像#
构建镜像
Docker 提供了两种构建镜像的方法:
- docker commit 命令
- Dockerfile 构建文件
Dockerfile 构建镜像
构建镜像 :
docker build -t 生成的镜像名字 .
. 表示当前路径下
Dockerfile 常用指令#
下面列出了 Dockerfile 中最常用的指令,完整列表和说明可参看官方文档。
-
FROM
指定 base 镜像。 -
MAINTAINER
设置镜像的作者,可以是任意字符串。 -
COPY
将文件从 build context 复制到镜像。
COPY 支持两种形式:
- COPY src dest
- COPY ["src", "dest"]
注意:src 只能指定 build context 中的文件或目录。
-
ADD
与 COPY 类似,从 build context 复制文件到镜像。不同的是,如果 src 是归档文件(tar, zip, tgz, xz 等),文件会被自动解压到 dest。 -
ENV
设置环境变量,环境变量可被后面的指令使用。例如:
...
ENV MY_VERSION 1.3
RUN apt-get install -y mypackage=$MY_VERSION
...
-
EXPOSE
指定容器中的进程会监听某个端口,Docker 可以将该端口暴露出来。我们会在容器网络部分详细讨论。 -
VOLUME
将文件或目录声明为 volume。我们会在容器存储部分详细讨论。 -
WORKDIR
为后面的 RUN, CMD, ENTRYPOINT, ADD 或 COPY 指令设置镜像中的当前工作目录。 -
RUN
在容器中运行指定的命令。
CMD
容器启动时运行指定的命令。
Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效。CMD 可以被 docker run 之后的参数替换。
ENTRYPOINT
设置容器启动时运行的命令。
Dockerfile 中可以有多个 ENTRYPOINT 指令,但只有最后一个生效。CMD 或 docker run 之后的参数会被当做参数传递给 ENTRYPOINT。
在上面这些指令中,RUN、CMD、ENTRYPOINT 很重要且容易混淆,
RUN vs CMD vs ENTRYPOINT#
简单的说:
- RUN 执行命令并创建新的镜像层,RUN 经常用于安装软件包。
- CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够被
docker run
后面跟的命令行参数替换。 - ENTRYPOINT 配置容器启动时运行的命令。
Shell 和 Exec 格式#
我们可用两种方式指定 RUN、CMD 和 ENTRYPOINT 要运行的命令:Shell 格式和 Exec 格式,二者在使用上有细微的区别。
Shell 格式
例如:
RUN apt-get install python3
CMD echo "Hello world"
ENTRYPOINT echo "Hello world"
当指令执行时,shell 格式底层会调用 /bin/sh -c
例如:
RUN apt-get install python3
下面来看 Exec 格式。
Exec 格式
例如:
ENTRYPOINT ["/bin/echo", "Hello world"]
当指令执行时,会直接调用
例如下面的 Dockerfile 片段:
ENV name Cloud Man
ENTRYPOINT ["/bin/echo", "Hello, $name"]
运行容器将输出:
Hello, $name
注意环境变量“name”没有被替换。
如果希望使用环境变量,照如下修改
ENV name Cloud Man
ENTRYPOINT ["/bin/sh", "-c", "echo Hello, $name"]
运行容器将输出:
Hello, Cloud Man
RUN#
RUN 指令通常用于安装应用和软件包。
RUN 在当前镜像的顶部执行命令,并通过创建新的镜像层。Dockerfile 中常常包含多个 RUN 指令。
RUN 有两种格式:
-
Shell 格式:RUN
-
Exec 格式:RUN ["executable", "param1", "param2"]
下面是使用 RUN 安装多个包的例子:
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
注意:apt-get update 和 apt-get install 被放在一个 RUN 指令中执行,这样能够保证每次安装的是最新的包。如果 apt-get install 在单独的 RUN 中执行,则会使用 apt-get update 创建的镜像层,而这一层可能是很久以前缓存的。
CMD#
CMD 指令允许用户指定容器的默认执行的命令。
此命令会在容器启动且 docker run 没有指定其他命令时运行。
- 如果 docker run 指定了其他命令,CMD 指定的默认命令将被忽略。
- 如果 Dockerfile 中有多个 CMD 指令,只有最后一个 CMD 有效。
CMD 有三种格式:
- Exec 格式:CMD ["executable","param1","param2"]
这是 CMD 的推荐格式。 - CMD ["param1","param2"] 为 ENTRYPOINT 提供额外的参数,此时 ENTRYPOINT 必须使用 Exec 格式。
- Shell 格式:CMD command param1 param2
Exec 和 Shell 格式前面已经介绍过了。
第二种格式 CMD ["param1","param2"] 要与 Exec 格式 的 ENTRYPOINT 指令配合使用,其用途是为 ENTRYPOINT 设置默认的参数。我们将在后面讨论 ENTRYPOINT 时举例说明。
ENTRYPOINT#
ENTRYPOINT 指令可让容器以应用程序或者服务的形式运行。
ENTRYPOINT 看上去与 CMD 很像,它们都可以指定要执行的命令及其参数。不同的地方在于 ENTRYPOINT 不会被忽略,一定会被执行,即使运行 docker run 时指定了其他命令。
ENTRYPOINT 有两种格式:
- Exec 格式:ENTRYPOINT ["executable", "param1", "param2"] 这是 ENTRYPOINT 的推荐格式。
- Shell 格式:ENTRYPOINT command param1 param2
在为 ENTRYPOINT 选择格式时必须小心,因为这两种格式的效果差别很大。
Exec 格式
ENTRYPOINT 的 Exec 格式用于设置要执行的命令及其参数,同时可通过 CMD 提供额外的参数。
ENTRYPOINT 中的参数始终会被使用,而 CMD 的额外参数可以在容器启动时动态替换掉。
比如下面的 Dockerfile 片段:
ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]
当容器通过 docker run -it [image] 启动时,输出为:
Hello world
而如果通过 docker run -it [image] CloudMan 启动,则输出为:
Hello CloudMan
Shell 格式
ENTRYPOINT 的 Shell 格式会忽略任何 CMD 或 docker run 提供的参数。
最佳实践
- 使用 RUN 指令安装应用和软件包,构建镜像。
- 如果 Docker 镜像的用途是运行应用程序或服务,比如运行一个 MySQL,应该优先使用 Exec 格式的 ENTRYPOINT 指令。CMD 可为 ENTRYPOINT 提供额外的默认参数,同时可利用 docker run 命令行替换默认参数。
- 如果想为容器设置默认的启动命令,可使用 CMD 指令。用户可在 docker run 命令行中替换此默认命令。
容器#
因为容器的生命周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。
运行容器#
docker run
是启动容器的方法。在讨论 Dockerfile 时我们已经学习到,可用三种方式指定容器启动时执行的命令:
- CMD 指令。
- ENDPOINT 指令。
- 在
docker run
命令行中指定。
容器的生命依赖#
因为容器的生命周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。
两种进入容器的方法#
我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容器:attach 和 exec。
docker attach
通过 docker attach
可以 attach 到容器启动命令的终端,例如:
这次我们通过 “长ID” attach 到了容器的启动命令终端,之后看到的是echo
每隔一秒打印的信息。
注:可通过 Ctrl+p 然后 Ctrl+q 组合键退出 attach 终端。
docker exec
通过 docker exec
进入相同的容器:
说明如下:
① -it
以交互模式打开 pseudo-TTY,执行 bash,其结果就是打开了一个 bash 终端。
② 进入到容器中,容器的 hostname 就是其 “短ID”。
③ 可以像在普通 Linux 中一样执行命令。ps -elf
显示了容器启动进程while
以及当前的 bash
进程。
④ 执行 exit
退出容器,回到 docker host。
docker exec -it <container> bash|sh
是执行 exec 最常用的方式。
attach VS exec
attach 与 exec 主要区别如下:
-
attach 直接进入容器 启动命令 的终端,不会启动新的进程。
-
exec 则是在容器中打开新的终端,并且可以启动新的进程。
-
如果想直接在终端中查看启动命令的输出,用 attach;其他情况使用 exec。
当然,如果只是为了查看启动命令的输出,可以使用 docker logs
命令:
-f
的作用与 tail -f
类似,能够持续打印输出。
运行容器的最佳实践#
按用途容器大致可分为两类:服务类容器和工具类的容器。
1. 服务类容器以 daemon 的形式运行,对外提供服务。比如 web server,数据库等。通过 -d
以后台方式启动这类容器是非常合适的。如果要排查问题,可以通过 exec -it
进入容器。
2. 工具类容器通常给能我们提供一个临时的工作环境,通常以 run -it
方式运行,比如:
运行 busybox,run -it
的作用是在容器启动后就直接进入。我们这里通过 wget
验证了在容器中访问 internet 的能力。执行 exit
退出终端,同时容器停止。
工具类容器多使用基础镜像,例如 busybox、debian、ubuntu 等。
容器运行小结
容器运行相关的知识点:
-
当 CMD 或 Entrypoint 或 docker run 命令行指定的命令运行结束时,容器停止。
-
通过
-d
参数在后台启动容器。 -
通过
exec -it
可进入容器并执行命令。
指定容器的三种方法:
-
短ID。
-
长ID。
-
容器名称。 可通过
--name
为容器命名。重命名容器可执行docker rename
。
容器按用途可分为两类:
-
服务类的容器。
-
工具类的容器。
常用命令#
stop/start/restart 容器#
通过 docker stop
可以停止运行的容器。
容器在 docker host 中实际上是一个进程,docker stop
命令本质上是向该进程发送一个 SIGTERM 信号。如果想快速停止容器,可使用 docker kill
命令,其作用是向容器进程发送 SIGKILL 信号。
对于处于停止状态的容器,可以通过 docker start
重新启动。
docker start
会保留容器的第一次启动时的所有参数。
docker restart
可以重启容器,其作用就是依次执行 docker stop
和docker start
。
容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。启动容器时设置 --restart
就可以达到这个效果。
--restart=always
意味着无论容器因何种原因退出(包括正常退出),就立即重启。该参数的形式还可以是 --restart=on-failure:3
,意思是如果启动进程退出代码非0,则重启容器,最多重启3次。
pause/unpause 容器#
有时我们只是希望暂时让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者 dcoker host 需要使用 CPU,这时可以执行 docker pause
。
处于暂停状态的容器不会占用 CPU 资源,直到通过 docker unpause
恢复运行。
删除容器#
使用 docker 一段时间后,host 上可能会有大量已经退出了的容器。
这些容器依然会占用 host 的文件系统资源,如果确认不会再重启此类容器,可以通过 docker rm
删除。
docker rm
一次可以指定多个容器,如果希望批量删除所有已经退出的容器,可以执行如下命令:
docker rm -v $(docker ps -aq -f status=exited)
顺便说一句:docker rm
是删除容器,而 docker rmi
是删除镜像。
一下学了这么多操作,很有必要做个总结。下一节我们会用一张图来描述容器的状态机。
启动容器#
-
docker pull
命令可以从 Registry 下载镜像。 -
docker run
命令则是先下载镜像(如果本地没有),然后再启动容器# -it 参数的作用是以交互模式进入容器,并打开终端。 docker run -it ubuntu # -d 是后台启动容器。 # -p 将容器的 5000 端口映射到 Host 的 5000 端口。5000 是 registry 服务端口。端口映射我们会在容器网络章节详细讨论。 # -v 将容器 /var/lib/registry 目录映射到 Host 的 /myregistry,用于存放镜像数据。-v 的使用我们会在容器存储章节详细讨论。
-
docker images
显示镜像列表 可以查看到 httpd 已经下载到本地。 -
docker ps
或者docker container ls
显示容器正在运行。 -
rmi
只能删除 host 上的镜像,不会删除 registry 的镜像。 -
search
让我们无需打开浏览器,在命令行中就可以搜索 Docker Hub 中的镜像。
一张图搞懂容器所有操作#
-
可以先创建容器,稍后再启动。
①
docker create
创建的容器处于 Created 状态。
②docker start
将以后台方式启动容器。docker run
命令实际上是docker create
和docker start
的组合。 -
只有当容器的启动进程 退出 时,
--restart
才生效。
退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时 docker 会根据--restart
的策略判断是否需要重启容器。但如果容器是因为执行docker stop
或docker kill
退出,则不会自动重启。
限制容器对内存的使用#
一个 docker host 上会运行若干容器,每个容器都需要 CPU、内存和 IO 资源。对于 KVM,VMware 等虚拟化技术,用户可以控制分配多少 CPU、内存资源给每个虚拟机。对于容器,Docker 也提供了类似的机制避免某个容器因占用太多资源而影响其他容器乃至整个 host 的性能。
内存限额
与操作系统类似,容器可使用的内存包括两部分:物理内存和 swap。 Docker 通过下面两组参数来控制容器内存的使用量。
-m
或--memory
:设置内存的使用限额,例如 100M, 2G。--memory-swap
:设置 内存+swap 的使用限额。
当我们执行如下命令:
docker run -m 200M --memory-swap=300M ubuntu
其含义是允许该容器最多使用 200M 的内存和 100M 的 swap。默认情况下,上面两组参数为 -1,即对容器内存和 swap 的使用没有限制。
下面我们将使用 progrium/stress 镜像来学习如何为容器分配内存。该镜像可用于对容器执行压力测试。执行如下命令:
docker run -it -m 200M --memory-swap=300M progrium/stress --vm 1 --vm-bytes 280M
--vm 1
:启动 1 个内存工作线程。
--vm-bytes 280M
:每个线程分配 280M 内存。
运行结果如下:
因为 280M 在可分配的范围(300M)内,所以工作线程能够正常工作,其过程是:
- 分配 280M 内存。
- 释放 280M 内存。
- 再分配 280M 内存。
- 再释放 280M 内存。
- 一直循环......
如果让工作线程分配的内存超过 300M,结果如下:
分配的内存超过限额,stress 线程报错,容器退出。
如果在启动容器时只指定 -m
而不指定 --memory-swap
,那么 --memory-swap
默认为 -m
的两倍,比如:
docker run -it -m 200M ubuntu
容器最多使用 200M 物理内存和 200M swap。
限制容器对CPU的使用#
默认设置下,所有容器可以平等地使用 host CPU 资源并且没有限制。
Docker 可以通过 -c
或 --cpu-shares
设置容器使用 CPU 的权重。如果不指定,默认值为 1024。
与内存限额不同,通过 -c
设置的 cpu share 并不是 CPU 资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的 CPU 资源取决于它的 cpu share 占所有容器 cpu share 总和的比例。
换句话说:通过 cpu share 可以设置容器使用 CPU 的优先级。
比如在 host 中启动了两个容器:
docker run --name "container_A" -c 1024 ubuntu
docker run --name "container_B" -c 512 ubuntu
container_A 的 cpu share 1024,是 container_B 的两倍。当两个容器都需要 CPU 资源时,container_A 可以得到的 CPU 是 container_B 的两倍。
需要特别注意的是,这种按权重分配 CPU 只会发生在 CPU 资源紧张的情况下。如果 container_A 处于空闲状态,这时,为了充分利用 CPU 资源,container_B 也可以分配到全部可用的 CPU。
下面我们继续用 progrium/stress 做实验。
- 启动 container_A,cpu share 为 1024:
--cpu
用来设置工作线程的数量。因为当前 host 只有 1 颗 CPU,所以一个工作线程就能将 CPU 压满。如果 host 有多颗 CPU,则需要相应增加--cpu
的数量。 - 启动 container_B,cpu share 为 512:
- 在 host 中执行
top
,查看容器对 CPU 的使用情况:
container_A 消耗的 CPU 是 container_B 的两倍。 - 现在暂停 container_A:
top
显示 container_B 在 container_A 空闲的情况下能够用满整颗 CPU:
限制容器的 Block IO#
前面学习了如何限制容器对内存和CPU的使用,本节我们来看 Block IO。
Block IO 是另一种可以限制容器使用的资源。Block IO 指的是磁盘的读写,docker 可通过设置权重、限制 bps 和 iops 的方式控制容器读写磁盘的带宽,下面分别讨论。
注:目前 Block IO 限额只对 direct IO(不使用文件缓存)有效。
block IO 权重
默认情况下,所有容器能平等地读写磁盘,可以通过设置 --blkio-weight
参数来改变容器 block IO 的优先级。
--blkio-weight
与 --cpu-shares
类似,设置的是相对权重值,默认为 500。在下面的例子中,container_A 读写磁盘的带宽是 container_B 的两倍。
docker run -it --name container_A --blkio-weight 600 ubuntu
docker run -it --name container_B --blkio-weight 300 ubuntu
限制 bps 和 iops
bps 是 byte per second,每秒读写的数据量。
iops 是 io per second,每秒 IO 的次数。
可通过以下参数控制容器的 bps 和 iops:
--device-read-bps
,限制读某个设备的 bps。
--device-write-bps
,限制写某个设备的 bps。
--device-read-iops
,限制读某个设备的 iops。
--device-write-iops
,限制写某个设备的 iops。
下面这个例子限制容器写 /dev/sda 的速率为 30 MB/s
docker run -it --device-write-bps /dev/sda:30MB ubuntu
我们来看看实验结果:
通过 dd 测试在容器中写磁盘的速度。因为容器的文件系统是在 host /dev/sda 上的,在容器中写文件相当于对 host /dev/sda 进行写操作。另外,oflag=direct
指定用 direct IO 方式写文件,这样 --device-write-bps
才能生效。
结果表明,bps 25.6 MB/s 没有超过 30 MB/s 的限速。
作为对比测试,如果不限速,结果如下:
实现容器的底层技术#
为了更好地理解容器的特性,本节我们将讨论容器的底层实现技术。
cgroup 和 namespace 是最重要的两种技术。cgroup 实现资源限额, namespace 实现资源隔离。
cgroup
cgroup 全称 Control Group。Linux 操作系统通过 cgroup 可以设置进程使用 CPU、内存 和 IO 资源的限额。相信你已经猜到了:前面我们看到的--cpu-shares
、-m
、--device-write-bps
实际上就是在配置 cgroup。
cgroup 到底长什么样子呢?我们可以在 /sys/fs/cgroup 中找到它。还是用例子来说明,启动一个容器,设置 --cpu-shares=512
:
查看容器的 ID:
在 /sys/fs/cgroup/cpu/docker 目录中,Linux 会为每个容器创建一个 cgroup 目录,以容器长ID 命名:
目录中包含所有与 cpu 相关的 cgroup 配置,文件 cpu.shares 保存的就是 --cpu-shares
的配置,值为 512。
同样的,/sys/fs/cgroup/memory/docker 和 /sys/fs/cgroup/blkio/docker 中保存的是内存以及 Block IO 的 cgroup 配置。
namespace
在每个容器中,我们都可以看到文件系统,网卡等资源,这些资源看上去是容器自己的。拿网卡来说,每个容器都会认为自己有一块独立的网卡,即使 host 上只有一块物理网卡。这种方式非常好,它使得容器更像一个独立的计算机。
Linux 实现这种方式的技术是 namespace。namespace 管理着 host 中全局唯一的资源,并可以让每个容器都觉得只有自己在使用它。换句话说,namespace 实现了容器间资源的隔离。
Linux 使用了六种 namespace,分别对应六种资源:Mount、UTS、IPC、PID、Network 和 User,下面我们分别讨论。
Mount namespace
Mount namespace 让容器看上去拥有整个文件系统。
容器有自己的 /
目录,可以执行 mount
和 umount
命令。当然我们知道这些操作只在当前容器中生效,不会影响到 host 和其他容器。
UTS namespace
简单的说,UTS namespace 让容器有自己的 hostname。 默认情况下,容器的 hostname 是它的短ID,可以通过 -h
或 --hostname
参数设置。
IPC namespace
IPC namespace 让容器拥有自己的共享内存和信号量(semaphore)来实现进程间通信,而不会与 host 和其他容器的 IPC 混在一起。
PID namespace
我们前面提到过,容器在 host 中以进程的形式运行。例如当前 host 中运行了两个容器:
通过 ps axf
可以查看容器进程:
所有容器的进程都挂在 dockerd 进程下,同时也可以看到容器自己的子进程。 如果我们进入到某个容器,ps
就只能看到自己的进程了:
而且进程的 PID 不同于 host 中对应进程的 PID,容器中 PID=1 的进程当然也不是 host 的 init 进程。也就是说:容器拥有自己独立的一套 PID,这就是 PID namespace 提供的功能。
Network namespace
Network namespace 让容器拥有自己独立的网卡、IP、路由等资源。我们会在后面网络章节详细讨论。
User namespace
User namespace 让容器能够管理自己的用户,host 不能看到容器中创建的用户。
在容器中创建了用户 cloudman,但 host 中并不会创建相应的用户。
了解#
支持运行多种 Linux OS#
不同 Linux 发行版的区别主要就是 rootfs。
比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。
参考:
作者:Esofar
出处:https://www.cnblogs.com/firsthelloworld/p/17507965.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本