Chaos Engineering для PHP: как сломать приложение до того, как это сделают пользователи

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Ваше PHP-приложение — хрупкая бабочка? Пора проверить это в бою

Представьте: пятница вечер. Вы только что запустили долгожданный релиз. Всё работает идеально. Вы наливаете чай, открываете Telegram... И тут падает база данных. Или Redis. Или внешний API. Знакомо? Если нет — скорее всего, вы просто ещё не сталкивались с настоящим хаосом.

В мире PHP-разработки принято надеяться на "авось". Мы пишем код, покрываем его юнит-тестами, иногда даже интеграционными. Но что происходит, когда всё идёт не по плану? Когда сервер теряет пакеты, база данных отвечает с задержкой в 10 секунд, или файловая система переполняется?

Тут на сцену выходит Chaos Engineering — дисциплина, которая пришла из мира Netflix и больших распределённых систем. Но не спешите закрывать статью: это работает и для обычных PHP-приложений, даже если у вас монолит на Laravel или Symfony.

Что такое Chaos Engineering и зачем он PHP-разработчику?

Chaos Engineering — это практика экспериментов над системой для выявления слабых мест до того, как они проявятся в реальном сбое. Вы намеренно вносите неисправности (отключаете сервисы, задерживаете ответы, убиваете процессы) и смотрите, как ведёт себя ваше приложение.

Многие думают: "У меня простое PHP-приложение, зачем мне это?". Но давайте честно:

  • Ваш код полагается на внешние API? Да.
  • У вас есть кэширование через Redis или Memcached? Да.
  • Вы используете очереди (RabbitMQ, Kafka, SQS)? Скорее всего, да.
  • База данных — MySQL/PostgreSQL? Да.

Каждая из этих точек — потенциальная точка отказа. И отказоустойчивость PHP приложения проверяется только в момент, когда что-то ломается. Chaos Engineering позволяет вам быть тем, кто ломает, а не тем, кто чинит.

Базовые принципы Chaos Engineering

Прежде чем писать код, запомните три главных правила:

  • Начинайте с малого. Не отключайте всю базу данных сразу. Начните с одного соединения.
  • Имейте гипотезу. "Если Redis упадёт, приложение должно работать, но медленнее" — это гипотеза. Проверьте её.
  • Автоматизируйте откат. Всегда имейте план возврата к нормальному состоянию.

Теперь перейдём к реальным примерам на PHP.

Пример 1: Эмуляция задержки базы данных

Допустим, у вас есть типичный запрос к MySQL. Вы используете PDO. Как проверить, что произойдёт, если база данных вдруг начнёт "тормозить"?

Самый простой способ — написать middleware или обёртку для PDO, которая добавляет случайную задержку. Вот пример:

<?php
class ChaoticPDO extends PDO {
    private float $chaosProbability;
    private int $maxDelayMs;

    public function __construct(
        string $dsn, 
        ?string $username = null, 
        ?string $password = null, 
        ?array $options = null,
        float $chaosProbability = 0.1, // 10% запросов будут "тормозить"
        int $maxDelayMs = 5000        // максимум 5 секунд задержки
    ) {
        parent::__construct($dsn, $username, $password, $options);
        $this->chaosProbability = $chaosProbability;
        $this->maxDelayMs = $maxDelayMs;
    }

    public function query(string $query, ?int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false
    {
        $this->maybeInjectDelay();
        return parent::query($query, $fetchMode, ...$fetchModeArgs);
    }

    private function maybeInjectDelay(): void {
        if (mt_rand() / mt_getrandmax() < $this->chaosProbability) {
            $delay = mt_rand(100, $this->maxDelayMs);
            usleep($delay * 1000);
            // Логируем для мониторинга
            error_log("[CHAOS] Injected delay of {$delay}ms in database query");
        }
    }
}

// Использование
$pdo = new ChaoticPDO('mysql:host=localhost;dbname=test', 'user', 'pass', [], 0.2, 3000);
$stmt = $pdo->query('SELECT * FROM users'); // Иногда будет тормозить
?>

Запустите такое на staging-окружении. Вы увидите, как ваше приложение реагирует на медленные запросы. Если страница падает с timeout — у вас проблема. Если показывается заглушка — молодец.

Пример 2: Отключение внешнего API (Circuit Breaker)

Теперь усложним задачу. Представьте, что ваше приложение вызывает внешний сервис погоды. Если сервис недоступен, что произойдёт? Правильный ответ: приложение должно работать, просто без погоды.

Напишем Circuit Breaker — паттерн, который предотвращает каскадные сбои. А затем протестируем его с помощью Chaos Engineering.

<?php
class CircuitBreaker {
    private int $failureThreshold;
    private int $retryTimeout;
    private int $failureCount = 0;
    private ?int $lastFailureTime = null;
    private string $state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN

    public function __construct(int $failureThreshold = 3, int $retryTimeout = 30) {
        $this->failureThreshold = $failureThreshold;
        $this->retryTimeout = $retryTimeout;
    }

    public function call(callable $action): mixed {
        if ($this->state === 'OPEN') {
            if (time() - $this->lastFailureTime > $this->retryTimeout) {
                $this->state = 'HALF_OPEN';
            } else {
                throw new \RuntimeException('Circuit breaker is OPEN');
            }
        }

        try {
            $result = $action();
            $this->reset();
            return $result;
        } catch (\Throwable $e) {
            $this->recordFailure();
            throw $e;
        }
    }

    private function recordFailure(): void {
        $this->failureCount++;
        $this->lastFailureTime = time();
        if ($this->failureCount >= $this->failureThreshold) {
            $this->state = 'OPEN';
        }
    }

    private function reset(): void {
        $this->failureCount = 0;
        $this->state = 'CLOSED';
    }
}

// Теперь эмулируем хаос: случайно отключаем API
class ChaosWeatherAPI {
    private float $failureProbability = 0.3; // 30% отказов

    public function getWeather(string $city): string {
        if (mt_rand() / mt_getrandmax() < $this->failureProbability) {
            throw new \RuntimeException("Weather API is down (chaos injected)");
        }
        return "Sunny, 25°C in {$city}";
    }
}

// Использование с Circuit Breaker
$weatherApi = new ChaosWeatherAPI();
$breaker = new CircuitBreaker(2, 10); // 2 ошибки -> открываем на 10 секунд

for ($i = 0; $i                

Блоги

Рекомендаций по книгам