У меня есть приложение 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 на том же физическом компьютере, по-прежнему испытывает ту же задержку при извлечении данных.
@ M.Deinum M.Deinum gzip для 10 МБ данных занимает около 0,06 секунды на моем локальном компьютере, так что накладные расходы вряд ли объясняют наблюдаемое поведение.
gzip в командной строке, что не означает, что в Java требуется столько времени. Однако, не видя, как вы работаете с GZIP или если вы используете значения по умолчанию для tomcat, невозможно помочь. Я подозреваю, что накладные расходы возникают как из-за GZIP, так и из-за того, что все делается в памяти, а также из-за накладных расходов GC и создания массива.
@M.Deinum M.Deinum Сжатие Gzip выполняется полностью за пределами кода нашего приложения, это все Spring Boot и Tomcat. У нас нет контроля над этим. Смотрите мое 1-е редактирование вопроса.
Итак, вы используете внешний кот, а не встроенный. Я понятия не имею, какова производительность GzipOutputFiler в Tomcat. Как выглядит ваш контроллер, как вы генерируете JSON?
@ M.Deinum контроллер просто возвращает объект Map<String, List<PriorityGraph>>, а сериализация JSON полностью выполняется Spring Boot. Он использует класс ObjectMapper из фреймворка Jackson2. Это также вне нашего контроля.
Насколько я понял, проблема заключается в том, что вы отправляете некоторую полезную нагрузку на сервер и делаете снимок того, сколько времени потребовалось для завершения HTTP-запроса. Что на самом деле сервер делает с данными? Вы «пробовали» (профилировали) ЦП сервер, чтобы проверить, что он там сделал? Таким образом, вы должны увидеть, где сервер потратил больше всего циклов ЦП (= затраченное время). Может быть, узким местом является обработка фактической несжатой полезной нагрузки, а не сжатие GZIP?! Не показывая нам код или то, как вы настроили вещи, мы можем только догадываться
Как упоминалось в предыдущем комментарии, вы можете проверить, что занимает все это время. Таким образом, измерять не только все HTTP-запросы, но и различные компоненты. Возможно, это ваш запрос или сопоставление, которое занимает большую часть времени. Поэтому измеряйте различные движущиеся части, а не фокусируйтесь только на части GZIP.
@Roman Vottner Я отправляю запрос в приложение, содержащее список идентификаторов, для которых приложение делает запрос к СУБД и возвращает результат. В запросе много JOIN, поэтому результат может стать довольно большим. Сначала мы подозревали, что запрос к базе данных является узким местом в производительности, но это не так. Профилирование приложения показало, что запрос, для выполнения которого в реальном времени потребовалось 42 секунды, потреблял всего 12 секунд процессорного времени на сервере (99% из которых — сериализация JSON), поэтому остальное кажется вводом-выводом, но я не могу подумайте, что это может быть.
Разница в 30 секунд между потребляемым процессорным временем на сервере и ответом, поступающим на ваш клиент, больше похожа на тайм-аут или что-то в этом роде. Но опять же, это только предположение, потому что у нас нет информации о вашей настройке и логике приложения.

Когда вы используете gzip, необходимо учитывать компромисс между использованием ЦП и пропускной способностью. То, что является преимуществом для конечного пользователя (меньшее время передачи, т. е. более высокая скорость загрузки), потребует дополнительных циклов ЦП на стороне сервера.
Прирост производительности, безусловно, будет заметен, если вы запустите нагрузочный тест. Кроме того, если ваш API используется внутри другими API в вашем приложении, это резко сократит использование сети. Использование процессора может быть не таким значительным.
Спасибо за ответ. Я сделал нагрузочный тест, JVisualVM говорит, что обработка всего запроса занимает 12 секунд в худшем случае. Однако общее время отклика с точки зрения клиента составляет в худшем случае 110 секунд, что означает, что еще 58 секунд остаются необъяснимыми.
Если вы планируете отправлять по сети сжатые данные, то сжимать их можно не при чтении из БД, а при записи в БД. Таким образом, время сжатия не будет частью запроса, и вы сэкономите место для хранения. Затем отключите сжатие HTTP и прочитайте ваши сжатые двоичные данные из БД и отправьте их
Извините, это невозможно. Это реляционная база данных, и данные должны быть сохранены в структурированной форме, чтобы можно было использовать SELECT с условиями.
Затем вы можете добавить поле BLOB в свою таблицу, называемую сжатым содержимым, и снова записать в него сжатое содержимое на этапе записи, а когда вам нужно прочитать его для передачи HTTP, просто прочитайте и отправьте это поле.
Все равно не осуществимо. Данные являются результатом сложного SELECT. Вы не можете выбирать или объединять данные, которые находятся внутри поля BLOB.
Мы разобрались: оказалось, что HTTP-обмен тут ни при чем.
На самом деле узким местом была база данных, но мы сначала этого не заметили, потому что запрос JPA возвращается почти сразу.
Чего мы не видели, так это того, что многие свойства в извлеченных объектах загружаются «лениво», поэтому запросы к БД для них выполняются только тогда, когда сериализатор JSON обращается к этим свойствам. Эти запросы не использовали процессорное время на машине Tomcat, поэтому мы не смогли определить потерю времени с помощью профилирования.
То, что вы предполагаете, что сжатие бесплатно, это не так. Сжатие вывода также требует времени. Если вы записываете в память
byte[], напримерByteArrayOutputStream, и заархивируете, это может занять больше времени из-за изменения размера, памяти и т. д.