> Что происходит с внешним сервисом при повторных обращениях во время сбоя (Go)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: Wildberries
Стек: Go
> Пример ответа
Короткий ответ
При повторных обращениях к внешнему сервису во время сбоя поведение зависит от реализации клиента и инфраструктуры. В худшем случае каждый запрос будет ждать таймаут, создавая нагрузку на вашу систему и внешний сервис. В лучшем - сработает circuit breaker, который быстро отклоняет запросы без обращения к сломанному сервису. Также важны retry с экспоненциальной задержкой и jitter, чтобы не создавать thundering herd. Без этих механизмов возможен каскадный отказ и исчерпание пула соединений.
Подробное объяснение
Когда внешний сервис недоступен, каждый повторный запрос проходит через несколько слоёв:
-
Клиентская библиотека - создаёт новое TCP-соединение или берёт из пула. Если пул исчерпан (все соединения висят в ожидании ответа), новые запросы блокируются или получают ошибку сразу.
-
Таймауты - если не задан общий таймаут на запрос, клиент может ждать ответа бесконечно (или до таймаута ОС, который может быть 2-5 минут). Это приводит к накоплению горутин в Go, каждая из которых держит память и, возможно, соединение.
-
Retry-логика - если клиент делает повторные попытки немедленно, он создаёт лавину запросов. Внешний сервис, который уже испытывает проблемы, получает ещё больше нагрузки. Это усугубляет сбой.
-
Балансировщик и прокси - если между вами и сервисом есть nginx или service mesh, они могут сами ретраить запросы, что добавляет ещё один слой повторных обращений.
-
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 (кэш, заглушку).
Пример кода
GOpackage mainimport ("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 и jitterfunc retryWithBackoff(ctx context.Context, fn func() error, maxRetries int) error {baseDelay := 100 * time.Millisecondfor attempt := 0; ; attempt++ {err := fn()if err == nil {return nil}if attempt >= maxRetries {return err}delay := baseDelay * (1 << attempt) // 100ms, 200ms, 400msjitter := 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без таймаута.
> Похожие задачи по backend
Имеет ли смысл передавать слайс по указателю в Go
Как происходит обмен сертификатами в HTTPS
Что такое селективность индекса и почему не всегда индекс используется?
Как реализовать паттерн fan-out fan-in в Go с использованием горутин?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью