Http запросы — мы все это делаем неправильно

в 7:59, , рубрики: curl, http, keep-alive, php, python, requests, stripe, Сетевые технологии, метки: , , , , , , ,

В проекте, над которым я работаю, мы используем огромное количество сторонних библиотек. Многие из них — адаптеры для различных сервисов. Что их объединяет, это то, что они работают с сетью. Json поверх http, soap поверх http, какие-то свои протоколы поверх http. Т.е. все так или иначе используют http. И как ни удивительно, мало кто из них пользуется преимуществами его последней версии. Я не поленился заглянуть в википедию, прошло ровно 14 лет как была принята спецификация http 1.1. И потому я решил обратиться с призывом:
image

Да, речь пойдет о keep alive. Суть в том, что, начиная с http 1.1, клиент и сервер могут договориться не закрывать установленное tcp-соединение после завершения запроса, а переиспользовать его для следующих запросов. Это нужно потому, что на установку соединения требуется время. Иногда это время больше, чем время самого запроса. И если все серверы уже давным-давно такую возможность поддерживают, а все браузеры и большинство других клиентов её используют, то у разработчиков различных библиотек для популярных языков программирования здесь почему-то пробел.
Рассмотрим простой код на PHP, который последовательно делает 10 запросов к одному серверу:

<?php
for ($i = 0; $i < 10; $i += 1) {
	$ch = curl_init();
	curl_setopt_array($ch, array(
		CURLOPT_URL => "https://evernote.com/favicon.ico",
		CURLOPT_VERBOSE => True,
		CURLOPT_RETURNTRANSFER => True,
	));
	$resp = curl_exec($ch);
	curl_close($ch);
}

Это каркас 95% библиотек, обращающихся к сторонним ресурсам. Опция CURLOPT_VERBOSE позволяет видеть в консоли все, что делает библиотека curl во время выполнения скрипта. И самые интересные строчки будут повторяться все 9 запросов (кроме первого):

* Connection #0 to host evernote.com left intact
* Closing connection #0
* About to connect() to evernote.com port 443 (#0)
*   Trying 204.154.94.73...

Как видите, curl оставляет соединение после запроса открытым, но мы его тут же закрываем. Результат печален: 10 запросов создают 10 соединений, скрипт выполняется не менее 17 секунд.

Это говорит о том, что сам curl знаком с http 1.1, а мы мешаем ему нормально работать. Но исправить это очень просто:

<?php
$ch = curl_init();
for ($i = 0; $i < 10; $i += 1) {
	curl_setopt_array($ch, array(
		CURLOPT_URL => "https://evernote.com/favicon.ico",
		CURLOPT_VERBOSE => True,
		CURLOPT_RETURNTRANSFER => True,
	));
	$resp = curl_exec($ch);
}
curl_close($ch);

Мы просто вынесли создание и удаление дескриптора из цикла, и картина при следующем запуске поменялась:

* Connection #0 to host evernote.com left intact
* Re-using existing connection! (#0) with host (nil)
* Connected to (nil) (204.154.94.73) port 443 (#0)

А время работы сократилось до 5,5 секунд. Конечно, тут я намеренно обращаюсь к статическому файлу. В реальных условиях некоторое время займет форматирование запроса. Плюс, если вы используете http без ssl, время соединения будет немного меньше. Тем не менее, постоянные соединения в любом случае дают существенный выигрыш.

Я провел несколько экспериментов, измеряя время, необходимое на 10 запросов по протоколам http и https с использованием отдельных соединений и keep-alive для файлов разного размера с разными пингами до сервера. Брался лучший результат за 5-6 измерений.

evernote.com/favicon.ico, Пинг ≈ 200 ms, размер 27054 байт.

Reconnect Keep-Alive Ratio
http 10 5 2x
https 17 5,5 3,1x

twitter.com/favicon.ico, Пинг ≈ 200 ms, размер 1150 байт.

Reconnect Keep-Alive Ratio
http 4,3 2,5 1,7x
https 8,5 2,7 3,1x

yandex.st/lego/_/pDu9OWAQKB0s2J9IojKpiS_Eho.ico, Пинг ≈ 17 ms, размер 1150 байт.

Reconnect Keep-Alive Ratio
http 0,33 0,17 1,9x
https 0,8 0,2 4x

Цифры говорят сами за себя. Но прелесть даже не в них, а в том, что добиться этого очень просто. Все что вам нужно сделать в случае использования curl — перенести вызов curl_init() из метода, который делает запрос, в конструктор класса (вот один и другой пример, где это легко можно сделать). При этом curl_close() можно выкинуть совсем, ресурсы и так освободятся при завершении запроса. Curl сам держит пул соединений для каждой пары хост и порт, к которым вы обращаетесь, и переоткрывает закрытые соединения.

На самом деле речь, конечно, не про curl и php. В каждом языке можно найти библиотеку, реализующую такой же пул. Например, для python это прекрасная urllib3 — на основе которой построена популярная библиотека requests. С ней дела обстоят точно так же, как с curl в php, её очень просто использовать, но не все это делают правильно. И я бы хотел показать несколько примеров, как это можно исправить. Вот так я сделал поддержку постоянных соединений в клиенте stripe. После этого функциональные тесты в нашем проекте стали выполняться в 2 раза быстрее, хотя они не только со страйпом обращались, конечно. А так я пофискил нашу библиотеку pyuploadcare. В обоих случаях все что нужно было сделать — заменить вызовы функций requests.request() на вызовы методов объекта session, созданного заранее.

Надеюсь, мне удалось убедить вас в необходимости обращать на это внимание при разработке библиотек, и показать, насколько это просто реализуется.

Автор: homm

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js