Оптимизация Cold Start для Serverless Functions
Cold start — это задержка при первом вызове Lambda после периода простоя или при масштабировании на новый экземпляр. Состоит из нескольких фаз: создание контейнера (~100–500ms), инициализация runtime (~50–200ms для Node.js), выполнение init-кода вашей функции (зависит от вас). Первые две фазы почти не поддаются оптимизации — всё, что можно сделать, это работать с третьей.
Реальные цифры для Node.js 20 на AWS Lambda: без оптимизаций init-фаза занимает 300–800ms. После оптимизаций — 50–150ms. На arm64 (Graviton2) runtime-фаза на 10–20% быстрее x86.
Что происходит при cold start
Фазы cold start:
1. Container init │ ~100-500ms │ AWS управляет, не оптимизируется
2. Runtime init │ ~50-200ms │ зависит от runtime и архитектуры
3. Function init │ ВАШ КОД │ require/import, DB-подключения, SDK init
4. Handler execution │ ВАШ КОД │ собственно работа функции
Профилирование init-фазы через переменную окружения:
# Добавляем в Lambda environment
AWS_LAMBDA_EXEC_WRAPPER=/opt/aws-lambda-exec-wrapper
# или используем встроенное профилирование
Лучший способ замерить — CloudWatch Logs. Ищем строку Init Duration в отчёте:
REPORT RequestId: abc123
Duration: 45.23 ms
Billed Duration: 46 ms
Memory Size: 512 MB
Max Memory Used: 89 MB
Init Duration: 312.45 ms ← это cold start overhead
Уменьшение размера бандла
Главая причина медленного cold start — большой бандл с ненужными модулями.
До оптимизации:
// Плохо — импортируем весь AWS SDK
import AWS from 'aws-sdk';
const s3 = new AWS.S3();
const dynamo = new AWS.DynamoDB.DocumentClient();
После:
// Хорошо — только нужные клиенты, AWS SDK v3 модульный
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
// Инициализируем один раз вне handler
const s3 = new S3Client({ region: process.env.AWS_REGION });
const dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({}));
Вынесение AWS SDK из бандла (он уже есть в Lambda runtime для Node.js 18+):
// esbuild конфиг
{
"external": ["@aws-sdk/*"],
"bundle": true,
"minify": true,
"target": "node20",
"platform": "node"
}
Размеры до/после. Типичный Express-приложение с aws-sdk v2:
- До: 8–15 MB zip
- После: 500KB–2MB zip
Lazy loading для редко используемых модулей
// Плохо — всё загружается при init
import { createCanvas } from 'canvas';
import sharp from 'sharp';
import { PDFDocument } from 'pdf-lib';
export const handler = async (event) => {
if (event.type === 'generate-pdf') {
// используется 5% вызовов
const pdf = await PDFDocument.create();
}
};
// Хорошо — тяжёлые модули только когда нужны
export const handler = async (event) => {
if (event.type === 'generate-pdf') {
const { PDFDocument } = await import('pdf-lib');
const pdf = await PDFDocument.create();
}
};
Инициализация вне handler
Код вне handler выполняется один раз при cold start и переиспользуется между горячими вызовами:
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// INIT PHASE — один раз
const client = new DynamoDBClient({
// Переиспользуем TCP-соединения
requestHandler: {
requestTimeout: 3000,
httpsAgent: { keepAlive: true, maxSockets: 50 },
},
});
const dynamo = DynamoDBDocumentClient.from(client);
// Переменные окружения читаем один раз
const TABLE_NAME = process.env.TABLE_NAME!;
const STAGE = process.env.STAGE ?? 'dev';
// HANDLER — вызывается каждый раз
export const handler = async (event) => {
// dynamo уже инициализирован, соединение переиспользуется
const result = await dynamo.send(new GetCommand({
TableName: TABLE_NAME,
Key: { pk: event.userId, sk: 'profile' },
}));
return result.Item;
};
Оптимизация подключений к базе данных
Обычный пул соединений (pg Pool, Sequelize) не работает в serverless — каждый экземпляр Lambda создаёт своё подключение, при 1000 concurrent executions получаем 1000 подключений к PostgreSQL.
Решение 1: RDS Proxy (AWS-managed connection pooler):
// Подключаемся к RDS Proxy, не напрямую к RDS
import { Pool } from 'pg';
const pool = new Pool({
host: process.env.RDS_PROXY_ENDPOINT, // xxx.proxy-xxx.rds.amazonaws.com
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 1, // 1 соединение на экземпляр Lambda
idleTimeoutMillis: 0,
connectionTimeoutMillis: 5000,
});
// Переиспользуем между горячими вызовами
let cachedClient: pg.PoolClient | null = null;
export const getDbClient = async () => {
if (!cachedClient) {
cachedClient = await pool.connect();
}
return cachedClient;
};
Решение 2: PlanetScale или Neon — HTTP-based serverless databases без постоянного соединения:
import { neon } from '@neondatabase/serverless';
// Каждый запрос — HTTP, не TCP
const sql = neon(process.env.DATABASE_URL!);
export const handler = async (event) => {
const users = await sql`SELECT id, email FROM users WHERE active = true LIMIT 10`;
return users;
};
Provisioned Concurrency
Provisioned Concurrency — AWS держит N экземпляров Lambda всегда прогретыми. Init-фаза выполняется заранее, пользователь получает ответ без задержки.
# serverless.yml или SAM template
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
# ...
AutoPublishAlias: live
ApiProvisionedConcurrency:
Type: AWS::Lambda::ProvisionedConcurrencyConfig
Properties:
FunctionName: !Ref ApiFunction
Qualifier: live
ProvisionedConcurrentExecutions: 5 # держим 5 горячих экземпляров
Через Serverless Framework:
functions:
api:
handler: src/handler.main
provisionedConcurrency: 5
# Автоматическое масштабирование через Application Auto Scaling
# настраивается отдельно через AWS Console или CDK
Provisioned Concurrency стоит денег (оплачивается даже в idle), поэтому используется только для критических эндпоинтов.
arm64 vs x86_64
Переключение на Graviton2 (arm64) — простейшая оптимизация с нулевыми изменениями кода:
# SAM template
Globals:
Function:
Architectures: [arm64] # было x86_64
Выигрыш: ~10–20% меньше init duration, ~20% дешевле по тарифам AWS. Единственное ограничение: нативные Node.js модули (.node файлы) нужно пересобирать под arm64. Обычные JS/TS модули работают без изменений.
Сроки
Аудит и базовая оптимизация бандла (esbuild, tree shaking, вынос AWS SDK) — 1 день. Переработка инициализации с вынесением клиентов из handler, настройка RDS Proxy — 2–3 дня. Настройка Provisioned Concurrency с мониторингом и автоскейлингом — 1–2 дня. Полный цикл: от аудита до production с измеримым результатом — 1 неделя.







