NestJS Logo

无服务器部署

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

无服务器架构中,您仅关注应用程序代码中的个别函数。诸如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)。

Express0.0079秒(7.9毫秒)
Nest(使用@nestjs/platform-express0.1974秒(197.4毫秒)
Nest(独立应用程序)0.1117秒(111.7毫秒)
纯粹的Node.js脚本0.0071秒(7.1毫秒)
注意 机器信息:MacBook Pro Mid 2014,2.5 GHz 四核 Intel Core i7,16 GB 1600 MHz DDR3 内存,SSD硬盘。

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


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文件。

使用这个配置,我们得到了以下结果:

Express0.0068秒(6.8毫秒)
Nest(使用@nestjs/platform-express0.0815秒(81.5毫秒)
Nest(独立应用程序)0.0319秒(31.9毫秒)
纯粹的Node.js脚本0.0066秒(6.6毫秒)
注意 机器信息:MacBook Pro Mid 2014,2.5 GHz 四核 Intel Core i7,16 GB 1600 MHz DDR3 内存,SSD硬盘。
提示 您甚至可以通过应用额外的代码最小化和优化技术(使用webpack插件等)进一步优化它。

正如您所见,编译方式(以及是否捆绑代码)至关重要,对整体启动时间有显著影响。使用webpack,您可以将独立的Nest应用程序(具有一个模块、控制器和服务的入门项目)的引导时间平均降低到约32毫秒,对于常规的基于HTTP和express的NestJS应用程序,引导时间降低到约81.5毫秒。

对于更复杂的Nest应用程序,例如,具有10个资源(通过$ nest g resource原理图生成 = 10个模块、10个控制器、10个服务、20个DTO类、50个HTTP端点 + AppModule),在MacBook Pro Mid 2014,2.5 GHz 四核 Intel Core i7,16 GB 1600 MHz DDR3 内存,SSD上,总体启动时间约为0.1298秒(129.8毫秒)。通常情况下,将单体应用程序作为无服务器函数运行并不太合适,因此请将这个基准测试视为一个示例,展示了随着应用程序的增长,引导时间可能会增加的情况。

运行时优化#

到目前为止,我们已经涵盖了编译时的优化。这与您在应用程序中定义提供者和加载Nest模块的方式无关,而这在应用程序变得更大时发挥着重要作用。

例如,想象一下将数据库连接定义为异步提供者。异步提供者旨在延迟应用程序启动,直到完成一个或多个异步任务。 这意味着,如果您的无服务器函数在启动时平均需要2秒钟来连接数据库,那么您的端点至少需要额外的两秒钟(因为它必须等待连接建立)才能发送响应(在冷启动时,您的应用程序尚未运行)。

正如您所见,您在无服务器环境中构建提供者的方式与传统环境有所不同,因为引导时间很重要。 另一个很好的例子是,如果您只在某些情况下使用Redis进行缓存。也许,在这种情况下,您不应将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服务器、设置中间件等)。

仅出于示例目的,我们将使用@nestjs/platform-express(因此启动整个功能完整的HTTP路由器)将Nest集成到Serverless框架中(在此示例中,针对AWS Lambda)。正如我们之前提到的,您的代码将根据您选择的云提供商以及许多其他因素而不同。

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


$ npm i @vendia/serverless-express aws-lambda
$ npm i -D @types/aws-lambda serverless-offline
提示 为了加快开发周期,我们安装了serverless-offline插件,该插件可以模拟AWS Lambda和API Gateway。

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


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+}'
提示 要了解有关Serverless框架的更多信息,请访问官方文档

有了这些准备,现在我们可以转到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
JS 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 不会使用增强器(守卫、拦截器等)包装控制器方法。为此,您必须使用 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);
};

支持一下