NodeJS 服务 Docker 镜像极致优化指北

这段时间在开发一个腾讯文档全品类通用的 HTML 动态服务,为了方便各品类接入的生成与部署,也顺应上云的趋势,考虑使用 Docker 的方式来固定服务内容,统一进行制品版本的管理 。本篇文章就将我在服务 Docker 化的过程中积累起来的优化经验分享出来,供大家参考 。
以一个例子开头,大部分刚接触 Docker 的同学应该都会这样编写项目的 Dockerfile,如下所示:
FROM node:14WORKDIR /appCOPY . .# 安装 npm 依赖RUN npm install# 暴露端口EXPOSE 8000CMD ["npm", "start"]构建,打包,上传,一气呵成 。然后看下镜像状态,卧槽,一个简单的 node web 服务体积居然达到了惊人的 1.3 个 G,并且镜像传输与构建速度也很慢:

NodeJS 服务 Docker 镜像极致优化指北

文章插图
要是这个镜像只需要部署一个实例也就算了,但是这个服务得提供给所有开发同学进行高频集成并部署环境的(实现高频集成的方案可参见我的 上一篇文章) 。首先,镜像体积过大必然会对镜像的拉取和更新速度造成影响,集成体验会变差 。其次,项目上线后,同时在线的测试环境实例可能成千上万,这样的容器内存占用成本对于任何一个项目都是无法接受的 。必须找到优化的办法解决 。
发现问题后,我就开始研究 Docker 的优化方案,准备给我的镜像动手术了 。
node 项目生产环境优化首先开刀的是当然是前端最为熟悉的领域,对代码本身体积进行优化 。之前开发项目时使用了 Typescript,为了图省事,项目直接使用 tsc 打包生成 es5 后就直接运行起来了 。这里的体积问题主要有两个,一个是开发环境 ts 源码并未处理,并且用于生产环境的 js 代码也未经压缩 。
NodeJS 服务 Docker 镜像极致优化指北

文章插图
另一个是引用的 node_modules 过于臃肿 。仍然包含了许多开发调试环境中的 npm 包,如 ts-node,typescript 等等 。既然打包成 js 了,这些依赖自然就该去除 。
一般来说,由于服务端代码不会像前端代码一样暴露出去,运行在物理机上的服务更多考虑的是稳定性,也不在乎多一些体积,因此这些地方一般也不会做处理 。但是 Docker 化后,由于部署规模变大,这些问题就非常明显了,在生产环境下需要优化的 。
对于这两点的优化的方式其实我们前端非常熟悉了,不是本文的重点就粗略带过了 。对于第一点,使用 Webpack + babel 降级并压缩 Typescript 源码,如果担心错误排查可以加上 sourcemap,不过对于 docker 镜像来说有点多余,一会儿会说到 。对于第二点,梳理 npm 包的 dependencies 与 devDependencies 依赖,去除不是必要存在于运行时的依赖,方便生产环境使用 npm install --production 安装依赖 。
优化项目镜像体积使用尽量精简的基础镜像我们知道,容器技术提供的是操作系统级别的进程隔离,Docker 容器本身是一个运行在独立操作系统下的进程,也就是说,Docker 镜像需要打包的是一个能够独立运行的操作系统级环境 。因此,决定镜像体积的一个重要因素就显而易见了:打包进镜像的 Linux 操作系统的体积 。
一般来说,减小依赖的操作系统的大小主要需要考虑从两个方面下手,第一个是尽可能去除 Linux 下不需要的各类工具库,如 python,cmake, telnet 等 。第二个是选取更轻量级的 Linux 发行版系统 。正规的官方镜像应该会依据上述两个因素对每个发行版提供阉割版本 。
以 node 官方提供的版本 node:14 为例,默认版本中,它的运行基础环境是 Ubuntu,是一个大而全的 Linux 发行版,以保证最大的兼容性 。去除了无用工具库的依赖版本称为 node:14-slim 版本 。而最小的镜像发行版称为 node:14-alpine 。Linux alpine 是一个高度精简,仅包含基本工具的轻量级 Linux 发行版,本身的 Docker 镜像只有 4~5M 大小,因此非常适合制作最小版本的 Docker 镜像 。

经验总结扩展阅读