Как правильно выбирать и устанавливать таймауты для сетевых запросов?

Ответ

Правильная настройка таймаутов критически важна для создания надежных и отказоустойчивых систем. Она предотвращает "зависание" запросов и каскадные сбои.

Стратегия выбора значения

  1. Ориентируйтесь на метрики:

    • Соберите статистику времени ответа для вызываемого сервиса.
    • Установите таймаут на основе 95-го или 99-го перцентиля времени ответа, добавив небольшой запас (например, +10-20%). Это покроет большинство нормальных запросов, но отсечет аномально долгие.
  2. Учитывайте бизнес-требования (SLA/SLO):

    • Таймаут не должен превышать требования к времени ответа вашего собственного сервиса. Если ваш сервис должен отвечать за 500 мс, нет смысла ставить таймаут на зависимость в 10 секунд.
  3. Цепочка вызовов:

    • При вызове сервиса A -> B -> C, таймаут у A должен быть больше, чем у B, а у B — больше, чем у C. Это позволяет ошибке таймаута корректно "всплыть" наверх.

Реализация в Go

Идиоматичный способ управления таймаутами и отменой в Go — это использование пакета context.

1. Общий таймаут на http.Client:
Хорошая практика для установки базовых ограничений, но негибкая для отдельных запросов.

client := &http.Client{
    Timeout: 15 * time.Second, // Общий таймаут на весь запрос
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   5 * time.Second, // Таймаут на установку TCP-соединения
        }).DialContext,
        TLSHandshakeTimeout: 5 * time.Second, // Таймаут на TLS-рукопожатие
    },
}

2. Таймаут на конкретный запрос (предпочтительный способ):
Использование context.WithTimeout дает гранулярный контроль и обеспечивает отмену запроса на всех уровнях (включая http.Client).

// Создаем контекст с таймаутом в 5 секунд
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Важно вызывать cancel для освобождения ресурсов

req, err := http.NewRequestWithContext(ctx, "GET", "http://example.com", nil)
if err != nil {
    // обработка ошибки
}

resp, err := client.Do(req)
if err != nil {
    // Если произошел таймаут, ошибка будет содержать context.DeadlineExceeded
    if errors.Is(err, context.DeadlineExceeded) {
        log.Println("Request timed out")
    }
}

Лучшие практики

  • Всегда используйте context для исходящих запросов.
  • Настраивайте и клиентские, и серверные таймауты. Сервер тоже должен иметь таймаут на чтение/запись, чтобы защититься от медленных клиентов.
  • Используйте Retry с Exponential Backoff: Для временных сетевых ошибок реализуйте повторные попытки с увеличивающейся задержкой (и джиттером), чтобы не "забить" сервис запросами.