Спойлер: здесь мы будем программно вытаскивать данные из 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 всеми силами, уж очень тяжко на ней писать после котлина) и дописать под свои нужды.
Алгоритм очень простой:
Распарсить все необходимые сущности из дампа в нормальные объекты, с которыми можно работать
Сериализовать их в нормальный формат, из которого уже скриптами или чем-нибудь можно сгенерить 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 сервер вышеупомянутого майнкрафт проекта