Рубрики
Без рубрики

Http-клиент Apache и асинхронные тайм-ауты HTTP-клиента, описанные на рисунках

Объяснение с картинками, что такое тайм-аут соединения, тайм-аут чтения и тайм-аут пула соединений, и как HTTP-клиент Apache сравнивается с асинхронным HTTP-клиентом при их обработке”. С тегами clojure, java, микросервисы.

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

Даже если мы будем говорить о HttpClient Apache, следующее объяснение полезно для любой связи на основе TCP, которая включает в себя большинство драйверов JDBC.

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

  • Время ожидания соединения
  • Время ожидания чтения

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

  • Время ожидания пула соединений
  • Время жизни Пула Соединений (TTL)

Вы найдете здесь как настроить эти тайм-ауты в Java. В наших примерах мы будем использовать clj-http , который представляет собой простую оболочку над HttpClient Apache. Мы также сравним, как тайм-ауты работают в Асинхронном HTTP-клиенте .

Весь код, включая среду создания докера для проверки настроек, можно найти по адресу https://github.com/dlebrero/apache-httpclient-timeouts .

Время ожидания соединения

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

Это делается с помощью рукопожатия:

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

В терминах TCP это называется 3-сторонним рукопожатием:

Время ожидания соединения определяет, сколько времени вы готовы потратить на это рукопожатие.

Давайте протестируем его, используя не маршрутизируемый IP-адрес:

;; Without connection timeout
(time
    (try
      (client/get "http://10.255.255.1:22220/")
      (catch Exception e)))
"Elapsed time: 75194.7148 msecs"

;; With connection timeout
(time
    (try
      (client/get "http://10.255.255.1:22220/"
        {:connection-timeout 2000})
      (catch Exception e
        (log/info (.getClass e) ":" (.getMessage e)))))
"Elapsed time: 2021.1883 msecs"
INFO  a.http-client - java.net.SocketTimeoutException : connect timed out

Обратите внимание на разное прошедшее время, напечатанное исключение и сообщение внутри исключения.

Время ожидания чтения

Как только соединение установлено, и вы с удовольствием разговариваете с сервером, вы можете указать, как долго вы готовы ждать ответа от сервера, используя время ожидания чтения:

Давайте протестируем его, используя на этот раз сервер Nginx, с Foxyproxy в середине, чтобы возиться со временем отклика:

;; With no socket timeout
(time
  (try
    (client/get "http://local.toxiproxy:22220/")
    (catch Exception e (.printStackTrace e))))
"Elapsed time: 240146.6273 msecs"

;; Same url, with socket timeout
(time
    (try
      (client/get "http://local.toxiproxy:22220/"
        {:socket-timeout 2000})
      (catch Exception e
        (log/info (.getClass e) ":" (.getMessage e)))))
"Elapsed time: 2017.7835 msecs"
INFO  a.http-client - java.net.SocketTimeoutException : Read timed out

Обратите внимание, что время ожидания сокета по умолчанию зависит от системы. Обратите внимание на разное прошедшее время, напечатанное исключение и сообщение внутри исключения.

Конфигурацию Foxyproxy можно найти здесь .

Викторина в пабе

С этими двумя тайм-аутами вы легко наберете одно очко для своей команды на следующем чемпионате по викторинам в IT-пабе:

Если вы настроите свой HTTP-клиент с таймаутом подключения 10 секунд и таймаутом чтения 1 секунда, как долго поток будет зависать после отправки HTTP-запроса в худшем случае?

Ты правильно угадал! Бесконечный! Одно очко в пользу вашей команды!

Ух ты? Вы не ответили бесконечно? Это оооочень очевидно (сарказм).

Давайте снова позвоним одному из ваших друзей и спросим его о Пи, но на этот раз мы собираемся позвонить одному из этих высокоточных умных друзей:

Что происходит?

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

Давайте посмотрим на это в действии. Сначала мы настраиваем Foxyproxy так, чтобы он был очень медленным при проксировании ответа Nginx (~ 2 байта в секунду):

(client/post "http://local.toxiproxy:8474/proxies/proxied.nginx/toxics"
    {:form-params {:attributes {:delay 1000000
                                :size_variation 1
                                :average_size 2}
                   :toxicity 1.0
                   :stream "downstream"
                   :type "slicer"}
     :content-type :json})

И теперь мы делаем точно такой же запрос, как и раньше, с таймаутом две секунды :

(time
    (try
      (client/get "http://local.toxiproxy:22220/"
        {:socket-timeout 2000})
      (catch Exception e
        (log/info (.getClass e) ":" (.getMessage e)))))
"Elapsed time: 310611.8366 msecs"

Это больше, чем пять минут! И, к счастью, это всего лишь 600 байт.

Вот как выглядят журналы HttpClient для простого чтения первых байтов заголовка:

Это выглядит довольно медленно. Конечно, с вами этого никогда не случится (здесь больше сарказма).

Мы увидим внизу, как избежать этой проблемы.

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

Предположим, что есть два трейдера фондового рынка, проявляющих особый интерес к акциям Мордора (Символ: M$). Оба смотрят один и тот же новостной канал, но один использует пул соединений (тот, что справа), а другой – нет:

Как вы можете видеть, трейдер с пулом подключений оставляет телефон отключенным, а брокер ждет новых заказов.

Когда совершенно неожиданно однометровому гуманоиду удается проехать 2900 км через несколько зон военных действий и негостеприимных районов и доставить единственную существующую ядерную бомбу в единственное существующее слабое место Саурона, трейдер может очень быстро продать все свои акции Мордора, в то время как трейдер без пула соединений обречен.

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

Это особенно верно, если вы используете HTTPS. Смотрите рукопожатие TLS .

Время ожидания пула соединений и TTL

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

Примечание: для очень хорошего разговора о том, как определить размер вашего пула соединений видеть “Прекратите Ограничивать Скорость! Управление Производительностью Сделано Правильно” около Джон Мур .

Сценарий 1. Бесплатные подключения.

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

Так что есть какой-то доступный телефон, но он на крючке. Вам придется столкнуться с дополнительной задержкой настройки подключения.

Сценарий 2. Соединение объединено в пул.

Второй сценарий:

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

  1. Соединение новое, создано меньше, чем настроенный TTL. Вам не нужно будет страдать от дополнительной задержки установки соединения.
  2. Соединение устарело, создано больше, чем настроенный TTL. Вам придется столкнуться с дополнительной задержкой настройки подключения.

Давайте проверим это:

;; Create a new connection pool, with a TTL of one second:
(def cp (conn-manager/make-reusable-conn-manager
        {:timeout 1 ; in seconds. This is called TimeToLive in PoolingHttpClientConnectionManager
         }))
;; Make ten calls, two per second:
(dotimes [_ 10]
  (log/info "Send Http request")
  (client/get "http://local.nginx/" {:connection-manager cp})
  (Thread/sleep 500))

Глядя на журналы:

16:56:24.905 INFO  - Send Http request
16:56:24.914 DEBUG - Connection established 172.24.0.4:51984<->172.24.0.2:80
16:56:25.416 INFO  - Send Http request
16:56:25.926 INFO  - Send Http request
16:56:25.933 DEBUG - Connection established 172.24.0.4:51986<->172.24.0.2:80
16:56:26.434 INFO  - Send Http request
16:56:26.942 INFO  - Send Http request
16:56:26.950 DEBUG - Connection established 172.24.0.4:51988<->172.24.0.2:80
16:56:27.452 INFO  - Send Http request
16:56:27.960 INFO  - Send Http request
16:56:27.967 DEBUG - Connection established 172.24.0.4:51990<->172.24.0.2:80
16:56:28.468 INFO  - Send Http request

Как и ожидалось, мы можем сделать два запроса, прежде чем воссоздавать соединение.

Тот же сценарий, но с 20-секундным TTL:

16:59:19.562 INFO  - Send Http request
16:59:19.570 DEBUG - Connection established 172.24.0.4:51998<->172.24.0.2:80
16:59:20.073 INFO  - Send Http request
16:59:20.580 INFO  - Send Http request
16:59:21.086 INFO  - Send Http request
16:59:21.593 INFO  - Send Http request
16:59:22.100 INFO  - Send Http request
16:59:22.607 INFO  - Send Http request
16:59:23.114 INFO  - Send Http request
16:59:23.623 INFO  - Send Http request
16:59:24.134 INFO  - Send Http request

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

Но зачем нам нужен TTL? В основном потому, что брандмауэры имеют тенденцию отбрасывать длительные соединения (особенно бездействующие), не сообщая ни одной из задействованных частей, что заставляет клиента потратить некоторое время, чтобы понять, что соединение больше не используется.

Сценарий 3. Все используемые соединения.

Последний сценарий:

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

Примечание. Если телефон становится доступным до истечения времени ожидания пула соединений, вы возвращаетесь ко второму сценарию. При некотором неудачном выборе времени вам также потребуется установить новое свежее соединение.

Давайте рассмотрим пример. Сначала мы делаем Nginx очень медленным, на ответ уходит до 20 секунд .

Затем мы создаем пул соединений максимум с тремя соединениями и отправляем четыре HTTP-запроса:

  (def cp-3 (conn-manager/make-reusable-conn-manager
              {:timeout 100
               :threads 3           ;; Max connections in the pool.
               :default-per-route 3 ;; Max connections per route (~ max connection to a server)
               }))

  (dotimes [_ 4]
    (future
      (time
        (client/get "http://local.toxiproxy:22220/" {:connection-manager cp-3}))))

"Elapsed time: 20017.1325 msecs"
"Elapsed time: 20016.9246 msecs"
"Elapsed time: 20020.9474 msecs"
"Elapsed time: 40024.5604 msecs"

Как вы можете видеть, последний запрос занимает 40 секунд, 20 из которых тратятся на ожидание подключения.

Добавление времени ожидания пула соединений в одну секунду:

(dotimes [_ 4]
  (future
    (time
      (try
        (client/get "http://local.toxiproxy:22220/"
          {:connection-manager cp-3
           :connection-request-timeout 1000 ;; Connection pool timeout in millis
           })
        (catch Exception e
          (log/info (.getClass e) ":" (.getMessage e)))))))

"Elapsed time: 1012.2696 msecs"
"2019-12-08 08:59:04.073 INFO  - org.apache.http.conn.ConnectionPoolTimeoutException : Timeout waiting for connection from pool"
"Elapsed time: 20014.1366 msecs"
"Elapsed time: 20015.3828 msecs"
"Elapsed time: 20015.962 msecs"

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

К сожалению, даже если время ожидания соединения, время ожидания чтения, время ожидания пула соединений и TTL пула соединений являются наиболее распространенными вещами для настройки, вы также должны знать:

Собрав все тайм-ауты вместе, мы имеем:

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

Если вы хотите настроить простой тайм-аут для всего запроса, вам следует использовать Изоляция потоков Hystrix , HTTP-клиент Apache FutureRequestExecutionService (сам никогда этим не пользовался) или, возможно, используйте другой HTTP-клиент.

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

Некоторые заметные различия между обоими HTTP-клиентами:

  1. Асинхронные HTTP-клиенты имеют свой собственный пул потоков для обработки ответа после его поступления.
  2. Нет тайм-аута пула соединений: если пул полностью используется, возникает исключение. Нет необходимости ждать, пока соединение будет доступно. Заинтересованный, я обычно настраиваю свои пулы HTTP-соединений Apache так, чтобы они вели себя одинаково, так как полный пул соединений обычно означает, что что-то не работает, и лучше выйти из игры пораньше.
  3. Время простоя пула соединений : как мы упоминали ранее, нам нужен был TTL пула соединений в основном из-за простоя соединений. Асинхронный HTTP-клиент поставляется с явным временем ожидания простоя, превышающим время ожидания TTL.
  4. Новый тайм-аут запроса: тайм-аут для ограничения времени, необходимого для поиска DNS, подключения и чтения всего ответа. Один единственный тайм-аут, который указывает, как долго вы готовы ждать завершения всего HTTP-разговора. Сладкий.

Таким образом, тайм-ауты для асинхронного HTTP-клиента выглядят следующим образом:

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

Рассуждать о наихудшем случае намного проще.

Таким образом, тайм-ауты раздражающе сложно настроить, если вы хотите иметь некоторый контроль над максимальным временем, выделяемым для HTTP-запроса/ответа. Если только вы не используете асинхронный HTTP-клиент (или, возможно, другие асинхронные клиенты).

Я предлагаю вам не использовать HTTP-клиент Apache?

Ну, это зависит от того, какую функциональность вы используете. HTTP-клиент Apache – это очень зрелый проект с множеством встроенных функций и возможностей для его настройки. Он даже имеет асинхронный модуль , а более новая версия 5.0 (бета-версия) поставляется с встроенной функцией асинхронности .

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

Оригинал: “https://dev.to/danlebrero/apache-http-client-and-asynchronous-http-client-timeouts-explained-in-pictures-15jo”