# 如何提高项目构建效率

微信云托管服务是通过 Docker 镜像来启动的,也就是说你需要将你自己的业务项目代码打包成 Docker 镜像,才能在微信云托管中正常运行。

# 业务项目代码打包成 Docker 镜像

对于Docker,容器等概念全然不知的同学来讲,似乎很难理解什么是镜像,以及如何把代码变成镜像。建议可以先通过这个视频教程有个大概了解后再来继续。

无论原理如何,最终一定需要你来描述一下,“你打算如何构建镜像?”

再说的直白一点,”如果有一台没有任何系统的服务器,你准备通过什么步骤,让自己的代码项目在这一台服务器中运行?“

我们以一个 nodejs 例子举例,用服务器的操作步骤来描述一下:

1. 安装一个 alpine 系统,版本随意
2. 然后在 alpine 系统上,下载 nodejs 并安装
3. 把我们的代码复制到服务器的一个目录中
4. npm安装一下项目所需要的依赖
5. 使用启动命令将项目运行起来

以上这些,不知从直觉上能大概理解了吗?相应的,java、php、python的整体步骤也是差不多的,都是「安装系统 - 安装语言支持 - 复制代码(或运行文件)-安装依赖 - 启动项目」这一个过程。

那么 Docker 构建的过程,其实也是类似的,只不过我们需要用一个标准的描述格式,把上面的步骤描述一下,这个格式文本叫 Dockerfile

比如上述 nodejs 例子,用 Dockerfile 描述如下:

# 安装一个 alpine 系统,版本随意
FROM alpine:3.15

# 然后在 alpine 系统上,下载 nodejs 并安装
RUN apk add --update --no-cache nodejs npm

# 把我们的代码复制到服务器的一个目录中
COPY . .

# npm安装一下项目所需要的依赖
RUN npm install

# 使用启动命令将项目运行起来
CMD ["node", "index.js"]

通过这个简单的例子,你就会发现,其实我们编写Dockerfile,主要是为了标准化项目部署过程。Docker根据你的描述,就可以不折不扣的执行,然后打包成一个镜像,这个镜像就包含了你项目以及项目所依赖的全部底层,就可以在任意一处建立容器来运行了,不需要再上去运维。

你可以在网上找到「你的语言框架+Dockerfile」搜索关键字的经验条目,绝大多数语言框架都有覆盖到,当你找到 Dockerfile 时,就可以通过阅读步骤,来了解它的构建过程,不会因为人与人自然语言描述的模糊而造成理解偏差。Dockerfile命令,可以参考官方文档

# 如何提升镜像的构建效率

当你还没有对 Dockerfile 和构建流程有更深刻认知时,你的项目 Dockerfile 一般是从网上和各模版中直接复制下来的。但有的时候这些 Dockerfile 可能并没有完全契合你的项目,甚至在构建的时候效率会非常低。

所以你可以尝试改造他们,来提升自己的构建效率,在这里我们总结了一些常见的优化点,希望你可以有所收获。

# 1. 变化的放后面,高效利用缓存

每次构建时,Docker都会充分利用之前构建的成果物,如果没有变化,则直接使用而不是重新构建。在构建的过程中遵循以下规则:

1、根据 FROM 命令指定的基本镜像,将每一条指令与从该基本镜像派生的所有子镜像进行比较,查看是否有使用完全相同的指令构建的,如果没有则缓存无效。

2、对于 ADD 和COPY指令,检查镜像中文件的内容,并为每个文件计算一个校验标识。在这些校验标识中通常不考虑文件的最后修改时间和最后访问时间,将校验标识与现有镜像中的进行比较,如果文件中的任何内容(例如内容和元数据)发生了更改,则缓存将无效。

3、除了 ADD 和COPY命令外,缓存检查不会查看容器中的文件来确定缓存是否匹配。

4、缓存无效后,所有后续命令都会重新构建。

举个例子:

FROM alpine:3.15
RUN apk add --update --no-cache nodejs npm
COPY . .
RUN npm install
CMD ["node", "/index.js"]

我们变化的只是项目代码,所以打包时 COPY 命令之前的直接使用缓存,但以上还不是更好的,因为我们每次还必须要安装依赖 npm install,而绝大多数情况下,我们根本不会变更项目依赖,所以就可以改进一下,如下:

FROM alpine:3.15
RUN apk add --update --no-cache nodejs npm
COPY ./package*.json .
RUN npm install
COPY . .
CMD ["node", "/app/index.js"]

我们先把npm install必要的 package*.json 文件复制进来,然后安装依赖,完成后再把其他的文件复制进来。这样做的好处是,只要你不变更 package*.json 文件,则不用执行 npm install,而只是复制最后的文件,打包时间基本都在1秒左右。

# 2. 减少层数,合并命令

Dockerfile中的所有命令最终都会形成一个 layer 层,减少层级从一定程度上会减少最终镜像的大小。

举个例子:

FROM alpine:3.13

# ...
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories
RUN apk add --update --no-cache php7 php7-json php7-ctype php7-exif php7-pdo php7-pdo_mysql php7-fpm nginx 
RUN rm -f /var/cache/apk/*

# ...
RUN cp /app/conf/nginx.conf /etc/nginx/conf.d/default.conf
RUN cp /app/conf/fpm.conf /etc/php7/php-fpm.d/www.conf
RUN cp /app/conf/php.ini /etc/php7/php.ini 
RUN mkdir -p /run/nginx
RUN chmod -R 777 /app/runtime
RUN mv /usr/sbin/php-fpm7 /usr/sbin/php-fpm

上述这些命令可以直接合并一下,这样会显著的减少层数

FROM alpine:3.13

# ...
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories && apk add --update --no-cache php7 php7-json php7-ctype php7-exif php7-pdo php7-pdo_mysql php7-fpm nginx && rm -f /var/cache/apk/*

# ...
RUN cp /app/conf/nginx.conf /etc/nginx/conf.d/default.conf \
    && cp /app/conf/fpm.conf /etc/php7/php-fpm.d/www.conf \
    && cp /app/conf/php.ini /etc/php7/php.ini \
    && mkdir -p /run/nginx \
    && chmod -R 777 /app/runtime \
    && mv /usr/sbin/php-fpm7 /usr/sbin/php-fpm

但需要注意的是,不要滥合并,有时候一些命令不稳定,可能会影响其他命令的有效运行,导致最终效率反而不是很好,建议合并相关的同一类操作,比如安装依赖,文件操作。

# 3. 更换源站

Docker基础镜像的下载,构建执行时,各种系统软件的安装下载,语言依赖的下载都需要通过网络加载,所以可以在安装前更换一下源站。

微信云托管线上构建的 Docker 镜像源已经是腾讯加速源了,所以不需要自己设置

{
   "registry-mirrors": [
       "https://mirror.ccs.tencentyun.com"
  ]
}

Dockerfile中,你可以在安装前指定源,以下列举一些常见的

# alpine-apk更换腾讯加速源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories

# NPM更换腾讯加速源
RUN npm config set registry https://mirrors.tencent.com/npm/

# Python更换腾讯加速源
RUN pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple && pip config set global.trusted-host mirrors.cloud.tencent.com

开源部分完成了,接下来就进行节流了,尽可能只保留最小可运行的内容,不要在生产包中安装调试之类的内容。

# 4. 选一个好的基础镜像

一个好的基础镜像将对你最终项目镜像产生重要影响。

一般来说,我们尽可能选择一个既符合项目要求的,又非常小的镜像。

我们可以先从Docker-Hub中找一找合适的镜像

比如我们想需要一个 node.js 环境,你可以自己安装

FROM alpine:3.15

RUN apk add --update --no-cache nodejs npm

# 55M

也可以直接引用已经封好的

FROM node:alpine3.15

# 171.8M,还有更小的 node 镜像

不过已经封装好的,虽然不用你再安装配置了,但体积也是比较大的,有时候他依赖的基础镜像还会不一样,所以看自己的需要来用。

如果有些环境的安装配置极为复杂,你很难做的既优雅又明白,这个时候不用考虑了,除非你对镜像有偏执的要求,用一个已经封装好的基础镜像,开局会非常轻松。

另外我们也会在构建环节缓存常见的一些镜像,所以不用太担心镜像太大的下载问题,除非你选了一些冷门的,或者偏门的镜像。

不过非常大的镜像对于拉起实例也是不友好的(因为有项目镜像下载过程),所以也希望各位开发者尽可能的提供小而美的构建步骤。

# 5. jar包的优化

当你做 springBoot 项目时,构建物 jar 包对于 Docker 镜像来说,元数据改变了,就需要重新打包上传,其实比较难受的。

这种情况下,可以使用 spring-boot-jarmode-layertools 试一试,它将 jar 包拆成若干个层,直接对应到 Docker 的镜像缓存中。

有兴趣可以自己研究一下,地址在这里

# 实际例子

以下是一个真实的例子

FROM alpine:3.14

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories \
    && apk add --update --no-cache python3 py3-pip \
    && rm -rf /var/cache/apk/*

ENV TZ Asia/Shanghai
RUN apk add tzdata && cp /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone 

RUN apk add ca-certificates

COPY . /app

WORKDIR /app

RUN pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple \
    && pip config set global.trusted-host mirrors.cloud.tencent.com \
    && pip install --upgrade pip \
    && pip install --user -r requirements.txt

EXPOSE 80

CMD ["python3", "run.py", "0.0.0.0", "80"]

上述构建过程中会出现一些错误,偶发性比较多,并且构建时间每次都很长。

我们在看了一下后,对 Dockerfile 进行了以下优化

FROM python:3.9.12-alpine3.14

ENV TZ Asia/Shanghai

RUN apk add tzdata && cp /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone \
    && apk add ca-certificates

WORKDIR /app

COPY requirements.txt .

RUN pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple \
    && pip config set global.trusted-host mirrors.cloud.tencent.com \
    && pip install --upgrade pip \
    && pip install --user -r requirements.txt

COPY . .

EXPOSE 80

CMD ["python3", "run.py", "0.0.0.0", "80"]

你可以根据自己掌握的知识,对上述修改做梳理,希望有助于你的 Dockerfile 优化过程。

这种 Dockerfile 以及相关概念本身不属于微信云托管的服务范畴,大部分情况下,主要依赖各位开发者提供优质的打包过程,我们才能够在部署构建过程中有显著的提速。

当然构建只是第一步,项目镜像最终大小,以及镜像在实例中启动速度(包含你的项目运行过程),也最终决定了云托管的服务创建速度,和冷启动速度。