или менее популярный заголовок: Введение в тайм-ауты net / http

Оригинал опубликован на blog.simon-frey.eu

Прежде всего, как вы, возможно, уже узнали из заголовков, этот блог стоит на плече гигантов. Следующие два сообщения в блоге вдохновили меня на пересмотр тайм-аутов net / http, поскольку связанное сообщение в блоге в некоторых частях устарело:

Посетите их после того, как прочтете этот пост, и посмотрите, как все изменилось за такое короткое время;)

Почему не стоит использовать стандартную конфигурацию net / http?

Команда разработчиков go решила вообще не устанавливать никаких тайм-аутов в стандартной конфигурации клиента или сервера net / http, и это вполне разумное решение. Почему?

Чтобы ничего не ломалось! Тайм-ауты - это очень индивидуальная настройка, и в большинстве случаев слишком короткий тайм-аут приведет к поломке вашего приложения из-за необъяснимой ошибки, чем слишком длинный (или, в случае GOs, нет).

Визуализация следующих различных вариантов использования клиента go net / http:

1) Скачивание большого файла (10 ГБ) с веб-сервера. При среднем (немецком) подключении к Интернету это займет около пяти минут.

= ›Тайм-аут для соединения должен быть больше пяти минут, потому что меньшее приведет к поломке вашего приложения из-за отмены загрузки в середине (или третьем, или другом проценте) файла.

2) Доступ к REST API с большим количеством одновременных подключений. Обычно это занимает не более нескольких секунд на одно соединение.

= ›Тайм-аут должен быть не более 10 секунд, так как все, что занимает больше времени, будет означать, что вы держите это соединение открытым надолго и истощаете свое приложение, поскольку оно может иметь только X (в зависимости от системы, конфигурации и кодирования). соединения. Поэтому, если этот REST API, к которому вы обращаетесь, не работает так, что он сохраняет соединения открытыми, не отправляя вам необходимые данные, вы хотите предотвратить это.

Итак, для какого сценария следует оптимизировать стандартную библиотеку? Поверьте мне, вы не хотите решать это для миллионов разработчиков по всему миру.

Вот почему мы должны установить тайм-ауты, чтобы они соответствовали нашему варианту использования!

Поэтому никогда не используйте стандартный клиент / сервер go http! Это сломает вашу производственную систему! (Это случилось со мной, так как я забыл свои собственные правила)

Какой тип тайм-аутов возникает при HTTP-соединении?

Я предполагаю, что вы имеете базовое представление о протоколах TCP и HTTP. (В противном случае, Википедия является хорошей отправной точкой для этого)

В основном могут возникать три разные категории тайм-аутов:

  • Во время настройки подключения
  • Во время приема / отправки информации заголовка
  • При приеме / отправке тела

Как вы уже могли ожидать из наших двух примеров во введении, таймаут, о котором мы должны больше всего заботиться, - это тайм-аут, касающийся тела. Другие в большинстве случаев короче и похожи в каждой настройке. (Например, есть только определенное количество заголовков, которые будут отправлены) Нам все еще нужно подумать и позаботиться о тайм-аутах в заголовке, поскольку есть определенные атаки DOS, которые играют с искаженными заголовками или никогда не закрывают заголовок (SLOWLORIS DOS-атака ), но мы вернемся к этому позже.

По крайней мере, вы должны сделать это: легкий путь

net / http дает вам возможность установить тайм-аут для полной передачи данных (настройка, заголовки, тело). Он не такой мелкозернистый, как в более поздних индивидуальных решениях, но поможет вам предотвратить наиболее очевидные проблемы:

  • Отсутствие связи
  • Атаки с искаженным заголовком

Поэтому вам следует хотя бы использовать эти тайм-ауты на каждом используемом вами net / http-клиенте / сервере!

Клиент

В следующем примере клиента полный тайм-аут составляет 5 секунд.

c := &http.Client{
    Timeout: 5 * time.Second,
}
c.Get("https://blog.simon-frey.eu/")

Если соединение все еще открыто, оно будет отменено с помощью net/http: request canceled (Client.Timeout exceeded while reading ...)

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

Сервер

Для сервера мы должны установить два таймаута в простой настройке. Прочитайте и напишите. Таким образом, ReadTimeout определяет, как долго вы позволяете подключению быть открытым во время отправки данных клиентом. А с WriteTimeout все в другом направлении. (Да, также может быть, что вы куда-то отправляете данные, и пакеты никогда не принимаются TCP-ACK, и ваш сервер снова будет голодать)

s := &http.Server{
    ReadTimeout: 1 * time.Second,
    WriteTimeout: 10 * time.Second,
    Addr:":8080",
}
s.ListenAndServe()

Таким образом, этот сервер будет прослушивать порт 8080 и иметь желаемое время ожидания.

Для многих случаев использования этого простого пути может быть достаточно. Но, пожалуйста, прочтите и посмотрите, что еще возможно: D

[Клиент] Подробная настройка таймаутов

Прежде чем мы начнем, следует отметить следующее различие:

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

Настройка подключения

В следующей настройке есть два параметра, которые мы устанавливаем с таймаутом. Они различаются по типу подключения:

  • DialContext: определяет тайм-аут установки для незашифрованного HTTP-соединения.
  • TLSHandshakeTimeout: заботится о тайм-ауте установки для обновления незашифрованного соединения до зашифрованного HTTPS

В настройке 2019 вы всегда должны пытаться разговаривать с зашифрованными конечными точками HTTPS, поэтому в очень редких случаях имеет смысл установить только один из двух параметров.

c := &http.Client{
    Transport: &http.Transport{
        DialContext:(&net.Dialer{
            Timeout:   3 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout:   10 * time.Second,
    }
}
c.Get("https://blog.simon-frey.eu/")

Устанавливая эти параметры, вы определяете, как долго должна длиться установка соединения. Это помогает вам «обнаруживать» (для фактического обнаружения вам нужно сделать больше, чем эти несколько строк) хостов, которые не работают, и быстрее. Таким образом, вы не ждете в своем проекте хоста, который в первую очередь не работает.

Заголовки ответа

Теперь, когда у нас есть установленное (надеюсь, HTTPS) соединение, мы должны получать метаинформацию о содержимом, которое мы получаем. Эта метаинформация хранится в заголовках. Мы можем установить таймауты, как долго мы хотим, чтобы сервер мог нам ответить.

Здесь снова необходимо определить два разных тайм-аута:

  • ExpectContinueTimeout: определяет, как долго вы хотите ждать после отправки полезной нагрузки для начала ответа (в форме начала заголовка).
  • ResponseHeaderTimeout: И с помощью этого параметра вы устанавливаете, как долго может длиться полная передача заголовка

Итак, вы хотите иметь полную информацию заголовка ExpectContinueTimeout + ResponseHeaderTimeout после того, как вы отправили вам полный запрос

c := &http.Client{
    Transport: &http.Transport{
        ExpectContinueTimeout: 4 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
    },
}
c.Get("https://blog.simon-frey.eu/")

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

Представьте себе следующий сценарий: Вы получаете доступ к API, который изменяет размер изображения, которое вы ему отправляете. Таким образом, вы загружаете изображение, и обычно требуется ~ 1 секунда, чтобы изменить размер изображения, а затем начать отправку его обратно в вашу службу. Но, возможно, API вылетает по каким-либо причинам и изменение размера изображения занимает 60 секунд. Теперь, когда вы определили таймауты, вы можете прервать работу через пару секунд и сообщить своим клиентам, что API xyz не работает и что вы находитесь в контакте с поставщиком ... лучше, чем когда ваш модный редактор изображений загружается целую вечность и не показывает никакого статуса информации, и все это из-за ошибки, даже не по вашей вине!

Тело

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

Мы рассмотрим два подхода, которые помогут вам определить тайм-аут для тела:

  • Статический тайм-аут, который прерывает передачу через определенное время
  • Переменный тайм-аут, который убивает тайм-аут после отсутствия передачи данных в течение определенного времени

Статический тайм-аут

Мы отбрасываем все ошибки в примере кода. Вы не должны этого делать!

c := &http.Client{}
resp, _ := c.GET("https://blog.simon-frey.eu")
defer resp.Body.Close()
​
time.AfterFunc(5*time.Second, func() {
    resp.Body.Close()
})
bodyBytes,_ := ioutil.ReadAll(resp.Body)

В примере кода мы устанавливаем таймер, который запускается resp.Body.Close() после завершения. С помощью этой команды мы закрываем тело, и ioutil.ReadAll выдаст read on closed response body ошибку.

Переменный тайм-аут

Мы опускаем большинство ошибок в примере кода. Вы не должны этого делать!

​
c := &http.Client{}
resp, _ := c.GET(https://blog.simon-frey.eu")
defer resp.Body.Close()
​
timer := time.AfterFunc(5*time.Second, func() {
    resp.Body.Close()
})  
                 
bodyBytes := make([]byte, 0)
for {
    //We reset the timer, for the variable time
    timer.Reset(1 * time.Second)
​
    _, err = io.CopyN(bytes.NewBuffer(bodyBytes), resp.Body, 256)
    if err == io.EOF {
        // This is not an error in the common sense
        // io.EOF tells us, that we did read the complete body
            break
    } else if err != nil {
        //You should do error handling here
        break
    }
}

Разница здесь в том, что у нас есть бесконечный цикл, который выполняет итерацию по телу и копирует из него данные. Есть два варианта, как этот цикл останется:

  • Мы получаем io.EOF файловую ошибку от io.CopyN, это означает, что мы читаем все тело, и не требуется срабатывания тайм-аута.
  • Мы получаем еще одну ошибку, если это ошибка read on closed response body, сработавший тайм-аут.

Это решение работает, потому что io.CopyN блокирует. Поэтому, если для чтения из тела недостаточно (в нашем случае 256 байт), он будет ждать. Если в течение этого времени срабатывает тайм-аут, мы останавливаем выполнение.

Моя конфигурация "по умолчанию"

Еще раз: Это мое собственное мнение о тайм-аутах, и вы должны адаптировать их к требованиям вашего проекта! Я не использую точно такую ​​же настройку в каждом проекте!

c := &http.Client{
    Transport: &http.Transport{
        DialContext:(&net.Dialer{
            Timeout:   10 * time.Second,
            KeepAlive: 10 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout:   10 * time.Second,
           
        ExpectContinueTimeout: 4 * time.Second,
        ResponseHeaderTimeout: 3 * time.Second,
        
        // Prevent endless redirects
        Timeout: 10 * time.Minute,
    },
}

[Сервер] Подробная настройка таймаутов

Поскольку для http.Server нет определенных тайм-аутов дозвона, мы сразу начнем с тайм-аутов для заголовков.

Заголовки

Для заголовков запроса у нас есть определенный тайм-аут: ReadeHeaderTimeout, который представляет время, пока не будет прочитан полный заголовок запроса (отправленный клиентом). Поэтому, если клиенту требуется больше времени для отправки заголовков, время ожидания соединения прерывается. Этот тайм-аут особенно важен против атак типа SLOWLORIS, поскольку здесь заголовок никогда не закрывается, и соединение, таким образом, будет оставаться открытым все время.

s := &http.Server{
    ReadHeaderTimeout:20 *time.Second,
}
s.ListenAndServe()

Как вы, возможно, уже заметили, есть только Read HeaderTimeout, потому что для отправки данных клиенту go не имеет определенного различия между заголовками и телом для тайм-аута.

Тело

Здесь мы должны различать запрос (который отправляется от клиента на сервер) и тело ответа.

Тело ответа

Для тела ответа есть только одно статическое решение для тайм-аута:

s := &http.Server{
	WriteTimeout:20 *time.Second,
}
s.ListenAndServe()

Пока соединение открыто, мы не можем различить, были ли данные отправлены правильно или клиент делает здесь подделку. Но поскольку мы знаем наши данные полезной нагрузки, довольно просто установить здесь тайм-аут для нашей прошлой информации о нашем сервере. Поэтому, если вы файловый сервер, этот таймаут должен быть больше, чем для сервера API. Вы можете не устанавливать тайм-аут для целей тестирования и отслеживать, сколько времени занимает «нормальный» запрос. Добавьте сюда несколько процентов дисперсии, и тогда все будет в порядке!

Тело запроса

Внимание: если вы установили WriteTimeout, это также повлияет на тайм-аут запроса. Это связано с определением, что WriteTimeout. Он начинается, когда читаются заголовки запроса. Итак, если чтение из тела запроса занимает 5 секунд, а тайм-аут записи равен 4 секундам, это также прервет чтение тела запроса!

Для тела запроса снова есть два возможных решения:

  • Статические таймауты, которые мы можем определить с помощью конфигурации http.Client
  • Переменные тайм-ауты для этого мы должны создать собственный обходной путь кода (поскольку в настоящее время для этого нет поддержки)

Статический

Для статического тайм-аута мы можем использовать параметр ReadTimeout, который мы уже использовали в простом пути:

s := &http.Server{
	ReadTimeout:20 * time.Second,
}
s.ListenAndServe()

Переменная

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

Мы должны определить собственный обработчик для сервера, в нашем примере мы называем его timeoutHandler. Этот обработчик ничего не делает, кроме чтения из тела с нашим циклом и тайм-аутом, если данные больше не отправляются.

type timeoutHandler struct{}
func (h timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
	defer r.Body.Close()
	timer := time.AfterFunc(5*time.Second, func() {
		r.Body.Close()
	})
	bodyBytes := make([]byte, 0)
	for {
		//We reset the timer, for the variable time
		timer.Reset(1 * time.Second)
        
		_, err := io.CopyN(bytes.NewBuffer(bodyBytes), r.Body, 256)
		if err == io.EOF {
			// This is not an error in the common sense
			// io.EOF tells us, that we did read the complete body
			break
		} else if err != nil {
			//You should do error handling here
			break
		}
	}
}
func main() {
	h := timeoutHandler{}
	s := &http.Server{
		ReadHeaderTimeout:20 *time.Second,
		Handler:h,
		Addr:":8080",
	}
	s.ListenAndServe()
}

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

Моя конфигурация "по умолчанию"

Еще раз: Это мое собственное мнение о тайм-аутах, и вы должны адаптировать их к требованиям вашего проекта! Я не использую точно такую ​​же настройку в каждом проекте!

s := &http.Server{
	ReadHeaderTimeout:20 *time.Second,
	ReadTimeout: 1 * time.Minute,
    WriteTimeout: 2 * time.Minute,
}

Заключение

Надеюсь, вам понравился этот пост в блоге, и он помог вам немного лучше понять различные тайм-ауты. Если у вас есть какие-либо отзывы, вопросы или вы просто хотите сказать «Servus» (баварский немецкий язык для приветствия), не стесняйтесь обращаться ко мне!

RSS Feed - эта работа находится под Международной лицензией Creative Commons Attribution 4.0.

Источники

Https://golang.org/pkg/net/http/

Https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779

Https://blog.cloudflare.com/exposing-go-on-the-internet/

Изображение Gopher (CC BY-SA 3.0): Викимедиа