Ваше 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