Chaos Engineering под нагрузкой
Что такое Chaos Engineering
Chaos Engineering — дисциплина экспериментирования с системой для выявления слабых мест до того, как они проявятся в production.
Классический Chaos:
- Выключаем случайный pod
- Добавляем network latency
- Симулируем packet loss
- Убиваем процессы
Chaos под нагрузкой (k6 + Chaos):
- Запускаем нагрузочный тест
- ВО ВРЕМЯ теста вводим хаос
- Смотрим, как система реагирует под нагрузкой
Почему важно тестировать под нагрузкой?
Система может нормально переживать сбой одного pod'а в idle, но упасть при 1000 RPS. Chaos Engineering под нагрузкой показывает реальную отказоустойчивость.
Инструменты
Chaos Mesh (Kubernetes)
- Поддержка: Network chaos, Pod chaos, IO chaos, Kernel chaos
- Интеграция: CRD в Kubernetes
- UI: Grafana-подобный dashboard
- Документация: https://chaos-mesh.org/
LitmusChaos (Kubernetes + любая платформа)
- Поддержка: Pod delete, network chaos, resource exhaustion
- Интеграция: Litmus Operator + CRD
- UI: Litmus Portal
- Документация: https://litmuschaos.io/
Toxiproxy (HTTP proxy)
- Поддержка: Network latency, slow connections, timeouts
- Интеграция: HTTP proxy между k6 и сервисом
- Документация: https://github.com/Shopify/toxiproxy
Сценарий 1: Network latency под нагрузкой
Архитектура
Chaos Mesh: NetworkChaos
# chaos/network-latency.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: api-network-delay
namespace: default
spec:
action: delay # Добавляем задержку
mode: all # Все pods с label
selector:
labelSelectors:
app: api-service
delay:
latency: "200ms" # Задержка 200ms
correlation: "50" # 50% корреляция (более реалистично)
jitter: "50ms" # Jitter ±50ms
duration: "5m" # Длительность chaos
scheduler:
cron: "@every 10m" # Повторять каждые 10 минутk6 тест с chaos
// chaos-network-latency.js
import http from "k6/http";
import { check, sleep } from "k6";
import { Trend, Rate } from "k6/metrics";
const latencyBeforeChaos = new Trend("latency_before_chaos");
const latencyDuringChaos = new Trend("latency_during_chaos");
const latencyAfterChaos = new Trend("latency_after_chaos");
const errorsDuringChaos = new Rate("errors_during_chaos");
export const options = {
scenarios: {
chaos_test: {
executor: "constant-arrival-rate",
rate: 100, // 100 RPS
duration: "15m",
preAllocatedVUs: 50,
maxVUs: 200,
},
},
thresholds: {
latency_before_chaos: ["p(95)<500"], // Без chaos
latency_during_chaos: ["p(95)<700"], // С chaos допускаем +200ms
latency_after_chaos: ["p(95)<500"], // Recovery
errors_during_chaos: ["rate<0.05"], // < 5% ошибок во время chaos
http_req_failed: ["rate<0.03"], // < 3% общая ошибка
},
};
const BASE_URL = __ENV.BASE_URL || "http://api-service:8080";
const CHAOS_START_TIME = 5 * 60 * 1000; // Chaos начинается через 5 минут
const CHAOS_DURATION = 5 * 60 * 1000; // Chaos длится 5 минут
export default function () {
const now = Date.now() - __ENV.TEST_START_TIME;
const chaosActive =
now > CHAOS_START_TIME && now < CHAOS_START_TIME + CHAOS_DURATION;
const res = http.get(`${BASE_URL}/api/products`);
// Записываем метрики в зависимости от фазы
if (now < CHAOS_START_TIME) {
latencyBeforeChaos.add(res.timings.duration);
} else if (chaosActive) {
latencyDuringChaos.add(res.timings.duration);
errorsDuringChaos.add(res.status >= 400 || res.status === 0);
} else {
latencyAfterChaos.add(res.timings.duration);
}
check(res, {
"status 200": (r) => r.status === 200,
});
sleep(0.1);
}Запуск chaos + k6
# 1. Применить Chaos Mesh манифест (запланировано на 5 минут после старта)
kubectl apply -f chaos/network-latency.yaml
# 2. Запустить k6 тест
TEST_START_TIME=$(date +%s%3N) k6 run chaos-network-latency.js
# 3. Наблюдать метрики в Grafana:
# - latency_before_chaos: p95 ~300ms
# - latency_during_chaos: p95 ~500ms (+200ms от chaos)
# - latency_after_chaos: p95 ~300ms (recovery)Ожидаемый результат:
latency_before_chaos....: avg=285ms p(95)=320ms p(99)=380ms
latency_during_chaos....: avg=485ms p(95)=520ms p(99)=600ms ← +200ms chaos
latency_after_chaos.....: avg=290ms p(95)=325ms p(99)=390ms ← recovery
errors_during_chaos.....: 1.2% (некоторые запросы timeout)Сценарий 2: Pod failure под нагрузкой
PodChaos: Убить случайный pod
# chaos/pod-kill.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: api-pod-kill
namespace: default
spec:
action: pod-kill # Убить pod
mode: one # Убить один случайный pod
selector:
labelSelectors:
app: api-service
scheduler:
cron: "@every 2m" # Убивать pod каждые 2 минутыk6 тест с метриками recovery
// chaos-pod-kill.js
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate, Trend } from "k6/metrics";
const podRestartDetected = new Rate("pod_restart_detected");
const recoveryTime = new Trend("recovery_time");
const errorsRate = new Rate("errors_rate");
let lastErrorTime = null;
let lastSuccessTime = null;
export const options = {
scenarios: {
pod_chaos: {
executor: "constant-arrival-rate",
rate: 50,
duration: "10m",
preAllocatedVUs: 25,
maxVUs: 100,
},
},
thresholds: {
pod_restart_detected: ["rate>0"], // Должен быть хотя бы 1 restart
recovery_time: ["p(95)<5000"], // Recovery < 5s
errors_rate: ["rate<0.10"], // < 10% ошибок (допускаем spike)
},
};
const BASE_URL = __ENV.BASE_URL || "http://api-service:8080";
export default function () {
const res = http.get(`${BASE_URL}/api/health`);
const success = res.status === 200;
errorsRate.add(!success);
if (!success) {
if (lastSuccessTime !== null) {
podRestartDetected.add(1);
lastErrorTime = Date.now();
}
} else {
if (lastErrorTime !== null) {
const recovery = Date.now() - lastErrorTime;
recoveryTime.add(recovery);
console.log(`Pod recovered in ${recovery}ms`);
lastErrorTime = null;
}
lastSuccessTime = Date.now();
}
sleep(1);
}Результат:
pod_restart_detected....: 5 restarts detected
recovery_time...........: avg=2.1s min=1.2s p(95)=4.5s max=6.8s
errors_rate.............: 8.5% (spike во время pod restart)Recovery time < 5s — хороший результат! Kubernetes быстро поднял новый pod, и load balancer перенаправил трафик.
Сценарий 3: Packet loss simulation
NetworkChaos: Packet loss
# chaos/packet-loss.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: api-packet-loss
namespace: default
spec:
action: loss # Packet loss
mode: all
selector:
labelSelectors:
app: api-service
loss:
loss: "10" # 10% packet loss
correlation: "25" # 25% корреляция
duration: "3m"k6 тест
// chaos-packet-loss.js
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate, Counter } from "k6/metrics";
const timeouts = new Counter("timeouts");
const retries = new Counter("retries");
const successAfterRetry = new Rate("success_after_retry");
export const options = {
scenarios: {
packet_loss_test: {
executor: "constant-arrival-rate",
rate: 50,
duration: "8m",
preAllocatedVUs: 25,
maxVUs: 100,
},
},
thresholds: {
timeouts: ["count<50"], // < 50 timeouts за тест
retries: ["count<100"], // < 100 retries
success_after_retry: ["rate>0.80"], // 80% retries успешны
},
};
const BASE_URL = __ENV.BASE_URL || "http://api-service:8080";
export default function () {
let res = http.get(`${BASE_URL}/api/products`, {
timeout: "5s", // Timeout 5s
});
if (res.status === 0) {
// Timeout или network error
timeouts.add(1);
// Retry once
retries.add(1);
sleep(0.5);
res = http.get(`${BASE_URL}/api/products`, {
timeout: "5s",
});
successAfterRetry.add(res.status === 200);
}
check(res, {
"status 200": (r) => r.status === 200,
});
sleep(1);
}Интеграция с xk6-disruptor
xk6-disruptor — расширение k6 для fault injection прямо из сценария.
Установка
# Собираем k6 с расширением
xk6 build --with github.com/grafana/xk6-disruptorПример: Inject latency из k6
import { ServiceDisruptor } from "k6/x/disruptor";
export const options = {
scenarios: {
disrupt: {
executor: "shared-iterations",
vus: 1,
iterations: 1,
exec: "disrupt",
},
load: {
executor: "constant-arrival-rate",
rate: 50,
duration: "5m",
preAllocatedVUs: 25,
maxVUs: 100,
exec: "loadTest",
startTime: "10s", // Начать после inject
},
},
};
// Inject chaos
export function disrupt() {
const disruptor = new ServiceDisruptor("api-service", "default");
const fault = {
averageDelay: "200ms",
errorRate: 0.1, // 10% errors
statusCode: 500,
};
disruptor.injectHTTPFaults(fault, "5m");
}
// Load test
export function loadTest() {
const res = http.get(`${BASE_URL}/api/products`);
check(res, {
"status 200": (r) => r.status === 200,
});
sleep(1);
}Best Practices
1. Graduated chaos (постепенное усиление)
# chaos/graduated-network-delay.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: Schedule
metadata:
name: graduated-network-chaos
spec:
schedule: "@every 15m"
type: NetworkChaos
networkChaos:
- name: mild-delay
delay:
latency: "50ms"
duration: "5m"
- name: moderate-delay
delay:
latency: "200ms"
duration: "5m"
- name: severe-delay
delay:
latency: "500ms"
duration: "5m"2. Мониторинг во время chaos
import { Trend, Rate } from "k6/metrics";
const latency = new Trend("latency");
const errors = new Rate("errors");
const circuitBreakerOpen = new Rate("circuit_breaker_open");
export default function () {
const res = http.get(`${BASE_URL}/api/products`);
latency.add(res.timings.duration);
errors.add(res.status >= 400);
// Детектим circuit breaker
if (res.status === 503) {
circuitBreakerOpen.add(1);
console.log("Circuit breaker detected!");
} else {
circuitBreakerOpen.add(0);
}
}3. Rollback plan
# Если chaos пошел не так — немедленно удалить
kubectl delete -f chaos/network-latency.yaml
# Или через Chaos Mesh UI: Pause/Delete experiment📊 Реальный кейс: Ride-sharing платформа и AWS AZ outage simulation
Контекст: Ride-sharing приложение (стиль Uber, 5M поездок/день), infrastructure на AWS в 3 availability zones (AZs). SLA обещает: "Система работает, даже если упадет 1 AZ".
Проблема: Никогда не тестировали отказоустойчивость под реальной нагрузкой. Маркетинг активно рекламирует "99.99% uptime".
Инцидент (что произошло в production):
-
AWS AZ outage (us-east-1c):
- 15:42: AWS объявил partial outage в AZ-c
- 15:43: 33% backend pods (в AZ-c) стали unavailable
- 15:44: Каскадный отказ:
- Оставшиеся 67% pods (AZ-a, AZ-b) получили 3x traffic
- Connection pool Postgres исчерпался (настроен на 3 AZ, не auto-scaled)
- Redis cluster lost quorum (2/3 nodes в AZ-c)
- API Gateway rate limiter сработал на оставшихся instances
-
Impact:
- 🔴 Полный downtime: 18 минут (15:44 - 16:02)
- 🔴 Partial degradation: еще 35 минут (error rate 12%)
- 280K ride requests failed
- Social media: #UberDownAgain trending (wrongly blamed competitors)
- Revenue loss: $1.2M (потеря поездок + SLA refunds)
-
Root cause:
- Система теоретически отказоустойчива (multi-AZ)
- Но никогда не тестировали под peak load при отказе 1 AZ
- Auto-scaling сработал через 8 минут (слишком медленно)
- Circuit breakers не были настроены для AZ failover
Что сделали после инцидента:
-
Настроили Chaos Engineering (Chaos Mesh + k6):
Сценарий: AZ failure simulation
# chaos/az-failure.yaml apiVersion: chaos-mesh.org/v1alpha1 kind: PodChaos metadata: name: az-c-failure spec: action: pod-kill mode: all selector: labelSelectors: topology.kubernetes.io/zone: us-east-1c duration: "10m" scheduler: cron: "@hourly" # Каждый час в test envk6 тест под chaos:
// tests/chaos-az-failure.js import http from "k6/http"; import { check, sleep } from "k6"; import { Trend, Rate } from "k6/metrics"; const recoveryTime = new Trend("recovery_time"); const errorsUnderChaos = new Rate("errors_during_chaos"); export const options = { scenarios: { load: { executor: "ramping-arrival-rate", startRate: 1000, timeUnit: "1s", preAllocatedVUs: 500, maxVUs: 2000, stages: [ { target: 3000, duration: "5m" }, // Warmup { target: 3000, duration: "15m" }, // Chaos injected at 10min { target: 1000, duration: "5m" }, // Cooldown ], }, }, thresholds: { // Допускаем degradation, но не полный отказ http_req_failed: ["rate<0.05"], // < 5% errors даже при AZ failure "http_req_duration{chaos:true}": ["p(95)<1500"], // Допускаем 3x latency recovery_time: ["max<120000"], // Recovery < 2 минут }, }; export default function () { const res = http.post(`${__ENV.BASE_URL}/api/ride/request`, { pickup: "37.7749,-122.4194", destination: "37.8044,-122.2712", }); const isChaos = __ITER > 600; // После 10 минут check(res, { "status 2xx or 503": (r) => r.status < 300 || r.status === 503, "not full outage": (r) => r.status !== 500, }); if (isChaos) { errorsUnderChaos.add(res.status >= 400); } sleep(1); } -
Первый chaos-тест (провал):
- ✅ k6 начал с 3000 RPS, все стабильно
- 🔴 Chaos injection (10 min): kill all pods в AZ-c
- 🔴 Результат:
- Error rate: 45% в первые 2 минуты (превысил threshold 5%)
- p95 latency: 3.8s (threshold: <1.5s)
- Recovery time: 8 минут (threshold: <2 минут)
- Вывод: Система НЕ отказоустойчива под нагрузкой
-
Что исправили (2 недели работы):
A. Auto-scaling агрессивнее:
# HPA config spec: minReplicas: 30 # Было: 20 maxReplicas: 150 # Было: 60 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 # Было: 80 behavior: scaleUp: stabilizationWindowSeconds: 15 # Было: 60 (быстрее реагируем) policies: - type: Percent value: 100 # Удвоить pods за раз periodSeconds: 15B. Connection pool per-AZ scaling:
// Динамически масштабируем pool в зависимости от доступных AZs const availableAZs = await getHealthyAZs(); const poolSize = Math.ceil(MAX_POOL_SIZE / availableAZs.length); pgPool.setMaxConnections(poolSize * availableAZs.length * 0.8);C. Circuit breaker для AZ-level failover:
// Если AZ-c не отвечает > 10s → переключаем весь трафик на AZ-a/b const circuitBreaker = new CircuitBreaker({ threshold: 0.5, // 50% errors timeout: 10000, // 10s resetTime: 30000, });D. Redis cluster quorum:
# Было: 3 nodes (1 per AZ) → quorum lost при 1 AZ down # Стало: 5 nodes (2+2+1) → quorum сохраняется при 1 AZ down -
Повторный chaos-тест (успех):
- ✅ k6: 3000 RPS stable
- ✅ Chaos injection: kill all pods в AZ-c
- ✅ Результат:
- Error rate: 2.8% (в threshold <5%) ✅
- p95 latency: 980ms (threshold <1.5s) ✅
- Recovery time: 45 секунд (threshold <2 минут) ✅
- Auto-scaling: сработал через 30s, добавил 40 pods в AZ-a/b
- Вывод: Система выдержала AZ failure под peak load
-
GameDay (симуляция в production-like env):
- Проводим GameDay раз в месяц: chaos в pre-prod под realistic load
- Сценарии: AZ failure, Redis crash, DB slow query, API rate limit
- Метрики: recovery time, error rate, latency during chaos
- Результаты за 6 месяцев: 12 GameDays, 0 критичных issues в production
Результат:
- Zero major outages за 6 месяцев (было 3 outage за предыдущие 6 месяцев)
- AWS AZ outage в октябре → система выдержала без инцидента (error rate 1.2%)
- Uptime: 99.98% (было 99.87%)
- ROI:
- Стоимость: Chaos Mesh setup + GameDays + инфра-оптимизации = $180K
- Избежали: 3 potential outages × $1.2M = $3.6M
- ROI: 20x
War story:
Incident Commander: "Раньше каждый AWS outage — это паника. Теперь мы заранее знаем, что система выдержит, потому что тестируем каждый месяц. Когда в октябре упал AZ-c, мы даже не создавали war room — просто мониторили dashboards".
CTO: "Chaos Engineering — это не про ломать production, а про заранее находить слабые места в безопасной среде. Мы ломаем систему в test env каждый день, чтобы она не сломалась в production".
Урок: "Multi-AZ" или "отказоустойчивый" — это не факт, а гипотеза, которую нужно проверять под нагрузкой. Chaos Engineering + k6 — единственный способ убедиться, что система реально выдержит отказ компонента при peak load.
Обязательные chaos сценарии:
- AZ/Region failure: Kill pods в 1 AZ, мерим recovery
- Network partition: Latency +500ms между сервисами
- Database failover: Kill primary DB, мерим switchover time
- Rate limiting: Overload upstream API, проверяем circuit breaker
- Resource exhaustion: CPU throttling, memory pressure
Проводите GameDays регулярно (раз в месяц), не только перед big events.
✅ Чек-лист завершения урока
После этого урока вы должны уметь:
Chaos Engineering концепция:
- Понимать, зачем тестировать отказоустойчивость под нагрузкой
- Знать инструменты: Chaos Mesh, LitmusChaos, xk6-disruptor
- Различать типы chaos: network, pod, IO, kernel
Интеграция k6 + Chaos:
- Запускать k6 одновременно с chaos injection
- Измерять latency до/во время/после chaos
- Детектить pod restarts и считать recovery time
Сценарии:
- Network latency: добавлять задержку и jitter
- Pod failure: убивать pods и мерить recovery
- Packet loss: симулировать потери пакетов
Метрики resilience:
- latency_during_chaos, recovery_time, errors_rate
- circuit_breaker_open, timeouts, retries
- Thresholds для допустимой деградации
Практическое задание:
- Установите Chaos Mesh или LitmusChaos в Kubernetes
- Создайте NetworkChaos с задержкой 200ms
- Запустите k6 тест и примените chaos через 5 минут
- Проанализируйте: как изменилась latency? Были ли ошибки? Как быстро восстановилось?
Если чек-лист пройден — вы готовы тестировать отказоустойчивость в production-like условиях! Переходите к уроку 10: масштабирование генераторов.