Circuit Breaker в Laravel: как защитить приложение от сбоев внешних сервисов

Современные веб-приложения редко работают в изоляции. Почти всегда они зависят от сторонних API: платежных систем, почтовых сервисов, аналитики и других интеграций. Проблема в том, что если внешний сервис начинает тормозить или полностью падает, это может потянуть за собой и ваше приложение. Решить эту проблему помогает паттерн Circuit Breaker.

Разберёмся, как он работает и как его можно использовать в Laravel.

Что такое Circuit Breaker и зачем он нужен

Circuit Breaker (предохранитель) - это шаблон проектирования, который отслеживает ошибки при обращении к внешнему сервису и временно блокирует повторные попытки, если тот ведёт себя нестабильно.

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

С Circuit Breaker логика меняется:

  • если ошибок слишком много, запросы временно прекращаются

  • система "даёт сервису отдохнуть"

  • через некоторое время пробует снова

Это снижает нагрузку и защищает ваше приложение от цепных сбоев.

Как это работает

У Circuit Breaker есть три состояния:

1. Closed (закрыт)

Нормальный режим. Все запросы проходят. Если количество ошибок превышает порог, состояние меняется.

2. Open (открыт)

Запросы к внешнему сервису блокируются сразу, без попытки выполнения. Вместо этого можно:

  • вернуть fallback-ответ

  • показать сообщение об ошибке

  • использовать кэш

3. Half-Open (полуоткрыт)

Система пробует отправить один или несколько тестовых запросов. Если всё работает то возвращается в Closed, если нет то снова Open.

Реализация в Laravel

В Laravel есть готовые решения, которые упрощают внедрение Circuit Breaker. Обычно они используют кэш (Redis, Memcached) для хранения состояния.

Пример простой реализации через сервис:

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

class ApiService
{
    public function getData()
    {
        if ($this->isOpen()) {
            return $this->fallback();
        }

        try {
            $response = Http::get('https://api.example.com/data');

            $this->resetFailures();

            return $response->json();
        } catch (\Exception $e) {
            $this->recordFailure();

            return $this->fallback();
        }
    }

    protected function isOpen()
    {
        return Cache::get('circuit_open', false);
    }

    protected function recordFailure()
    {
        $failures = Cache::increment('failures');

        if ($failures >= 5) {
            Cache::put('circuit_open', true, now()->addSeconds(30));
        }
    }

    protected function resetFailures()
    {
        Cache::forget('failures');
        Cache::forget('circuit_open');
    }

    protected function fallback()
    {
        return ['data' => [], 'message' => 'Service unavailable'];
    }
}

Пример с задержкой и таймаутом

Важно не только отслеживать ошибки, но и ограничивать время ожидания ответа:

$response = Http::timeout(2)->get('https://api.example.com/data');

Если сервис отвечает слишком долго, запрос будет прерван, и это тоже можно считать ошибкой для Circuit Breaker.

Использование с повторными попытками

Иногда полезно комбинировать Circuit Breaker с retry-логикой:

$response = Http::retry(3, 100)
    ->timeout(2)
    ->get('https://api.example.com/data');

Здесь:

  • 3 попытки

  • задержка 100 мс между ними

Если все попытки неудачны то фиксируем ошибку.

Пример с кастомным fallback

Fallback - это не просто заглушка. Он может быть умнее:

protected function fallback()
{
    return Cache::get('cached_api_data', [
        'data' => [],
        'message' => 'Using cached data',
    ]);
}

А при успешном запросе можно обновлять кэш:

$data = $response->json();

Cache::put('cached_api_data', $data, 300);

Пример для платежного сервиса

Допустим, у вас интеграция с платежным API:

public function charge(array $payload)
{
    if ($this->isOpen()) {
        throw new \Exception('Payment service temporarily unavailable');
    }

    try {
        return Http::post('https://payment.api/charge', $payload)->json();
    } catch (\Exception $e) {
        $this->recordFailure();

        throw $e;
    }
}

В этом случае лучше явно уведомить пользователя, чем зависнуть на долгом запросе.

Использование готовых пакетов

Чтобы не писать всё вручную, можно использовать пакеты с готовой реализацией Circuit Breaker которые обычно предлагают:

  • гибкую настройку порогов

  • автоматическое переключение состояний

  • интеграцию с Laravel Cache

  • метрики и логирование

Это особенно полезно в крупных проектах.

Когда стоит использовать Circuit Breaker

Этот паттерн нужен не всегда, но он критически важен, если:

  • у вас много внешних API

  • есть платёжные интеграции

  • важна стабильность и SLA

  • приложение работает под нагрузкой

В простых проектах можно обойтись retry и таймаутами. Но по мере роста системы Circuit Breaker становится практически обязательным.

Комментарии (0)

Войдите, чтобы оставить комментарий

Похожие статьи

Как обнаруживать и устранять race condition в приложениях Laravel

Race condition - распространённая проблема в веб-приложениях с параллельными запросами. В статье разбирается, как обнаружить такие ошибки в Laravel и какие механизмы помогают их устранить.

Осваиваем сервис-контейнер Symfony с помощью современных PHP-атрибутов

Объясняем как использовать современные PHP-атрибуты для настройки сервис-контейнера Symfony, чтобы избавиться от сложной YAML-конфигурации и сделать код более компактным и понятным.

Оптимизация Laravel-приложений с Octane

Полное руководство по Laravel Octane: от базовой концепции до настройки, особенностей и сравнения с классическим подходом. Узнайте, как Octane ускоряет Laravel-приложения и когда его стоит использовать.