无服务器计算(Serverless Computing)是一种云计算执行模型。在该模型中,云服务平台会根据请求自动分配计算资源,并负责服务器的运维管理。当应用处于空闲状态时,不会消耗任何计算资源;你只需为实际使用的资源付费。
采用无服务器架构(Serverless Architecture)的开发方式,意味着你无需关注底层基础设施,只需专注于编写函数逻辑。像 AWS Lambda、Google Cloud Functions、Microsoft Azure Functions 等云服务会自动处理底层的服务器、虚拟机和操作系统配置,甚至包括 Web 服务器的维护。
本章不讨论无服务器函数的优缺点,也不会深入探讨具体云厂商的实现细节。
冷启动(Cold Start)是指函数在长时间未被调用后再次触发时,所经历的初始化过程。这个过程可能包括:加载函数代码、启动运行时环境、初始化依赖等操作,直到你的业务逻辑真正开始执行。
冷启动的延迟取决于多个因素,包括使用的编程语言、依赖数量、部署包大小等。有些语言(如 Java)或框架加载时间较长,冷启动延迟可能更加明显。
尽管某些冷启动过程不可避免,但仍有方法可以优化其响应速度。例如:减小部署包体积、移除不必要的依赖、合理拆分函数逻辑等。
虽然 Nest 通常被认为是一款面向企业级复杂应用的全功能框架,但它同样适用于轻量化的场景,比如:
借助独立应用特性,你依然可以在这些场景中充分利用 Nest 强大的依赖注入机制和模块组织能力,获得一致的开发体验。
为了评估在无服务器函数场景中,NestJS 与其他常见方案(如 express 或原生 Node.js)在冷启动性能方面的差异,我们编写了几段示例脚本,并测量它们在 Node.js 运行时中的启动耗时。
以下是用于测试的四种脚本示例:
// #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))
}
// #2 使用 NestJS(基于 @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(process.env.PORT ?? 3000)
}
// #3 使用 NestJS 的独立应用模式(不依赖 HTTP 服务)
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())
}
// #4 原生 Node.js 脚本
async function bootstrap() {
console.log('Hello world!')
}上述所有脚本均使用 TypeScript 编译器(tsc)进行编译,未经过打包处理(即未使用 webpack 等工具)。因此,测试的均为未压缩的原始 TypeScript 编译结果。
| 框架 | 启动时间 |
|---|---|
| 原生 Node.js 脚本 | 0.0071 秒(7.1 毫秒) |
| Express | 0.0079 秒(7.9 毫秒) |
Nest(基于 @nestjs/platform-express) | 0.1974 秒(197.4 毫秒) |
| Nest(独立应用) | 0.1117 秒(111.7 毫秒) |
测试设备:MacBook Pro(2014 款),配备 2.5 GHz 四核 Intel Core i7、16 GB 1600 MHz DDR3 内存和 SSD 存储。
接下来,我们将对这些脚本进行打包,再次进行基准测试。
你可以使用 Nest CLI 中的 nest build --webpack 命令,将 Nest 应用打包为一个独立的 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 {
return true
}
}
return false
},
}),
],
}
}若希望 Nest CLI 使用该配置文件,请在项目根目录下创建 webpack.config.js。
使用上述配置后,得到如下启动时间测试结果:
| 框架 | 启动时间 |
|---|---|
| 原生 Node.js 脚本 | 0.0066 秒(6.6 毫秒) |
| Express | 0.0068 秒(6.8 毫秒) |
Nest(基于 @nestjs/platform-express) | 0.0815 秒(81.5 毫秒) |
| Nest(独立应用) | 0.0319 秒(31.9 毫秒) |
设备:MacBook Pro(2014 年中款),2.5GHz 四核 Intel Core i7,16GB DDR3(1600 MHz)内存,SSD 存储。
你还可以进一步通过代码压缩、使用 Webpack 插件等手段来优化性能。
从测试结果可以看出,构建与打包方式对应用启动时间有明显影响。
采用 Webpack 打包的 Nest 独立应用(包含一个模块、一个控制器和一个服务),平均启动时间约为 32 毫秒;而标准的基于 Express 的 Nest 应用,启动时间则为 81.5 毫秒。
如果是一个更复杂的 Nest 应用,例如通过 $ nest g resource 命令生成的 10 个资源模块(共包含 10 个模块、10 个控制器、10 个服务、20 个 DTO 类和 50 个 HTTP 端点,再加上 AppModule),在相同环境下的启动时间约为 129.8 毫秒。
当然,将大型单体应用作为单个无服务器函数运行,在实际中并不现实。不过这些测试数据可以作为性能基准,帮助你评估应用复杂度对启动时间的影响。
前文我们讲到了编译时优化,这类优化主要与提供者的定义方式或模块加载顺序无关。但随着应用规模的扩大,运行时机制对应用启动性能的影响将愈发显著。
以一个常见场景为例:假设你定义了一个数据库连接作为异步提供者。异步提供者的特点是,应用会等待相关的异步任务完成后,才正式启动。这种机制可能在无服务器架构中引发问题 —— 如果数据库连接平均需要 2 秒建立,那么在发生冷启动时,请求的响应时间就会被强制延迟 2 秒。
正因如此,在无服务器环境中组织提供者的方式,需要区别于传统应用。启动速度至关重要,因此必须避免不必要的初始化操作。
比如:若你仅在特定情况下才使用 Redis 缓存服务,那么就不应将 Redis 连接定义为异步提供者。否则,即便本次请求根本不涉及缓存逻辑,也会因等待 Redis 连接建立而拖慢整个应用的启动。
为了解决这类问题,你可以使用 LazyModuleLoader 类(详见懒加载模块章节),按需加载模块。以缓存模块为例:
假设你的应用中有一个 CacheModule,该模块负责初始化 Redis 并导出 CacheService。如果并非所有请求都需要缓存支持,就可以通过懒加载的方式,仅在必要时加载该模块,从而显著加快冷启动速度:
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)
// ...
}Nest 应用的入口文件(通常为 main.ts)的写法会因多种因素而有所不同,因此不存在放之四海而皆准的模板。例如,针对不同的云平台(如 AWS、Azure、GCP)部署函数时,入口文件的结构往往也会有所差异。
此外,入口逻辑还取决于你希望实现的目标:是构建一个带有多个路由的完整 HTTP 应用,还是仅需运行某个独立函数?(在「函数即端点」的场景中,你可能会使用 NestFactory.createApplicationContext 来创建无 HTTP 服务的上下文。)
在本节中,我们将演示如何将 Nest(借助 @nestjs/platform-express 启动完整的 HTTP 路由能力)与 Serverless Framework 集成,目标部署平台为 AWS Lambda。需要注意的是,实际实现可能会因云厂商或项目需求的不同而有所调整。
首先,安装所需依赖包:
npm install @codegenie/serverless-express aws-lambda
npm install -D @types/aws-lambda serverless-offline这里使用的 serverless-offline 插件可以在本地模拟 AWS Lambda 和 API Gateway
的运行环境,极大提高开发与调试效率。
创建 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+}'更多关于 Serverless Framework 的配置说明,请参阅官方文档。
接下来,更新 main.ts 以适配无服务器部署方式:
import { NestFactory } from '@nestjs/core'
import serverlessExpress from '@codegenie/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 进行 API
文档生成,在无服务器环境中可能需要额外配置以确保其正常工作。详见相关讨论。
确保在 tsconfig.json 中启用了 esModuleInterop,以正确引入 @codegenie/serverless-express 包:
{
"compilerOptions": {
...
"esModuleInterop": true
}
}构建项目后,可使用 serverless CLI 启动本地模拟环境:
npm run build
npx serverless offline本地服务启动后,可通过浏览器访问 http://localhost:3000/dev/[ANY_ROUTE],其中 [ANY_ROUTE] 为你在 Nest 应用中定义的任意路由路径。
通过 webpack 对函数进行打包,可显著减少冷启动耗时。但为了正常导出 handler 函数,需要在 webpack.config.js 中做一些配置:
return {
...options,
externals: [],
output: {
...options.output,
libraryTarget: 'commonjs2',
},
// 其他配置项
}构建时可使用以下命令:
nest build --webpack
npx serverless offlineclass-validator在某些情况下(例如使用 class-validator 时),压缩工具会移除类名,导致运行时类型判断失败。为避免此类问题,我们建议(但非强制)安装并配置 terser-webpack-plugin,在压缩过程中保留类名:
npm install -D terser-webpack-pluginconst TerserPlugin = require('terser-webpack-plugin')
return {
...options,
externals: [],
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
keep_classnames: true,
},
}),
],
},
output: {
...options.output,
libraryTarget: 'commonjs2',
},
// 其他配置项
}如果你希望函数尽可能轻量,且不依赖任何 HTTP 相关功能(如路由、守卫、拦截器、管道等),可以选择使用 NestFactory.createApplicationContext,而不是启动完整的 HTTP 服务(默认基于 Express)。这种方式非常适合在无服务器(Serverless)环境中,仅借助依赖注入功能执行特定逻辑的场景。
以下示例展示了如何在 AWS Lambda 中创建一个独立的 Nest 应用上下文:
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 {
statusCode: HttpStatus.OK,
body: appService.getHello(),
}
}使用 createApplicationContext
创建的应用上下文不会执行任何控制器相关逻辑,也不会触发守卫、拦截器、管道等「增强器」。如果你需要完整的请求处理流程,请使用
NestFactory.create 启动 HTTP 应用。
你也可以将 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)
}