Тестирование производительности serverless
Updated Jul 2026
Уникальные задачи serverless
Serverless-функции (AWS Lambda, Google Cloud Functions, Azure Functions) вводят характеристики производительности, отсутствующие в традиционных приложениях. Модель выполнения — эфемерные вычислительные экземпляры, масштабирующиеся до нуля и обратно — создаёт задачи тестирования, которые стандартные подходы к нагрузочному тестированию не решают.
Ключевые задачи и стратегии тестирования
| Задача | Почему это важно | Стратегия тестирования |
|---|---|---|
| Холодные старты | Первый вызов после простоя может занять 1-10 с | Периодические тесты холодного старта с интервалами простоя |
| Лимиты конкурентности | Ограничения, установленные провайдером (напр., 1 000 одновременных Lambda) | Тесты с нарастанием до лимита для определения потолка |
| Связь памяти и CPU | Lambda привязывает CPU к выделению памяти | Тестирование на разных конфигурациях памяти |
| Лимиты времени выполнения | Lambda макс. 15 мин, Cloud Functions макс. 60 мин | Стресс-тесты длительных операций |
| Отсутствие состояния | Нет локального состояния между вызовами | Проверка производительности внешнего хранилища состояния |
| Размер пакета деплоя | Влияет на длительность холодного старта | Измерение холодного старта в зависимости от размера пакета |
| Холодные старты VPC | Функции в VPC имеют более длительные холодные старты (подключение ENI) | Тестирование с VPC и без |
Углублённый анализ холодных стартов
Холодные старты — наиболее значимая проблема производительности serverless. Холодный старт происходит, когда облачному провайдеру необходимо:
- Выделить новую среду выполнения
- Скачать ваш пакет деплоя
- Инициализировать среду выполнения (Node.js, Python, Java и т.д.)
- Выполнить ваш код инициализации (импорты, соединения, загрузка моделей)
Бенчмарки холодного старта по средам выполнения
| Среда выполнения | Типичный холодный старт | С VPC | С большим бандлом |
|---|---|---|---|
| Node.js | 100-300 мс | +300-500 мс | +50-200 мс |
| Python | 200-500 мс | +300-500 мс | +100-300 мс |
| Go | 50-100 мс | +300-500 мс | +20-50 мс |
| Java | 1-5 с | +300-500 мс | +500 мс - 2 с |
| .NET | 500 мс - 2 с | +300-500 мс | +200-500 мс |
Измерение холодных стартов с помощью k6
// k6-serverless-cold-start.js
import http from 'k6/http';
import { Trend, Counter } from 'k6/metrics';
import { sleep } from 'k6';
const coldStartLatency = new Trend('cold_start_ms', true);
const warmLatency = new Trend('warm_latency_ms', true);
const coldStartCount = new Counter('cold_start_detected');
export const options = {
scenarios: {
cold_start_measurement: {
executor: 'per-vu-iterations',
vus: 1,
iterations: 8,
maxDuration: '60m',
},
},
thresholds: {
cold_start_ms: ['p(95)<5000'], // cold starts under 5s
warm_latency_ms: ['p(95)<500'], // warm requests under 500ms
},
};
export default function () {
// Wait for scale-to-zero (adjust based on provider settings)
const idleMinutes = [0, 1, 3, 5, 10, 15, 20, 30];
const iteration = __ITER;
const idleTime = idleMinutes[iteration] || 30;
console.log(`Waiting ${idleTime} minutes for idle...`);
sleep(idleTime * 60);
// First request after idle = likely cold start
const coldRes = http.get('https://api-gw.example.com/function', {
timeout: '60s',
});
const firstLatency = coldRes.timings.duration;
// If first request is >3x the expected warm latency, it is a cold start
const isColdStart = firstLatency > 1000; // threshold: 1s
if (isColdStart) {
coldStartLatency.add(firstLatency);
coldStartCount.add(1);
console.log(`Cold start after ${idleTime}min idle: ${firstLatency}ms`);
}
// Warm requests for comparison
for (let i = 0; i < 5; i++) {
const warmRes = http.get('https://api-gw.example.com/function');
warmLatency.add(warmRes.timings.duration);
sleep(0.5);
}
}
Тестирование лимитов конкурентности
Каждый serverless-провайдер устанавливает лимиты конкурентности. Достижение лимита приводит к тротлингу (ошибки 429), который может каскадироваться через ваше приложение:
// k6-concurrency-limit.js
import http from 'k6/http';
import { Counter, Rate } from 'k6/metrics';
const throttled = new Counter('throttled_requests');
const throttleRate = new Rate('throttle_rate');
export const options = {
scenarios: {
ramp_to_limit: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
preAllocatedVUs: 100,
maxVUs: 2000,
stages: [
{ duration: '1m', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '1m', target: 200 },
{ duration: '1m', target: 500 },
{ duration: '1m', target: 1000 }, // likely exceeds limit
{ duration: '2m', target: 100 }, // cool down
],
},
},
};
export default function () {
const res = http.get('https://api-gw.example.com/function');
if (res.status === 429) {
throttled.add(1);
throttleRate.add(true);
} else {
throttleRate.add(false);
}
}
Что показывают результаты
- При какой частоте запросов начинается тротлинг? Это ваш фактический потолок конкурентности.
- Как провайдер ведёт себя при тротлинге? Одни провайдеры ставят запросы в очередь; другие немедленно отклоняют.
- Каково время восстановления после всплеска? Сколько времени до возврата частоты тротлинга к нулю?
- Достаточна ли зарезервированная конкурентность? Если вы настроили зарезервированную конкурентность, выдерживает ли она нагрузку?
Тестирование конфигурации памяти
Для AWS Lambda CPU пропорционален памяти. Больше памяти означает больше CPU, что может сократить время выполнения достаточно, чтобы компенсировать более высокую стоимость за миллисекунду:
# lambda_memory_optimizer.py
"""
Test a Lambda function across memory configurations to find the cost-optimal setting.
More memory = faster execution but higher per-ms cost.
The sweet spot minimizes (execution_time_ms * memory_mb * cost_per_gb_ms).
"""
import boto3
import time
import json
lambda_client = boto3.client('lambda')
def benchmark_memory_config(function_name: str, payload: dict, memory_sizes: list[int]) -> list:
results = []
for memory_mb in memory_sizes:
# Update function memory
lambda_client.update_function_configuration(
FunctionName=function_name,
MemorySize=memory_mb,
)
time.sleep(10) # wait for update to propagate
# Run 10 invocations and collect timings
durations = []
for _ in range(10):
start = time.perf_counter()
response = lambda_client.invoke(
FunctionName=function_name,
Payload=json.dumps(payload),
)
wall_time = (time.perf_counter() - start) * 1000
billed_ms = json.loads(response['Payload'].read())
durations.append(wall_time)
avg_duration = sum(durations) / len(durations)
# AWS pricing: $0.0000166667 per GB-second
cost_per_invocation = (memory_mb / 1024) * (avg_duration / 1000) * 0.0000166667
results.append({
"memory_mb": memory_mb,
"avg_duration_ms": round(avg_duration, 1),
"p99_duration_ms": round(sorted(durations)[8], 1),
"cost_per_invocation": f"${cost_per_invocation:.8f}",
})
return results
# Example usage
results = benchmark_memory_config(
"my-function",
{"key": "test-payload"},
[128, 256, 512, 1024, 2048, 3072],
)
for r in results:
print(f"{r['memory_mb']}MB: {r['avg_duration_ms']}ms avg, {r['cost_per_invocation']}/invocation")
Чек-лист оптимизации производительности serverless
| Оптимизация | Влияние | Трудозатраты |
|---|---|---|
| Минимизация размера пакета деплоя | Сокращение холодного старта на 50-200 мс | Низкие |
| Использование provisioned concurrency для критичных функций | Устранение холодных стартов | Средние (стоимость) |
| Вынос VPC-зависимых функций за пределы VPC где возможно | Сокращение холодного старта на 300-500 мс | Средние |
| Использование пула соединений для БД | Предотвращение исчерпания соединений | Средние |
| Реализация keep-alive пингов в часы низкого трафика | Предотвращение масштабирования до нуля | Низкие |
| Выбор Go или Node.js вместо Java для функций, чувствительных к задержке | 5-10x сокращение холодного старта | Высокие (переписывание) |
| Использование Lambda layers для общих зависимостей | Сокращение размера пакета, улучшение кеширования | Низкие |
| Включение ARM64 (Graviton2) для Lambda | Снижение стоимости на 20%, аналогичная производительность | Низкие |
Когда serverless — НЕ правильный выбор
Тестирование производительности может показать, что serverless не подходит для вашего сценария использования:
| Сигнал | Вывод |
|---|---|
| Холодные старты превышают ваш SLO по задержке | Рассмотрите контейнеры (ECS, Cloud Run с min-instances) |
| Постоянная высокая конкурентность (>500 одновременных) | Контейнеры более экономичны при устойчивой нагрузке |
| Время выполнения регулярно достигает лимитов (15 мин) | Перейдите на контейнеры или пакетную обработку |
| Требования к памяти > 10 ГБ | Максимум Lambda — 10 ГБ; используйте ECS/Fargate |
| Требуется GPU (ML-инференс) | Используйте выделенные GPU-инстансы или SageMaker |
Цель тестирования производительности serverless — не доказать, что serverless работает, а найти границы, где он перестаёт работать, и спланировать действия соответственно.