> Что происходит с внешним сервисом при повторных обращениях во время сбоя (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Wildberries

Стек: Go

> Пример ответа

Короткий ответ

При повторных обращениях к внешнему сервису во время сбоя поведение зависит от реализации клиента и инфраструктуры. В худшем случае каждый запрос будет ждать таймаут, создавая нагрузку на вашу систему и внешний сервис. В лучшем - сработает circuit breaker, который быстро отклоняет запросы без обращения к сломанному сервису. Также важны retry с экспоненциальной задержкой и jitter, чтобы не создавать thundering herd. Без этих механизмов возможен каскадный отказ и исчерпание пула соединений.

Подробное объяснение

Когда внешний сервис недоступен, каждый повторный запрос проходит через несколько слоёв:

  1. Клиентская библиотека - создаёт новое TCP-соединение или берёт из пула. Если пул исчерпан (все соединения висят в ожидании ответа), новые запросы блокируются или получают ошибку сразу.

  2. Таймауты - если не задан общий таймаут на запрос, клиент может ждать ответа бесконечно (или до таймаута ОС, который может быть 2-5 минут). Это приводит к накоплению горутин в Go, каждая из которых держит память и, возможно, соединение.

  3. Retry-логика - если клиент делает повторные попытки немедленно, он создаёт лавину запросов. Внешний сервис, который уже испытывает проблемы, получает ещё больше нагрузки. Это усугубляет сбой.

  4. Балансировщик и прокси - если между вами и сервисом есть nginx или service mesh, они могут сами ретраить запросы, что добавляет ещё один слой повторных обращений.

  5. DNS и discovery - при сбое некоторые клиенты начинают заново резолвить DNS или обновлять список инстансов, что добавляет задержку и нагрузку на инфраструктуру.

Ключевые механизмы защиты:

  • Circuit breaker - после N ошибок подряд переходит в состояние open и отклоняет запросы мгновенно (fail fast). Периодически переходит в half-open для проверки восстановления.
  • Bulkhead - ограничивает количество одновременных запросов к конкретному сервису, изолируя сбой.
  • Retry with backoff - повторные попытки с экспоненциальной задержкой (например, 100ms, 200ms, 400ms) и случайным jitter, чтобы запросы не приходили синхронно.
  • Timeout per request - жёсткий лимит ожидания ответа (например, 2-3 секунды), после которого запрос отменяется.

На практике

В Go-сервисах типичная схема:

  • Используйте http.Client с заданным Timeout - это обязательно.
  • Для retry используйте библиотеки типа go-retryablehttp или собственную реализацию с backoff.
  • Для circuit breaker - sony/gobreaker или github.com/afex/hystrix-go.
  • Для bulkhead - семафоры или golang.org/x/sync/semaphore.
  • Обязательно логируйте ошибки с контекстом (какой сервис, сколько попыток, сколько времени ждали).
  • Настраивайте мониторинг: количество ошибок, время ответа, состояние circuit breaker.

Важно: при сбое не делайте retry на каждый запрос бесконечно. Обычно 2-3 попытки с backoff достаточно. Дальше - отдавайте ошибку клиенту или используйте fallback (кэш, заглушку).

Пример кода

GO
package main
import (
"context"
"fmt"
"math/rand"
"net/http"
"time"
"github.com/sony/gobreaker"
)
func main() {
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "external-api",
MaxRequests: 5, // в half-open состоянии
Interval: 60 * time.Second, // сброс счётчика
Timeout: 30 * time.Second, // время в open состоянии
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 3
},
})
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 10; i++ {
result, err := cb.Execute(func() (interface{}, error) {
return callExternal(client)
})
if err != nil {
fmt.Printf("request %d failed: %v\n", i, err)
} else {
fmt.Printf("request %d ok: %v\n", i, result)
}
time.Sleep(500 * time.Millisecond)
}
}
func callExternal(client *http.Client) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://external.example.com/api", nil)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return "", fmt.Errorf("server error: %d", resp.StatusCode)
}
return "success", nil
}
// retry с экспоненциальным backoff и jitter
func retryWithBackoff(ctx context.Context, fn func() error, maxRetries int) error {
baseDelay := 100 * time.Millisecond
for attempt := 0; ; attempt++ {
err := fn()
if err == nil {
return nil
}
if attempt >= maxRetries {
return err
}
delay := baseDelay * (1 << attempt) // 100ms, 200ms, 400ms
jitter := time.Duration(rand.Int63n(int64(delay / 2)))
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay + jitter):
}
}
}

Как отвечать на собеседовании

Начните с того, что поведение зависит от конфигурации клиента. Затем перечислите основные сценарии: без защиты (таймауты, retry без backoff), с базовой защитой (таймауты), с продвинутой (circuit breaker, bulkhead). Обязательно упомяните риск каскадного отказа и thundering herd. Приведите конкретный пример из практики: как вы диагностировали проблему (метрики, логи) и что изменили. Покажите понимание trade-off: circuit breaker может преждевременно отключать сервис, retry увеличивает latency, но повышает надёжность.

Что проверяет интервьюер

  • Понимание сетевых сбоев и их последствий для распределённых систем.
  • Знание паттернов отказоустойчивости: circuit breaker, retry, backoff, timeout, bulkhead.
  • Умение применять эти паттерны в Go: конкретные библиотеки, настройка http.Client, работа с контекстами.
  • Понимание эксплуатационных аспектов: мониторинг, логирование, алерты.
  • Способность рассуждать о trade-off и edge cases: когда retry вреден, когда circuit breaker опасен.

Типичные ошибки

  • Ответ без упоминания таймаутов - главная ошибка. Если нет таймаута, запрос может висеть вечно.
  • Предложение делать retry без backoff - это усугубляет сбой.
  • Игнорирование jitter - синхронные retry создают пиковую нагрузку.
  • Непонимание разницы между circuit breaker и retry: это разные уровни защиты.
  • Утверждение, что "внешний сервис сам разберётся" - без защиты ваша система упадёт первой.
  • В Go-коде: забыть про context.WithTimeout или использовать глобальный http.DefaultClient без таймаута.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью