Docker 核心技术和实现原理
一、制作镜像
1.1 docker commit
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]直接从某个容器中制作出来一个镜像,相对来说更加快捷,但是docker commit关注的点在于给一个正在运行的容器制作一份快照,而不是从零创建一个镜像
1.2 Dockerfile
1.2.1 为什么制作镜像更建议使用 Dockerfile?
Dockerfile 可以自动化构建,而使用
docker commit,需要先手动进入容器中完成相应构建后,再制作出来一个容器Dockerfile 中,所有构建操作可复现、修改、检查,是透明的;但是
docker commit并不会保留这些信息Dockerfile 是文本文件,可以进行版本控制;
docker commit无法完成Dockerfile 可以利用 docker 缓存,节省时间
docker commit更偏向冗余,是给容器做一个快照,这时就更适合使用docker commit
1.2.2 Dockerfile 常用指令
FROM
FROM 用来指定基础镜像,后续所有步骤都基于这个镜像操作。
FROM 可以有多个,但是最后只有一个生效,即最后的构建结果只基于最后一个 FROM,但是这不意味着前面的 FROM 都是没意义的,在需要多阶段构建的情况下,比如编译环境和运行环境分离的情况下,就需要多个 FROM
FROM ubuntu:22.04LABEL
指定镜像的附加信息,通过键值对的方式实现,比如
LABEL author="wjl" company="com.fudan"这些镜像附加信息,可以通过docker image inspect查看
ENV
用来指定环境变量,在制作出的镜像、通过镜像构建的容器中都将保留;可以在构建容器时通过-e覆盖。
ENV WEB_ROOT=/data/web/www/使用环境变量,通过与 Bash Script 相同的方式进行,例如
ADD https://nginx.org/download/${NGINX_VERSION}.tar.gz ./srcCOPY
将一个宿主机文件拷贝到镜像目录,格式为
COPY <src> <dst>有以下几点需要注意
src必须在 dockerfile 所在目录的子目录中,即必须是 build 上下文中的路径如果
src是目录,那么会递归拷贝目录中的所有目录和文件,同时src本身不会被复制如果
src有多个,或者使用了通配符,那么dst必须是目录,而且以/结尾如果
dst事先不存在,那么将创建
ADD
相当于 plus 版本的 COPY,格式为
ADD <url> <dst> ADD <file> <dst>ADD 有两种表现:
如果给定
src为url,将下载指定资源到dst如果给定
src为文件,则将指定文件拷贝到dst;如果其为压缩包(如 tar),将先进行解压
WORKDIR
为 CMD、ADD、COPY、RUN 等指定工作目录,默认是/;如果给相对路径,那么将相对上一个 WORKDIR
WORKDIR /a WORKDIR b WORKDIR C # 最后,所处的目录就是 /a/b/CRUN
在镜像构造阶段执行命令。Dockerfile 相对于docker commit,这些命令就是一大优势:可见、可修改
RUN <command> RUN ["executable","param1", "param2"]如果通过docker commit进行,这些都可以对应懂啊手动执行容器环境搭建的命令
CMD
在构造完成的镜像创建容器时执行命令,在一个 Dockerfile 中只可以有一个(只有最后一个生效)
CMD <command> CMD ["executable", "param1", "param2"]ENTRYPOINT
用来指定容器的启动命令,让容器像一个可执行命令一样,在一个 Dockerfile 中只可以有一个
ENTRYPOINT <command> ENTRYPOINT ["executable", "param1", "param2"]CMD 和 ENTRYPOINT 的区别
CMD 可以被 docker run 的参数覆盖,而 ENTRYPOINT 不会,除非使用 --entrypoint 修改
CMD 和 ENTRYPOINT 可以结合在一起使用,此时 ENTRYPOINT 用来指定命令,CMD 用来提供参数。此时参数也可以在 docker run 中被参数覆盖
EXPOSE
对容器会监听的端口进行声明,并不会实际建立端口映射,而是作为镜像构造者对镜像使用者的提示,哪些端口需要进行映射供给外界访问
EXPOSE 80/tcpARG
ARG 也用来指定变量,但是 ARG 并不会像 ENV 一样将变量保存到镜像和容器运行时
ARG UBUNTU_VERSION=22.10 ARG DEBIAN_VERSIONARG,定义之前为空
ARG,可以指定默认值
可以通过 --build-arg 在根据镜像构建容器时可以进行修改
USER
指定容器运行时的用户,默认是 root
USER <user>[:<group>] USER <uid>[:<gid>]VOLUME
声明一个匿名卷,但是并不会进行实际的绑定,用来进行构建容器时没有指定-v的保险(比如 mysql 不能因为容器没了导致数据没了),即会被构建容器时的-v选项覆盖
VOLUME ["/var/lib/mysql"]SHELL
用来指定 RUN、CMD、ENTRYPOINT 的 shell,在 Windows 这种有 cmd 和 powershell 两个 shell 的系统常用
SHELL ["/bin/bash", "-cvx"]HEALTHCHECK
对创建的容器进行检查确保可以正常使用,有如下参数
--interaval每次检查间隔时间,默认是 30s--timeout,每次检查超时时间,默认是 30s--retries,重试次数,默认 3 次--start-period,在容器创建完成后等多长时间再进行检查,默认是 0s退出码,0 表示 healthy,1 表示 unhealthy,其他的退出码留给 docker 使用,所以可以写为:
HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost/ || exit 1
ONBUILD
设置一个触发器,当使用现在构建的镜像作为基础镜像时,会触发后面的指令
ONBUILD RUN echo "on build" >> /tmp/build.txtSTOPSIGNAL
发送系统指令到容器,需对应到系统的指令编号
STOPSIGNAL 9Linux 提供的指令可以通过kill -l查看
11:25:49 root@DESKTOP-OFM10JG db ±|master ✗|→ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN #...1.2.3 空悬镜像与中间镜像
空悬镜像是仓库和标签都为 none 的镜像,这种镜像可以认为是无用的镜像,主要会因为两种操作产生
docker pull 获取了更新版本的镜像,旧的镜像名转移到了新的镜像上,旧的镜像名被取消
docker build 也会通过类似的方式导致这种问题
查看空悬镜像
docker image ls -f dangling=true中间镜像是某些镜像所依赖的镜像。虽然这些镜像没有标签,但这是 docker 为了加速镜像构建并重复利用镜像资源而采取的方式,不应进行删除,也没有必要,因为相同的层智慧存储一遍。
查看中间镜像,使用docker images -a。
二、镜像原理
2.1 bootfs 和 rootfs
Bootfs 主要包含两部分:boot loader 和 kernel,后者就是我们常说的 Linux 内核。开机时,会首先挂载 bootfs,有 boot loader 引导内核加载进内存中,之后卸载 bootfs。之后开始以只读的方式挂载 rootfs,并进行检查,正常后会改为 readwrite 供用户使用
2.2 Union FS
Union FS 让容器的创建就像 PS 处理图片时的一个个图层一样,最后呈现给上层的只有一层:一个完整的文件系统。Union FS 使用了写时拷贝的技术,让一层 layer 能够被循环使用,节省空间;这里的写时拷贝,思路上(不是实现上)可以通过 Linux 父子进程间虚拟地址空间的写时拷贝理解。但是 Union FS 的这种写时拷贝机制,势必会造成磁盘写入的效率低下,所以对于 MySQL、Redis 这些需要频繁写磁盘的容器往往需要进行卷的挂载,不直接使用 Union FS 写数据
现代 Docker 在所有 Linux 发行版上,使用的存储引擎默认是 overlay2。
overlay2 的底层目录称为 lowerdir,高层称为 upperdir,中间临时层为 workdir,统一层为 merged
lowerdir,是只读层的镜像,其中就包含 bootfs/rootfs,也有其他通过 DockerFile 建立的 image 层
upperdir,是容器的读写层,采用了写时拷贝的机制,是 lowerdir 的上一层,对于容器的所有修改操作都发生在这一层
workdir ,充当中间层的作用,当需要修改 lowerdir 中的内容时,将相应文件拷贝到workdir,在 workdir 中完成修改后,再拷贝到 upperdir 想容器外界展示
merged,负责整合上面三层的内容,容器最后对外展示的就是 merged 层
那么读写是如何完成的呢?
读:当 upperdir 中有相应内容时,直接从 upperdir 中读取;没有时,到 lowerdir 中完成读取
写:
首次写入时,会将 lowerdir 中的内容拷贝到 upperdir(写时拷贝),之后进行的修改都会在这个拷贝的“副本”上进行,lowerdir 中的内容,并不会被修改。
当删除文件时,并不会操作lowerdir 中的内容,而是会在 upperdir 中新建 whiteout 文件,遮盖住 lowerdir 中的文件;当删除文件夹时,也会在 upperdir 中新建不透明目录,阻止用户继续访问。无论如何,image 层都不会发生改变。
简单来说,容器层的文件删除是“障眼法”,底层的文件都没有发生改变,这也是为什么 docker commit
提交保存的镜像会越来越大——image层的文件压根没有被删除,只有顶层的 whiteout 文件的遮挡发生了改变
2.3 docker 镜像的加载原理
典型的 Linux 在启动时,再 bootfs 引导内核加载进内存后,会开始加载 rootfs,以只读的方式进行挂载,检查完成后允许用户通过 readwrite 操作访问。docker 镜像的加载采取类似的方式,在 rootfs 通过 readonly 的方式完成挂载检查后,继续将 readwrite 的文件系统挂载在 readonly 的 rootfs 上,之后叠加并允许将下层的文件系统设置为 readonly,最后只保留一个 readwrite 的 upperdir 作为容器的读写层。这里的每一个层,都是一个 layer;这样的一组 readonly 和一个 readwrite 的结构就构成了容器的运行目录
三、docker 卷机制
在容器进程创建后,在执行 chroot 之前(chroot 可以改变进程根目录,使得进程只能够看见根目录及其子目录)虽然已经创建了 Mount Namespace,但是宿主机的整个文件系统依然对容器进程可见,包括构建 rootfs 所需的镜像。
在构建 rootfs 时,会将所需的镜像联合挂载在相应目录,作为我们的 rootfs;在 rootfs 准备好后,会将 -v 指定的目录、文件挂载到 rootfs 的相应位置。
最后执行 chroot,从容器内部不可见宿主机的非挂载文件、目录;在宿主机上,因为 Mount Namespace 的作用,这些挂载点在宿主机也不可见
docker commit 会将 volume 中的文件拷贝进镜像吗?
docker commit 制作镜像的对象是 Union FS,而我们的 volume 是外部挂载,不属于联合文件系统,所以不会被制作进镜像;但是我们在进行挂载的时候,会在联合文件系统中创建一个挂载点,这个挂载点属于联合文件系统的一部分,这个挂载点会以空文件夹的方式被制作进镜像,比如
-v index.html:/test,就会在镜像中有一个 /test 空目录
四、docker 网络
4.1 tun、tap
tun、tap 是操作系统内核中的虚拟网络设备,所谓虚拟即完全通过软件实现,但是这些虚拟网络设备的流量都在本机的范围内
tun 工作在网络层,可以收发第三层数据报文包,如 ip 封包
tap 工作在数据链路层,等同于一个以太网设备,可以收发第二层数据报文包,如以太网数据帧。tap 最常见的用处就是作为虚拟机的网卡,因为更接近于普通的物理网卡
通过上图所示的方式,就可以实现所有网络流量都从指定的应用通过指定的方式走了,比如某些应用的 TUN 模式,但是这种方式会两次经过网络协议栈,所以会有一定的性能损耗
在 Linux 中添加、激活虚拟网卡,并设置 ip
添加 tun/tap 网卡
ip tuntap add dev tun0 mode tun # 添加 tun ip tuntap add dev tap0 mode tap # 添加 tap激活网卡
ip link set tun0 up分配 ip 地址
ip addr add 10.5.0.1/24 dev tun0删除网卡
ip tuntap del dev tun0 mode tun ip tuntap del dev tap0 mode tap
4.2 veth
Veth 是 Linux 提供的一个虚拟以太网设备,用来让两个隔离的网络命名空间之间可以进行通信。将 veth 直接比作网卡不太恰当,更恰当的说法是用网线连起来的一对网卡。veth 是一对互相连接的设备,同时各自连接着各自网络命名空间的网络协议栈。一个网络协议栈通过一个 veth 输出的数据,会从配对的 veth 原封不动的输出到自己的网络命名空间
相对于 tun/tap,veth 不需要重复经过网络协议栈,性能更高。veth 连接了两个 namespace,但是当需要把更多的 namespace 连接起来时,就需要其他的方式了。
在物理网络中,如果需要连接多个设备,我们会使用网桥,或者叫做交换机;Linux 也提供了网桥的虚拟实现
4.3 Linux Bridge
Linux Bridge 是 Linux 提供的虚拟网桥实现,通过brctl进行创建和管理;创建后,真实和虚拟的网络设备都可以与网桥配合