添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
发财的太阳  ·  GitHub - ...·  1 月前    · 
勤奋的鸭蛋  ·  RUN Instruction Using ...·  1 月前    · 
小眼睛的火车  ·  How to run Docker ...·  3 周前    · 
爽快的伤痕  ·  组件问题 - JumpServer 文档·  6 天前    · 
憨厚的感冒药  ·  升级 ...·  4 月前    · 
干练的充电器  ·  observablecollection ...·  5 月前    · 
·  阅读

当我们实现一个高度可水平扩展的微服务架构时,需要考虑到一个关键的点: 当前端的访问压力突增的时候,我们的微服务群怎么快速的增加对应的容器的数量,通过达到足够数量的容器来应对的压力. 短时间扩展出大量后端服务资源,需要考虑架构和运维的方方面面,其中通过减少Docker镜像的体积,在同样的网络带宽的情况下就能缩短镜像的拉取时间,进而缩短整个容器的部署就绪时间。

根据 Best practices for writing Dockerfiles Best Practices - Running your application with Amazon ECS 这2篇文章,我们对docker镜像瘦身的几种技巧做一次梳理。

总而言之我们有下面的几种方式来共同作用减少容器的体积:

  • 使用合适的小体积的基础镜像
  • 减少docker镜像的layer的数量
  • 镜像只包含程序运行所需的最小的资源
  • 排除不必要的资源
  • 按照程序语言的运行特性做对应的资源裁剪
  • 使用多阶段构建技术
  • 使用合适的小体积的基础镜像

    这个是最容易做到并且很容易理解的一种常见策略,也就是将 FROM xxx 语句中使用的基础镜像在满足程序运行性能和稳定性的前提下越小越好。

    举个例子,我有一个Java项目需要运行,那么我需要引入一个openjdk镜像做基础镜像,那么我们就需要考虑用那种openjdk镜像做为基础最合适.

    下面我列举如下的几个选项,这些选项可以在 docker hub openjdk 上检索到.

    TAG Compressed Size
    openjdk:11-jre-slim 76.39 MB
    openjdk:11-jre 117.76 MB
    openjdk:11-slim 225.52 MB
    openjdk:11 318.71 MB
    openjdk:17 231.66 MB
    openjdk:17-slim 210.62 MB
    openjdk:17-alpine 181.71 MB

    很明显,我们当我们运行的项目需要 JDK11 的情况下,那么 openjdk:11-jre-slim 是最佳选择,如果我们的项目是 JDK17 的情况下,那么 openjdk:17-slim 是最佳选择。

    为什么我不建议使用 openjdk:xxx-alpine 选项呢?

    This image is based on the popular Alpine Linux project, available in the alpine official image. Alpine Linux is much smaller than most distribution base images (~5MB), and thus leads to much slimmer images in general.

    以alpine linux为基础的openjdk在大多数情况下,的确是有最小的体积,但是不是绝对的;java程序运行只需要jre并不一定需要全套的JDK环境,在有jre版本的镜像面前,alpine linux为基础的镜像不一定最小,比如 openjdk:11-jre-slim 在上表中是最小的基础镜像

    The OpenJDK port for Alpine is not in a supported release by OpenJDK, since it is not in the mainline code base. It is only available as early access builds of OpenJDK Project Portola. See also this comment. So this image follows what is available from the OpenJDK project's maintainers.

    What this means is that Alpine based images are only released for early access release versions of OpenJDK. Once a particular release becomes a "General-Availability" release, the Alpine version is dropped from the "Supported Tags"; they are still available to pull, but will no longer be updated.

    不优先选alpine的原因是如上的2个说明:

  • 适配alpine的openjdk并不是官方支持的,并不在openjdk的主线代码中,它一般在openjdk GA阶段前能被早期的测试和验证,某个openjdk LTS版本GA后,后续的维护力度不大. 所以java运行的可靠性和性能并不一定有官方的严格测试和保证
  • alpine版本openjdk只在该版本的GA前的早期构建阶段被构建和释放,在该版本正式GA后,这个TAG只能被保留并拉取,但是并未更新了
  • slim jre 版本的是什么含义?

    首先, jre 版本表示对java运行环境做裁剪,jre是java的基础可执行环境,它移除了JDK中不需要的开发套件,进而节约了体积,通常我们要有限选择 jre 版本的基础镜像,完全不会影响java程序的运行指标.

    其次, slim 版本表示对linux做裁剪,相比完整的linux它移除了容器运行中几乎不可能使用到的一些linux组件,比如 curl 等shell命令就由于裁剪可能不会预装进去

    总结就是,最好是选择 jre slim 后缀的基础镜像,它同时对linux和JDK做裁剪,能最小化镜像的体积而不影响java程序的运行性能

    普通的镜像的价值在哪里?

    不带 jre slim alpine 后缀的openjdk镜像我成为普通镜像,它有完整的JDK和完整的Linux环境,在大部分情况下是我们最无脑的选择,如果你不知道怎么选,选它就准没错,当然镜像的体积就大很多,它有一个优势就是全面,比如有完整的linux包管理和shell工具,能让你做一些细节的操作,比如健康检查就可能用到 curl ,如果你使用 slim 版本的就可能由于缺失 curl 而导致健康检查被误认为不通过,当然最好是你在 slim 上安装对应的软件,这样既有 slim 的小体积优势也能满足你的细节需要。

    减少docker镜像的layer的数量

    Docker镜像是由很多镜像层(Layers)组成的(最多127层),Dockerfile中的每条指定都会创建镜像层,但是只有 RUN , COPY , ADD 会使镜像的体积增加.

    Only the instructions RUN, COPY, ADD create layers. Other instructions create temporary intermediate images, and do not increase the size of the build.

    所以我们在使用上面讲的三个指令的使用,就需要特别注意尽量把它们合并为一条shell语句,而不是每个语句一行

    例如下面的用法

    RUN apt update -y RUN apt install curl -y

    我们就可以合并为

    RUN apt update -y && apt install curl -y
    

    这样在理论上减少了一个layer. 此外对于上面的2个指令,合并还能获取避免意料之外的问题

    可能的问题 best-practices/#run

    Using apt-get update alone in a RUN statement causes caching issues and subsequent apt-get install instructions fail

    Docker sees the initial and modified instructions as identical and reuses the cache from previous steps. As a result the apt-get update is not executed because the build uses the cached version. Because the apt-get update is not run, your build can potentially get an outdated version of the curl and nginx packages.

    所以合并指令是一个百利无一害的方式

    镜像只包含程序运行所需的最小的资源

    我们在使用Dockerfile构建docker镜像的时候,可能会产生一些中间文件或者由于考虑步骤意外的引入了程序运行无关的文件,这个也需要考虑

    首先是排除不必要的资源

    我们的程序只需要它需要的资源,一切与他无关的文件,比如某些图片,文档等,我们可以使用.dockerignore.排除出去,原理和.gitignore一样

    按照程序语言的运行特性做对应的资源裁剪

    有时我们在构建的时候需要安装某种linux工具,比如为有些slim版本的linux安装curl执行内部的健康检查,这是我们执行apt/dnf包管理软件的时候,可能无意间产生了大量的临时缓存,这很容易被无意间打包到镜像中,所以我建议使用结合指令合并技术加上rm -rf /var/lib/apt/lists/*来立即在软件安装完后清理缓存,比如我安装curl时会这样写RUN apt update -y && apt install -y curl && rm -rf /var/lib/apt/lists/*

    此外在经验不足的情况下,我们没有具体的语言具体分析,比如golang它是一种纯粹的编译型语言,在程序被编译出来后,它只需要linux基础环境即可运行

    有些人可能会这样写FROM golang:1.17并以为它作为golang的运行环境是最合适的,其实未必,golang编译出的二进制可执行程序,只需linux环境即可,我们就可以先编译golang并FROM debian:bullseye-slim 即可,它能有更小的体积并完全不影响正常的运行性能.

    还有就是nodejs,python3这类解释性语言,我们需要在运行前执行包管理程序下载依赖包,然后程序才能运行,但是实际上程序的运行只需要有程序本身和依赖包即可,包管理器和包管理器执行过程产生的缓存以及临时文件是我们不需要的,我们也可以使用分阶段构建技术(后文会讲)来在后续的阶段移除这些运行无关的文件,哪怕它们在构建早期是如此重要。

    此外解释性语言在安装包时可能会附带一些调试和附加工具,它们在开发阶段很实用但是正式环境运行时不再必要,我们最好也是要注意一下只安装生产环境级别的依赖,排除一些辅助性的开发组件。

    In the case of building a Docker image for production we want to ensure that we only install production dependencies in a deterministic way, and this brings us to the following recommendation for the best practice for installing npm dependencies in a container image: RUN npm ci --only=production

    参考: 10 best practices to containerize Node.js web applications with Docker

    使用多阶段构建技术

    multi-stage builds是我们构建过程中相对来将复杂一段的技术,但是也异常的实用,比如前面讲的nodejs等解释性语言就可以充分利用本技术来瘦身。当然java,go等编译性语言也能受益于此技术

    它的核心思想是:将Docker镜像的构建分为多个阶段,下一个阶段依赖上一个阶段的输出,将上一个阶段的输出作为输入来排除掉上个阶段构建过程中无法排除的废弃文件

    通常会分为2个大致的阶段 编译/依赖下载->实际的镜像构建

    以官方的例子为例

    golang的构建,第一阶段使用golang基础镜像做依赖的下载和编译工作,第二阶段只复制上一阶段产生的二进制文件并使用更加精简的debian scratch镜像作为实际的运行环境

    # syntax=docker/dockerfile:1
    FROM golang:1.16-alpine AS build
    # Install tools required for project
    # Run `docker build --no-cache .` to update dependencies
    RUN apk add --no-cache git
    RUN go get github.com/golang/dep/cmd/dep
    # List project dependencies with Gopkg.toml and Gopkg.lock
    # These layers are only re-built when Gopkg files are updated
    COPY Gopkg.lock Gopkg.toml /go/src/project/
    WORKDIR /go/src/project/
    # Install library dependencies
    RUN dep ensure -vendor-only
    # Copy the entire project and build it
    # This layer is rebuilt when a file changes in the project directory
    COPY . /go/src/project/
    RUN go build -o /bin/project
    # This results in a single layer image
    FROM scratch
    COPY --from=build /bin/project /bin/project
    ENTRYPOINT ["/bin/project"]
    CMD ["--help"]
    

    nodejs构建,第一步下载依赖,第二步只将上一阶段的依赖复制过来,排除了上一阶段的npm和npm运行过程中的中间文件,同时使用更精简的slim镜像来提供实际的运行环境

    FROM node:14 AS build
    WORKDIR /srv
    ADD package.json .
    RUN npm install
    FROM node:14-slim
    COPY --from=build /srv .
    ADD . .
    EXPOSE 3000
    CMD ["node", "index.js"]
    

    通过这2个例子,相信大家可以清楚的理解多阶段构建对编译型/解释性语言都有很好的瘦身效果。

    分类:
    后端
    标签: