发布于 2026-01-06 0 阅读
0

如何在 TypeScript Lambda 函数中使用 Source Map(附基准测试)

如何在 TypeScript Lambda 函数中使用 Source Map(附基准测试)

TypeScript 是一种深受各类开发者欢迎的语言,它在无服务器领域也占据了一席之地。大多数主流 Lambda 框架现在都对 TypeScript 提供了完善的支持。我们再也不用为 webpack 配置而烦恼了。

目录

堆栈跟踪

既然我们已经要将代码转译为 Lambda 兼容的 JavaScript,那么不妨也对代码进行压缩tree-shaking。更小的包可以加快部署速度,甚至有助于缩短冷启动时间和执行时间。然而,当出现类似这样的堆栈跟踪时,调试就会变得非常困难:

{
    "errorType": "SyntaxError",
    "errorMessage": "Unexpected end of JSON input",
    "stack": [
        "SyntaxError: Unexpected end of JSON input",
        "    at JSON.parse (<anonymous>)",
        "    at VA (/var/task/index.js:25:69708)",
        "    at Runtime.R8 [as handler] (/var/task/index.js:25:69808)",
        "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
    ]
}
Enter fullscreen mode Exit fullscreen mode

这里的错误信息告诉我们解析 JSON 字符串失败了,但堆栈跟踪本身对查找代码出错的行毫无用处。我们只能检查代码,希望能找到错误所在。我们或许可以进行文本搜索,JSON.parse但如果错误发生在某个依赖项中,搜索就行不通了。接下来怎么办?在代码中添加一堆日志语句吗?不!如果我们使用 Source Maps,就能获得更有用的堆栈跟踪:

{
    "errorType": "SyntaxError",
    "errorMessage": "Unexpected end of JSON input",
    "stack": [
        "SyntaxError: Unexpected end of JSON input",
        "    at JSON.parse (<anonymous>)",
        "    at VA (/fns/db.ts:39:8)",
        "    at Runtime.R8 (/fns/list.ts:6:24)",
        "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
    ]
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以看到堆栈中包含来自第 6 行调用,该调用最终在第 39list.ts失败db.ts

要在我们的应用程序中启用源映射,我们需要告诉我们的构建工具生成源映射,并且我们需要在运行时启用源映射支持。

辐射源图

使用esbuild生成 Source Map 非常简单。我们只需在配置中设置布尔属性即可。现在运行构建后,我们会得到一个index.js.map文件以及我们的 Source Map index.js。这个 Source Map 文件必须上传到 Lambda 服务。稍后我们将在本文中介绍如何使用 AWS CDK、AWS SAM 和 Serverless Framework 来完成此操作。

源映射支持

仅仅在 Lambda 运行时环境中放置index.js.map文件并不足以启用 Source Maps。我们还需要确保运行时环境知道如何使用它们。幸运的是,自Node.js 版本 12.12.0起,这变得非常简单。我们只需要设置--enable-source-maps命令行选项即可。在 AWS Lambda 中,可以通过设置环境变量来设置命令行选项NODE_OPTIONS。截至撰写本文时,AWS Lambda 支持 Node.js 版本 12 和 14。AWS 没有公布 Lambda 中使用的次要版本号,但我们可以通过process.version在函数中注销来查看。截至 2022 年 1 月下旬,美国东部 1 区域 Lambda 中使用的 Node.js 版本为 12.12.0,v12.22.7因此v14.18.1我们可以使用 Source Maps 而不会遇到任何问题。

如果我们需要在不支持原生版本的运行时环境中启用源映射,我们始终可以使用源映射支持

CDK 示例

所有示例代码都可以在GitHub上找到。

AWS CDK 是我编写和部署无服务器应用程序的首选工具,部分原因在于aws-lambda-nodejs架构。该架构使得使用 TypeScript 变得非常容易。它封装了 esbuild 并公开了各种选项,还支持设置环境变量。

当我使用多个 Lambda 函数时,我经常发现创建一个单独的 props 对象,然后在多个函数之间共享该对象会很有帮助。

const lambdaProps = {
  architecture: Architecture.ARM_64,
  bundling: { minify: true, sourceMap: true },
  environment: {
    NODE_OPTIONS: '--enable-source-maps',
  },
  logRetention: RetentionDays.ONE_DAY,
  runtime: Runtime.NODEJS_14_X,
  memorySize: 512,
  timeout: Duration.minutes(1),
};

new NodejsFunction(this, 'FuncOne', {
  ...lambdaProps,
  entry: `${__dirname}/../fns/one.ts`,
});

new NodejsFunction(this, 'FuncTwo', {
  ...lambdaProps,
  entry: `${__dirname}/../fns/two.ts`,
});
Enter fullscreen mode Exit fullscreen mode

如我们所见,当已经在使用 AWS CDK 和 NodejsFunction 时,启用 Source Maps 非常简单。

无服务器堆栈

Serverless Stack (SST)是一个非常实用的增值功能,它基于 AWS CDK 构建,提供卓越的开发者体验和控制面板。SST 自带的 NodejsFunction 版本Function会自动生成源映射。只需按照说明设置 NODE_OPTIONS即可启用此功能

SAM 示例

SAM 对 TypeScript 的支持已经滞后一段时间了,但最近合并了一个pull request,有望改变这一现状。我们似乎可以在 package.json 文件中添加一个aws_samsam build键,从而启用在 SAM 引擎内进行构建。虽然这个 PR 已经合并到 aws-lambda-builders(SAM 引擎的底层组件)sam build,但它仍然需要添加到aws-sam-cli并发布(预计会引起广泛关注),才能与 SAM 一起使用。

同时,或者如果我们考虑其他部署函数的方案,我们可以添加一个额外的构建步骤。我们将创建一个esbuild.ts 文件来转译函数,然后将我们的 SAMtemplate.yaml文件指向该步骤的输出。

import { build, BuildOptions } from 'esbuild';

const buildOptions: BuildOptions = {
  bundle: true,
  entryPoints: {
    ['create/index']: `${__dirname}/../fns/create.ts`,
    ['delete/index']: `${__dirname}/../fns/delete.ts`,
    ['list/index']: `${__dirname}/../fns/list.ts`,
  },
  minify: true,
  outbase: 'fns',
  outdir: 'sam/build',
  platform: 'node',
  sourcemap: true,
};

build(buildOptions);
Enter fullscreen mode Exit fullscreen mode

SAM 模板会接收所有内容,CodeUri因此上述代码会将 TypeScript 文件转译为 `<typescript_file>`fns/list.ts和 `<output_file> sam/build/list/index.js` sam/build/list/index.js.map。通过设置`--packages-files` CodeUri: sam/build/list,SAM 会将这两个文件打包并上传到 Lambda。

现在我们只需要设置环境变量。这在 SAM 模板中很容易实现。我们可以将其设置为全局变量,这样只需要在模板中设置一次即可。

Globals:
  Function:
    Environment:
      Variables:
        NODE_OPTIONS: '--enable-source-maps'
Enter fullscreen mode Exit fullscreen mode

为了确保我们始终在部署之前进行构建,我们可以添加一些npm 脚本

"scripts": {
  "build:lambda": "npm run clean && ts-node --files sam/esbuild.ts",
  "clean": "rimraf cdk.out sam/build",
  "deploy:sam": "npm run build:lambda && sam deploy --template template.yaml",
  "destroy:sam": "sam delete"
}
Enter fullscreen mode Exit fullscreen mode

这种方法虽然可行,但确实需要额外付出一些努力。SAM 用户无疑都在翘首以盼更好的 TypeScript 支持。

建筑师

或者,可以使用Architect。Architect是一个基于 AWS SAM 构建的第三方开发者体验平台。Architect 包含一个TypeScript 插件

无服务器框架示例

Serverless Framework以其插件系统而闻名。serverless -esbuild将打包功能以及支持 TypeScript 中 Source Map 所需的所有选项集成到 ` sls packageand`sls deploy命令中。

插件已在我们的serverless.yml文件中配置。

custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: true
Enter fullscreen mode Exit fullscreen mode

然后我们只需将函数指向我们的 TypeScript 处理程序即可。

functions:
  create:
    handler: fns/create.handler
Enter fullscreen mode Exit fullscreen mode

与 SAM 类似,Serverless 允许我们为函数设置全局环境变量。

provider:
  name: aws
  lambdaHashingVersion: 20201221
  runtime: nodejs14.x
  environment:
    NODE_OPTIONS: '--enable-source-maps'
Enter fullscreen mode Exit fullscreen mode

与 AWS CDK 类似,这对 TypeScript 开发者来说也是一种不错的体验。已经在使用 Serverless Framework 的开发者应该能够轻松地将 Source Map 添加到他们的应用程序中。

基准

由于据说 Source Maps 会对性能产生负面影响,因此有很多指南不建议在生产环境中使用。让我们来测试一下这个看似简单的list函数,看看代码压缩或使用 Source Maps 是否会产生任何显著影响。

我使用AutoCannon对该函数进行了 30 秒的 100 次并发执行测试。我还使用Lambda Power Tuning找到了理想的内存配置,结果表明最佳内存配置为 512MB。所有结果均可查看

没有源映射,则未压缩。

未压缩的函数大小为 1.2MB。这主要是由于……@aws-sdk/client-dynamodb和…… @aws-sdk/lib-dynamodb。是否坚持使用 SDK v2 能带来更好的性能,这可以另写一篇文章来讨论。尽管自定义代码不多,但该函数的大小仍然超过 1MB。

测试结果显示,该函数的平均执行时间为 46.99 毫秒,最大执行时间为 914 毫秒。99% 的请求执行时间都在 90 毫秒或以下。

已压缩,不含源映射

函数压缩后大小降至 534.8kb,不到原来的一半。我的测试显示平均响应时间为 47.83ms,最大值为 1010ms,第 99 百分位数为 90ms。性能略有下降,但差异不具有统计学意义。我预计如果反复进行这些测试,会发现代码压缩对性能没有实际影响。这并不意外。1.2MB 仍然相当小,我预计在这个大小下不会增加太多延迟。

已使用源映射进行压缩

当然,压缩后的函数仍然是 534.8kb,但源映射为 1.5MB,因此这将是最大的上传文件,不过 ~2MB 并不算多,也不会显著减慢我们的部署速度。

本次测试的平均响应时间为 46.52 毫秒,最大值为 968 毫秒,第 99 百分位数为 82 毫秒。这是目前为止的最佳结果,但并不具有统计学意义。我认为这实际上是三者并列,表明向该函数添加源映射并没有增加延迟。

这是因为 Node.js 本身就支持此功能!由于没有生成堆栈跟踪信息,所以 Source Map 从未被引用。如果我们需要依赖库来实现此功能,则可能无法获得相同的结果。

错误已最小化,未包含源映射。

触发错误条件会有什么影响吗?我们来看看。我运行了同样的测试,但这次函数JSON.parse中包含了错误。错误发生在调用 DynamoDB 之前,所以我们可以预期它会比成功执行的函数快一些。平均延迟为 37.86 毫秒,最大延迟为 1004 毫秒,99% 延迟为 58 毫秒。令人印象深刻的是,调用 DynamoDB 似乎只增加了大约 10 毫秒的延迟!

使用源映射表缩小错误

启用错误源映射确实会影响性能。平均延迟降至 97.54 毫秒,最高延迟为 1129 毫秒,99% 的延迟为 243 毫秒。这是一个显著的提升。错误发生时,源映射确实会影响延迟。这符合预期,也印证了源映射仅在发生错误时才会被引用的观点——但现在必须解析源映射,这需要时间。

结论

在生产环境中使用 Source Maps!在我看来,如果 Source Maps 导致错误过多,性能下降影响到您的最终收益,那么您应该着手修复这些错误。在各种流行的 Lambda 框架中实现 Source Maps 非常简单,而且不会影响函数的成功执行。即使函数失败,有用的堆栈跟踪信息也足以弥补增加的延迟。开发人员的时间成本永远比几毫秒的 Lambda 执行时间要高得多。

覆盖

文章来源:https://dev.to/aws-builders/how-to-use-source-maps-in-typescript-lambda-functions-with-benchmarks-4bo4