无服务器部署
无服务器计算是一种云计算执行模型,其中云提供商根据需求分配机器资源,代表其客户管理服务器。当应用程序不在使用时,不会为应用程序分配计算资源。定价是基于应用程序实际消耗的资源量的(来源)。
在无服务器架构中,您仅关注应用程序代码中的个别函数。诸如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.0079秒(7.9毫秒) |
Nest(使用@nestjs/platform-express ) | 0.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
文件。
使用这个配置,我们得到了以下结果:
Express | 0.0068秒(6.8毫秒) |
Nest(使用@nestjs/platform-express ) | 0.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 build
或tsc
),然后使用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
),如下所示:
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);
};