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