7.3 容器镜像原理及应用

前面文章中反复提及一个概念 —— 镜像。那镜像到底是什么呢?

所谓的“容器镜像”,其实就是容器内部通过系统调用 chroot/pivot_root 为进程运行切换的根目录,也就是包含了进程所需的库、资源、配置等完整的 rootfs 文件系统。

额外知识

需要明确的是,Linux 操作系统中只有开机启动时会加载指定版本的内核镜像,rootfs 只是一个操作系统包含的文件、目录,并不包含操作系统内核。

因此,同一台宿主机上所有的容器,都共享宿主机操作系统的内核。如果应用程序需要配置内核参数、加载内核模块、与内核进行交互,那么是有很大风险的,这也是容器相比虚拟机主要的缺陷之一。

7.3.1 联合文件系统

Docker 镜像并不是粗暴地把所有的依赖文件封包,而是做了一个巧妙的创新:基于 UnionFS(Union File System,联合文件系统)采用堆叠的方式,对 rootfs 进行分层设计

UnionFS 是什么

UnionFS 技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。

UnionFS 有很多种实现,譬如 OverlayFS、Btrfs 等。在 Linux 内核 3.18 版本中,OverlayFS 代码正式合入 Linux 内核的主分支。在这之后,OverlayFS 也就逐渐成为各个主流 Linux 发行版本里缺省使用的容器文件系统了。

下面,用代码演示 OverlayFS 的工作原理。代码中最后一条指令使用 mount 命令挂载,-t 参数指定挂载后的文件系统类型为 overlay,并包含了 overlay 类型的参数 lowerdir(只读层)、upperdir(读写层)、merged(挂载后,最终呈现给用户视图)。

#!/bin/bash

umount ./merged
rm upper lower merged work -r

mkdir upper lower merged work
echo "I'm from lower!" > lower/in_lower.txt
echo "I'm from upper!" > upper/in_upper.txt
# `in_both` is in both directories
echo "I'm from lower!" > lower/in_both.txt
echo "I'm from upper!" > upper/in_both.txt

// 使用 mount 命令即将 lower、upper 挂载到 merged。

$ sudo mount -t overlay overlay \
 -o lowerdir=./lower,upperdir=./upper,workdir=./work \
 ./merged

挂载后的文件系统视图如图 7-8 所示。


图 7-8 OverlayFS 挂载后的文件系统视图

当在挂载后的 merged 目录里内进行增删改操作时,OverlayFS 文件驱动会执行 CoW(Copy-On-Write,写时复制)策略。

什么是 CoW

写时复制是一种共享和复制文件的策略,可最大程度地提高效率。如果文件或目录位于镜像的较低层中,而另一层(包括可写层)需要对其进行读取访问,则它仅使用现有的已经存在的文件。另一层第一次需要修改文件时(在构建镜像或运行容器时),将文件复制到该层并进行修改。这样可以将I/O和每个后续层的大小最小化

写时复制的大致的流程如下:

  • 新建文件时:文件会出现在 upper 目录中。
  • 删除文件时:
    • 如果删除“in_upper.txt”,文件会在 upper 目录中消失。
    • 如果删除“in_lower.txt”, lower 目录里内的 ”in_lower.txt” 文件不会有变化,但 upper 目录中会增加一个特殊文件表示“in_lower.txt”这个文件不能出现在 merged 里了,通过这种方式表示它已经被删除了。
  • 修改文件:如果修改“in_lower.txt”,会在 upper 目录中新建一个“in_lower.txt”文件,包含更新后的内容,而 lower 中原来的实际文件“in_lower.txt”不会改变。

7.3.2 Docker 镜像的分层设计

下面是 Docker 官方的一张描述文件系统的图片,显示了联合文件系统在镜像层和容器层起到的作用。


图 7-9 使用联合文件系统的 Docker 镜像结构 图片来源open in new window

通过图 7-9 概览启动后的 Docker 镜像,该容器从下往上由 6 个层构成:

  • 最下层的基础镜像 debian stretch;
  • 往上 3 层为 Dockerfile 通过指令 ADD、ENV、CMD 设置 JAVA SDK、环境变量等生成的只读层;
  • Init Layer 夹在只读层和可写层之间,主要存放可能会被修改的 /etc/hosts、/etc/resolv.conf 等文件,这些文件本来属于 debian 镜像,但容器启时,用户往往会写入一些指定的配置,所以 Docker 单独生成了这个层;
  • 最上面的利用 CoW 技术创建的可写层(Read/Write Layer),容器内部的任何增、删、改都发生在这里,但该层的数据不具备持久性,当容器被销毁时,写入的数据也随之消失。不过你也可以通过 docker commit 命令生成一个新的层堆叠到成一个新的 Docker 镜像,而之前的只读层不会有任何变化。


图 7-9 容器镜像层概览

最终,这 6 个层被联合挂载到 /var/lib/docker/aufs/mnt 目录中,表现为一个带有 JAVA JDK 的 debian 操作系统供容器使用。

7.3.3 构建足够小的镜像

构建镜像最有挑战性之一的就是使用镜像尽可能小,小的镜像不论在大规模集群部署、故障转移、存储成本方面都有巨大的优势。

构建足够小的镜像,效果明显的方式有两种:选用精简的基础镜像以及使用多阶段构建

# 第 1 阶段
FROM skillfir/alpine:gcc AS builder01
RUN wget https://nginx.org/download/nginx-1.24.0.tar.gz -O nginx.tar.gz && \
tar -zxf nginx.tar.gz && \
rm -f nginx.tar.gz && \
cd /usr/src/nginx-1.24.0 && \
 ./configure --prefix=/app/nginx --sbin-path=/app/nginx/sbin/nginx && \
  make && make install
  
# 第 2 阶段 只打包最终可执行文件
FROM skillfir/alpine:glibc
RUN apk update && apk upgrade && apk add pcre openssl-dev pcre-dev zlib-dev 

COPY --from=builder01 /app/nginx /app/nginx
WORKDIR /app/nginx
EXPOSE 80
CMD ["./sbin/nginx","-g","daemon off;"]

使用 docker build 命令构建镜像,并查看镜像产物,可以看到生成的镜像大小只有 23.4 MB。

$ docker build -t alpine:nginx .
$ docker images 
REPOSITORY                TAG             IMAGE ID       CREATED          SIZE
alpine                    nginx           ca338a969cf7   17 seconds ago   23.4MB

7.3.4 镜像下载加速

镜像文件从远程仓库下载而来,那么镜像下载的效率会受带宽、镜像仓库瓶颈的影响,这也直接影响到容器的启动时间。

思考如何提高镜像下载的效率?你大概率会想到 P2P 网络加速。Dragonfly 就是基于 P2P 网络实现的容器镜像分发加速系统。

什么是 Dragonfly

Dragonfly 是一款基于 P2P 技术的文件分发和镜像加速系统,它旨在提高大规模文件传输的效率,最大限度地利用网络带宽。在镜像分发、文件分发、日志分发、AI 模型分发以及 AI 数据集分发等领域被大规模使用。

Dragonfly 提供了一种无侵入(不用修改容器、仓库等源码)的镜像下载加速解决方案,它的工作流程如图 7-10 所示。当下载一个镜像或文件时,通过 Peer(类似 p2p 的节点) 中的 HTTP Proxy 将下载请求代理到 Dragonfly。Peer 首先会 Scheduler(类似 p2p 调度器)注册 task。Scheduler 会查看 Task 的信息,判断 Task 是否在 P2P 集群内第一次下载。

  • 第一次下载优先触发 Seed Peer 进行回源下载,并且下载过程中对 Task 基于 Piece 级别切分。Peer 每下载成功一个 Piece,都将信息上报给 Scheduler 供下次调度使用。
  • 如果 Task 在 P2P 集群内非第一次下载,那么 Scheduler 会调度其他 Peer 加速当前的 Peer 中的下载。
  • 最后,Peer 从不同的 Peer 下载 Piece,拼接并返回整个文件,整个 P2P 下载流程就结束了。


图 7-10 Dragonfly 是怎么工作的

7.3.5 镜像启动加速

容器镜像的大小会影响容器启动的时间,譬如 tensorflow 的镜像有 1.83 GB,冷启动这个镜像至少需要 3 分钟的时间。

这么大的镜像启动慢、容器镜像中的文件也并不会被充分利用,业内也研究证明一般镜像只有 6% 的内容会被实际用到[1]

如果能实现按需加载,肯定能大幅降低容器启动时间,这便是 Nydus 出现的背景。

Nydus 是什么

Nydus 是蚂蚁集团、阿里云和字节等共建的开源容器镜像加速项目,是 CNCF Dragonfly 的子项目,Nydus 优化了现有的 OCIv1 容器镜像架构,设计了 RAFS (Registry Acceleration File System) 磁盘格式,最终呈现为一种“文件系统”的容器镜像格式的镜像加速实现。

Nydus 主要优化了镜像中 Layer 数据层的数据结构,将容器镜像文件系统的数据 (blobs)和元数据 (bootstrap) 分离,让原来的镜像层只存储文件的数据部分,并且把文件以 chunk 为粒度分割,每层 blob 存储对应的 chunk 数据。

由于元数据被单独分离出来合为一处,因此对于元数据的访问不需拉取对应的 blob 数据,需要拉取的数据量要小很多,I/O 效率更高。最后再利用 FUSE(Filesystem in Userspace,用户态文件系统)重写文件系统,实现了容器启动后,按需从远端(镜像中心)拉取镜像数据。


图 7-11 Nydus 是怎么工作的

由于使用了按需加载镜像数据的特性,容器的启动时间明显缩短。从官网给到的数据中,Nydus 能够把常见镜像的启动时间,从数分钟缩短到数秒钟。


图 7-12 OCIv1 与 Nydus 镜像启动时间对比

如此,使用 P2P 的方式加速镜像下载,再结合 Nydus 容器启动延迟加载技术实现瞬时启动几百、几千 Pod 的能力,对于大规模集群或者对冷启动扩容延迟要求较高的场景(大促扩容、游戏服务器扩容、函数计算)来说,不仅能大幅降低容器启动时间,还能大量节省网络/存储开销。


  1. 参见 https://indico.cern.ch/event/567550/papers/2627182/files/6153-paper.pdf ↩︎

总字数:2577
Last Updated:
Contributors: isno