Skip to main content
Back to course
k6: нагрузочное тестирование как система
13 / 1776%

Chaos Engineering под нагрузкой

25 минут

Что такое 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):

  1. 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
  2. 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)
  3. Root cause:

    • Система теоретически отказоустойчива (multi-AZ)
    • Но никогда не тестировали под peak load при отказе 1 AZ
    • Auto-scaling сработал через 8 минут (слишком медленно)
    • Circuit breakers не были настроены для AZ failover

Что сделали после инцидента:

  1. Настроили 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 env

    k6 тест под 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);
    }
  2. Первый 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 минут)
    • Вывод: Система НЕ отказоустойчива под нагрузкой
  3. Что исправили (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: 15

    B. 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
  4. Повторный 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
  5. 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 сценарии:

  1. AZ/Region failure: Kill pods в 1 AZ, мерим recovery
  2. Network partition: Latency +500ms между сервисами
  3. Database failover: Kill primary DB, мерим switchover time
  4. Rate limiting: Overload upstream API, проверяем circuit breaker
  5. 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: масштабирование генераторов.

Chaos Engineering под нагрузкой — k6: нагрузочное тестирование как система — Potapov.me