0Pricing
PHP Academy · Урок

Отказоустойчивость: размыкатели и повторные попытки

Поддерживайте работоспособность систем при частичных сбоях

«Отказоустойчивость: размыкатели и повторные попытки» — бесплатный урок PHP Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.

Паттерны отказоустойчивости

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

В этом уроке рассматривается основной набор инструментов: тайм-ауты, повторные попытки с увеличивающейся задержкой, автоматические выключатели, изоляция ресурсов и плавная деградация — всё на PHP.

Сначала — тайм-ауты

Самая важная настройка отказоустойчивости — это тайм-аут. Без него медленный зависимый сервис удерживает рабочие процессы PHP-FPM в ожидании; запросы накапливаются; рабочие процессы заканчиваются; Ваш сервис выходит из строя из-за того, что другой сервис работал медленно. Это классический пример каскадного сбоя.

Для каждого исходящего вызова всегда задавайте и тайм-аут подключения, и общий тайм-аут запроса.

<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;

$http = new Client([
    'connect_timeout' => 0.5, // fail fast if we can't even connect
    'timeout'         => 2.0, // hard cap on the whole call
]);
// A hung dependency now fails in 2s instead of holding a worker forever.

Повторные попытки — осторожно

Временные сбои (кратковременная неполадка, короткий обрыв сети, ответ 503) часто устраняются со второй попытки. Но повторные попытки опасны: если выполнять их слишком поспешно, Вы увеличите нагрузку на уже испытывающий проблемы сервис. Правила:

  • Повторяйте только идемпотентные операции и операции при допустимых для повтора ошибках (тайм-ауты, ошибки 5xx, 429).
  • Ограничивайте число попыток.
  • Никогда не повторяйте запросы при клиентских ошибках 4xx — они снова завершатся ошибкой.

Экспоненциальная задержка и случайный разброс

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

<?php
function backoffDelay(int $attempt, float $base = 0.1, float $cap = 5.0): float {
    $exp = min($cap, $base * (2 ** $attempt));   // 0.1, 0.2, 0.4, ...
    return mt_rand(0, (int)($exp * 1000)) / 1000; // full jitter: 0..exp
}
for ($i = 0; $i < 5; $i++) {
    printf("attempt %d -> wait %.3fs\n", $i, backoffDelay($i));
}

Цикл повторных попыток

Объединим все элементы: ограниченное число попыток, повтор только при допустимых для повтора ошибках, ожидание между попытками с задержкой и случайным разбросом, а также возврат последней ошибки, если все попытки завершились неудачей.

<?php
function withRetry(callable $op, int $maxAttempts = 4): mixed {
    $attempt = 0;
    while (true) {
        try {
            return $op();
        } catch (\Throwable $e) {
            $attempt++;
            if ($attempt >= $maxAttempts || !isRetryable($e)) {
                throw $e; // give up
            }
            usleep((int)(backoffDelay($attempt) * 1_000_000));
        }
    }
}
function isRetryable(\Throwable $e): bool { return $e->getCode() === 0 || $e->getCode() >= 500; }
function backoffDelay(int $a): float { return min(5.0, 0.1 * (2 ** $a)) * (mt_rand(0, 100) / 100); }

Автоматический выключатель

Повторные попытки помогают при кратковременных сбоях, но если зависимость действительно недоступна, повторять каждый запрос — значит лишь напрасно тратить время и ресурсы. Автоматический выключатель отслеживает сбои и, когда они превышают заданный порог, размыкается: вызовы сразу прекращаются и завершаются ошибкой вместо ожидания недоступного сервиса.

У него три состояния: Замкнут (вызовы проходят, сбои подсчитываются), Разомкнут (вызовы немедленно отклоняются) и Полуразомкнут (несколько пробных вызовов проверяют восстановление).

Машина состояний автоматического выключателя

Переходы выглядят так: «Замкнут» → «Разомкнут», когда число сбоев пересекает порог. «Разомкнут» → «Полуразомкнут» после периода охлаждения. «Полуразомкнут» → «Замкнут» после успешной пробной проверки или обратно в «Разомкнут» при сбое. Состояние необходимо хранить общим для процессов PHP (Redis/APCu), поскольку каждый запрос запускается в новом процессе.

<?php
final class CircuitBreaker {
    public function __construct(
        private int $threshold = 5,
        private int $coolDown = 30, // seconds
    ) {}

    public function call(callable $op, array &$state): mixed {
        if ($state['status'] === 'open') {
            if (time() - $state['openedAt'] < $this->coolDown) {
                throw new \RuntimeException('Circuit open - failing fast');
            }
            $state['status'] = 'half-open'; // time to probe
        }
        try {
            $result = $op();
            $state = ['status' => 'closed', 'failures' => 0]; // recovered
            return $result;
        } catch (\Throwable $e) {
            $state['failures'] = ($state['failures'] ?? 0) + 1;
            if ($state['failures'] >= $this->threshold || $state['status'] === 'half-open') {
                $state['status'] = 'open';
                $state['openedAt'] = time();
            }
            throw $e;
        }
    }
}

Изоляция ресурсов

Паттерн изоляции ресурсов получил название от переборок на корабле: он разделяет ресурсы, чтобы сбой в одной области не потопил всё судно. Если все Ваши рабочие процессы могут вызывать медленный сервис Reports, его отказ способен занять каждый рабочий процесс и оставить Checkout без ресурсов.

Разделяйте ресурсы: используйте отдельные пулы соединений, отдельные пулы рабочих процессов или очереди для каждой зависимости либо ограничения параллелизма для каждого зависимого сервиса. Тогда одна неисправная зависимость исчерпает только выделенную ей часть ресурсов.

Плавная деградация и резервные сценарии

Если некритичная зависимость недоступна, переходите к упрощённому режиму вместо возврата ошибки. Показывайте кэшированное значение, значение по умолчанию или урезанную версию интерфейса. Разомкнутое состояние автоматического выключателя естественным образом запускает резервный сценарий.

<?php
function getRecommendations(callable $remoteCall, Redis $cache, string $userId): array {
    try {
        $recs = $remoteCall($userId);
        $cache->setex("recs:$userId", 3600, json_encode($recs));
        return $recs;
    } catch (\Throwable $e) {
        // Fallback 1: last-known-good from cache
        if ($cached = $cache->get("recs:$userId")) {
            return json_decode($cached, true);
        }
        // Fallback 2: generic popular items - never block the page
        return ['popular-1', 'popular-2'];
    }
}

Ключи идемпотентности для безопасных повторов

Повторные попытки безопасны только для идемпотентных операций. Для неидемпотентного действия, например «списать средства с карты», добавляйте ключ идемпотентности, который сервер использует для устранения дубликатов: если повторный запрос поступил после того, как первая попытка уже завершилась успешно, но ответ был потерян, сервер возвращает исходный результат, а не списывает средства дважды.

<?php
function chargeWithRetry(callable $http, string $orderId, int $cents): array {
    // Same key across all retries of THIS logical charge
    $key = 'charge-' . $orderId;
    return withRetry(fn() => $http('POST', '/charges', [
        'headers' => ['Idempotency-Key' => $key],
        'json'    => ['order' => $orderId, 'amount' => $cents],
    ]));
}
function withRetry(callable $op) { return $op(); } // see earlier scene

Объединение паттернов

Паттерны комбинируются, и порядок имеет значение. Надёжный исходящий вызов обычно строится так:

  • Тайм-аут для каждой отдельной попытки (самый внутренний уровень).
  • Повторная попытка с увеличивающейся задержкой, оборачивающая вызов с тайм-аутом для временных сбоев.
  • Автоматический выключатель, оборачивающий повторную попытку, чтобы быстро среагировать на продолжительный отказ.
  • Изоляция ресурсов, ограничивающая объём ресурсов, который может потребить эта зависимость.
  • Резервный сценарий на внешнем уровне, перехватывающий всё, что дошло до него.

Существуют обёртки в стиле Resilience4PHP, но понимание уровней важнее знания конкретного пакета.

Быстрая проверка

Выбор подходящего паттерна.

Итоги

Устойчивость к частичным сбоям:

  • Тайм-ауты для каждого вызова предотвращают исчерпание рабочих процессов и каскадные сбои.
  • Повторные попытки выполняются только для идемпотентных операций и допустимых для повтора ошибок, с экспоненциальной задержкой и случайным разбросом.
  • Автоматические выключатели быстро завершают вызовы ошибкой (Замкнут → Разомкнут → Полуразомкнут), когда зависимость недоступна.
  • Изоляция ресурсов не позволяет одному сбою лишить ресурсов все остальные компоненты.
  • Плавная деградация и резервные сценарии сохраняют работоспособность основных потоков.

Вместе эти уровни превращают неизбежные сбои в локализованные события, после которых можно восстановиться.

Часто задаваемые вопросы

Урок «Отказоустойчивость: размыкатели и повторные попытки» бесплатный?

Да — полный текст урока «Отказоустойчивость: размыкатели и повторные попытки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.

Чему я научусь в уроке «Отказоустойчивость: размыкатели и повторные попытки»?

Поддерживайте работоспособность систем при частичных сбоях Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать PHP Academy?

Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Отказоустойчивость: размыкатели и повторные попытки»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке PHP Academy?

Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. От монолита к микросервисам
  2. Взаимодействие сервисов: REST и gRPC
  3. Шлюзы API и обнаружение сервисов
  4. Отказоустойчивость: размыкатели и повторные попытки
← Назад к PHP Academy