Docker容器的优化思路

2022年 9月 30日 31.0k 0

不能忽视对 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 的最小 JRE(Java运行时)。中间构建器阶段将被缓存但不会出现在最终映像中。要将构建的内容添加到最终镜像的话,请使用 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 的镜像创建容器。否则,我们尝试在其他平台系统上面,则根本获取不到对应镜像内容。
  • 为不同系统架构打包不同的镜像,则会导致十分繁琐而且很难维护,所以后来官方引入了 manifest 和 buildx(在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 指令执行后,id 为 my_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-compose、jq、kubectl、docker 等命令,可以考虑直接从他们的镜像里直接 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:[email protected]:dcc529a...51223c19

相关文章

KubeSphere 部署向量数据库 Milvus 实战指南
探索 Kubernetes 持久化存储之 Longhorn 初窥门径
征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
那些年在 Terraform 上吃到的糖和踩过的坑
无需 Kubernetes 测试 Kubernetes 网络实现
Kubernetes v1.31 中的移除和主要变更

发布评论