本文摘自PHP中文网,作者青灯夜游,侵删。
本文将以 Node 程序展示如何优化 Docker 镜像(优化思想是通用的,不分程序),主要解决镜像大小过大、CI/CD 构建镜像速度,本文演示如何一步步优化 Dockerfile 文件,绝对的干货,建议先赞再看,看完不是干货再取消赞也不是不行。
优化的结果如下:
大小从 1.06G 到 73.4M
构建速度从 29.6 秒到 1.3 秒(对比的是第二次构建的速度)
【推荐学习:《nodejs 教程》】
Node 项目
简单写了一个自己用的 wechat-bot ,接下来就以这个项目演示怎么去优化 Docker 镜像
以下是我没有仔细研究 Docker 刚开始写的 Dockerfile 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
build 之后,如下图,我这个简单的 Node 程序镜像竟然有 1G 多,接下来我们将逐步去优化减少这个大小
优化前言
在优化之前,有些东西我们必须了解,解决问题的第一步就是先找出导致问题的原因
Dockerfile 文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层如何构建
Docker 镜像并非只是一个文件,而是由一堆文件组成,最主要的文件是 层(Layers)
镜像构建时,会一层层构建,前一层是后一层的基础
每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像
镜像层将会被缓存和复用(这也是从第二次开始构建镜像时,速度会快的原因,优化镜像构建速度的原理也是利用缓存原理来做)
当 Dockerfile 的指令修改了,操作的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效
docker build 的缓存机制,docker 是怎么知道文件变化的呢?
Docker 采取的策略是:获取 Dockerfile 下内容(包括文件的部分 inode 信息),计算出一个唯一的 hash 值,若 hash 值未发生变化,则可以认为文件内容没有发生变化,可以使用缓存机制,反之亦然。
某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效
镜像的每一层只记录文件变更,在容器启动时,Docker 会将镜像的各个层进行计算,最后生成一个文件系统
当我知道这点时,我恍然大悟,我们使用的操作系统,比如安卓、ios、win、mac 等,其实就是一个文件系统,我们的软件界面交互等,其实就是在读写文件,我们网页写个弹框,操作 dom,就是在读写本地文件或者是读写内存里的数据,个人的一些见解不知道对不对,本人非科班出身的前端 coder
参考资料:docker 镜像分层原理
ok,我们已经知道镜像是由多层文件系统组成,想要优化它的大小,就需要去减少层数、每一层尽量只包含该层需要的东西,任何额外的东西应该在该层构建结束前清理掉,下面开始正文
优化 Dockerfile
优化第一层 FROM node:14.17.3
方案一:使用 node 的 Alpine 版本
这也是绝多数人知道的优化镜像手段,Alpine 是一个很小的 Linux 发行版,只要选择 Node 的 Alpine 版本,就会有很大改进,我们把这一句改成指令改成 FROM node:14.17.4-alpine
(可以去 dockerhub 查看 node 有哪些版本标签),build 后镜像大小如下图,瞬间从 1.06G 降到 238M,可以说是效果显著
还可以使用其它的基础小镜像,比如 mhart/alpine-node,这个还能再小,改成 FROM mhart/alpine-node:14.17.3
再试试,可以看到又小了 5M ,虽然不多,但是秉着能压榨一点是一点的“老板原则”,积少成多,极致压榨
方案二:使用纯净 Alpine 镜像手动装 Node
既然 Alpine 是最小的 Linux,那我们试下用纯净的 Alpine 镜像,自己再装 Node 试试
1 2 3 4 5 6 |
|
build 后看下图,只有 174M 了,又小了不少
结论就是不嫌麻烦追求极致就用方案二,从 1.06G 减少到 174M
减少层数、不经常变动的层提到前面去
ENV
指令是可以一次性设置多个环境变量,能一次指令执行完,就不用两次,多一个指令就多一层EXPOSE
指令是暴露端口,其实也可以不用写这个指令,在启动容器的时候自己映射端口,如果写了这个指令的话,因为端口不经常变,所以把这个指令提前,写上这个指令有两个好处:帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
在运行时使用随机端口映射时,也就是
docker run -P
时,会自动随机映射EXPOSE
的端口
至于写还是不写,看个人吧,我个人一般不写,因为我在项目启动命令会指定项目端口,启动容器的时候映射出来就好,这样我就要维护一个地方,Dockerfile 也写了的话,项目端口变了,这里也要修改,多了点维护成本,当然也有办法让两边端口变量取自配置文件,只要改配置文件即可
下面是改写后的 Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
这一步的优化,无论从镜像大小还是构建镜像速度都看不到明显的差别,因为改动的层内容少(体现不出来),但是可以查看到镜像的层是变少了的,可以自行试试查看镜像的层试试
减少镜像层数是“好老板”的传统优良习惯,不让“员工”浪费资源
package.json 提前提高编译速度
从下图可以看到每次我们 build 的时候最耗时的就是在执行 yarn
命令装依赖的时候,大部分时候我们只是改代码,依赖不变,这时候如果可以让这一步缓存起来,依赖没有变化的时候,就不需要重新装依赖,就可以大大改进编译速度
前面我们说了镜像构建时,是一层层构建,前一层是后一层的基础,既然是这样的话,我们就把 package.json 文件单独提前拷贝到镜像,然后下一步装依赖,执行命令装依赖这层的前一层是拷贝 package.json 文件,因为安装依赖命令不会变化,所以只要 package.json 文件没变化,就不会重新执行 yarn
安装依赖,它会复用之前安装好的依赖,原理讲清楚了,下面我们看效果
改变后的 Dockerfile 文件
相关阅读 >>
更多相关阅读请进入《Node.js.优化docker镜像》频道 >>
Vue.js 设计与实现 基于Vue.js 3 深入解析Vue.js 设计细节
本书对 Vue.js 3 技术细节的分析非常可靠,对于需要深入理解 Vue.js 3 的用户会有很大的帮助。——尤雨溪,Vue.js作者