深度理解 Docker 镜像与容器
后知后觉 暂无评论

容器的使用离不开镜像,创建的容器实例的资源占用在很大程度上也取决于镜像的质量。

镜像结构

说到镜像,很容易想到之前的 CD/DVD 光盘,还有很是常用的 .iso 格式的镜像文件。在这种镜像中一般是不可修改的只读文件,比如一般都叫 CD 为 CD-ROM ,是指文件的内容不可擦写。

容器的镜像与之类似,却又不完全一致,容器的镜像是分层的,容器的镜像并不是一个单纯的“文件夹”,而是一个多层覆盖后的层级“文件夹”,如下图所示。

镜像分层(!AVIF)

为什么要设计为分层的镜像结构呢?这个其实很好理解,如下图:

“正视图”(!AVIF)

如果有过 Photoshop 之类的软件使用经验,可以很直观得将其理解为其中的图层,不同的图层进行覆盖,最后得出整体的效果,即:如果每个层中的存在相同的文件,则“上层”的文件会覆盖“下层”的文件,层级结构的最大好处也就显而易见了,那就是重复利用,不同的应用镜像,虽然可能存在区别,但是运行环境和系统环境的大部分文件可能是相同的,因此分层设计可以很大程度上实现复用,并且可以大幅度节约存储镜像所使用的仓库的磁盘空间

举个例子:拉取某个镜像可以看到以下信息

$ sudo docker image pull nginx:1.16.1
1.16.1: Pulling from library/nginx
68ced04f60ab: Pull complete
e6edbc456071: Pull complete
9f915b082e59: Pull complete
Digest: sha256:bfb2cb1b47766fc424a7826d5ae79dc3aa70a8bbb697de7e683b965d47eb4295
Status: Downloaded newer image for nginx:1.16.1
docker.io/library/nginx:1.16.1

可以看到其中含有三个镜像层:

运行原理

可以看出来镜像是“死的”,也就是不可写,这个特点在实际使用中有很大的局限性,比如程序运行所需的配置文件、运行产生的日志等都需要写入,因此容器的镜像引入了读写层的概念,在运行容器时,会在基础的镜像上添加一个可读写层,如上图中的 writable Container ,将程序运行所需的配置或者日志吞吐到此层中。

注意:此层为临时层,不会永久保留,容器停止/删除等操作都会导致此层丢失。

用立体视图来描述如下:当系统启动多个容器时,会在内核上加载基础镜像,然后加载镜像的各个功能层,最后在上面覆盖可写层

立体镜像层(!AVIF)

如果只运行一个容器,当然就只有一个柱状结构。

立体只读层(!AVIF)

因此在实际使用中都会将宿主机目录挂载至容器内,或者使用数据卷(数据卷可以理解为一个专为容器设计的长期存储池,可挂载至容器内,甚至可以多个容器挂载同一数据卷,数据卷内的文件长期保留,占用宿主机磁盘空间)进行数据持久化。

小贴士:到目前为止,容器化的文件存储还是存在一定的性能问题,因此不建议读写密集型程序部署至容器环境内,比如数据库是否应该部署至容器环境在 Reddit 上就饱受争议。

容器化目前最适合部署那种微量读写需求,不涉及数据处理和存储的程序使用(个人见解),对于非生产环境或轻压力服务,还是建议使用容器环境的,容易处理,方便部署,但是引入新中间层可能带来更多的中间层 bug,比如之前的内核问题导致的容器内网通讯bug。

镜像制作

接下来简单谈谈了解了以上内容后,对于镜像制作的优化和建议。

底层选择

前面说了,底层镜像是镜像的基础,因此决定了最终生产镜像的大小,以下为三种常用底层镜像的大小。

$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
debian              buster-slim         2f14a0fb67b9        12 days ago         69.2MB
ubuntu              bionic              72300a873c2c        2 weeks ago         64.2MB
alpine              latest              e7d92cdc71fe        7 weeks ago         5.59MB
小贴士:以上内容补充于 20200309 ,镜像大小仅供参考。

再来看看很多容器新手习惯使用的 CentOS 大小,不用继续说了吧。

$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              7                   5e35e350aded        3 months ago        203MB

对于底层镜像的选择,遵循以下原则:优先使用 Alpine ,因为实际上 Alpine 的镜像实际上只是一个极度精简的内核,因此对于部分依赖系统底层实现的功能可能存在限制或者困难,这种情况下可以考虑使用 Debian 或者 Ubuntu 来使用。

FROM debian:buster-slim

LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"

ENV NGINX_VERSION   1.16.1
ENV NJS_VERSION     0.3.8
ENV PKG_RELEASE     1~buster

RUN set -x \
# create nginx user/group first, to be consistent throughout docker variants
    && addgroup --system --gid 101 nginx \
    && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx \
    && apt-get update \
    && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates \
    && \
    NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
    found=''; \
    for server in \
        ha.pool.sks-keyservers.net \
        hkp://keyserver.ubuntu.com:80 \
        hkp://p80.pool.sks-keyservers.net:80 \
        pgp.mit.edu \
    ; do \
        echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
        apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
    done; \
    test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
    apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \
    && dpkgArch="$(dpkg --print-architecture)" \
    && nginxPackages=" \
        nginx=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-${PKG_RELEASE} \
    " \
    && case "$dpkgArch" in \
        amd64|i386) \
# arches officialy built by upstream
            echo "deb https://nginx.org/packages/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \
            && apt-get update \
            ;; \
        *) \
# we're on an architecture upstream doesn't officially build for
# let's build binaries from the published source packages
            echo "deb-src https://nginx.org/packages/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \
            \
# new directory for storing sources and .deb files
            && tempDir="$(mktemp -d)" \
            && chmod 777 "$tempDir" \
# (777 to ensure APT's "_apt" user can access it too)
            \
# save list of currently-installed packages so build dependencies can be cleanly removed later
            && savedAptMark="$(apt-mark showmanual)" \
            \
# build .deb files from upstream's source packages (which are verified by apt-get)
            && apt-get update \
            && apt-get build-dep -y $nginxPackages \
            && ( \
                cd "$tempDir" \
                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
                    apt-get source --compile $nginxPackages \
            ) \
# we don't remove APT lists here because they get re-downloaded and removed later
            \
# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies)
            && apt-mark showmanual | xargs apt-mark auto > /dev/null \
            && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \
            \
# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be)
            && ls -lAFh "$tempDir" \
            && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \
            && grep '^Package: ' "$tempDir/Packages" \
            && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \
# work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes")
#   Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
#   ...
#   E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages  Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
            && apt-get -o Acquire::GzipIndexes=false update \
            ;; \
    esac \
    \
    && apt-get install --no-install-recommends --no-install-suggests -y \
                        $nginxPackages \
                        gettext-base \
    && apt-get remove --purge --auto-remove -y ca-certificates && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
    \
# if we have leftovers from building, let's purge them (including extra, unnecessary build deps)
    && if [ -n "$tempDir" ]; then \
        apt-get purge -y --auto-remove \
        && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
    fi

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log

EXPOSE 80

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

上面的 Dockerfile 是 NGINX 的官方镜像,安装过程可供参考,然而其中的逻辑和写法很值得学习,可以看到上文中的环境的部署和删除都是一条命令中进行实现。

小贴士:上面关于镜像层级的问题解释过,每条命令都会产生一个镜像层,因此如果在上一层中添加了文件,在后一层中删除,这样制作出来的镜像还是会保留此文件,然后在后一层中生成一个删除文件的指针,因此在最后的镜像中无法看到这个文件,但是在层级文件中依然存在其影子,这样生成的镜像体积还是会撑大。

由此可见对于制作镜像所需要的临时文件应该在同一层中使用并删除,这样可以生成最佳体积的镜像文件。体现在 Dockerfile 中就是将命令写在同一条中,使用 && 进行连接。


附录

参考链接

本文撰写于一年前,如出现图片失效或有任何问题,请在下方留言。博主看到后将及时修正,谢谢!
禁用 / 当前已拒绝评论,仅可查看「历史评论」。