Пути назад нет
Пути назад нет

Спойлер: здесь мы будем программно вытаскивать данные из JVM дампа кучи.

Контекст и предыстория

Смоделируем ситуацию: у вас есть приложение на JVM (без разница, будь то Kotlin, Java или Scala), а еще у вас есть уверенность в себе и немного не хватает ответственности.

В пачке с приложением, а именно сервером, идет несколько стандартных штук - база данных, небольшой http слой, в общем, все стандартно.

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

Мне кажется, любой уважающий себя программист на +- хобби проекте захочет навернуть нереально крутое самописное решение, чем, собственно говоря, я и занялся.

Модель данных

У нас есть несколько типов сущностей, примерно по 20 штук на каждого игрока, которые хранятся в базе данных и активно меняются в рантайме. Соответственно, нужно эти данные еще и сохранять в базу данных, а то после перезапуска сервера они все пропадут.

Сохранять в базу данных после каждого изменения - отвратительная затея при таких часто меняющихся данных, поверьте, в ногу стрелял, состояния синхронизировал.

Было принято решение написать своего рода read - write модель, которая отлично себе живет в рантайме, ее быстро запрашивать из главного игрового потока и менять, соответственно, тоже. А в отдельном потоке периодически сохранять все это дело в базу данных, чтобы игроки не чувствовали лагов.

На словах все отлично, да и на деле все тоже было отлично, пока на сервере одновременно не оказалось примерно 20 игроков.

Казалось бы, 20 параллельных юзеров - фигня, а не нагрузка. Так и есть, но главный фактор мы не учли, главный разработчик - наивный самоуверенный лох. Короче говоря, в отдельном потоке для сохранений в базу не было проверок на исключения, и наш `while true delay persist` цикл успешно сдох вместе с потоком.

И где-то тут у разработчика (в данном случае меня) перед глазами возникает именно вот тот мерзкий кусок кода, который он сам написал, сразу всплывают все подводные камни - короче говоря, все проясняется, ты бьешь себя по лбу, сетуешь как ты раньше это не учел...
А потом ты по логам понимаешь, что последнее сохранение в базу было 2 суток назад. И вот тут ты понимаешь, что ты вдвойне наивный болван, и вручную вызвать сохранение у тебя нельзя, и ты начинаешь чесать репу, как бы из рантайма вытащить все модели и сохранить их в базу.

Хьюстон, у нас проблемы

Конец лирической части. Где данные, Лебовски?

Очевидно, в JVM приложении - в куче. В целом, появляется очевидный план - сделать дамп, взять из него все нужные сущности, запихнуть их в базу данных.

Ну и не забыть исправить сохранение в коде, конечно же.

Однако "взять данные из дампа" - не самая тривиальная задача. Большинство утилит, а именно

  • JVisualVM

  • Eclipse Memory Analyser

  • YourKit

  • А так же встроенные в разные IDE профайлеры и тому подобные штуки

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

Далее. Имеем дело с java 17, то есть большинство тул(например, jhat и jmap) могут иметь неполный функционал. Например, мне не удалось запустить jhat сервер для запуска OQL(Object Query Language) для дампа с 17 жавы, как бы я не старался.

Object Query Language - аля sql для объектно ориентированных структур данных, его можно использовать для индексированного дампа.

Да, я потратил 3 часа, пытаясь пропатчить jhat под 17 жаву. Не пытайтесь, затея так себе.

После некоторых потуг, кстати, начинаешь замечать, что дамп довольно велик и могуч - в зависимости от масштабов приложения, он может быть от гигабайта до очень большого количества гигабайтов. А ведь тужимся мы не на выделенном сервере / виртуалке, мы тужимся на своей локальной машине, соответственно нам нужно этот дамп скачать.

Пользователи Intellij Idea: не пытайтесь скачать дамп больше 4 гигабайта "перетаскиванием" с удаленного хоста в локальную папку (Remote host window). Идея сдохнет и продолжит что-то качать даже после полного скачивания, насколько много она будет скачивать - не знаю. Пока я ждал, она скачала что-то в 5 раз превышающее размером дамп. Используйте scp или что-нибудь понадежнее.

Итак, просуммируем наши наработки:

  • Через интерфейс вышеперечисленных утилит вытащить данные не получится (у меня по крайней мере не получилось)

  • jhat сервер для OQL - стоит попытаться, вижу в нем перспективы, если удастся запустить для вашего дампа

  • Надеюсь, тебе еще не пришло в голову писать парсер дампа?

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

Начал искать готовые решения. В идеале - найти библиотеку / скомпилировать среду разработки / скомпилировать утилиту вроде JVisualVM для того, чтобы пихнуть ее себе в classpath и заиспользовать все, что мне нужно для работы с дампом.

Очевидно, компилировать целый Eclipse или NetBeans - вообще не хочется.

Заварил чай, начал листать гугл, наткнулся на статью

https://cuprak.info/2018/11/12/exploring-java-heap-dumps/

И это был свет небесный на мою грешную голову - там, по сути, была инструкция и готовый проект, в котором есть небольшие примеры, как загрузить дамп и получить из него инстансы. 8 чудо света, короче говоря.

В целом, дальше все было довольно таки просто - склонить проект, перенести на котлин (стараюсь избегать Java всеми силами, уж очень тяжко на ней писать после котлина) и дописать под свои нужды.

Алгоритм очень простой:

  1. Распарсить все необходимые сущности из дампа в нормальные объекты, с которыми можно работать

  2. Сериализовать их в нормальный формат, из которого уже скриптами или чем-нибудь можно сгенерить sql апдейт или что душе угодно

А теперь, наконец то, код.

// Загружаем дамп в память(второй аргумент - индекс сегмента, по дефолту 0, 
// что делает - не знаю, спасибо, что еще не пришлось узнавать
val heap: Heap = HeapFactory.createHeap(File(path), 0)
println("Loaded heap dump")
// Получаем объект для работы с инстансами класса по полному пути класса
val strClass: JavaClass = heap.getClassFor("ru.kingdomrp.krp.persistence.model.SkillPathModel")
println("We got class, now will compute instances")
// А теперь долгий этап. Если большой дамп - заваривайте кофе, 
// гуляйте с собакой, ложитесь спать. Для 7 гигабайт дампа на макбуке 20 года - 15 минут минимум.
val instances: MutableList<Instance> = strClass.instances as MutableList<Instance>
// Мы получили список инстансов - репрезентаций сущностей в куче. Теперь нужно считать данные
  

val instance: Instance = instances.first()
// Получаем примитив:
val intFieldValue = instance.getValueOfField("primitiveField123") as Int
// Получаем объект:
val objectField: Instance = instance.getValueOfField("status") as Instance
// Снова получаем объект. Если это enum, например, то можем просто получить ordinal
// А из ординала уже получить реальный енам
val enumFieldOrdinal = objectField.getValueOfField("ordinal") as Int

Должен признаться. Я не осилил вытащить строковые данные из кучи. Час времени и пара вырванных волос и я понял, что не очень то и хотелось. Что же это, будущему поколению все готовенькое? Нет уж, сами разбирайтесь.

Из полученных объектов, примитивов и тд собираете вашу модель, сериализуете ее, как хотите, заливаете в базу, веселитесь, радуетесь.

В целом то говоря, все.

На этой части снова начинается лирическая часть, так сказать, небольшая комедия.

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

Заключение

Это было чудесное путешествие в мир нерешенных проблем, полное стресса и чудес, которых становится все меньше и меньше с течением времени. Программист становится опытнее, наступает на все меньшее количество граблей, и приключения вроде этого рано или поздно покидают его жизнь.

Мне кажется, иногда полезно аккуратно подложить себе пару свиней, граблей, выкопать пару ям с пиками и так далее. Очень освежающий опыт, который позволяет снова почувствовать себя неопытным новичком, для которого каждая задача, за которую он берется - приключение и огромное испытание.

Ну и уж если ты такой большой любитель лирики, что дошел до этого места, то может тебе захочется поиграть в средневековый майнкрафт? :)

Ссылки

https://github.com/enchantinggg4/java-heap-adventures

https://cuprak.info/2018/11/12/exploring-java-heap-dumps

https://mvnrepository.com/artifact/org.netbeans.modules/org-netbeans-lib-profiler/RELEASE802

https://discord.gg/H5WEnQk5rX - discord сервер вышеупомянутого майнкрафт проекта

Комментарии (0)