本文不涉及自定义镜像的部署方式
冷启动(Cold Start)
一直是 Serverless
的一个缺点。
它往往出现在这样一个场景:
当一个云函数已经不存在空闲的可提供服务的容器时。再有额外的请求传入,函数本身就不得不去启动一个新的容器来处理这次的请求。由于启动新容器会有延迟,这就导致了这个请求需要更多时间来响应,用户就会感觉我们应用程序似乎 "变慢"
了。
这对于用户的体验来说是个大问题,需要尽可能的去减短这个时间。
我们知道当容器从 "冷态"
开始启动时,函数需要:
其中,在第一步拉取代码时,代码体积越小,拉取代码速度越快,冷启动时间自然就变短了。这块是我们开发者能够间接控制的。
而对我们 nodejs
开发者来说,在项目里,往往占据巨大体积的,不是我们自己写的代码,而是在 node_modules
中依赖各种包。
像传统的npm
包安装方式,在 开发者本地 或者在 云端 安装依赖,都会附带过多的 "无用" 垃圾文件,白白占据了大量的空间。我们要想办法来解决这个问题,在保证项目稳定性的同时,减小包的体积来优化冷启动时间。
本地作为开发环境,开发者往往会把 devDependencies
,dependencies
都给安装进来。
而 devDependencies
往往是 eslint
, webpack
这类和真正的服务端运行时无关的包。要是把它们也部署到函数中,不论是直接压缩上传代码包,还是做成 层函数(layer)
去绑定,都是在浪费代码包体积,因为那一部分代码,在运行时永远不会被调用。
yarn install --production
算一个"不充分"
的解决方案,这个指令作用是:只安装 dependencies
里的包。这也要求开发者,安装npm
包时,对所需的环境做准确的划分。
注:这个指令在我们开发时候,往往是无用的,举个例子: 我们通常会把
typescript
安装到devDependencies
里 要是只安装dependencies
,那我们连tsc
都做不到了。
操作系统大体上分为 darwin
, linux
, win32
,mas
这几类。
而 cpu 指令集,常见的也有 arm64
, x64
, armv7l
,ia32
等等。
node_modules
里面,往往也会存在 cpp
,rust
,python
这类非 js
代码,有些包的作者也往往会在 npm hook
里,去检测当前系统的发行版本,根据它,再去远程下载 对应平台对应指令集 的二进制包。
这里我举个例子,来说明这个问题的危害。
我们在 win10
上开发,下载了 win32-x64
的二进制包,本地运行非常的正常,做成 layer
层函数,再部署到 SCF
上,结果就挂了。
这是因为 SCF
函数运行环境,需要的是 linux-x64
的包,但运行时从 layer
里读到的是 win32-x64
的二进制包,平台不符合,自然就挂了。
本地开发使用 docker
+ scf镜像
,尽力来仿造 scf
运行环境。
当然,更好的方案还是直接在云端进行开发,或者云端映射到本地进行开发。这样在保证运行环境的绝对准确的同时,也能感知到其他云服务的存在,更便于开发调试。
云端安装依赖实际上是 SCF
提供的功能,它只需要一个installDependency
配置项 并上传 package.json
,就可以在云端自己安装依赖并做成代码包,省去了本地上传压缩 node_modules
的麻烦。
云端安装依赖 可以规避 本地安装依赖
中 操作系统或指令集绑定的第三方包
这个问题,毕竟依赖都是在云函数环境下现装的。
node_modules
的处理我们前端开发者对 webpack
,rollup
这类打包工具可谓是非常熟悉了。当然它们这些工具,除了可以打包 Web应用
,也可以去打包 Nodejs
。我们的重点目标,主要是 node_modules
里依赖的第三方模块。
将代码打包成单文件,优化nodejs
模块加载,减少读磁盘的次数,这也能在一定程度上减少 nodejs
应用启动时间。
注:
builtin-modules
不打包
esbuild
为例通过上述的思路写出 esbuild
打包的配置:
/**
* @type {import('esbuild').BuildOptions}
*/
const config = {
entryPoints: ['./src/index.js'],
bundle: true,
platform: 'node',
target: ['node12'], // scf runtime node version
outfile: path.resolve(__dirname, 'dist', 'index.js'),
sourcemap: isDev, // 调试用
minify: isProd, // 压缩代码
external: [] // 跳过打包
}
await esbuild.build(config)
这样它便会去分析我们的js
代码依赖来抽取代码,只不过有时会遇到非 js
依赖,打包工具分析不出来,例如这种 fs
读取文件的,也算一种依赖:
// dist/index.js
var trie = new UnicodeTrie(fs.readFileSync(__dirname + "/data.trie"));
这时候为了能够顺利的把函数跑起来,最简单的方案还是 copy
: 哪里缺,哪里找。
然而这种解决方案有一个巨大的问题:
打包成单文件,会导致原先的目录结构被抹平。这样就容易出现多个 非js
文件重名,导致相互覆盖。
举个例子,unicode-properties
和 fontkit
都用 fs
依赖了它们各自目录下的 data.trie
文件,但是它们依赖的 data.trie
内容不同,这也会导致 函数挂掉。
这种情况就可以使用 external
来规避:即把能打包的打包了,不能打包的就不打包。
部署的时候,再把没打包的 external
们,单独抽离出来,做成独立的 dist/package.json
, 再在云端安装依赖,我们的云函数就能够直接运行了。
代码包体积小了之后,大大提升了函数部署到 腾讯云 SCF
平台的速度(避免了压缩上传 node_modules
)
同时通过分析依赖,构建产出,并压缩代码的方式,将一些本要在云端 拉取解压近 300多M
的代码包,减小到 8M
左右,有效的减少了函数获取代码包的时间,从而整体优化了函数的冷启动时间。