• Docker容器的优化思路🐟



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

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

    1. 选择基础镜像

    缩短构建时间

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

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

     
    dockerfile
    FROM ubuntu:18.04
    
    FROM openjdk:latest
    
    FROM openjdk:8
    
    容器优化的思路和方法
    容器优化的思路和方法
    容器优化的思路和方法
    容器优化的思路和方法
    容器优化的思路和方法
    容器优化的思路和方法

    2. 优化指令顺序

    缩短构建时间

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

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

     
    dockerfile
    FROM ubuntu:18.04
    
    WORKDIR /opt/app
    
    ARG env=env
    
    ENV ENV=${env}
    
    COPY . /opt/app/
    
    RUN apt update -y htop
    
    容器优化的思路和方法
    容器优化的思路和方法
    • 同时,在使用 COPY/ADD 等命令的时候越具体越好,最好只复制所需的内容。
     
    bash
    FROM ubuntu:18.04
    
    WORKDIR /opt/app
    
    COPY target/docker_patch.py /opt/app/
    
    RUN apt update -y htop
    
    容器优化的思路和方法
    容器优化的思路和方法

    3. 合并构建指令

    缩短构建时间

    • 我们都知道,在编写 Dockerfile 的时候,每一个指令都会创建一层并构成新的镜像。当运行多个指令时,会产生非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。因此,在这种情况下,我们需要将同类型的指令合并然后再一起运行。合并指令时,一定要注意格式(比如换行、缩进、注释等)会让维护、排障更为容易。
     
    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. 清理中间结果

    减少镜像大小

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

    5. 减少冗余文件

    减少镜像大小

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

    6. 使用 runtime 镜像

    提高可维护性

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

    容器优化的思路和方法
    容器优化的思路和方法
    • 只构建某一阶段的镜像
     
    bash
    # 使用as来为某一阶段命名
    FROM golang:1.9-alpine as builder
    
    # 当我们只想构建builder阶段的镜像时,增加--target=builder参数即可
    $ docker build --target builder -t username/imagename:tag .
    
    • 构建时从其他镜像复制文件
     
    bash
    # 我们也可以复制任意镜像中的文件
    $ 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 是下一代的镜像构建组件。

     
    bash
    # 可以看到现在都处于试验阶段
    $ 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 实例。
     
    bash
    $ docker buildx create --name mybuilder
    $ docker buildx use mybuilder
    
    • 构建镜像之前,我们首先需要创建所需的 Dockerfile 文件。使用 buildx 命令构建镜像,注意将 myusername 替换为自己的 Docker Hub 用户名,而 --push 参数表示将构建好的镜像推送到 Docker 仓库。
     
    bash
    # 构建多平台镜像
    $ docker buildx build --platform \
        linux/arm,linux/arm64,linux/amd64 \
        -t <myusername>/hello . --push
    
    # 查看镜像信息
    $ docker buildx imagetools inspect <myusername>/hello
    
    • 在不同架构运行该镜像,可以得到该架构的信息。
     
    bash
    # 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 这一层会重新执行,变更多次后,生成了大量的中间层镜像。

     
    bash
    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 指明缓存来源的文件夹。

    OptionDescription
    id id 设置一个标志,以便区分缓存。
    source 来源的文件夹路径。
    target (必填) 缓存的挂载目标文件夹。
    from 缓存来源(构建阶段),不填写时为空文件夹。
    ro,readonly 只读,缓存文件夹不能被写入。
    sharing 有 shared private locked 值可供选择。sharing 设置当一个缓存被多次使用时的表现,由于 BuildKit 支持并行构建,当多个步骤使用同一缓存时(同一 id)会发生冲突。shared 表示多个步骤可以同时读写,private 表示当多个步骤使用同一缓存时,每个步骤使用不同的缓存,locked 表示当一个步骤完成释放缓存后,后一个步骤才能继续使用该缓存。
     
    bash
    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 更加云原生
     
    dockerfile
    # docker
    COPY --from=docker:20.10.12-dind-rootless /usr/local/bin/docker /usr/local/bin/docker
    
     
    dockerfile
    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
     
    bash
    # 镜像加MD5校验
    docker:20.10.15-dind-rootless@sha256:dcc529a...51223c19
    

     
     
     
  • 相关阅读:
    mysql死锁问题分析
    你应该知道的RPC原理
    如何健壮你的后端服务?
    如何用消息系统避免分布式事务?
    一个故事讲清楚NIO
    地图匹配实践
    利用模拟退火提高Kmeans的聚类精度
    大数据并行计算利器之MPI/OpenMP
    GPU---并行计算利器
    如何设计实现一个地址反解析服务?
  • 原文地址:https://www.cnblogs.com/haoee/p/16285011.html
Copyright © 2020-2023  润新知