Docker容器的优化思路


不能忽视对 dockerfile 的优化,但也不要为了优化而优化。

容器优化的思路和方法

  • 镜像构建的过程,视具体业务场景的不同而不同。在很多情况下,我们需要先以满足业务目标为准,而不是镜像的构建层数。如果需要减少镜像层数,一定要选择合适的基础镜像或者创建符合需要的基础镜像。

容器优化的思路和方法


1. 选择基础镜像

缩短构建时间

  • 选择合适产品的基础镜像,这点相对来说非常重要。选择一个合适的基础镜像,需要能够满足运行应用所需要的最小的镜像。理论上是能用小的就不要用大的,能用轻量级的就不要用重量级的,能用性能好的就不要用性能差的,能用稳定版就不要用开发版。

  • 比如我们构建 Java 语言的程序,最好的方式就是使用官方提供的 openjdk:8 作为基础镜像,而非使用比较大的 ubuntu:18.04 作为基础镜像。另外,还有一个需要我们注意的地方就是,尽可能使用官方的特定版本的镜像,而不要使用 latest 这个频繁变动的 tag 作为基础镜像。

FROM ubuntu:18.04

FROM openjdk:latest

FROM openjdk:8

容器优化的思路和方法

容器优化的思路和方法

容器优化的思路和方法


2. 优化指令顺序

缩短构建时间

  • 构建 Docker 镜像的时候,会缓存 Dockerfile 中尚未更改的所有步骤。所以,如果新构建时更改任何指令,将后的指令步骤将会重新来不再使用缓存。举例来说,就是指令 3 发生了变更,其后的 4-n 就会重跑并重新生成缓存。

  • 因此,编写 Dockerfile 的时候,就需要将最不可能产生更改的指令放在前面。比如,可以把 WORKDIR/ENV 等命令放在前面,COPY/ADD 等命令放在后面。这样,在构建过程中较多的使用了缓存,就可以节省很多时间了。

FROM ubuntu:18.04

WORKDIR /opt/app

ARG env=env

ENV ENV=${env}

COPY . /opt/app/

RUN apt update -y htop

容器优化的思路和方法

  • 同时,在使用 COPY/ADD 等命令的时候越具体越好,最好只复制所需的内容。
FROM ubuntu:18.04

WORKDIR /opt/app

COPY target/docker_patch.py /opt/app/

RUN apt update -y htop

容器优化的思路和方法


3. 合并构建指令

缩短构建时间

  • 我们都知道,在编写 Dockerfile 的时候,每一个指令都会创建一层并构成新的镜像。当运行多个指令时,会产生非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。因此,在这种情况下,我们需要将同类型的指令合并然后再一起运行。合并指令时,一定要注意格式(比如换行、缩进、注释等)会让维护、排障更为容易。
RUN apt update -y && \
    DEBIAN_FRONTEND=noninteractive apt install --no-install-recommends -y \
        python3.6-dev \
        python3-distutils \
        nginx \
        vim.tiny \
        supervisor \
        ca-certificates \
        tzdata \
        locales \
        build-essential \
        fontconfig

容器优化的思路和方法


4. 清理中间结果

减少镜像大小

  • 这点很易于理解,通常来讲,体积更小,部署更快!因此在构建过程中,我们需要清理那些最终不需要的代码或文件。比如说,临时文件、源代码、缓存等等。
rm -rf /var/lib/apt/lists/* /tmp/*
rm -rf /etc/nginx/sites-enabled/default

容器优化的思路和方法


5. 减少冗余文件

减少镜像大小

  • 使用 .dockerignore 文件用于忽略那些镜像构建时非必须的文件,这些文件可以是开发文档、日志、其他无用的文件。注意再添加文件或者文件夹的时候,最好的方式是先全部排除,之后再添加。
*
.*
!config/config-docker.yml
!config/logging.conf
!docker/nginx
!docker/supervisor
......

6. 使用 runtime 镜像

提高可维护性

  • 当我们维护的项目越来越大的时候,使用单独的 Dockerfile 文件来维护就变得越来越笨重,且构建时间也越来越长。为了便捷且高效的管理,就需要我们构建基本的 runtime 镜像。在 runtime 镜像中,主要包含项目所依赖的基础环境包,多为很少改动的内容。而我们的项目就不用从基础镜像开始,而是从 runtime 镜像开始,大大的加速的构件速度。
FROM ubuntu:18.04

ENV LANG=en_US.UTF-8

WORKDIR /opt/app

COPY misc/prod-requirements.txt /opt/app/misc/prod-requirements.txt

RUN sed -i s/archive.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list && \
    apt update -y && \
    DEBIAN_FRONTEND=noninteractive apt install --no-install-recommends -y \
        python3.6-dev \
        python3-distutils \
        nginx \
        vim.tiny \
        supervisor \

7. 使用多段构建

提高可维护性

  • Docker v17.05 开始支持多阶段构建。多段构建的目的就是,使用多阶段构建来删除构建依赖项。容器多阶段构建可由多个 FROM 语句构成,每个 FROM 语句开始一个新的阶段。它们可以用 AS 关键字进行别名命名。我们用它来别名作为我们的第一阶段构建器,以便稍后引用,它将在一致的环境中包含所有构建依赖项。

  • 第二阶段是我们的最后阶段,将产生最终镜像包。它将包括运行时的严格必要条件,在本例中是基于 Alpine 的最小 JREJava运行时)。中间构建器阶段将被缓存但不会出现在最终映像中。要将构建的内容添加到最终镜像的话,请使用 COPY --from=STAGE_NAME 参数进行制定。

容器优化的思路和方法

  • 只构建某一阶段的镜像
# 使用as来为某一阶段命名
FROM golang:1.9-alpine as builder

# 当我们只想构建builder阶段的镜像时,增加--target=builder参数即可
$ docker build --target builder -t username/imagename:tag .
  • 构建时从其他镜像复制文件
# 我们也可以复制任意镜像中的文件
$ COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

8. 构建多种系统架构

使用 buildx 构建多种系统架构支持的 Docker 镜像

  • 我们知道使用镜像创建一个容器,该镜像必须与 Docker 宿主机系统架构一致,例如 Linux x86_64 架构的系统中只能使用 Linux x86_64 的镜像创建容器。否则,我们尝试在其他平台系统上面,则根本获取不到对应镜像内容。

  • 为不同系统架构打包不同的镜像,则会导致十分繁琐而且很难维护,所以后来官方引入了 manifestbuildx(在Docker19.03+版本引入) 两个子命令来处理上述问题。但是,BuildKit 是下一代的镜像构建组件。

# 可以看到现在都处于试验阶段
$ docker buildx build xxx
docker manifest is only supported on a Docker cli with experimental cli features enabled

# 命令属于实验特性必须设置环境变量
$ export DOCKER_CLI_EXPERIMENTAL=enabled
  • 由于 Docker 默认的 builder 实例不支持同时指定多个 --platform,我们必须首先创建一个新的 builder 实例。
$ docker buildx create --name mybuilder
$ docker buildx use mybuilder
  • 构建镜像之前,我们首先需要创建所需的 Dockerfile 文件。使用 buildx 命令构建镜像,注意将 myusername 替换为自己的 Docker Hub 用户名,而 --push 参数表示将构建好的镜像推送到 Docker 仓库。
# 构建多平台镜像
$ docker buildx build --platform \
    linux/arm,linux/arm64,linux/amd64 \
    -t <myusername>/hello . --push

# 查看镜像信息
$ docker buildx imagetools inspect <myusername>/hello
  • 在不同架构运行该镜像,可以得到该架构的信息。
# arm
$ docker run -it --rm myusername/hello
Linux buildkitsandbox 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2019 armv7l Linux

# arm64
$ docker run -it --rm myusername/hello
Linux buildkitsandbox 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2019 aarch64 Linux

# amd64
$ docker run -it --rm myusername/hello
Linux buildkitsandbox 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2019 x86_64 Linux

9. 使用 Buildx 优化构建

使用 BuildKit 提供的 Dockerfile 新指令来更快的构建 Docker 镜像。 => 官网地址

  • RUN --mount=type=cache
  • RUN --mount=type=bind
  • RUN --mount=type=tmpfs
  • RUN --mount=type=secret
  • RUN --mount=type=ssh
  • 目前,几乎所有的程序都会使用依赖管理工具,例如 Node.js 中的 npm 等等,当我们构建一个镜像时,往往会重复的从互联网中获取依赖包,难以缓存,大大降低了镜像的构建效率。

  • 例如一个前端工程需要用到 npm 工具,使用多阶段构建,构建的镜像中只包含了目标文件夹 dist,但仍然存在一些问题,当 package.json 文件变动时,RUN npm i && rm -rf ~/.npm 这一层会重新执行,变更多次后,生成了大量的中间层镜像。

FROM node:alpine as builder

WORKDIR /app

COPY package.json /app/

RUN npm i --registry=https://registry.npm.taobao.org \
        && rm -rf ~/.npm

COPY src /app/src

RUN npm run build

FROM nginx:alpine

COPY --from=builder /app/dist /app/dist
  • 为解决这个问题,进一步的我们可以设想一个类似 数据卷 的功能,在镜像构建时把 node_modules 文件夹挂载上去,在构建完成后,这个 node_modules 文件夹会自动卸载,实际的镜像中并不包含 node_modules 这个文件夹,这样我们就省去了每次获取依赖的时间,大大增加了镜像构建效率,同时也避免了生成了大量的中间层镜像。BuildKit 提供了 RUN --mount=type=cache 指令,可以实现上边的设想。

  • 第一个 RUN 指令执行后,idmy_app_npm_module 的缓存文件夹挂载到了 /app/node_modules 文件夹中。多次执行也不会产生多个中间层镜像。

  • 第二个 RUN 指令执行时需要用到 node_modules 文件夹,node_modules 已经挂载,命令也可以正确执行。

  • 第三个 RUN 指令将上一阶段产生的文件复制到指定位置,from 指明缓存的来源,这里 builder 表示缓存来源于构建的第一阶段,source 指明缓存来源的文件夹。

Option Description
id id 设置一个标志,以便区分缓存。
source 来源的文件夹路径。
target (必填) 缓存的挂载目标文件夹。
from 缓存来源(构建阶段),不填写时为空文件夹。
ro,readonly 只读,缓存文件夹不能被写入。
sharing shared private locked 值可供选择。sharing 设置当一个缓存被多次使用时的表现,由于 BuildKit 支持并行构建,当多个步骤使用同一缓存时(同一 id)会发生冲突。shared 表示多个步骤可以同时读写,private 表示当多个步骤使用同一缓存时,每个步骤使用不同的缓存,locked 表示当一个步骤完成释放缓存后,后一个步骤才能继续使用该缓存。
FROM node:alpine as builder

WORKDIR /app

COPY package.json /app/

RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
    --mount=type=cache,target=/root/.npm,id=npm_cache \
        npm i --registry=https://registry.npm.taobao.org

COPY src /app/src

RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
# --mount=type=cache,target=/app/dist,id=my_app_dist,sharing=locked \
        npm run build

FROM nginx:alpine

# COPY --from=builder /app/dist /app/dist

# 为了更直观的说明 from 和 source 指令,这里使用 RUN 指令
RUN --mount=type=cache,target=/tmp/dist,from=builder,source=/app/dist \
    # --mount=type=cache,target/tmp/dist,from=my_app_dist,sharing=locked \
    mkdir -p /app/dist && cp -r /tmp/dist/* /app/dist

10. 二进制工具安装

二进制文件安装和使用都相对来说很简单,考虑自己维护还是依赖第三方镜像了!- 来自

  • 当我们需要安装一个二进制(binary)工具的时候,比如 docker-composejqkubectldocker 等命令,可以考虑直接从他们的镜像里直接 COPY 过来,替代使用 wget/curl 下载安装的方式。
    • 简洁高效,省去下载、解压、chmod、清理临时文件等操作
    • 可以无缝适配多 CPU 体系架构
    • 可以充分利用 docker build 的缓存
    • 避免在 base image 中引入 wget/curl/tar/gzip/ca-certificates/openssl 这类工具的依赖
    • OCI Artifacts 更加云原生
# docker
COPY --from=docker:20.10.12-dind-rootless /usr/local/bin/docker /usr/local/bin/docker
FROM golang:1.17

ARG GOLANG_LINT_VERSION=V1.43.0

ENV GOV PATH=/usr/local/bin/govc

COPY --from=vmware/govc:v0.27.2 /govc /usr/local/bin
CoPY --from=dtzar/helm-kubectl:3.8.0 /usr/local/bin /usr/local/bin
COPY --from=koalaman/shellcheck:stable /bin/shellcheck /usr/local/bin/shellcheck
COPY -from=docker: 20.10.12-dind-rootless /usr/local/bin/docker /usr/local/bin/docker
COPY --from=hashicorp/packer:1.8 /bin/packer /usr/local/bin
COPY --from=quay.io/argoproj/argocli:v3.2.6 /bin/argo /usr/local/bin
  • 虽然有很多好处,但是容易被供应链攻击,特别是国内受限于 DockerHub 被墙的环境下,大家都会依赖五花八门的 Registry 加速,而 Docker 默认对于镜像的 checksum 支持很差,容易被加料一波带走。而且国内大部分用 curl 的研发也没 checksum 的习惯。—— @Manjusaka_Lee
# 镜像加MD5校验
docker:20.10.15-dind-rootless@sha256:dcc529a...51223c19

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录