> Как подбирать тайм-ауты для backend взаимодействия, например HTTP запросов (Go)

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

Компании: Avito

Стек: Go

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

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

Тайм-ауты подбираются исходя из анализа распределения времени ответа сервиса: p50, p95, p99, а также с учётом бюджета всего запроса. Для HTTP-запросов в Go используется http.Client{Timeout: ...} и контекст с дедлайном. Правило: тайм-аут должен быть достаточно большим, чтобы покрыть нормальные пики (p99 + запас), но достаточно маленьким, чтобы не накапливать деградировавшие запросы. Обычно начинают с p99 × 1.5-2, затем калибруют под реальную нагрузку.

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

Тайм-ауты решают три задачи: защита от зависших зависимостей, ограничение потребления ресурсов (горутины, соединения, память) и обеспечение предсказуемого времени ответа для пользователя. В распределённой системе каждый вызов должен иметь свой тайм-аут, а сумма тайм-аутов по цепочке вызовов не должна превышать общий бюджет запроса.

Ключевые метрики для подбора:

  • p50, p95, p99 - медиана и хвосты распределения. Тайм-аут на p99 отсекает 1% запросов, которые скорее всего деградировали.
  • Коэффициент деградации - как быстро растёт время ответа при увеличении нагрузки. Если сервис деградирует резко, тайм-аут должен быть агрессивнее.
  • Стоимость ретраев - если ретраи допустимы, тайм-аут можно уменьшить, чтобы быстрее переключаться на резервный путь.

Важно различать:

  • Connect timeout - время на установление соединения (обычно 1-3 секунды).
  • Response header timeout - время до получения первых байт ответа.
  • Total timeout - полное время на весь запрос, включая чтение тела.

В Go это реализуется через http.Transport (DialContext, ResponseHeaderTimeout) и http.Client.Timeout (общий). Для тонкой настройки используют context.WithTimeout - он отменяет запрос и освобождает ресурсы.

Типичная ошибка - ставить один большой тайм-аут на всё. Правильный подход - каскад: общий бюджет на уровне handler, затем тайм-ауты на каждый внешний вызов, причём их сумма должна быть меньше общего бюджета.

На практике

  1. Сначала собираем метрики: latency распределение для каждого внешнего вызова в проде (не в тестовой среде).
  2. Определяем бюджет всего запроса: например, SLO 500ms на p99.
  3. Распределяем бюджет по цепочке вызовов: если запрос делает 3 внешних вызова последовательно, каждому выделяем не более 150ms.
  4. Устанавливаем тайм-аут = p99 × 1.5-2, но не больше выделенного бюджета.
  5. Для критичных вызовов добавляем fallback: если тайм-аут истёк, возвращаем кэш или деградированный ответ.
  6. Мониторим: если процент таймаутов превышает 0.1-1%, пересматриваем значение или ищем проблему в зависимом сервисе.
  7. Используем jitter и retry с экспоненциальным backoff, но только для идемпотентных операций.

Важно: тайм-аут - это не константа, а параметр, который пересматривается при изменении нагрузки, деплое новых версий или изменении инфраструктуры.

Пример кода

GO
// Настройка HTTP клиента с раздельными тайм-аутами
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 2 * time.Second, // connect timeout
KeepAlive: 30 * time.Second,
}).DialContext,
ResponseHeaderTimeout: 3 * time.Second, // время до первого байта
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
}
client := &http.Client{
Transport: transport,
Timeout: 5 * time.Second, // общий тайм-аут на запрос
}
// Использование контекста для каскадного тайм-аута
func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
// 2 секунды на этот конкретный вызов
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}

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

Начни с главного: тайм-аут подбирается на основе метрик latency, а не интуиции. Упомяни p99 и бюджет запроса. Покажи понимание каскада тайм-аутов и разницу между connect/header/total. Обязательно скажи про контекст в Go и почему http.Client.Timeout недостаточно для распределённых сценариев. Приведи пример: если p99 = 200ms, ставь 300-400ms, но не 5 секунд. Подчеркни, что тайм-аут - это динамический параметр, который нужно мониторить и пересматривать.

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

  • Понимание распределений latency и умение работать с перцентилями.
  • Умение проектировать бюджет времени в распределённой системе.
  • Знание конкретных механизмов в Go: http.Client, context.WithTimeout, http.Transport.
  • Понимание trade-off: слишком маленький тайм-аут - ложные ошибки, слишком большой - деградация под нагрузкой.
  • Умение рассуждать о ретраях и их взаимодействии с тайм-аутами.

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

  • Ставить тайм-аут "на глаз" без метрик.
  • Использовать один тайм-аут на все вызовы, игнорируя разную природу зависимостей.
  • Не учитывать сумму тайм-аутов в цепочке вызовов - итоговое время превышает SLO.
  • Путать connect timeout и общий тайм-аут.
  • Забывать про контекст: http.Client.Timeout не отменяет чтение тела, если оно уже началось.
  • Делать ретраи без учёта оставшегося времени бюджета - ретраи могут умножить нагрузку на деградировавший сервис.
  • Не мониторить процент таймаутов после изменения значения.

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

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