PlayFramework: плохая производительность десериализации json

Инфраструктура и преамбула

У меня есть приложение PlayFramework (2.3.8), размещенное на экземпляре AWS EC2. У меня есть массив сложных объектов, которые должны быть возвращены в виде строки JSON через веб-API. Мне нужна глубокая копия массива со всеми дочерними объектами, полностью загруженными до самого последнего слоя. Массив имеет размер 30-100 записей, каждая запись имеет около 1-10 записей, каждая запись из которых имеет до 100 свойств, в конце концов, нет никаких BLOB или подобных элементов, все сводится к строкам/двойникам/ целые/логические значения. Я не уверен, насколько важна точная структура данных. Пожалуйста, дайте мне знать, если вам нужна дополнительная информация. Размер результирующего файла .json составляет около 1 МБ.

Производительность десериализации этого массива ужасна, для ~ 1 МБ на моей локальной машине это занимает 3-5 минут; на EC2 это занимает около 20-30 секунд.

Первоначальная проблема: низкая производительность при использовании play.libs json

Мой массив объектов загружается и сохраняется как JsonNode. Затем этот JsonNode перенаправляется в ObjectMapper, который, наконец, записывает его в prettyPrinted:

List<myObject> myObjects = myObjectService.getInstance().getAllObjects(); // simplified example

JsonNode myJsonNode = Json.toJson(myObjects); // this line of code takes a huge amount of time!

ObjectMapper om = new ObjectMapper();
return om.writerWithDefaultPrettyPrinter().writeValueAsString(myJsonNode); // this runs in <10 ms

Итак, я определил виновника десериализации Json.toJson. Насколько мне удалось выяснить, это своего рода обернутая библиотека Джексона, которая используется PlayFramework.

Хотя я читал о некоторых проблемах с производительностью при десериализации JSON, я не уверен, следует ли нам говорить о каких-то сотнях миллисекунд или секундах, а не о минутах. Во всяком случае, я попытался внедрить некоторые другие библиотеки JSON (GSON, argonaut, flexjson), что на самом деле не прошло гладко.

ГСОН

Я «просто» попытался заменить библиотеку play-json на библиотеку GSON, как и в другой небольшой части проекта. Там он работал нормально, но хотя у меня НЕТ циклических ссылок, он выдает StackOverflowErrors прямо мне в лицо, даже если я пытаюсь десериализовать крошечный объект, созданный вручную. Как на моей машине разработки, так и на экземпляре EC2.

FlexJson

List<myObject> myObjects = myObjectService.getInstance().getAllObjects(); // simplified example

JSONSerializer serializer = new JSONSerializer().prettyPrint(true);

return serializer.deepSerialize(myObjects); // returns a prettyPrinted String

До сих пор работало нормально, это занимает всего около 20% времени по сравнению с методом Json.toJson выше. Однако это может быть потому, что он НЕ ДЕЙСТВИТЕЛЬНО глубоко копирует объекты. Он глубоко копирует его на первом слое, однако, поскольку моя модель имеет более сложные свойства (с дочерними элементами, внуками и внуками...), и их довольно много, я не уверен, как действовать здесь.

Вот пример вывода одного из моих вложенных объектов (это одно из свойств «верхнего» объекта):

 "class": "com.avaje.ebean.common.BeanList",
                "empty": false,
                "filterMany": null,
                "finishedFetch": true,
                "loaderIndex": 0,
                "modifyAdditions": null,
                "modifyListenMode": "NONE",
                "modifyRemovals": null,
                "populated": true,
                "propertyName": "elements",
                "readOnly": false,
                "reference": false

Есть ли у вас какие-либо другие предложения по решению или намеки на то, что может быть сломано? Я также думал о том, что, возможно, сущности загружаются ПОЛНОСТЬЮ только после того, как я вызову .toJson()? Тем не менее, это не должно занимать столько времени.

Заранее спасибо!


person konrad_pe    schedule 09.11.2016    source источник
comment
Возможно, что объекты ПОЛНОСТЬЮ загружаются только после того, как я вызову .toJson(). Как я вижу, вы используете Ebean. Попробуйте активировать ведение журнала для операторов sql, чтобы узнать, граф объекта загружается до или во время вызова toJson.   -  person marcospereira    schedule 09.11.2016
comment
Также вы можете попробовать использовать что-то вроде VisualVM или YourKit, чтобы узнать, что делает его медленным, или сохранить сгенерированный файл json. загрузите его в память (анализируя его), а затем визуализируя его с помощью toJson (если этот процесс быстрее, чем предыдущий, то явно что-то другое, чем анализ/генерация json, делает его медленнее)   -  person Salem    schedule 10.11.2016
comment
Спасибо за подсказку с логированием sql, хорошая идея. Оказывается, сущности и их дочерние сущности загружаются только во время Json.toJson(). Я попытался изменить fetch = FetchType.Eager для этих дочерних объектов, но это не имело никакого значения. И в принципе я не очень понимаю, какая разница, в какой момент загружаются сущности, так почему бы не во время вывода для .toJson(). Но похоже, что узкое место связано с объектами/БД, а не с сериализацией JSON..?   -  person konrad_pe    schedule 10.11.2016


Ответы (1)


TLDR: эта проблема не имеет ничего общего с производительностью десериализации PlayFrameworks JSON, а не с некоторыми проблемами eBean/базы данных. Включение ведения журнала SQL в application.conf указало мне на это.


Дополнительные замечания и мысли: Благодаря намеку на marcospereira в комментариях, я решил, что проблема связана с получением в play/ebeans, а не с производительностью JSON.

Очевидно, что мои объекты сначала загружаются лениво (/flat), включив ведение журнала SQL, я мог видеть, что правильные подготовленные SELECT запускаются только после того, как мой код попадает в .toJson(). Так много дочерних объектов извлекаются из базы данных только при вызове .toJson(), что приводит к паре сотен SELECT и, следовательно, к завершению довольно долго.

Немного поэкспериментировав с масштабами экземпляров RDS, я обнаружил очень странные результаты. Это НА САМОМ ДЕЛЕ не связано с первоначально заданным вопросом, но я хочу поделиться своими выводами, возможно, это может кому-то помочь. Читайте об этом в разделе ниже.

Эксперименты с масштабированием RDS...

В моей среде разработки (t1.micro) я подключил скопированный экземпляр моей рабочей БД к небольшому экземпляру RDS (db.t2.micro), чтобы посмотреть, не изменится ли что-нибудь.

Моей среде prod (t2.large) + prod RDS (db.t2.large) потребовалось около 19,5 с, чтобы завершить вызов API. НОВАЯ среда разработки (t1.micro + db.t2.micro), которая слабее как в вычислениях, так и в db, заняла всего около 10,5 с, что крайне неубедительно, поскольку в основном оба экземпляра запускали один и тот же код, только указывая на другой сервер БД (с идентичным содержимым БД). Я переключил базу данных разработчиков на db.m4.large, чтобы посмотреть, принесет ли это какие-либо улучшения, и время загрузки сократилось примерно до 5,5 с.

Я совершенно озадачен, почему более быстрому экземпляру prod EC2 требуется больше времени для точно такого же вызова API, чем экземпляру dev. В конце концов я изменил класс prod db с db.t2.large на db.m4.large, и теперь время отклика составляет 4,0 с.

Такое ощущение, что "старый" инстанс prod DB был какой-то изношенный/забитый (есть ли такое? Я как-то в этом сомневаюсь...). Даже меньший экземпляр dev + dev db ответил намного быстрее. Несмотря на то, что различные масштабы RDS принесли некоторое улучшение, я сомневаюсь, что разница между db.t2.large -> db.m4.large приведет к изменению этой величины.

Может быть, если у кого-то есть идеи, что происходит, я был бы очень рад обсудить это.

person konrad_pe    schedule 10.11.2016