Ответ
Правильная настройка таймаутов критически важна для создания надежных и отказоустойчивых систем. Она предотвращает "зависание" запросов и каскадные сбои.
Стратегия выбора значения
-
Ориентируйтесь на метрики:
- Соберите статистику времени ответа для вызываемого сервиса.
- Установите таймаут на основе 95-го или 99-го перцентиля времени ответа, добавив небольшой запас (например, +10-20%). Это покроет большинство нормальных запросов, но отсечет аномально долгие.
-
Учитывайте бизнес-требования (SLA/SLO):
- Таймаут не должен превышать требования к времени ответа вашего собственного сервиса. Если ваш сервис должен отвечать за 500 мс, нет смысла ставить таймаут на зависимость в 10 секунд.
-
Цепочка вызовов:
- При вызове сервиса
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: Для временных сетевых ошибок реализуйте повторные попытки с увеличивающейся задержкой (и джиттером), чтобы не "забить" сервис запросами.
Ответ 18+ 🔞
А, ну вот, опять про таймауты, святое дело! Слушай, если их нихуя не настроить, то твоя система будет висеть, как последний мудак, пока все клиенты не пошлют её нахуй. Это как ждать автобус, который, может, и приедет, а может, и нет — а ты стоишь, ебучка, и жизнь проходит.
Так вот, главная мысль, блядь: таймаут должен быть короче, чем терпение того, кто тебя вызывает. Если твой сервис обещает ответ за полсекунды, а ты внутри дергаешь какой-нибудь левый микросервис с таймаутом в 10 секунд — ты просто конченый идиот, прости меня. Он ляжет, и ты ляжешь вместе с ним, как дурак.
Как не быть мудаком при выборе цифр
-
Смотри на метрики, епта! Не с потолка же берёшь. Посмотри, сколько обычно отвечает тот сервис, который ты дергаешь. Возьми 95-й процентиль его времени ответа, прибавь чуть-чуть сверху, чтоб не обосраться на ровном месте, и вот тебе золотая середина. Если он обычно отвечает за 200 мс, а ты ставишь таймаут 30 секунд — ты либо гений, либо просто нихуя не понимаешь.
-
Помни про цепочку. Если у тебя вызовы идут
A -> B -> C, то таймаут уAдолжен быть больше, чем уB, а уB— больше, чем уC. Иначе получится ебля: сервисCуже давно сдох по таймауту, аBвсё ещё его ждёт, как дурак, иAждётB. И все дружно накрываются одним медным тазом. Красота!
Как это в Go делать, чтобы не позориться
В Go за это отвечает context. Без него — ты дикарь, блядь.
Вариант первый, для ленивых: настроить http.Client один раз и забыть. Но это негибко, честно говоря.
client := &http.Client{
Timeout: 15 * time.Second, // Общий лимит на весь запрос
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second, // Сколько пытаться соединиться
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second, // Сколько ждать, пока порукопожатимся по TLS
},
}
Вариант второй, правильный: использовать context для каждого запроса. Это даёт власть, ахуенную власть!
// Допустим, мы не хотим ждать дольше 5 секунд, ебать его в сраку
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // ЭТУ ХУЙНЮ НЕ ЗАБЫВАТЬ, а то ресурсы потекут
req, err := http.NewRequestWithContext(ctx, "GET", "http://example.com", nil)
if err != nil {
// ну тут понятно
}
resp, err := client.Do(req)
if err != nil {
// Вот здесь можно понять, кто виноват — сеть или таймаут
if errors.Is(err, context.DeadlineExceeded) {
log.Println("Всё, пиздец, время вышло!")
}
}
Итог, коротко, как в лоб
- Контекст — твой друг. Используй его для ВСЕХ исходящих вызовов. Всегда.
- Таймауты должны быть и там, и тут. Настраивай и на клиенте, и на сервере. Сервер тоже должен уметь послать медленного клиента нахуй.
- Повторные попытки — это искусство. Если что-то упало не навсегда, а временно, имеет смысл попробовать ещё раз. Но не сразу, а с умной задержкой (exponential backoff), чтобы не добить и так хреновый сервис. И добавь немного случайности (джиттера), чтобы все клиенты не полезли повторно в одну и ту же секунду — это будет пиздец, а не восстановление.
Вот и вся магия. Без таймаутов твой код — просто кусок говна, который зависнет при первой же проблеме. С таймаутами — это уже осознанный, взрослый кусок говна, который хотя бы упадёт с понятной ошибкой. Прогресс, блядь!