Как вы организуете взаимодействие с системой в случае недоступности внешнего сервиса?

Ответ

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

Мой подход включает несколько уровней защиты:

  1. Повторные попытки с экспоненциальной задержкой (Retry with Backoff): Для обработки временных сетевых сбоев или кратковременной недоступности сервиса.

    class ExternalServiceClient {
        public function fetchDataWithRetry(string $url, int $maxRetries = 3): array {
            $retryDelay = 100; // Начальная задержка в миллисекундах
            $lastException = null;
    
            for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
                try {
                    return $this->makeHttpRequest($url);
                } catch (NetworkException | TimeoutException $e) {
                    $lastException = $e;
                    if ($attempt < $maxRetries) {
                        usleep($retryDelay * 1000); // Микросекунды
                        $retryDelay *= 2; // Экспоненциальное увеличение задержки
                    }
                }
            }
            throw new ServiceUnavailableException('Service failed after retries', 0, $lastException);
        }
    }
  2. Аварийный переключатель (Circuit Breaker): Чтобы предотвратить "забивание" системы бессмысленными вызовами к полностью упавшему сервису. После определенного количества ошибок выключатель "размыкается", и последующие вызовы мгновенно завершаются ошибкой, не делая реального запроса. Периодически делается попытка "прозвона" для проверки восстановления сервиса.

    // Использую библиотеку, например, league/circuit-breaker
    $breaker = new CircuitBreaker($storage, 'my-service', 5, 60); // 5 ошибок, таймаут 60 сек
    if ($breaker->isAvailable()) {
        try {
            $response = $client->fetchData();
            $breaker->success(); // Сообщаем об успехе
        } catch (Exception $e) {
            $breaker->failure(); // Сообщаем об ошибке
            throw $e;
        }
    } else {
        // Сервис недоступен, используем fallback
        return $this->getFallbackData();
    }
  3. Резервные данные (Fallback/Cache): Если сервис критичен для отображения данных, я настраиваю возврат закешированной или дефолтной версии.

    public function getProductDetails(int $productId): array {
        $cacheKey = "product_details_{$productId}";
        // Пытаемся получить свежие данные
        try {
            $data = $this->externalCatalogService->fetch($productId);
            $this->cache->set($cacheKey, $data, 300); // Кешируем на 5 минут
            return $data;
        } catch (ServiceUnavailableException $e) {
            // В случае ошибки возвращаем устаревшие, но доступные данные из кеша
            $cached = $this->cache->get($cacheKey);
            if ($cached !== null) {
                $cached['source'] = 'cache (stale)';
                return $cached;
            }
            // Или минимальный набор дефолтных данных
            return ['id' => $productId, 'name' => 'Product Info Temporarily Unavailable'];
        }
    }
  4. Асинхронная обработка и очереди: Для операций, не требующих немедленного ответа пользователю (например, отправка email, обновление аналитики), вызов внешнего сервиса помещается в очередь (например, RabbitMQ, Redis). Отдельный worker-процесс будет повторять задачу до успеха, не блокируя основной поток выполнения.

  5. Детальное логирование и мониторинг: Все сбои логируются с контекстом (ID запроса, код ошибки, URL сервиса). Это позволяет быстро диагностировать проблемы. Настраиваются алерты в мониторинге (например, Grafana + Prometheus) при высокой частоте ошибок.

Эта многоуровневая стратегия позволяет системе грациозно деградировать при проблемах у зависимостей, сохраняя базовую работоспособность для пользователей.

Ответ 18+ 🔞

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

Вот как я обычно выстраиваю оборону, чтоб не бздеть перед каждым деплоем:

  1. Повторяшки с умом (Retry with Backoff): Первое, что приходит в голову — просто переспросить, если не ответили. Но если делать это тупо и сразу, то можно добить и так хромую систему. Поэтому — экспоненциальная задержка. Сначала ждём чуть-чуть, потом в два раза дольше, и так далее. Часто помогает, потому что 90% проблем — это какая-нибудь ядрёна вошь в сети на секунду.

    class ExternalServiceClient {
        public function fetchDataWithRetry(string $url, int $maxRetries = 3): array {
            $retryDelay = 100; // Начальная задержка в миллисекундах
            $lastException = null;
    
            for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
                try {
                    return $this->makeHttpRequest($url);
                } catch (NetworkException | TimeoutException $e) {
                    $lastException = $e;
                    if ($attempt < $maxRetries) {
                        usleep($retryDelay * 1000); // Микросекунды
                        $retryDelay *= 2; // Экспоненциальное увеличение задержки
                    }
                }
            }
            throw new ServiceUnavailableException('Service failed after retries', 0, $lastException);
        }
    }
  2. Предохранитель, он же Circuit Breaker: Это вообще must have. Представь, сервис упал окончательно, а твоё приложение, как манда с ушами, продолжает слать ему запросы раз за разом, забивая все соединения и тормозя всё подряд. Так вот, предохранитель это лечит. После нескольких подряд ошибок он говорит: «Всё, хуй с горы, не трать время» — и следующие вызовы сразу падают, не доходя до сети. Периодически он проверяет, не очухался ли сервис.

    // Использую библиотеку, например, league/circuit-breaker
    $breaker = new CircuitBreaker($storage, 'my-service', 5, 60); // 5 ошибок, таймаут 60 сек
    if ($breaker->isAvailable()) {
        try {
            $response = $client->fetchData();
            $breaker->success(); // Сообщаем об успехе
        } catch (Exception $e) {
            $breaker->failure(); // Сообщаем об ошибке
            throw $e;
        }
    } else {
        // Сервис недоступен, используем fallback
        return $this->getFallbackData();
    }
  3. Запасной парашют (Fallback/Cache): Ну а если уж совсем пидарас шерстяной и ничего не работает, что показывать-то пользователю? А показываем то, что у нас уже есть. Либо старые данные из кеша (помечая, что они могут быть устаревшие), либо какой-то минимальный дефолтный набор. Главное — не показывать белую страницу с ошибкой. Пусть лучше пользователь увидит вчерашнюю цену, чем ёперный театр с пятисотым кодом.

    public function getProductDetails(int $productId): array {
        $cacheKey = "product_details_{$productId}";
        // Пытаемся получить свежие данные
        try {
            $data = $this->externalCatalogService->fetch($productId);
            $this->cache->set($cacheKey, $data, 300); // Кешируем на 5 минут
            return $data;
        } catch (ServiceUnavailableException $e) {
            // В случае ошибки возвращаем устаревшие, но доступные данные из кеша
            $cached = $this->cache->get($cacheKey);
            if ($cached !== null) {
                $cached['source'] = 'cache (stale)';
                return $cached;
            }
            // Или минимальный набор дефолтных данных
            return ['id' => $productId, 'name' => 'Product Info Temporarily Unavailable'];
        }
    }
  4. Отложил в сторонку (Очереди): А есть такие задачи, которым вообще не нужно отвечать пользователю прямо сейчас. Отправка нотификации, синхронизация данных — да похуй, когда именно это выполнится, в течение пары минут. Вот такие вещи я просто швыряю в очередь (Redis, RabbitMQ), и пусть отдельный воркер взъёбывается с ними, пытается достучаться хоть сто раз. Основное приложение даже не почувствует, что там что-то упало.

  5. Глаза и уши (Логи и мониторинг): И последнее — без этого вообще никуда. Надо всё логировать: что упало, когда, с каким кодом ошибки. И поставить алерты, чтобы не сидеть и не гадать, а тебе сразу пришло: «Э, сабака сука, сервис платёжки второй час хрипит, иди смотри». Иначе будешь как полупидор — последним узнаешь о проблеме от разъярённого клиента.

Вот такая многослойная защита. Суть в том, чтобы система не ломалась полностью из-за чужого косяка, а грациозно деградировала. Пользователь может получить не самую свежую информацию, но он получит хоть что-то и сможет продолжить работу. А это, поверь, в овердохуища раз лучше, чем белая страница с Connection timed out.