DevGang
Авторизоваться

Тайм-ауты HTTP-запросов в Go для начинающих 

Фото Хао Чжана на Unsplash
Фото Хао Чжана на Unsplash

Тайм-ауты - это одна из примитивных концепций надежности в распределенных системах, которая смягчает последствия неизбежных отказов распределенных систем, как упоминалось в этом твите.

Проблема

Как условно смоделировать ответ 504 http.StatusGatewayTimeout?

При попытке реализовать проверку токена OAuth в zalando/skipper мне пришлось понять и реализовать тест, имитирующий 504 http.StatusGatewayTimeout используя httptest при превышении времени ожидания сервера, но только при превышении времени ожидания клиента из-за задержки на сервере. Будучи новичком в языке, я делал то, что делает большинство из нас; создайте стандартный HTTP-клиент и добавьте время ожидания, как показано ниже:

client := http.Client{Timeout: 5 * time.Second}

Вышеуказанное кажется очень простым и интуитивно понятным, если вы хотите создать клиент для выполнения http-запроса. Но под ним скрываются многие детали низкого уровня, включая время ожидания клиента, время ожидания сервера и время ожидания для балансировщиков нагрузки.

Тайм-ауты на стороне клиента

Тайм-аут HTTP-запроса может быть определен на стороне клиента несколькими способами, в зависимости от тайм-аута, намеченного в цикле запроса. Цикл запрос-ответ состоит из DialerTLS HandshakeRequest HeaderRequest BodyResponse Header и Response Body. В зависимости от вышеуказанных частей запроса-ответа Go предоставляет следующие способы создания запроса с тайм-аутами

  1. http.client
  2. context
  3. 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 необходимо прочитать.

Источник:

#Golang
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Оплатив хостинг 25$ в подарок вы получите 100$ на счет

Получить