Docker 为什么这么快?深入理解镜像、分层存储、网络与容器运行原理

Docker 为什么这么快?深入理解镜像、分层存储、网络与容器运行原理
Docker 核心技术和实现原理一、制作镜像1.1 docker commitdocker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]直接从某个容器中制作出来一个镜像相对来说更加快捷但是docker commit关注的点在于给一个正在运行的容器制作一份快照而不是从零创建一个镜像1.2 Dockerfile1.2.1 为什么制作镜像更建议使用 DockerfileDockerfile 可以自动化构建而使用docker commit需要先手动进入容器中完成相应构建后再制作出来一个容器Dockerfile 中所有构建操作可复现、修改、检查是透明的但是docker commit并不会保留这些信息Dockerfile 是文本文件可以进行版本控制docker commit无法完成Dockerfile 可以利用 docker 缓存节省时间docker commit更偏向冗余是给容器做一个快照这时就更适合使用docker commit1.2.2 Dockerfile 常用指令FROMFROM 用来指定基础镜像后续所有步骤都基于这个镜像操作。FROM 可以有多个但是最后只有一个生效即最后的构建结果只基于最后一个 FROM但是这不意味着前面的 FROM 都是没意义的在需要多阶段构建的情况下比如编译环境和运行环境分离的情况下就需要多个 FROMFROM ubuntu:22.04LABEL指定镜像的附加信息通过键值对的方式实现比如LABEL authorwjl companycom.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 dstADD 有两种表现如果给定src为url将下载指定资源到dst如果给定src为文件则将指定文件拷贝到dst如果其为压缩包如 tar将先进行解压WORKDIR为 CMD、ADD、COPY、RUN 等指定工作目录默认是/如果给相对路径那么将相对上一个 WORKDIRWORKDIR /a WORKDIR b WORKDIR C # 最后所处的目录就是 /a/b/CRUN在镜像构造阶段执行命令。Dockerfile 相对于docker commit这些命令就是一大优势可见、可修改RUN command RUN [executableparam1, 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/tcpARGARG 也用来指定变量但是 ARG 并不会像 ENV 一样将变量保存到镜像和容器运行时ARG UBUNTU_VERSION22.10 ARG DEBIAN_VERSIONARG定义之前为空ARG可以指定默认值可以通过 --build-arg 在根据镜像构建容器时可以进行修改USER指定容器运行时的用户默认是 rootUSER 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 表示 healthy1 表示 unhealthy其他的退出码留给 docker 使用所以可以写为HEALTHCHECK --interval5m --timeout3s CMD curl -f http://localhost/ || exit 1ONBUILD设置一个触发器当使用现在构建的镜像作为基础镜像时会触发后面的指令ONBUILD RUN echo on build /tmp/build.txtSTOPSIGNAL发送系统指令到容器需对应到系统的指令编号STOPSIGNAL 9Linux 提供的指令可以通过kill -l查看11:25:49 rootDESKTOP-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 danglingtrue中间镜像是某些镜像所依赖的镜像。虽然这些镜像没有标签但这是 docker 为了加速镜像构建并重复利用镜像资源而采取的方式不应进行删除也没有必要因为相同的层智慧存储一遍。查看中间镜像使用docker images -a。二、镜像原理2.1 bootfs 和 rootfsBootfs 主要包含两部分boot loader 和 kernel后者就是我们常说的 Linux 内核。开机时会首先挂载 bootfs有 boot loader 引导内核加载进内存中之后卸载 bootfs。之后开始以只读的方式挂载 rootfs并进行检查正常后会改为 readwrite 供用户使用2.2 Union FSUnion FS 让容器的创建就像 PS 处理图片时的一个个图层一样最后呈现给上层的只有一层一个完整的文件系统。Union FS 使用了写时拷贝的技术让一层 layer 能够被循环使用节省空间这里的写时拷贝思路上不是实现上可以通过 Linux 父子进程间虚拟地址空间的写时拷贝理解。但是 Union FS 的这种写时拷贝机制势必会造成磁盘写入的效率低下所以对于 MySQL、Redis 这些需要频繁写磁盘的容器往往需要进行卷的挂载不直接使用 Union FS 写数据现代 Docker 在所有 Linux 发行版上使用的存储引擎默认是 overlay2。overlay2 的底层目录称为 lowerdir高层称为 upperdir中间临时层为 workdir统一层为 mergedlowerdir是只读层的镜像其中就包含 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、taptun、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 tap4.2 vethVeth 是 Linux 提供的一个虚拟以太网设备用来让两个隔离的网络命名空间之间可以进行通信。将 veth 直接比作网卡不太恰当更恰当的说法是用网线连起来的一对网卡。veth 是一对互相连接的设备同时各自连接着各自网络命名空间的网络协议栈。一个网络协议栈通过一个 veth 输出的数据会从配对的 veth 原封不动的输出到自己的网络命名空间相对于 tun/tapveth 不需要重复经过网络协议栈性能更高。veth 连接了两个 namespace但是当需要把更多的 namespace 连接起来时就需要其他的方式了。在物理网络中如果需要连接多个设备我们会使用网桥或者叫做交换机Linux 也提供了网桥的虚拟实现4.3 Linux BridgeLinux Bridge 是 Linux 提供的虚拟网桥实现通过brctl进行创建和管理创建后真实和虚拟的网络设备都可以与网桥配合