9f08803c4452728ab9495acf61b310fd.png

最近项目上 k8s,用 jenkins 做 CI,跑得比较顺了,开始考虑优化。

主要有两个问题:

  1. docker build 耗时长。
    我们把 docker build 单独放在一个 stage, 所以很容易从 Jenkins Stage View 看出这个 stage 最耗时
  2. 构建后的 image 很大,超过 1G

接下来谈谈我们是怎么优化

构建速度

想要优化 docker build,我们就需要知道构建过程每一个步骤的耗时。经过 Google,很容易找到了方法:

计算 dockerfile 每一条 instruction 的耗时

# 1. 开启这个实验性 flag
export DOCKER_BUILDKIT=1

# 2. 添加参数 --progress plain 获得更加直观的输出结果
docker build -t YOUR_IMAGE_NAME:YOUR_TAG -f YOUR_DOCKERFILE --progress plain .

从输出结果我们发现最耗时的命令是 npm install,不出意外

有什么办法可以优化?我们知道,复用 node_modules 缓存可大大减少 npm install 时间。我们可以从两个方向做到这一点。

Solution 1:基于历史最新的业务镜像构建

比如你的镜像名叫 exampledockerfile 可能涨这样:

FROM node

我们可以已有的 example 镜像(因为它已经包含 node_modules)作为基础镜像:

FROM example

那第一次构建时,example 并不存在怎么办?我们需要一点点脚本来做判断:

YOUR_IMAGE_NAME_TAG=example
# 如果 demo 存在,用 demo 做基础镜像,否则用 node
base=$(DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect $YOUR_IMAGE_NAME_TAG > /dev/null 2>&1 && echo $YOUR_IMAGE_NAME_TAG || echo "node")

# 作为参数传给 docker build
docker build --build-arg base=$base ...

对应的 Dockerfile

# 给个默认值,坐下兼容
ARG base=node
FROM $base

这种方式可以适用于任何语言,不限于 Node.js。但是两个点需要注意:

  1. 需要考虑什么时候更新 :latest tag。
    我们并不希望基础镜像是一个不稳定的版本
  2. 基础镜像本身不 clean
    其实我们复用了一切,包含构建过程中产生的文件,不仅仅是 node_modules 目录

Solution 2:利用 build cache

官方的 dockerfile 最佳实践将缓存和指令之间的相互作用讲得很清楚了,简单理解就是:

  1. Dockerfile 每一条指令会产生一个新的 layer
  2. 如果当前指令将要产生layer已经在 cache 里,那就直接复用缓存,而不是重复创建 layer指令本身也不会再次执行
  3. 如何判断是否在 cache 里?
    特殊指令 ADDCOPY,docker 会对文件系统发生的变更(增删改)做一次 checksum,然后去缓存中匹配这个 checksum
    其余指令,就是简单的比较一下指令本身有没变化,如指令 RUN your_command 就是比较 your_command 部分有没有变化(就是这么简单)
  4. 如果某个指令没有命中缓存,那么后续指令也不会读取缓存,每条指令均会创建新的 layer

根据这些规则,我们可以对 Dockerfile 进行优化,最大化利用 build cache

比如我某个项目一开始的 Dockerfile 长这样:

FROM node:10.15.3

WORKDIR /data/server

COPY . .

# Configure apt and install packages
RUN apt-get update 
  # install vim
  && apt-get install -y vim 
  # clean up
  && apt-get autoremove -y 
  && apt-get clean -y 
  && rm -rf /var/lib/apt/lists/*

RUN npm install && npm run build

CMD ["./deploy/entrypoint.sh"]

因为 COPY . . 放在最前面,只要整个项目任意文件发生变更,后续的所有指令都会重新执行,几乎没有利用 build cache。怎么改呢?

注意看代码中的注释:

FROM node:10.15.3

# 1. 将全局安装的依赖,置于最前面
# Configure apt and install packages
RUN apt-get update 
  # install vim
  && apt-get install -y vim 
  # clean up
  && apt-get autoremove -y 
  && apt-get clean -y 
  && rm -rf /var/lib/apt/lists/*

WORKDIR /data/server

# 2. 只有部分文件如 package.json 发生变更的时候,才重新执行 npm install
COPY package.json package-lock.json .
RUN npm install

# 复制剩余文件
COPY . .
RUN npm run build

CMD ["./deploy/entrypoint.sh"]

利用 build cache,我们完全跳过了 npm install,耗时接近 0!

6818919a507e4fe10dda33883ba6dc0f.png

这个方法我们可以用来优化任何耗时比较长的命令:

  1. COPY 该命令的依赖文件,而不是所有文件
  2. 执行该命令

镜像大小

我们注意到,一个简单的 Node.js 的镜像大小超过 1G,出乎意料

找出是哪些部分最占空间

我们可以用 du -sh 来找出每个文件夹占用的空间大小:

# 忽略一些 du 的错误
docker run -it YOUR_IMAGE du -sh /* 2>&1 | grep -v "cannot access"

我们可以看到最占空间的是(忽略体积较小的目录):

279M    /data # 项目目录 /data/server,见前面的 dockerfile
920M    /usr # OS目录

Step 1:寻找更轻量的 OS 镜像

很显然,OS 占了我们 75% 以上的空间,我们需要优先减少这一块。再一次 Google,发现 alpine 是目前被广泛采用、为容器定制的轻量 OS(也有缺陷,见 reddit 讨论)

因此,我们的基础镜像改为 node:lts-alpine

FROM node:lts-alpine
alpine 不自带 bash,需要手动安装 RUN apk add --no-cache bash,不会增加多少空间

Step 2:减少 node_modules 大小

对于 /data,我们在执行一遍 du -sh /data/server/*,可以看出主要是 node_modules 吃硬盘:

16K     ./__tests__
4.0K    ./commitlint.config.js
8.0K    ./jest.config.js
196K    ./lib
277M    ./node_modules
4.0K    ./nodemon.json
336K    ./package-lock.json
4.0K    ./package.json
120K    ./src
4.0K    ./tsconfig.json

npm install 会安装 dependenciesdevDependencies,但是运行时我们并不需要 devDependencies,因此,我们可以再执行一次 npm i --only=production 移除 devDependencies

# 复制剩余文件
COPY . .
RUN npm run build

# cleanup devDependencies
RUN npm prune --production

经过上述两个优化,

# 117.7M  /data/
238.3M  /data
111.7M  /usr

容器大小也从 1.2G 减少为 421M,镜像大小只有原来的 1/3!

以上。

UPDATE 2019.10.08:有评论表示优化后的体积很大,这个是和项目有关的,你不可能要求一个只包含静态资源的 nginx 项目和一个 Node.js 项目体积一样,每个 Node.js的依赖项也不一样,容器体积大小也不是越小越好,有时候你会愿意牺牲一些空间安装一些辅助工具,所以, 关键在于你接受什么样的 tradeoff。本文知识点很简单,结论也是稀疏平常,我写下来只是想沉淀其中的方法论。
经过我再一次对比,我发现确实还有一个吃硬盘的目录 /root/.npm/,比如上述项目,该目录占到 74M=。=
Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐