Тайм-ауты HTTP-запросов в Go для начинающих
Тайм-ауты - это одна из примитивных концепций надежности в распределенных системах, которая смягчает последствия неизбежных отказов распределенных систем, как упоминалось в этом твите.
Проблема
Как условно смоделировать ответ 504 http.StatusGatewayTimeout
?
При попытке реализовать проверку токена OAuth в zalando/skipper мне пришлось понять и реализовать тест, имитирующий 504 http.StatusGatewayTimeout используя httptest
при превышении времени ожидания сервера, но только при превышении времени ожидания клиента из-за задержки на сервере. Будучи новичком в языке, я делал то, что делает большинство из нас; создайте стандартный HTTP-клиент и добавьте время ожидания, как показано ниже:
client := http.Client{Timeout: 5 * time.Second}
Вышеуказанное кажется очень простым и интуитивно понятным, если вы хотите создать клиент для выполнения http-запроса. Но под ним скрываются многие детали низкого уровня, включая время ожидания клиента, время ожидания сервера и время ожидания для балансировщиков нагрузки.
Тайм-ауты на стороне клиента
Тайм-аут HTTP-запроса может быть определен на стороне клиента несколькими способами, в зависимости от тайм-аута, намеченного в цикле запроса. Цикл запрос-ответ состоит из Dialer
, TLS Handshake
, Request Header
, Request Body
, Response Header
и Response Body
. В зависимости от вышеуказанных частей запроса-ответа Go предоставляет следующие способы создания запроса с тайм-аутами
http.client
context
http.Transport
http.client
http.client является собой высокоуровневый тайм - аут , который включает в себя весь цикл запроса от Dial
до Response Body
. Мудрая реализация http.client
- это тип структуры, который принимает необязательное свойство Timeout
типа time.Duration
, которое определяет ограничение на время начала запроса до сброса тела ответа.
client := http.Client{Timeout: 5 * time.Second}
context
Пакет Go context содержит полезные инструменты для настройки тайм - аута, срок и расторжение запросов через методы WithTimeout, WithDeadline
и WithCancel
. Используя WithTimeout
, вы можете добавить тайм-аут к методу http.Request
использования req.WithContext
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error("Request error", err)
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
http.Transport
Вы также можете указать время ожидания, используя низкоуровневую реализацию http.Transport создания пользовательского интерфейса, DialContext
и использовать его для создания http.client
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
}
client := http.Client{Transport: transport}
Решение
Так что с вышеупомянутой проблемой и вариантами под рукой я создал http.request
с context.WithTimeout()
. Но это все равно выдало ошибку ниже
client_test.go:40: Response error Get http://127.0.0.1:49597: context deadline exceeded
Тайм-ауты на стороне сервера
Проблема с подходом context.WithTimeout()
состоит в том, что он все еще только симулирует клиентскую сторону запроса. И в случае, если заголовок или тело запроса занимает больше времени, чем предусмотренное в бюджете время ожидания, запрос не выполняется на стороне клиента, а не на стороне сервера с кодом состояния 504 http.StatusGatewayTimeout
.
Один из способов создания сервера httptest
, время ожидания которого истекает каждый раз:
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request){
w.WriteHeader(http.StatusGatewayTimeout)
}))
Но я хотел, чтобы время ожидания было только на основе значения времени ожидания клиента. Чтобы сервер возвращал 504 на основе тайм-аута клиента, вы можете заключить обработчик в функцию-обработчик http.TimeoutHandler()
для тайм-аута запроса на сервере. Ниже приводится рабочий тест, который применяет этот сценарий
func TestClientTimeout(t *testing.T) {
handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
d := map[string]interface{}{
"id": "12",
"scope": "test-scope",
}
time.Sleep(100 * time.Millisecond) //<- Any value > 20ms
b, err:= json.Marshal(d)
if err != nil {
t.Error(err)
}
io.WriteString(w, string(b))
w.WriteHeader(http.StatusOK)
})
backend := httptest.NewServer(http.TimeoutHandler(handlerFunc, 20*time.Millisecond, "server timeout"))
url := backend.URL
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error("Request error", err)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Error("Response error", err)
return
}
defer resp.Body.Close()
}
Детали вышеупомянутой проблемы используются в реализации tokeninfo_test.go/TestOAuth2TokenTimeout в zalando/skipper
Вероятно, начинающий суслик считает полезным понимать работу тайм-аутов http высокого уровня! Если вы хотите узнать более подробную информацию о http timeouts
, эту статью из Cloudflare необходимо прочитать.