NestJS——Serverless(官方文档翻译)

无服务器计算是一种云计算执行模型,其中云提供商按需分配计算机资源,代表其客户照顾服务器。当应用未使用时,不会为应用分配计算资源。定价基于应用程序(源)消耗的实际资源量。

使用无服务器体系结构,您可以完全关注应用程序代码中的各个函数。AWS Lambda、Google Cloud Functions 和 Microsoft Azure Functions 等服务负责所有物理硬件、虚拟机操作系统和 Web 服务器软件管理。

提示本章没有涵盖无服务器功能的优缺点,也没有深入探讨任何云提供商的细节。

冷启动

冷启动是一段时间内首次执行代码。根据您使用的云提供商,它可能跨越几个不同的操作,从下载代码和引导运行时到最终运行代码。此过程会显著增加延迟,具体取决于多个因素、语言、应用程序所需的包数等。

冷启动很重要,尽管有些事情超出了我们的控制范围,但我们仍然可以做很多事情来使其尽可能短。

虽然你可以将 Nest 视为一个成熟的框架,旨在用于复杂的企业应用程序,但它也适用于“更简单”的应用程序(或脚本)。例如,通过使用独立应用程序功能,您可以在简单工作线程、CRON作业、CLI或无服务器功能中利用 Nest 的 DI 系统。

基准测试

为了更好地了解在无服务器函数上下文中使用 Nest 或其他知名库(如 express )的成本是多少,让我们比较一下 Node 运行时运行以下脚本需要多少时间:

// #1 Express
import * as express from 'express';

async function bootstrap() {
  const app = express();
  app.get('/', (req, res) => res.send('Hello world!'));
  await new Promise<void>((resolve) => app.listen(3000, resolve));
}
bootstrap();

// #2 Nest (with @nestjs/platform-express)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { logger: ['error'] });
  await app.listen(3000);
}
bootstrap();

// #3 Nest as a Standalone application (no HTTP server)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AppService } from './app.service';

async function bootstrap() {
  const app = await NestFactory.createApplicationContext(AppModule, {
    logger: ['error'],
  });
  console.log(app.get(AppService).getHello());
}
bootstrap();

// #4 Raw Node.js script
async function bootstrap() {
  console.log('Hello world!');
}
bootstrap();

对于所有这些脚本,我们使用了 tsc (TypeScript)编译器,因为这些代码没有打包(没使用 webpack )。

Express 0.0079s (7.9ms)
Nest 使用 @nestjs/platform-express 0.1974s (197.4ms) 0.1974秒
Nest (独立应用程序) 0.1117s (111.7ms)
原生 Node.js 脚本 0.0071s (7.1ms)

注意:MacBook Pro 2014 年中,2.5 GHz 四核英特尔酷睿 i7,16 GB 1600 MHz DDR3,固态硬盘。

现在,让我们重复所有基准测试,但这一次,使用 webpack (如果您安装了 Nest CLI,则可以运行 nest build --webpack )将我们的应用程序捆绑到一个可执行的 JavaScript 文件中。但是,我们将确保将所有依赖项 ( node_modules ) 捆绑在一起,而不是使用 Nest CLI 附带的默认 webpack 配置,如下所示:

module.exports = (options, webpack) => {
  const lazyImports = [
    '@nestjs/microservices/microservices-module',
    '@nestjs/websockets/socket-module',
  ];

  return {
    ...options,
    externals: [],
    plugins: [
      ...options.plugins,
      new webpack.IgnorePlugin({
        checkResource(resource) {
          if (lazyImports.includes(resource)) {
            try {
              require.resolve(resource);
            } catch (err) {
              return true;
            }
          }
          return false;
        },
      }),
    ],
  };
};

提示:要指示 Nest CLI 使用此配置,请在项目的根目录中创建一个新的 webpack.config.js 文件。

使用此配置,我们收到了以下结果:

Express 0.0068s (6.8ms) 0.0068
Nest 使用 @nestjs/platform-express 0.0815s (81.5ms)
Nest (独立应用程序) 0.0319s (31.9ms)
原生 Node.js 脚本 0.0066s (6.6ms)

注意:MacBook Pro 2014 年中,2.5 GHz 四核英特尔酷睿 i7,16 GB 1600 MHz DDR3,固态硬盘。

提示:你可以通过应用额外的代码压缩和优化技术(使用 webpack 插件等)进一步优化它。

如您所见,编译方式(以及是否捆绑代码)至关重要,并且对整体启动时间有重大影响。使用 webpack ,您可以将独立 Nest 应用程序(具有一个模块、控制器和服务的新项目)的引导时间平均缩短到 ~32ms,对于常规的 HTTP、基于 Express 的 NestJS 应用程序,引导时间缩短到 ~81.5ms。

对于更复杂的Nest应用,例如,有10个资源(通过 $Nest g resource 示意 = 10个模块、10个控制器、10个服务、20个 DTO 类、50个HTTP端点 + AppModule),在2014年中期的MacBook Pro上,2.5 GHz四核Intel Core i7, 16 GB 1600 MHz DDR3, SSD大约需要0.1298s (129.8ms)。将单体应用程序作为无服务器功能运行通常没有太大意义,因此可以将此基准测试更多地视为一个示例,说明引导时间如何随着应用程序的增长而潜在增加。

运行时优化

到目前为止,我们介绍了编译时优化。这些与您在应用程序中定义提供程序和加载 Nest 模块的方式无关,随着应用程序变大,这起着至关重要的作用。

例如,假设将数据库连接定义为异步提供程序。异步提供程序旨在延迟应用程序启动,直到一个或多个异步任务完成。这意味着,如果您的无服务器函数平均需要 2 秒才能连接到数据库(在引导时),您的终端节点至少需要两秒钟(因为它必须等到连接建立)才能发送回响应(当它是冷启动并且您的应用程序尚未运行时)。

如您所见,在引导时间很重要的无服务器环境中,构建提供程序的方式有些不同。另一个很好的例子是,如果您使用 Redis 进行缓存,但仅限于某些场景。也许,在这种情况下,您不应将 Redis 连接定义为异步提供程序,因为它会减慢引导时间,即使此特定函数调用不需要它也是如此。

此外,有时您可以使用 LazyModuleLoader 类延迟加载整个模块,如本章中所述。缓存在这里也是一个很好的例子。假设您的应用程序具有 CacheModule ,它在内部连接到 Redis,并导出 CacheService 以与 Redis 存储进行交互。如果您不需要它进行所有潜在的函数调用,则可以按需懒惰地加载它。这样,对于所有不需要缓存的调用,您将获得更快的启动时间(发生冷启动时)。

if (request.method === RequestMethod[RequestMethod.GET]) {
  const { CacheModule } = await import('./cache.module');
  const moduleRef = await this.lazyModuleLoader.load(() => CacheModule);

  const { CacheService } = await import('./cache.service');
  const cacheService = moduleRef.get(CacheService);

  return cacheService.get(ENDPOINT_KEY);
}

另一个很好的例子是 webhook 或 worker,它根据某些特定条件(例如,输入参数),可能会执行不同的操作。在这种情况下,您可以在路由处理程序中指定一个条件,该条件延迟加载用于特定函数调用的适当模块,然后懒惰地加载所有其他模块。

if (workerType === WorkerType.A) {
  const { WorkerAModule } = await import('./worker-a.module');
  const moduleRef = await this.lazyModuleLoader.load(() => WorkerAModule);
  // ...
} else if (workerType === WorkerType.B) {
  const { WorkerBModule } = await import('./worker-b.module');
  const moduleRef = await this.lazyModuleLoader.load(() => WorkerBModule);
  // ...
}

集成示例

应用程序的入口文件(通常为 main.ts 文件)的外观取决于多个因素,因此没有一个模板仅适用于每个方案。例如,启动无服务器函数所需的初始化文件因云提供商(AWS、Azure、GCP 等)而异。此外,根据您是要运行具有多个路由/终结点的典型 HTTP 应用程序,还是仅提供单个路由(或执行代码的特定部分),应用程序的代码看起来会有所不同(例如,对于每个函数的终结点方法,您可以使用 NestFactory.createApplicationContext 而不是启动 HTTP 服务器、设置中间件等)。

出于说明目的,我们将 Nest(使用 @nestjs/platform-express 并启动整个功能齐全的 HTTP 路由器)与无服务器框架(在本例中为目标,针对 AWS Lambda)集成。正如我们之前提到的,您的代码将根据您选择的云提供商以及许多其他因素而有所不同。

首先,让我们安装所需的软件包:

$ npm i @vendia/serverless-express aws-lambda
$ npm i -D @types/aws-lambda serverless-offline

提示:为了加快开发周期,我们安装了模拟 AWS λ 和 API Gateway 的 serverless-offline 插件。

安装过程完成后,让我们创建 serverless.yml 文件来配置无服务器框架:

service: serverless-example

plugins:
  - serverless-offline

provider:
  name: aws
  runtime: nodejs14.x

functions:
  main:
    handler: dist/main.handler
    events:
      - http:
          method: ANY
          path: /
      - http:
          method: ANY
          path: '{proxy+}'

提示:要了解有关无服务器框架的更多信息,请访问官方文档

有了这个位置,我们现在可以导航到 main.ts 文件并使用所需的样板更新我们的引导代码:

import { NestFactory } from '@nestjs/core';
import serverlessExpress from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';

let server: Handler;

async function bootstrap(): Promise<Handler> {
  const app = await NestFactory.create(AppModule);
  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp });
}

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  server = server ?? (await bootstrap());
  return server(event, context, callback);
};

提示:创建多个无服务器函数并在它们之间共享公共模块,我们建议使用 CLI Monorepo 模式

警告:如果使用 @nestjs/swagger 包,则需要执行一些其他步骤才能使其在无服务器函数的上下文中正常工作。查看此文章以获取更多信息。

接下来,打开 tsconfig.json 文件并确保启用 esModuleInterop 选项以使 @vendia/serverless-express 包正确加载。

{
  "compilerOptions": {
    ...
    "esModuleInterop": true
  }
}

现在我们可以构建我们的应用程序(使用 nest buildtsc ),并使用 serverless CLI 在本地启动我们的 lambda 函数:

$ npm run build
$ npx serverless offline

应用程序运行后,打开浏览器并导航到 http://localhost:3000/dev/[ANY_ROUTE] (其中 [ANY_ROUTE] 是在应用程序中注册的任何终结点)。

在上面的部分中,我们已经展示了使用 webpack 并捆绑应用会对整体引导时间产生重大影响。但是,要使其适用于我们的示例,您必须在 webpack.config.js 文件中添加一些其他配置。通常,为了确保我们的 handler 函数将被拾取,我们必须将 output.libraryTarget 属性更改为 commonjs2

return {
  ...options,
  externals: [],
  output: {
    ...options.output,
    libraryTarget: 'commonjs2',
  },
  // ... the rest of the configuration
};

完成此操作后,您现在可以使用 $ nest build --webpack 编译函数的代码(然后使用 $ npx serverless offline 对其进行测试)。

还建议(但不是必需的,因为它会减慢构建过程)安装 terser-webpack-plugin 包并覆盖其配置,以便在缩小生产构建时保持类名不变。不这样做可能会导致在应用程序中使用 class-validator 时出现不正确的行为。

const TerserPlugin = require('terser-webpack-plugin');

return {
  ...options,
  externals: [],
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          keep_classnames: true,
        },
      }),
    ],
  },
  output: {
    ...options.output,
    libraryTarget: 'commonjs2',
  },
  // ... the rest of the configuration
};

使用独立应用程序功能

或者,如果你想保持你的函数非常轻量级,并且你不需要任何与 HTTP 相关的功能(路由,还有守卫、拦截器、管道等),你可以只使用 NestFactory.createApplicationContext (如前所述)而不是运行整个 HTTP 服务器(和 express 在后台),如下所示:

// main.ts
import { HttpStatus } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';
import { AppService } from './app.service';

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  const appContext = await NestFactory.createApplicationContext(AppModule);
  const appService = appContext.get(AppService);

  return {
    body: appService.getHello(),
    statusCode: HttpStatus.OK,
  };
};

提示:注意 NestFactory.createApplicationContext 没有使用增强器(guard、interceptor 等)包装控制器方法。为此,必须使用 NestFactory.create 方法。

您还可以将 event 对象向下传递到 EventsService 提供程序,该提供程序可以处理它并返回相应的值(取决于输入值和业务逻辑)。

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  const appContext = await NestFactory.createApplicationContext(AppModule);
  const eventsService = appContext.get(EventsService);
  return eventsService.process(event);
};
posted @ 2023-03-06 15:14  菠萝橙子丶  阅读(74)  评论(0编辑  收藏  举报