无服务器框架:Otimizando Lambda“冷启动”com serverless-webpack
图片来源
AWS 最近在减少所有_运行时_(例如:Node.js、Go、Python 等)上的 Lambda 冷启动(冷启动)时间方面做得非常出色,例如VPC 网络改进和添加本机支持以自动保持您的 Lambda _容器_温暖。
这些变化减少了开发人员为提高 Lambda 启动性能而需要做的事情的数量。
但是在开发人员的控制下,仍然有一个变量在冷启动时会显着影响- 压缩包大小。
为什么数据包大小仍然很重要
尽管 AWS 做出了改进,您的 Lambda 仍然可以冷启动。这个额外的启动时间将影响针对您的用户的功能的“响应-响应”能力,例如 API 或 Slack 应用程序。
如果你使用Serverless Framework和 JavaScript,它默认将你的代码与所有 npm 依赖打包在一起,这会创建非常繁琐的 Lambda 函数。大部分代码通常是未使用的,但由于它位于应用程序包中,因此每次配置新容器时仍需要将其复制并提取到文件系统中。结果是客户的响应时间变慢。
后端开发者需要快速学习的地方
压缩代码的大小一直是 Web 开发人员的重要指标。很多时间都花在优化交付给 Web 应用程序的 JavaScript、图像、CSS 和 HTML 上——下载所需的时间会影响第一次绘制时间和第一次迭代时间,并可能对客户保留率和转化率产生真正的影响。
由于打包和执行的方式,后端开发人员能够长时间忽略代码大小。单体应用程序通常部署到长时间运行的服务器上,启动一次后运行数天,因此启动时间过长并不是真正的问题。
通常,这些应用程序聚集在一起以横向扩展性能并弹性调整更改客户端负载的能力。由于启动时间已经够糟糕了,集群通常会被过度配置以进行补偿。开发人员设法将他们糟糕的应用程序启动时间隐藏在这背后。
一般来说,使用无服务器,前端架构问题会转移到后端。虽然它使开发人员无需考虑各种运营问题,如配置基础设施和优化资源,但他们现在必须考虑冷启动和管理资源连接等过去不需要的事情。
包大小直接影响 Lambda 的启动时间,因此优化使用捆绑程序部署的代码量将有助于减少冷启动时间。
对于 JavaScript 开发人员来说幸运的是,用于打包代码的工具在前端已经相当成熟 -现在有许多JavaScript 包装器,其中大多数都适合包装后端代码运行在 Node.js 上。 js也是!
打包机对此有何帮助?
打包程序通过检查代码和引用的 npm 包中的导入和导出来工作,创建依赖关系树并生成包含所有累积代码的单个 JavaScript 文件。
这带来了几个好处:
- u200c仅包含您的函数使用的代码
您可以在同一个无服务器框架项目中拥有多个共享一些代码的 Lambda 函数,但不太可能所有函数都使用完全相同的一组共享依赖项。您可以通过仅包含您的函数而不是所有函数导入的代码来优化 Lambda 的大小。
- 优化你的 npm 依赖
您可能只是使用了一些 npm 依赖项,甚至只是引用的代码的一部分。您的某些 npm 依赖项可能存在两次,因为它们被多个模块引用。
打包程序可以遵循 npm 模块中的依赖关系图,并仅获取代码传递引用的文件,以及重复数据删除(存在兼容版本)。
- u200c为您的源代码使用单个文件
使用单个文件而不是多个文件可以减少 Node.js 所需的文件系统访问量。加载您的代码。
这种改进通常很小,但它是 webpack 的副作用,为 web 打包单个文件会对网络产生巨大的影响。
与您压缩所有代码并自行部署相比,结果通常是一个小得多的 Lambda 函数。
serverless-webpack 入门
第一步是安装插件并将其注册到您的serverless.yaml中。
切换到项目目录并安装模块serverless-webpack和 webpack:
npm install --save-dev serverless-webpack@5.3.1 webpack@4.42.1
将插件添加到您的serverless.yaml文件中:
plugins:
- serverless-webpack
...
# se você estiver usando o `serverless-offline-plugin`
# se certifique de colocá-lo APÓS o `serverless-webpack`
- serverless-offline-plugin
最后,在项目的根目录下添加一个webpack.config.js文件:
const slsw = require('serverless-webpack');
module.exports = {
target: 'node',
entry: slsw.lib.entries,
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
node: false,
optimization: {
minimize: false,
},
devtool: 'inline-cheap-module-source-map',
};
至此,我们配置了serverless-webpack插件,并提供了一个最小的 webpack 配置。
如果您阅读过serverless-webpack文档,您可能已经注意到我已经包含了一些示例中没有出现的额外选项。
node: false- 这很不寻常,但我发现它对我的设置是必要的。
将node设置为target通常足以让 webpack 编译为 Node.js - 它使用 Common.jsrequire()作为依赖项,并确保内部依赖项不会被存根替换。但是,我发现它仍然会干扰全局process.env,这是在 Lambda 中使用环境变量所需要的。
minimize: false-禁用代码最小化(丑化)。
混淆使 Lambda 的 stack traces 更难阅读,它稍微减小了包大小,但不足以证明其与无服务器框架项目的使用是合理的),并且在代码不直接分发给用户(如在网络上)的情况下并不真正需要。
我的设置关闭了缩小,因为它大大减少了编译时所需的 CPU 和内存量,并有助于避免 'out-of-memory' 错误。
创建单个包或每个 Lambda 包
此时,如果您运行serverless package,您的终端将显示典型的 webpack 报告,显示构建时间和包大小。

默认情况下,无服务器框架会为您的所有代码生成一个包,并为您的所有 Lambda 函数实现它。serverless-webpack也做同样的事情,不同之处在于它为每个 Lambda 函数创建一个 JavaScript 文件,其中包含每个 Lambda 所需的所有代码,包括 npm 依赖项、捆绑和打包。
您可以找到打包在<project>/.serverless下的项目。您会发现,与非平凡应用程序的默认值相比,这仍然是一个巨大的尺寸减小。
如果您想进一步优化并为每个 Lambda 函数创建特定的包,您可以通过将以下选项设置为serverless.yaml来告诉 Serverless Framework 单独打包 Lambda:
package:
individually: true
serverless-webpack 也使用此值,并将为每个 Lambda 创建一个单独的包,每个包中都有一个 webpack 包。这需要更长的时间,但结果是更优化的功能。
使用通天塔
插件文档描述了如何在转译中使用 babel(但我已经包含了一些我自己的更改以确保您充分利用它)。
首先,让我们安装 Babel 的依赖项:
npm install --save-dev @babel/core@7.9.0 @babel/preset-env@7.9.5 babel-loader@8.1.0 corejs@3.6.5
将以下条目添加到您的webpack.config.js(确保设置您在 Lambda 中使用的 Node.js 的运行时版本):
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{ targets: { node: '12' }, useBuiltIns: 'usage', corejs: 3 }
]
]
}
}
]
}
]
},
...
}
与 serverless-webpack 文档相比,与我的设置的主要区别是:
-
添加
@babel/preset-env以确保我们不包含 Node.js 中已有功能的转换或 polyfill。 -
设置
useBuiltins: 'usage'以便根据具体情况包括所有使用的 polyfill -
配置
corejs: 3以便不使用旧的和有缺陷的 core-js v2 来提供 polyfills
将其他文件复制到包中
由于无服务器框架通常会将所有内容复制到部署包中,当您的代码突然崩溃时,您可能会感到惊讶,因为加载了不属于您的代码但属于文件系统的任意文件。
如前所述,serverless-webpack仅包含源代码,有效地忽略了serverless.yaml的第package节中选项include和exclude中定义的内容。您必须配置 webpack 以使用copy-webpack-plugin复制非源但需要的文件。
让我们用 npm 安装:
npm install --save-dev copy-webpack-plugin
在您的webpack.config.js上包含并使用要复制的文件路径和 glob 配置插件:
const CopyPlugin = require('copy-webpack-plugin`);
...
module.exports = {
...
plugins: [
new CopyPlugin([
'path/to/specific/file',
'recursive/directory/**',
]),
],
...
};
常见问题
内存不足
使用 webpack 打包您的代码比平时使用更多的 CPU 和内存,Node.js 报告内存不足的错误并不少见。serverless-webpack每个函数运行一个 webpack 实例,每个实例都必须组合并最小化其输出:
Serverless: Bundling with Webpack...
<--- Last few GCs --->
[8233:0x393fa70] 207373 ms: Scavenge 1885.9 (2042.4) -> 1885.9 (2042.9) MB, 9.3 / 0.0 ms (average mu = 0.262, current mu = 0.159) allocation failure
[8233:0x393fa70] 207386 ms: Scavenge 1886.4 (2042.9) -> 1886.4 (2043.6) MB, 11.4 / 0.0 ms (average mu = 0.262, current mu = 0.159) allocation failure
[8233:0x393fa70] 207404 ms: Scavenge 1887.0 (2043.6) -> 1886.9 (2044.1) MB, 15.8 / 0.0 ms (average mu = 0.262, current mu = 0.159) allocation failure
<--- JS stacktrace --->
==== JS stack trace =========================================
0: ExitFrame [pc: 0x1374fd9]
1: StubFrame [pc: 0x13afd14]
Security context: 0x39352fbc08a1 <JSObject>
2: replace [0x39352fbccf51](this=0x2c60c4e7f591 <String[#27]\: \n//# sourceMappingURL=[url]>,0x16535c280e41 <JSRegExp <String[#7]: \[url\]>>,0x16535c280e79 <JSFunction (sfi = 0x35c9e77a48b9)>)
3: /* anonymous */(aka /* anonymous */) [0x17c38da35a9] [/home/chris/dev/slack-app/bot/node_modules/webpack/lib/SourceMapDevToolPlu...
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
1: 0x9da7c0 node::Abort() [node]
2: 0x9db976 node::OnFatalError(char const*, char const*) [node]
3: 0xb39f1e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
4: 0xb3a299 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
5: 0xce5635 [node]
6: 0xce5cc6 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [node]
7: 0xcf1b5a v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
8: 0xcf2a65 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
9: 0xcf5478 v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [node]
10: 0xcc30c6 v8::internal::Factory::NewRawOneByteString(int, v8::internal::AllocationType) [node]
11: 0x103253a v8::internal::Runtime_StringBuilderConcat(int, unsigned long*, v8::internal::Isolate*) [node]
12: 0x1374fd9 [node]
解决方案是在启动 Serverless Framework 部署命令时增加 Node.js 可用的堆空间量(这仅适用于 Node.js v8 及更高版本):
node --max-old-space-size=4096 \
node_modules/.bin/serverless package --stage dev ...
另请注意,我们需要通过文件夹node_modules/bin中的完整路径调用无服务器框架,因为我们正在启动 Node.js。直接地。
如果您不想直接调用 Node.js,可以使用环境变量设置 Node.js 选项:
export NODE_OPTIONS=--max-old-space-size=4096
npx serverless package --stage dev ...
在堆栈跟踪中正确的行号和函数名称
由于 webpack 会转换您的代码并将其打包到同一个文件中,因此 CloudWatch 日志中的堆栈跟踪将反映默认打包的内容。这会妨碍您调试代码的能力。
webpack 使用选项devtool来控制源映射(source maps),这是包含在代码中的额外注释,用于帮助调试器和堆栈跟踪生成器将这些对包源代码的引用转换回原始源代码,或转换。
在我们的webpack.config.js示例中,我们将devtool设置为'inline-cheap-module-source-map',这应该使用对原始源代码的行引用来呈现堆栈跟踪。
这个选项会影响 webpack 构建速度,所以如果你想使用其他东西,请参阅 devtool](https://webpack.js.org/configuration/devtool/)上的[webpack 文档。
学分
- 优化您的 Lambda 冷启动 serverless-webpack,escrito originalmente porChris Armstrong。
更多推荐
所有评论(0)