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

«Как вы организуете взаимодействие с системой в случае недоступности внешнего сервиса?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

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

  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) при высокой частоте ошибок.

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