Сжатие HTTP gzip не экономит время

У меня есть приложение Spring Boot, которое иногда должно обслуживать очень большую полезную нагрузку JSON (несколько МБ) через REST API, загрузка которой занимает значительное время.

Данные считываются из БД, сериализуются в JSON и отправляются обратно клиенту. Операция чтения БД выполняется быстро даже для больших наборов данных, обычно менее 1 секунды. Итак, я пришел к выводу, что самая трудоемкая часть — это HTTP-обмен.

Я включил сжатие GZIP для HTTP-обмена, поэтому полезная нагрузка должна быть сжата перед отправкой. Кажется, это работает (возвращаемая полезная нагрузка действительно сжата), однако заметного прироста производительности нет.

Запрос curl к конечной точке приложения без сжатия занимает 49 секунд и дает полезную нагрузку JSON ~ 10 МБ:

curl -H "Content-Type: application/json" -H "Accept: application/json" -H "Authorization: Basic <REDACTED>" --data-binary @priorities-request.json  'https://<REDACTED>/api/rest/priorities' > priorities-response.json                
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10.0M    0  9.9M  100 85081   205k   1715  0:00:49  0:00:49 --:--:--  239k

С включенным сжатием GZIP тот же запрос занимает 42 секунды и дает полезный груз JSON, сжатый GZip примерно на 260 КБ:

curl -H "Content-Type: application/json" -H "Accept: application/json" -H "Accept-Encoding: gzip,deflate,br" -H "Authorization: Basic <REDACTED>" --data-binary @priorities-request.json  'https://<REDACTED>/api/rest/priorities' > priorities-response.json 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  259k    0  176k  100 85081   4221   1991  0:00:42  0:00:42 --:--:-- 14408

Я ожидаю, что загрузка сжатой полезной нагрузки 260 КБ займет значительно меньше времени, чем несжатая загрузка 10 МБ.

В чем моя ошибка?

Редактировать: потому что в комментариях меня спросили, как я настроил сжатие GZIP: я установил compression = "on" и compressableMimeType = "application/json" в server.xml Tomcat. Вот и все. Остальное делает класс Tomcat org.apache.coyote.http11.filters.GzipOutputFilter.

Редактировать 2: Чтобы исключить сериализацию данных в JSON, где теряется время, я протестировал локально с помощью Jackson2JsonMessageConverter, но потребовалось всего около 0,5 секунды, чтобы записать даже огромную структуру данных в строку JSON размером 10 МБ.

Изменить 3. Что меня больше всего озадачивает, так это то, что клиентское приложение, использующее API, которое работает на другом экземпляре Tomcat на том же физическом компьютере, по-прежнему испытывает ту же задержку при извлечении данных.

То, что вы предполагаете, что сжатие бесплатно, это не так. Сжатие вывода также требует времени. Если вы записываете в память byte[], например ByteArrayOutputStream, и заархивируете, это может занять больше времени из-за изменения размера, памяти и т. д.

M. Deinum 11.10.2022 16:19

@ M.Deinum M.Deinum gzip для 10 МБ данных занимает около 0,06 секунды на моем локальном компьютере, так что накладные расходы вряд ли объясняют наблюдаемое поведение.

ronin667 11.10.2022 17:12

gzip в командной строке, что не означает, что в Java требуется столько времени. Однако, не видя, как вы работаете с GZIP или если вы используете значения по умолчанию для tomcat, невозможно помочь. Я подозреваю, что накладные расходы возникают как из-за GZIP, так и из-за того, что все делается в памяти, а также из-за накладных расходов GC и создания массива.

M. Deinum 11.10.2022 20:02

@M.Deinum M.Deinum Сжатие Gzip выполняется полностью за пределами кода нашего приложения, это все Spring Boot и Tomcat. У нас нет контроля над этим. Смотрите мое 1-е редактирование вопроса.

ronin667 12.10.2022 11:14

Итак, вы используете внешний кот, а не встроенный. Я понятия не имею, какова производительность GzipOutputFiler в Tomcat. Как выглядит ваш контроллер, как вы генерируете JSON?

M. Deinum 12.10.2022 11:59

@ M.Deinum контроллер просто возвращает объект Map<String, List<PriorityGraph>>, а сериализация JSON полностью выполняется Spring Boot. Он использует класс ObjectMapper из фреймворка Jackson2. Это также вне нашего контроля.

ronin667 12.10.2022 12:19

Насколько я понял, проблема заключается в том, что вы отправляете некоторую полезную нагрузку на сервер и делаете снимок того, сколько времени потребовалось для завершения HTTP-запроса. Что на самом деле сервер делает с данными? Вы «пробовали» (профилировали) ЦП сервер, чтобы проверить, что он там сделал? Таким образом, вы должны увидеть, где сервер потратил больше всего циклов ЦП (= затраченное время). Может быть, узким местом является обработка фактической несжатой полезной нагрузки, а не сжатие GZIP?! Не показывая нам код или то, как вы настроили вещи, мы можем только догадываться

Roman Vottner 12.10.2022 12:23

Как упоминалось в предыдущем комментарии, вы можете проверить, что занимает все это время. Таким образом, измерять не только все HTTP-запросы, но и различные компоненты. Возможно, это ваш запрос или сопоставление, которое занимает большую часть времени. Поэтому измеряйте различные движущиеся части, а не фокусируйтесь только на части GZIP.

M. Deinum 12.10.2022 12:59

@Roman Vottner Я отправляю запрос в приложение, содержащее список идентификаторов, для которых приложение делает запрос к СУБД и возвращает результат. В запросе много JOIN, поэтому результат может стать довольно большим. Сначала мы подозревали, что запрос к базе данных является узким местом в производительности, но это не так. Профилирование приложения показало, что запрос, для выполнения которого в реальном времени потребовалось 42 секунды, потреблял всего 12 секунд процессорного времени на сервере (99% из которых — сериализация JSON), поэтому остальное кажется вводом-выводом, но я не могу подумайте, что это может быть.

ronin667 12.10.2022 13:01

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

Roman Vottner 12.10.2022 13:14
Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
0
10
248
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

Спасибо за ответ. Я сделал нагрузочный тест, JVisualVM говорит, что обработка всего запроса занимает 12 секунд в худшем случае. Однако общее время отклика с точки зрения клиента составляет в худшем случае 110 секунд, что означает, что еще 58 секунд остаются необъяснимыми.

ronin667 12.10.2022 11:10

Если вы планируете отправлять по сети сжатые данные, то сжимать их можно не при чтении из БД, а при записи в БД. Таким образом, время сжатия не будет частью запроса, и вы сэкономите место для хранения. Затем отключите сжатие HTTP и прочитайте ваши сжатые двоичные данные из БД и отправьте их

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

ronin667 12.10.2022 11:37

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

Michael Gantman 12.10.2022 12:12

Все равно не осуществимо. Данные являются результатом сложного SELECT. Вы не можете выбирать или объединять данные, которые находятся внутри поля BLOB.

ronin667 12.10.2022 12:25
Ответ принят как подходящий

Мы разобрались: оказалось, что HTTP-обмен тут ни при чем.

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

Чего мы не видели, так это того, что многие свойства в извлеченных объектах загружаются «лениво», поэтому запросы к БД для них выполняются только тогда, когда сериализатор JSON обращается к этим свойствам. Эти запросы не использовали процессорное время на машине Tomcat, поэтому мы не смогли определить потерю времени с помощью профилирования.

Другие вопросы по теме