正在加载,请稍候…

AWS Lambda 上的无服务器 Node.js:性能、冷启动与模式

使用 AWS Lambda 和 Node.js 构建生产级无服务器 API,涵盖冷启动优化、Lambda 层、容器镜像、预置并发和 Serverless Fra

AWS Lambda 上的无服务器 Node.js:性能、冷启动与模式

Lambda 上的无服务器架构

Lambda 抽象了基础设施,但引入了新的挑战:冷启动、无状态和按请求计费。

AWS Lambda 上的无服务器 Node.js:性能、冷启动与模式 插图

基本 Lambda 函数

// handler.js
export const handler = async (event, context) => {
  // event: API Gateway, SQS, S3 等
  // context: Lambda 运行时信息
  
  context.callbackWaitsForEmptyEventLoop = false; // 对数据库连接很重要!
  
  try {
    const { httpMethod, path, body, headers } = event;
    const payload = body ? JSON.parse(body) : null;
    
    // 业务逻辑
    const result = await processRequest({ method: httpMethod, path, payload });
    
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify(result),
    };
  } catch (err) {
    console.error('Handler error:', err);
    return {
      statusCode: err.statusCode ?? 500,
      body: JSON.stringify({ error: err.message }),
    };
  }
};

冷启动优化

当 Lambda 创建新的执行环境时会发生冷启动。主要原因和修复方法:

// 1. 将重初始化移到 handler 外部
import { DynamoDB } from '@aws-sdk/client-dynamodb';

// 每次冷启动执行一次,而不是每次调用
const ddb = new DynamoDB({ region: 'us-east-1' });
const tableName = process.env.TABLE_NAME;

// 2. 仅在需要时懒加载模块
let heavyLibrary;
function getHeavyLib() {
  if (!heavyLibrary) {
    heavyLibrary = require('./heavy-module');
  }
  return heavyLibrary;
}

// 3. 最小化包大小——避免打包整个 AWS SDK
import { GetItemCommand } from '@aws-sdk/client-dynamodb'; // 可摇树优化的 v3

包大小缩减

// serverless.js / webpack.config.js
import { nodeExternalsPlugin } from 'esbuild-node-externals';

export default {
  functions: {
    api: {
      handler: 'src/handler.handler',
      package: {
        individually: true,
      },
    },
  },
  custom: {
    esbuild: {
      bundle: true,
      minify: true,
      target: 'node20',
      exclude: ['@aws-sdk/*'],  // Lambda 运行时中可用
    },
  },
};

AWS Lambda 上的无服务器 Node.js:性能、冷启动与模式 插图

数据库连接管理

// 在 warm 调用间复用连接池
let pgPool;

async function getPool() {
  if (!pgPool) {
    const { Pool } = await import('pg');
    pgPool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 1,  // Lambda:每个容器一个连接
      idleTimeoutMillis: 0,
      connectionTimeoutMillis: 5000,
    });
  }
  return pgPool;
}

export const handler = async (event) => {
  const pool = await getPool();
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT * FROM users LIMIT 10');
    return { statusCode: 200, body: JSON.stringify(result.rows) };
  } finally {
    client.release();
  }
};

预置并发

# serverless.yml
functions:
  api:
    handler: src/handler.handler
    provisionedConcurrency: 5  # 保持 5 个实例 warm
    events:
      - http:
          path: /api/{proxy+}
          method: any

Lambda 层

# 共享依赖层
layers:
  dependencies:
    path: layer
    compatibleRuntimes:
      - nodejs20.x

functions:
  api:
    handler: src/handler.handler
    layers:
      - !Ref DependenciesLambdaLayer

AWS Lambda 上的无服务器 Node.js:性能、冷启动与模式 插图

容器镜像 Lambda

FROM public.ecr.aws/lambda/nodejs:20

COPY package*.json ./
RUN npm ci --only=production

COPY src ./src

CMD ["src/handler.handler"]

中间件模式 (Middy)

import middy from '@middy/core';
import httpRouterHandler from '@middy/http-router';
import httpErrorHandler from '@middy/http-error-handler';
import httpJsonBodyParser from '@middy/http-json-body-parser';
import validator from '@middy/validator';

const getUsers = middy()
  .use(httpJsonBodyParser())
  .use(validator({ eventSchema: getUsersSchema }))
  .use(httpErrorHandler())
  .handler(async (event) => {
    const users = await userService.list(event.queryStringParameters);
    return { statusCode: 200, body: JSON.stringify(users) };
  });

export const handler = httpRouterHandler([
  { method: 'GET', path: '/users', handler: getUsers },
]);

使用 Lambda Powertools 进行监控

import { Logger } from '@aws-lambda-powertools/logger';
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
import { Tracer } from '@aws-lambda-powertools/tracer';

const logger = new Logger({ serviceName: 'user-service' });
const metrics = new Metrics({ namespace: 'MyApp' });
const tracer = new Tracer({ serviceName: 'user-service' });

export const handler = async (event) => {
  const segment = tracer.getSegment();
  const subsegment = segment.addNewSubsegment('processRequest');
  
  try {
    logger.info('Processing request', { event });
    const result = await processRequest(event);
    
    metrics.addMetric('SuccessfulRequests', MetricUnits.Count, 1);
    return result;
  } catch (err) {
    metrics.addMetric('FailedRequests', MetricUnits.Count, 1);
    logger.error('Request failed', { err });
    throw err;
  } finally {
    subsegment.close();
    metrics.publishStoredMetrics();
  }
};