Оптимізація 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 тиждень.







