有奖征文|投稿上云技术实践,赢取价值5000元大奖> HOT

云托管服务是通过 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 优化过程。

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

目录