Реализация Serverless Warming для снижения латентности
Cold start — главная проблема serverless функций в latency-sensitive приложениях. Первый вызов после периода бездействия занимает 200ms-2s в зависимости от runtime, размера пакета и конфигурации. Для API, обрабатывающего реальные запросы пользователей, это неприемлемо. Warming решает проблему, поддерживая функции «тёплыми».
Природа cold start
Что происходит при cold start:
- Облако находит доступный контейнер/VM
- Загружает образ функции
- Инициализирует runtime (Node.js, Python, JVM)
- Выполняет initialization code (вне handler)
- Выполняет handler
Шаги 1-4 — overhead cold start. Шаги 1-3 контролирует провайдер, шаги 4-5 — разработчик.
Типичные времена cold start:
- Python 3.12 (AWS Lambda, 256MB) — 200-400ms
- Node.js 20 — 100-300ms
- Java 17 — 800ms-2s (JVM startup)
- Go — 50-150ms
Scheduled Warming
Самый простой подход: запускать функцию каждые 5 минут через CloudWatch Events / EventBridge, чтобы она не остывала.
# lambda_warmer.py — ping-функция
import json
def handler(event, context):
if event.get('source') == 'warming':
# Это ping от warmers, не реальный запрос
return {'statusCode': 200, 'body': json.dumps({'warm': True})}
# Реальная логика функции
return process_request(event)
# Terraform: CloudWatch rule для warming
resource "aws_cloudwatch_event_rule" "warmer" {
name = "lambda-warmer"
schedule_expression = "rate(5 minutes)"
}
resource "aws_cloudwatch_event_target" "warmer" {
rule = aws_cloudwatch_event_rule.warmer.name
arn = aws_lambda_function.api.arn
input = jsonencode({"source": "warming"})
}
Ограничение: каждый EventBridge trigger запускает только один concurrent инстанс. При нескольких желаемых тёплых инстансах нужно N параллельных вызовов.
Warming нескольких параллельных инстансов
import boto3
import asyncio
lambda_client = boto3.client('lambda')
async def warm_instance(function_name: str, instance_num: int):
lambda_client.invoke(
FunctionName=function_name,
InvocationType='RequestResponse',
Payload=json.dumps({
'source': 'warming',
'instance': instance_num,
'sleep': 10 # Держать инстанс занятым 10 секунд
})
)
async def warm_function(function_name: str, concurrent_count: int = 5):
"""Запустить N параллельных warmup вызовов"""
tasks = [warm_instance(function_name, i) for i in range(concurrent_count)]
await asyncio.gather(*tasks)
Пока один вызов держит инстанс занятым (sleep 10s), Lambda создаёт новый контейнер для следующего параллельного вызова. Результат: 5 тёплых инстансов.
AWS Lambda Provisioned Concurrency
Официальное решение от AWS: резервирование инициализированных инстансов. За доплату, но гарантирует P99 latency без cold start.
resource "aws_lambda_provisioned_concurrency_config" "api" {
function_name = aws_lambda_function.api.function_name
qualifier = aws_lambda_alias.live.name
provisioned_concurrent_executions = 5
}
Auto Scaling Provisioned Concurrency — масштабировать провижнинг по расписанию (утром больше, ночью меньше):
resource "aws_appautoscaling_target" "lambda_pc" {
max_capacity = 20
min_capacity = 2
resource_id = "function:${aws_lambda_function.api.function_name}:live"
scalable_dimension = "lambda:function:ProvisionedConcurrency"
service_namespace = "lambda"
}
resource "aws_appautoscaling_policy" "lambda_pc_tracking" {
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.lambda_pc.resource_id
scalable_dimension = aws_appautoscaling_target.lambda_pc.scalable_dimension
service_namespace = aws_appautoscaling_target.lambda_pc.service_namespace
target_tracking_scaling_policy_configuration {
target_value = 0.7 # 70% utilization провижнинга
predefined_metric_specification {
predefined_metric_type = "LambdaProvisionedConcurrencyUtilization"
}
}
}
Оптимизация initialization code
Warming помогает, но уменьшение самого cold start — лучшая стратегия:
# ПЛОХО: создавать клиенты внутри handler
def handler(event, context):
dynamodb = boto3.resource('dynamodb') # Каждый cold start
db_client = psycopg2.connect(DSN) # Создаёт connection
...
# ХОРОШО: создавать клиенты на уровне модуля (один раз)
import boto3
import psycopg2
dynamodb = boto3.resource('dynamodb') # Инициализируется при cold start
_connection = None # Lazy connection pool
def get_connection():
global _connection
if _connection is None or _connection.closed:
_connection = psycopg2.connect(DSN)
return _connection
def handler(event, context):
conn = get_connection() # Переиспользует существующее соединение
...
Lambda SnapStart (Java)
AWS Lambda SnapStart для Java: создаёт снапшот инициализированного состояния функции. Cold start для Java сокращается с 1-2s до 100-200ms.
resource "aws_lambda_function" "java_api" {
...
snap_start {
apply_on = "PublishedVersions"
}
}
Сроки реализации
- Scheduled warming (EventBridge) — 0.5 дня
- Parallel warming скрипт — 1 день
- Provisioned Concurrency + Auto Scaling — 1-2 дня
- Оптимизация initialization code — 1-3 дня (зависит от кодовой базы)







