# 如何提高项目构建效率

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

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

对于 Docker，容器等概念全然不知的同学来讲，似乎很难理解什么是镜像，以及如何把代码变成镜像。建议可以先通过这个[视频教程](https://developers.weixin.qq.com/community/business/doc/0002e26df14a50f7bc8c139eb51c0d)有个大概了解后再来继续。

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

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

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

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

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

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

比如上述 nodejs 例子，用 Dockerfile 描述如下：

``` 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 命令，可以参考[官方文档](https://docs.docker.com/engine/reference/builder/)

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

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

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

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

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

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

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

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

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

举个例子：

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

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

``` dockerfile
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 层，减少层级从一定程度上会减少最终镜像的大小。

举个例子：

``` Dockerfile
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
```

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

```Dockerfile
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 镜像源已经是腾讯加速源了，所以不需要自己设置

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

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

``` 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](https://hub.docker.com/)中找一找合适的镜像

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

``` Dockerfile
FROM alpine:3.15

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

# 55M
```

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

``` Dockerfile
FROM node:alpine3.15

# 171.8M，还有更小的node镜像
```

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

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

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

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

### 5. jar 包的优化

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

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

有兴趣可以自己研究一下，地址[在这里](https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/#packaging.examples.layered-archive-tools)

## 实际例子

以下是一个真实的例子

``` Dockerfile
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 进行了以下优化

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

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