Русский
Русский
English
Статистика
Реклама

Блог компании нпо криста

Eще один бэкап больше, чем скрипт, проще, чем система

28.07.2020 10:08:24 | Автор: admin
Систем резервного копирования множество, но что делать, если обслуживаемые сервера разбросаны по разным регионам и клиентам и нужно обходиться средствами операционной системы?



Добрый день, Habr!
Меня зовут Наталья. Я тимлид группы администраторов приложений в НПО Криста. Мы Ops для группы проектов нашей компании. У нас довольно своеобразная ситуация: мы устанавливаем и сопровождаем наше ПО как на серверах нашей компании, так и на серверах, расположенных у клиентов. При этом бэкапить сервер целиком нет необходимости. Важны лишь существенные данные: СУБД и отдельные каталоги файловой системы. Конечно, клиенты имеют (или не имеют) свои регламенты резервного копирования и часто предоставляют некое внешнее хранилище для складывания туда резервных копий. В этом случае после создания бэкапа мы обеспечиваем отправку во внешнее хранилище.

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

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

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

Условия задачи были следующие:
1. базовый инстанс бэкапа автономен, работает локально
2. хранение резервных копий и логов всегда в пределах сети клиента
3. инстанс состоит из модулей такой своеобразный конструктор
4. необходима совместимость с используемыми дистрибутивами Linux, включая устаревшие, желательна потенциальная кроссплатформенность
5. для работы с инстансом достаточно доступа по ssh, открытие дополнительных портов необязательно
6. максимальная простота настройки и эксплуатации
7. возможно (но не обязательно) существование отдельного инстанса, позволяющего централизованно просмотреть состояние бэкапов с разных серверов

То, что у нас получилось, можно посмотреть здесь: github.com/javister/krista-backup
ПО написано на python3; работает на Debian, Ubuntu, CentOS, AstraLinux 1.6.
Документация выложена в каталоге docs репозитария.

Основные понятия, которыми оперирует система:
action действие, реализующее одну атомарную операцию (бэкап БД, бэкап каталога, перенос из каталога А в каталог Б и т. д.). Существующие actions лежат в каталоге core/actions
task задание, набор actions, описывающий одну логическую задачу бэкапа
schedule расписание, набор task с опциональным указанием времени выполнения задачи

Конфигурация бэкапа хранится в yaml-файле; общая структура конфига:
общие настройки
раздел actions: описание действий, используемых на этом сервере
раздел schedule: описание всех заданий (наборов действий) и расписание их запуска по крону, если такой запуск требуется
Пример конфига можно посмотреть здесь: github.com/javister/krista-backup/blob/master/KristaBackup/config.yaml.example

Что умеет приложение на текущий момент:
поддерживаются основные для нас операции: бэкап PostgreSQL, бэкап каталога файловой системы через tar; операции с внешним хранилищем; rsync между каталогами; ротация бэкапов (удаление старых копий)
вызов внешнего скрипта
выполнение вручную отдельного задания
/opt/KristaBackup/KristaBackup.py run make_full_dump

можно добавить (или убрать) в кронтабе отдельное задание или все расписание
/opt/KristaBackup/KristaBackup.py enable all

генерация триггер-файла по результатам бэкапа. Эта функция полезна в связке с Zabbix для мониторинга бэкапов
может работать в фоне в режиме webapi или web
/opt/KristaBackup/KristaBackup.py web start [--api]


Разница между режимами: в webapi нет собственно веб-интерфейса, но приложение отвечает на запросы другого инстанса. Для режима web нужно установить flask и несколько дополнительных пакетов, а это не везде приемлемо, например в сертифицированной AstraLinux SE.

Через веб-интерфейс можно посмотреть состояние и логи бэкапов подключенных серверов: веб-инстанс запрашивает данные с бэкап-инстансов через API. Доступ к web требует авторизации, доступ к webapi нет.



Логи некорректно прошедших бэкапов размечаются цветом: warning желтым, error красным.





Если администратору не нужна шпаргалка по параметрам и серверные ОС однородны, можно скомпилировать файл и распространять уже готовый пакет.

Распространяем мы эту утилиту в основном через Ansible, выкатывая сначала на часть наименее важных серверов, а после тестирования на все остальные.

В итоге мы получили компактную автономную утилиту копирования, поддающуюся автоматизации и пригодную для эксплуатации даже малоопытными администраторами. Нам удобно может, пригодится и вам?
Подробнее..

Конструктор интерактивных туров

11.09.2020 10:13:26 | Автор: admin


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


Процесс обучения


Современные компании-разработчики стараются взять под контроль и теоретическое, и практическое обучение пользователя. Это подразумевает присутствие учителя (специалиста предметной области) в процессе обучения. Ему необходимо в режиме реального времени контролировать обучение, чтобы пользователь получил теоретические знания о работе с функционалом программы, а также закрепил их на практике.


Одной из компаний, которая занимается не только разработкой программного обеспечения, но и помогает пользователям своих решений освоить управление ими, является НПО Криста. Для этого на базе компании создан специальный учебный центр.


Автоматизация обучения


С распространением компьютерных технологий стало возможным внедрение автоматического управления процессом обучения. Т. е. контроль за практическим обучением пользователя возлагается на программу. Она представляет собой интерактивное руководство, которое направляет пользователя по функционалу приложения, подобно GPS-навигатору. Программа указывает на элементы в пользовательском интерфейсе, объясняя, что делает каждый значок или кнопка или где найти меню. Такие руководства называются интерактивными турами по функциональным возможностям программы. Специалист лишь единожды пишет программу обучающего тура, который впоследствии воспроизводится на компьютерах конечных пользователей. Обучение считается завершенным, если пользователь прошел тур от начала и до конца.



Постановка задачи


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


При выборе инструмента, прежде всего, необходимо было учитывать, что его потенциальными пользователями являются не программисты, а специалисты предметных областей. Следовательно, разработка туров должна происходить на основе манипуляции графическими объектами, а не на языке программирования. Существующие решения, удовлетворяющие данному критерию, ориентированы на создание туров по обычным сайтам и не подходят для предприятий, занимающихся автоматизацией сложных бизнес-процессов. Также существующие решения не позволяют получить программный код созданного тура. Следовательно, нет возможности управлять его хранением. Это критично для НПО Криста, так как информационная безопасность является важным требованием к программным продуктам компании. Поэтому было принято решение разработать собственный инструмент для создания интерактивных туров по функционалу web-приложений.


Входными данными для разрабатываемой системы является загружённое в браузер web-приложение с функционалом, по которому создается тур. Выходными данными являются туры, сохранённые в реляционную базу данных под управлением PostgreSQL. Для получения входных данных необходимо, чтобы разрабатываемая программа работала в браузере. Следовательно, требуется создать web-приложения, что предполагает использование клиент-серверной технологии.



Структура системы


Инструментарий для создания туров представлен пользователю через графический интерфейс клиентской программы. Она состоит из вспомогательной и центральной подсистем. Центральная подсистема предоставляет функционал для создания кода тура, а вспомогательная подсистема обеспечивает воспроизведение тура, а также определение элементов интерфейса для вставки их в сценарий тура. Созданные туры необходимо хранить в базе данных при помощи подсистемы хранения.



Взаимодействие между клиентом и сервером происходит посредством передачи HTTP-сообщений с данными о турах в формате JSON. Обращение к БД осуществляется на языке SQL. Основным языком программирования, используемым при разработке клиентского приложения, является TypeScript. Архитектура клиента определяется использованием библиотек React и Redux. Серверное приложение разработано как Maven-проект на языке Java.



Технологические решения


Основной частью при создании туров является редактор кода тура. За его основу была взята библиотека Blockly. Она разрабатывается и поддерживается компанией Google с 2012 года. Blockly свободно распространяется вместе c исходным кодом и включает в себя графический редактор, а также генераторы кода для подготовки исполнения программы в среде web-приложения. Программы в этом редакторе создаются на визуальном языке программирования Blockly путём соединения соответствующих блоков. Существует возможность определения новых блоков с заданием их формы и генерируемого ими программного кода (подробнее). Благодаря этому редактор может быть расширен за счёт добавления блоков, реализующих логику создания тура.


Так как клиентское приложение предоставляет инструментарий для web-разработки, было принято решение добавить его в инструменты разработчика DevTools браузера Chrome в качестве расширения (подробнее: раз, два). При этом часть клиента, реализующая вспомогательную подсистему, встраивается на инспектируемую страницу web-приложения, по которому создается тур (по примеру). Связь между частями обеспечивается по схеме.


Следовательно, программа функционирует в двух окнах: в окне web-приложения и в окне DevTools. Соответствующий пользовательский интерфейс изображён ниже. В окне DevTools пользователь управляет созданием тура. На инспектируемой странице он выбирает элементы интерфейса, которые необходимо выделить, а также тестирует созданный тур.



Алгоритм работы системы


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



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



Самое первое взаимодействие между клиентом и сервером (6 7) происходит всякий раз, когда пользователь открывает вкладку с плагином (4). Тогда же происходит добавление части клиентской программы к веб-приложению, по которому создается тур (5).



Результат разработки


При создании нового тура откроется встроенный редактор Blockly, в левой части которого расположены стандартные разделы. К ним добавлен раздел Тур. Он содержит специальные графические объекты, которые позволяют, например, добавить подсказку к элементу на инспектируемой странице, затемнение вокруг элемента, ожидание события и прочие возможности.



При помощи таких блоков описывается сценарий тура, на основании которого генерируется соответствующий JavaScript-код.



При запуске воспроизведения в окно web-приложения, по которому создавался тур, загружается и выполняется JavaScript-код тура. При этом на странице появляется навигационная панель для управления воспроизведением тура, а также описанные в сценарии элементы. В представленном примере на первом шаге отрисуется пульсирующий указатель на элемент интерфейса, нажатие на который запустит воспроизведение следующего шага тура.



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



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


Подробнее..

IntelliJ IDEA Structural Search amp Replace

10.07.2020 22:15:48 | Автор: admin


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


Простой пример одной такой функции

А вы знаете, что, если в IDEA нажать F2, курсор перескочит к ближайшей ошибке в файле? А если нет ошибки, то к замечанию? Как-то так получается, что об этом знают далеко не все.


Одной такой функцией является Structural Search & Replace (SSR). Она может быть невероятно полезна в тех ситуациях, когда пасует всё богатое разнообразие других функций.


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


  1. 3D-движка для создания игр jMonkeyEngine, как пример большого проекта, в котором всегда можно найти что-то интересненькое.
  2. моего собственного проекта plantuml-native-image, в котором я провожу эксперименты по компиляции PlantUML в нативный исполняемый код с помощью GraalVM native-image.

Собственно, случай во втором проекте и побудил меня к написанию статьи. Но обо всём по порядку...


Простая задача для препарирования


Прежде чем приступить к изучению структурного поиска как такового, давайте определимся с какой-либо простой задачей, в решении которой этот поиск будет полезен. Тут я хочу рассмотреть пример, который не так давно пригодился мне лично в одном из рабочих проектов (с тем лишь исключением, что вместо закрытого рабочего кода я буду демонстрировать пример на конкретной ревизии проекта jMonkeyEngine): поиск открытых объектов блокировок с использованием ключевого слова synchronized (см. раздел "Item 82 Document thread safety" главы "11 Concurrency" в книге Joshua Bloch "Effective Java").


Если кратко, суть в том, что использование синхронизации на публично доступных объектах является не очень хорошей идеей, т.к. тогда теряется контроль над синхронизацией, и любой сторонний код может начать в неё вмешиваться, приводя к нежелательным эффектам различного рода, вплоть до взаимоблокировок (deal locks).


Тут надо понимать, что ключевое слово synchronized имеет два варианта использования:


в качестве модификатора метода:


class ClassA {    public synchronized void someMethod() {        // ...    }}

и в качестве внутренней конструкции метода:


class ClassA {    public void someMethod() {        synchronized(this) {            // ...        }    }}

По сути, оба приведённых выше примера являются примерами синхронизации на открытом объекте. Правильным было бы написание такого кода:


class ClassA {    private final Object sync = new Object();    public void someMethod() {        synchronized(sync) {            // ...        }    }}

В таком примере никакой сторонний код не сможет вмешаться в синхронизацию.


Но как понять, есть ли в коде проекта такой паттерн?


Простейший подход поиск по тексту ключевого слова synchronized и внимательный разбор каждого вхождения. Но такой подход годится только для совсем маленьких проектов. Если мы попытаемся поискать такое слово в проекте jMonkeyEngine, то найдём там аж 117 вхождений, причём не только в качестве искомой конструкции, но и в комментариях и просто строках. И разбираться в таком количестве вхождений уже далеко не так весело.


Так что же делать? Вот тут нам на помощь и приходит уникальная функция IntelliJ IDEA: Structural Search & Replace.


Structural Search. Основы


Для начала разберёмся с интерфейсом структурного поиска в IDEA.


Откроем проект jMonkeyEngine, и вызовем окно структурного поиска (Edit -> Find -> Search Structurally...) с двумя областями ввода:




  1. областью задания шаблона поиска;
  2. областью задания фильтров.

Чем же оно отличается от обычного поиска, и что мы можем с этим делать?


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


В целом, если вы хотите посмотреть примеры разных поисков с этим инструментом, то можно воспользоваться расширяемым каталогом примеров. Для этого можно открыть меню под кнопкой гаечного ключа и выбрать пункт Existing Templates...:




В результате откроется окно с богатым выбором готовых шаблонов:




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


Начнём со случая, когда ключевое слово synchronized используется в качестве модификатора метода, т.к. в этом случае синхронизация получается по открытому методу просто by design. Для этого введём в левое поле следующую странную конструкцию:


synchronized $type$ $method$ ($ptype$ $param$) {    $statement$;}

Тут мы определяем некоторый паттерн поиска. Не точное соответствие, а только набор синтаксических конструкций, который должен попадать под критерий.


В этом шаблоне можно увидеть большое количество лексем, ограниченных спереди и сзади символами $. Это так называемые переменные шаблона поиска. Если лексема задана без символов $ (как, например synchronized), то она должна присутствовать в найденном фрагменте кода, как есть. А если лексема находится внутри "долларов", то в этом месте по умолчанию может находиться что угодно. Просто тут должно находиться что-то, не важно, что.


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


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


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


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


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


Т.е. если мы слева выделим переменную $param$, то справа можно задать для неё, например, критерий количества вхождений параметров в объявлении функции. Для этого достаточно щёлкнуть на гиперссылку Add Filter и выбрать второй пункт меню Count.




В появившейся строке фильтра отметим, что параметров может быть от 0 до бесконечности. Для этого второе поле оставим пустым:




Теперь шаблон будет искать все синхронизируемые методы с произвольным количеством параметров Но только с одной строкой кода в теле. Чтобы это исправить, зададим точно такое же ограничение и для переменной $statement$.


Всё, теперь можно нажать кнопку Find и увидеть все синхронизируемые методы в проекте, которых всего 18 штук:




Отличное начало!


Спускаемся в кроличью нору. Script Filter


Ну а что с синхронизацией на произвольном объекте? Казалось бы, тут всё даже проще:


synchronized($Obj$) {    $statement$;}

Но вот беда, как определить, что переменная $Obj$ приватная? Можно попробовать отталкиваться от паттерна из книги, что это должна быть переменная типа Object. Тогда мы можем добавить фильтр Type и задать имя типа класса Object. Причём соответствие должно быть строгим, без учёта иерархии. Т.е. галки под фильтром должны быть убраны:




Тогда мы с некоторой вероятностью найдём все места, где синхронизация использует закрытые объекты (да и то не все!). Что как бы даст нам результат, противоположный требуемому В частных случаях это может быть полезно, если нам надо найти все синхронизации по объектам определённого типа и даже по наследникам этого типа (если отметить пункт with type hierarchy под фильтром).


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


К сожалению, простых способов найти это не существует. Единственное, что нам может помочь ограничение типа Script. Самая мощная и самая сложно используемая функция структурного поиска.


Дело в том, что в качестве ограничения можно написать произвольный код на Groovy, возвращающий true или false. А в качестве параметров будут переменные из паттерна поиска, которые определены в символах $. Плюс ещё пара служебных переменных __context__ и __log__.


И вот тут находится самый расстраивающий меня момент. Данные переменные объекты кусков Psi дерева синтаксического разбора. При этом:


  • поле ввода скрипта (внезапно) не предоставляет никакого code IntelliSense. Никаких подсказок;
  • угадать какой именно элемент Psi дерева окажется в качестве переменной практически нереально;
  • никакой справки по структуре Psi дерева. Только исходники;
  • общее юзабилити ввода фильтра ужасно. Необходимо смириться с его схлопываниями при потере фокуса ввода и необходимостью каждый раз расхлопывать поле и изменять размеры элементов, чтобы увидеть скрипт полностью, если он большой и многострочный.

Так что же делать? Будем разбираться. Прежде всего надо понять что же попадает в переменную $Obj$. Мне не пришло в голову ничего лучше идеи воспользоваться штатной функцией из Groovy: println. Добавляем к переменной $Obj$ фильтр типа Script и вводим следующий текст:


println(Obj)return true



Как видим, в скрипте можно использовать имена переменных, только без "долларов". При выполнении этого поиска мы увидим все вхождения кода с synchronized, но нас интересует не это, а то, куда же напечатались логи с помощью println?


А напечатались они в лог самой IDEA. Найти их можно через меню: Help->Show Log in.... Т.к. у меня KDE, то пункт меню полностью называется Show Log in Dolphin. В результате откроется системный диспетчер файлов с указанием на актуальный файл лога. Вот в него-то и нужно заглянуть, чтобы увидеть информацию об интересующем нас объекте. В данном случае можно найти следующие строки:


2020-07-05 15:03:00,998 [14151177] INFO - STDOUT - PsiReferenceExpression:pending2020-07-05 15:03:01,199 [14151378] INFO - STDOUT - PsiReferenceExpression:source2020-07-05 15:03:01,216 [14151395] INFO - STDOUT - PsiThisExpression:this2020-07-05 15:03:01,219 [14151398] INFO - STDOUT - PsiReferenceExpression:receiveObjectLock2020-07-05 15:03:01,222 [14151401] INFO - STDOUT - PsiReferenceExpression:invoke2020-07-05 15:03:01,226 [14151405] INFO - STDOUT - PsiReferenceExpression:chatServer2020-07-05 15:03:01,231 [14151410] INFO - STDOUT - PsiReferenceExpression:obj2020-07-05 15:03:01,236 [14151415] INFO - STDOUT - PsiReferenceExpression:sync2020-07-05 15:03:01,242 [14151421] INFO - STDOUT - PsiReferenceExpression:image2020-07-05 15:03:01,377 [14151556] INFO - STDOUT - PsiClassObjectAccessExpression:TerrainExecutorService.class2020-07-05 15:03:01,409 [14151588] INFO - STDOUT - PsiReferenceExpression:byteBuffer2020-07-05 15:03:01,429 [14151608] INFO - STDOUT - PsiReferenceExpression:lock2020-07-05 15:03:01,432 [14151611] INFO - STDOUT - PsiReferenceExpression:eventQueue2020-07-05 15:03:01,456 [14151635] INFO - STDOUT - PsiReferenceExpression:sensorData.valuesLock2020-07-05 15:03:01,593 [14151772] INFO - STDOUT - PsiReferenceExpression:createdLock2020-07-05 15:03:01,614 [14151793] INFO - STDOUT - PsiReferenceExpression:taskLock2020-07-05 15:03:01,757 [14151936] INFO - STDOUT - PsiReferenceExpression:loaders2020-07-05 15:03:01,765 [14151944] INFO - STDOUT - PsiReferenceExpression:threadLock

Т.о., мы видим, что в качестве значения Obj могут выступать объекты как минимум трёх видов:


  • PsiThisExpression лексема this;
  • PsiClassObjectAccessExpression синхронизация по объекту типа Class (synchronized (TerrainExecutorService.class) {...});
  • PsiReferenceExpression некоторое выражение, результат вычисления которого используется как объект синхронизации.

Первые два типа можно автоматически рассматривать как синхронизацию по открытому объекту. Т.е. если у нас Obj является объектом типа PsiThisExpression или PsiClassObjectAccessExpression, то надо вернуть true.


Но что делать с типом PsiReferenceExpression? Точнее, даже не так. Что с ним вообще можно сделать?


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


Не буду утомлять подробностями ковыряния исходников Psi. Просто приведу получившийся результирующий скрипт:


if (Obj instanceof com.intellij.psi.PsiThisExpression) return trueif (Obj instanceof com.intellij.psi.PsiClassObjectAccessExpression) return trueif (Obj instanceof com.intellij.psi.PsiReferenceExpression) {    def var = Obj.advancedResolve(false).element    if (var instanceof com.intellij.psi.PsiParameter) return true    if (var instanceof com.intellij.psi.PsiLocalVariable) {        return !(var.initializer instanceof com.intellij.psi.PsiNewExpression)    }    if (var instanceof com.intellij.psi.PsiField) {        return !var.hasModifier(com.intellij.lang.jvm.JvmModifier.PRIVATE) &&               !var.hasModifier(com.intellij.lang.jvm.JvmModifier.PROTECTED)    }}return true

В таком виде поиск находит уже только 12 вхождений. Полученный результат не идеален, т.к. некоторые из этих вхождений ложноположительные. Но думаю, что для данного примера этого достаточно. Тем не менее видно, что структура Psi весьма мощная и позволяет оперировать структурой кода на весьма высоком уровне абстракции.


Ваша первая в жизни статическая инспекция кода


Как мы увидели чуть ранее, поиск некоторых вещей может оказаться довольно непростой задачей. И будет обидно, если столько усилий пропадёт зря. Да и вообще, хотелось бы, чтобы IDE сразу подсказывала, что ты пишешь что-то не то.


И тут IDEA тоже может помочь. Дело в том, что в ней есть огромное количество всяких инспекций, которые помогают разработчику писать более правильный код, подсвечивая некорректные места и объясняя, что в них не так. И среди них есть одна замечательная инспекция, которая выключена по умолчанию. Это: Structural search inspection.


Найти эту инспекцию можно в окне настройки (File->Settings...->Editor->Inspections):




Включаем эту инспекцию и нажимаем плюсик, чтобы добавить шаблон структурного поиска. По умолчанию окно поиска автоматически заполнится тем, что искалось в последний раз. Нажимаем Ok и вводим имя шаблона поиска. Например, Open object sync. Но на деле лучше придумать что-то более развёрнутое, чтобы было понятно, что тут имеется в виду.


Всё, теперь IDEA начнёт автоматически подсвечивать все места в коде, которые подпадают под этот поиск:




Ву-а-ля!!! Вы создали первую в своей жизни инспекцию кода! Поздравляю! Остаётся только закоммитить её вместе с проектом, чтобы другие тоже могли ею воспользоваться. Для этого достаточно добавить в версионное хранилище каталог .idea/inspectionProfiles, где сохранились эти настройки.


Structural Replace


Как и в Structural Search, в IDEA есть и функция структурной замены Structural Replace
(Edit -> Find -> Replace Structurally...):




В отличие от окна Search Structurally, в данном окне появляется ещё одна область. Она предназначена для задания замены найденному шаблону. Так же, как и в шаблоне, в подстановке могут содержаться переменные, ограниченные с двух сторон символами $. И точно так же эти переменные могут настраиваться в области ввода фильтров. Только в этом случае это будут не условия фильтрации, а исключительно Groovy скрипты для вычисления текста подстановки вместо переменной.


Тут тоже хочу привести пример из жизни. Мне надо было в одном огромном классе заменить все вызовы функций вида:


classInitializationSupport.initializeAtRunTime(WindowPropertyGetter.class, AWT_SUPPORT);

на вызовы вида:


classInitializationSupport.initializeAtRunTime("sun.awt.X11.WindowPropertyGetter", AWT_SUPPORT);

Т.е. надо было уйти от явного использования классов в коде. И далее сделать то же самое в отношении нескольких видов функций.


Проделывать всё это вручную мне очень не хотелось. Вместо этого я использовал Replace Structurally. Для начала задал шаблон поиска:


classInitializationSupport.initializeAtRunTime($Clazz$.class, AWT_SUPPORT)

На переменную $Class$ не навешивал никаких ограничений, т.к. мне всё равно какой класс туда попадёт.


Далее задал подстановку:


classInitializationSupport.initializeAtRunTime("$FullClass$", AWT_SUPPORT)

И вот тут-то мне надо вычислить новое значение переменной $FullClass$. Для чего выделяю её курсором и в области ввода ограничений задаю скрипт вида:


Clazz.type.canonicalText

Т.е. берём тип, попадающий в переменную из шаблона поиска $Clazz$, получаем полное имя этого типа и подставляем в параметр метода в виде строки.


В конечном итоге окно структурной замены получило следующий вид:




Далее нажимаем Find и получаем список возможных замен:




Тут мы можем посмотреть каждое вхождение, и во что оно превратится (кнопка Preview Replacement). Так же можно исключить какие-либо вхождения (из контекстного меню) или сделать все преобразования разом (кнопка Replace All).


В целом, если освоиться с Psi, то это уже не так и сложно, не правда ли?


Structural Replace as Intention


Тут пришла пора упомянуть ещё один мощнейший механизм IDEA Intentions. Интеншены позволяют ковать железо, не отходя от кассы, т.е. применять какие-либо преобразования кода прямо по месту нахождения курсора.


Например, если вы напишете такой код:


public static void main(String[] args) {    List<Integer> list = Arrays.asList(0, 10, 20, 30);    for (int i = 0; i < list.size(); i++) {        System.out.println(list.get(i));    }}

а потом встанете курсором на for и нажмёте на Alt+Enter, то вам будет выдано предложение переделать этот цикл в несколько вариантов другого представления того же самого. Например, в for-each:


public static void main(String[] args) {    List<Integer> list = Arrays.asList(0, 10, 20, 30);    for (Integer integer : list) {        System.out.println(integer);    }}

Так вот, к чему это я? А к тому, что с помощью Structural Replace можно сделать такой же intention. Для этого достаточно дополнить ту же самую инспекцию, которую мы задавали для Structural Search. Только при добавлении нового пункта нужно указать, что это будет не поиск, а замена:






Теперь в коде соответствующие места станут подсвеченными, и там будет предлагаться автозамена:






Поздравляю ещё раз! Вы сделали первый в своей жизни Intention!


Заключение


В данной статье я постарался познакомить читателей с одной из самых сложных для понимания, но при этом и одной из самых мощных функций в IntelliJ IDEA. Да, не всё в ней сейчас удобно, тем не менее бывают ситуации, когда ни одно другое средство не справится.


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


Дерзайте. Удачи. И всем Java!

Подробнее..

Собственные метрики JFR и их анализ

16.04.2021 10:20:09 | Автор: admin

Недавно я описывал, как осуществлять запуск и управление Java Flight Recorder (JFR). Теперь решил затронуть тему записи в JFR метрик, специфичных для приложения, а также способов их анализа в Java Mission Control (JMC). Это позволяет расширить понимание происходящего с приложением и значительно упростить анализ производительности и поиск узких мест.

В предлагаемой статье мы рассмотрим пример создания своих событий, записываемых в JFR, и то, как эти данные могут помогать в анализе работы приложения. Будет рассмотрен только один частный пример, не претендующий на полноту описания всех возможностей и нюансов использования JFR и JMC.

Что такое JFR?

NOTE: JFR - это механизм легковесного профилирования Java-приложения. Он позволяет записывать и впоследствии анализировать огромное количество метрик и событий, происходящих внутри JVM, что значительно облегчает анализ проблем. Более того, при определённых настройках его накладные расходы настолько малы, что многие (включая Oracle) рекомендуют держать его постоянно включённым везде, в том числе на продуктовых серверах, чтобы в случае возникновения проблем сразу иметь полную картину происходившего с приложением. Просто мечта любого SRE!

Раньше этот механизм был доступен только в коммерческих версиях Java от корпорации Oracle версии 8 и более ранних. В какой-то момент его реимплементировали с нуля в OpenJDK 12, затем бэкпортировали в OpenJDK 11, которая является LTS-версией. Однако вот OpenJDK 8 оставалась за бортом этого праздника жизни. Вплоть до выхода апдейта 8u272, в который наконец-то тоже бэкпортировали JFR. Теперь все (за редким исключением) пользователи OpenJDK могут начинать использовать эту функциональность.

Краткое описание задачи

Для начала определимся с тем, что мы собираемся мониторить и какие данные собирать.

В одном из приложений, разрабатываемых нашей компанией, реализована уникальная система контроля, досчёта и свода вводимых данных. Даже между документами, что позволяет формировать один документ из множества других автоматически.

Так как данные вводятся в табличном виде, то их обработка очень хорошо ложится на выражения наподобие формул Excel. Причём эти формулы являются фактически основной бизнес-логикой приложения, покрывая почти все потребности. У нас они называются "контрольными соотношениями".

Таким образом, для того, чтобы проверить корректность заполнения документа, досчитать промитоги или произвести свод в новый документ из пачки существующих, достаточно запустить механизм вычислений, который пройдётся по каждому разделу документа и запустит в нём соответствующие запущенной операции выражения с формулами:

Дополнительными бонусами этого подхода являются:

  • Возможность обратного вычисления. То есть, если в итоговом документе, после всех досчётов и сводов, выявляется ошибочное значение, источник ошибки можно достаточно легко найти, посмотрев, откуда пришли исходные данные для его вычисления.

  • Простота обновления. Можно выкатывать в день по несколько обновлений, которые содержат только новые строки контрольных соотношений.

  • Простота задания. Формулы в Excel знакомы практически всем. И такая форма задания достаточно проста, чтобы её могли использовать не только разработчики, но и аналитики, техподдержка и кто угодно ещё.

Однако при этом имеется и обратная сторона медали: достаточно легко получить слишком тяжёлую для вычисления формулу. Типичные проблемы:

  • Инварианты, неинвариантны при вычислении.

  • В обработку попадает слишком много неправильно отфильтрованных данных, которые на самом деле не нужны.

  • При вычислении формулы генерируются слишком сложные запросы в БД.

  • Генерируется слишком много запросов в БД, где может быть только один запрос.

  • Используется слишком много памяти из кучи.

  • И т. п.

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

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

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

Только подобный подход всё равно имеет изъян. Не всегда можно легко понять причину медленного вычисления результата. Помимо просто неэффективно написанной формулы, могут быть и другие факторы:

  • Сработал GC.

  • Долго отвечала БД.

  • Было много запросов в БД.

  • CPU был занят чем-то ещё.

  • И т. д.

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

Наше приложение до сих пор работает на Java 8 (хотя и есть планы по обновлению версии Java). А значит, мы не могли использовать JFR, как это можно было делать в Java 11 и старше.

Но времена меняются, и JFR пришёл в Java 8 с обновлением 8u272. Теперь появляется возможность использовать его для большого количества задач вообще и для аудита вычисления контрольных соотношений в частности.

Так давайте посмотрим, как это можно сделать и что от этого получить.

Описание своих событий JFR

Для того, чтобы записать в JFR какую-либо свою информацию, необходимо объявить события, которые будут записываться. Сделать это можно двумя путями: статически и динамически.

Статический способ подразумевает написание своего класса, унаследованного от класса jdk.jfr.Event. А динамический способ подразумевает не написание самого класса, а создание описания модели через метаданные.

В данной статье мы будем использовать первый способ. А те, кого интересует второй, могут обратиться к JavaDoc документации класса jdk.jfr.EventFactory.

Модель событий

Всё, что записывает JFR, - это события, объекты специальных классов, унаследованных от jdk.jfr.Event и содержащих как предопределённые поля, так и предметные, специфичные для конкретного типа события. Каждое такое поле доступно для просмотра и анализа при открытии результирующего файла с записью.

Стандартные поля наследуются от класса jdk.jfr.Event, но не объявлены явно, а формируются при старте JVM. Это сделано для того, чтобы в случае отключенного JFR не создавать накладные расходы на работающее приложение. К таким полям относятся:

  • eventType - идентификатор типа события. Может отличаться от реального программного типа и предназначен для стабильности идентификации события, невзирая на жизненный цикл приложения и возможные переименования программного класса;

  • startTime - время начала события. Точнее, момент времени, в который был создан объект события;

  • endTime - время завершения события. Точнее, момент времени, когда был вызван метод Event.commit();

  • duration - длительность события. То есть разница между endTime и startTime;

  • eventThread - поток, в котором было создано событие.

Также можно объявлять свои дополнительные поля. Но эти поля могут быть только следующих типов:

  • Примитивные типы: boolean, char, byte, short, int, long, float и double.

  • Строки: java.lang.String.

  • Потоки: java.lang.Thread.

  • Классы: java.lang.Class.

Никакие другие типы не допускаются. Это требуется для обеспечения максимально компактного хранения записи JFR на диске. Соответственно, требуется внимательно подходить к выбору типов полей и не использовать, например, String для хранения времени, когда для этого достаточно использовать long и аннотацию jdk.jfr.Timestamp.

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

  • InterpreterReportEvent - для измерения времени выполнения всех вычислений для одного конкретного отчёта.

  • InterpreterSectionEvent - для измерения времени выполнения вычислений для раздела.

  • InterpreterRelationEvent - для измерения времени выполнения отдельного вычисления.

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

Код классов событий
@Name("ru.krista.consolidation.interpreter.Report")public class InterpreterReportEvent extends Event {    public long key;    public String docState;    public String caption;    public String formCode;    public String formName;    public long endDate;    public long beginDate;    public long deliveryDate;    public long evalDate;    public long checkDate;    public int deliveryYear;    public String collectingPeriod;    public String inputMode;    public String operationUuid;    public boolean assignAsEquality;    public String section;    public long formDescriptorId;}
@Name("ru.krista.consolidation.interpreter.Section")public class InterpreterSectionEvent extends Event {    public String key;    public String name;}
@Name("ru.krista.consolidation.interpreter.Relation")public class InterpreterRelationEvent extends Event {    public int order;    public long sectionId;    public long relationId;    public int rowsCount;    public long groupId;    public String groupName;    public String sectionName;    public transient CheckRelationResult results;}

Как видно, это весьма простые классы, унаследованные от jdk.jfr.Event.

Также каждый класс помечен аннотацией jdk.jfr.Name. Эта аннотация необходима для явного задания имени события. По умолчанию в качестве имени берётся полное имя класса. Но со временем полное имя класса может поменяться, и тогда будет утеряно соответствие метрик между новой и старой версиями приложения. Чтобы этого избежать, рекомендуется явно задавать имя событий и не менять их.

Ещё один интересный момент находится в классе InterpreterRelationEvent. Это поле result. Оно, с одной стороны, имеет неподдерживаемый тип, с другой, помечено как транзиентное. В результате JFR будет игнорировать его при сохранении. А нужно оно по техническим причинам: для того, чтобы туда сохранился статус результата вычисления в бизнес-логике, и в конце можно было бы вынуть этот результат в виде строки и положить в событие (просто такая особенность реализации, которую я посчитал уместным продемонстрировать тут).

Добавляем описания для модели событий

На данный момент мы описали саму модель в таком виде, что её уже можно использовать, но это будет несколько неудобно. Дело в том, что в инструментах анализа записей JFR модель в данном виде будет отображаться, как есть. И это не очень-то информативно видеть, например, на поле с именем `evalDate` и пытаться понять, что это значит. Да и формат отображения получится не дата, а число, которое тоже ещё как-то интерпретировать надо:

Чтобы это исправить, модель необходимо разметить специальными аннотациями:

Аннотированные классы событий
@Name("ru.krista.consolidation.interpreter.Report")@Label("КС: Отчёт")@Category({"consolidation", "interpreter", "report"})@SuppressWarnings({"squid:S1820", "pmd:TooManyFields"})public class InterpreterReportEvent extends Event {    @Label("ID")    public long key;    @Label("Состояние")    public String docState;    @Label("Заголовок")    public String caption;    @Label("Код формы")    public String formCode;    @Label("Имя формы")    public String formName;    @Timestamp    @Label("Дата окончания сбора")    public long endDate;    @Timestamp    @Label("Дата начала сбора")    public long beginDate;    @Timestamp    @Label("Дата сдачи")    public long deliveryDate;    @Timestamp    @Label("Дата последнего досчёта")    public long evalDate;    @Timestamp    @Label("Дата последней проверки")    public long checkDate;    @Label("Год сдачи")    public int deliveryYear;    @Label("Период сдачи")    public String collectingPeriod;    @Label("Режим параметров вычислений")    public String inputMode;    @Label("Идентификатор операции")    public String operationUuid;    @Label("Запущена проверка КС")    public boolean assignAsEquality;    @Label("ID секции")    public String section;    @Label("ID дескриптора формы")    public long formDescriptorId;}
@Name("ru.krista.consolidation.interpreter.Section")@Label("КС: Секция")@Category({"consolidation", "interpreter", "report", "section"})public class InterpreterSectionEvent extends Event {    @Label("ID")    public String key;    @Label("Имя")    public String name;}
@Name("ru.krista.consolidation.interpreter.Relation")@Label("КС: Контрольное соотношение")@Category({"consolidation", "interpreter", "report", "section", "relation"})public class InterpreterRelationEvent extends Event {    @Label("Порядок вычисления")    public int order;    @Label("ID секции")    public long sectionId;    @Label("ID контрольного соотношения")    public long relationId;    @Label("Обработано строк")    public int rowsCount;    @Label("ID группы КС")    public long groupId;    @Label("Имя группы КС")    public String groupName;    @Label("Имя секции")    public String sectionName;    public transient CheckRelationResult results;}

В новом варианте модели использованы следующие дополнительные аннотации:

  • jdk.jfr.Label - для задания человекочитаемых имён как у самих типов событий, так и у полей.

  • jdk.jfr.Timestamp - для задания формата отображения полей с датами.

  • jdk.jfr.Category - для задания категории событий. В Java Mission Control эта информация используется для группировки всех типов событий в некое подобие дерева.

Существует ещё некоторое количество аннотаций, как минимум для форматирования отображения значения полей. Но в данном конкретном примере они не используются, так что оставлю их для самостоятельного изучения в JavaDoc.

В результате события окажутся сгруппированными:

и поля будут иметь осмысленные описание и формат:

Создание экземпляров событий и их запись

Для того, чтобы записать событие JFR, необходимо выполнить следующие действия:

  1. Создать новый объект события.

  2. Заполнить поля созданного объекта.

  3. Вызвать метод Event.begin() перед выполнением соответствующего действия.

  4. Вызвать метод Event.commit() после этого действия.

В случае с событием InterpreterSectionEvent описанные события могут иметь следующий вид:

void calcSection(ReportSection section) {    // Создание объекта события    InterpreterSectionEvent event = new InterpreterSectionEvent();    // Наполнение события метаданными    event.key = section.getKeyString();    event.name = section.getReportFormSection(session).getName();    // Фиксация времени начала события    event.begin();    // Выполнение полезной работы    calcSectionInternal(section);    // Фиксация времени завершения события    // и запись его в выходной файл JFR    event.commit();}

Всё выглядит довольно просто, но надо не забывать, что в настройках JFR запись данного события может быть отключена. В таком случае получится, что операции по заполнению полей объекта события будут проделываться вхолостую, создавая бессмысленные накладные расходы. И если в данном примере это не выглядит чем-то страшным, то для события InterpreterReportEvent накладные расходы уже могут быть весьма существенными.

Для того, чтобы избегать подобного рода накладных расходов, можно запросить у объекта события статус его активности методом Event.isEnabled(). В таком случае приведённый ранее пример должен принять следующий вид:

void calcSection(ReportSection section) {    // Создание объекта события    InterpreterSectionEvent event = new InterpreterSectionEvent();    // Если запись данного типа событий активирована в JFR    if (event.isEnabled()) {        // Наполнение события метаданными        event.key = section.getKeyString();        event.name = section.getReportFormSection(session).getName();    }    // Фиксация времени начала события    event.begin();    // Выполнение полезной работы    calcSectionInternal(section);    // Фиксация времени завершения события    // и запись его в выходной файл JFR    event.commit();}

Причём экранировать подобным образом вызовы методов begin() и commit() не обязательно, так как механизм JFR гарантирует отсутствие накладных расходов, если событие отключено в настройках.

Но это не единственный способ оптимизации конструирования объекта события. Дело в том, что в настройках JFR, помимо полного отключения записи события, может ещё присутствовать настройка записи события, только если длительность этого события превысила некоторый порог. Таким образом, если необходимо сделать какие-то действия непосредственно перед вызовом метода commit(), нужно проверить, а действительно ли это событие будет записано или оно будет отброшено как слишком короткое. Для этого необходимо использовать метод Event.shouldCommit().

Вот как это выглядит при записи события InterpreterRelationEvent:

void calcRelation(        TreeAndRelation tar,        CheckRelationResult checkRelationResult,        PrimaryReportSection section) {    // Создание объекта события    InterpreterRelationEvent event = new InterpreterRelationEvent();    // Если запись данного типа событий активирована в JFR    if (event.isEnabled()) {        // Наполнение события метаданными        event.order =                tar.getRelation().getOrder() != null ?                tar.getRelation().getOrder() : 0;        event.relationId = tar.getRelation().getId();        if (tar.getRelation().getGroup() != null) {            event.groupId = tar.getRelation().getGroup().getId();            event.groupName = tar.getRelation().getGroup().getName();        }        ReportFormSection formSection = section.getReportFormSection(session);        event.sectionId = formSection.getId();        event.sectionName = formSection.getName();    }    // Фиксация времени начала события    event.begin();    // Выполнение полезной работы    calcRelationInternal(tar, checkRelationResult, section);    // Если продолжительность события превысила заданный порог    if (event.shouldCommit()) {        // Записываем статистику выполнения        event.rowsCount = checkRelationResult.getRowsCount();    }    // Фиксация времени завершения события    // и запись его в выходной файл JFR    event.commit();}

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

Настройка событий в конфигурации JFR

В предыдущей статье я описывал, как создать свой файл настроек JFR с помощью Java Mission Control. На этот раз отредактируем такой файл вручную, чтобы добавить в него настройки для вновь созданных событий.

Для этого можно добавить в .jfc файл следующие строки:

    <event name="ru.krista.consolidation.interpreter.Relation">        <setting name="enabled">true</setting>    </event>    <event name="ru.krista.consolidation.interpreter.Report">        <setting name="enabled">true</setting>    </event>    <event name="ru.krista.consolidation.interpreter.Section">        <setting name="enabled">true</setting>    </event>

Тем самым активируются все три типа событий. Тут важно отметить, что в качестве имени события необходимо использовать тот идентификатор, который задавался в аннотации jdk.jfr.Name.

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

    <event name="ru.krista.consolidation.interpreter.Relation">        <setting name="enabled">true</setting>        <setting name="threshold">1 s</setting>    </event>    <event name="ru.krista.consolidation.interpreter.Report">        <setting name="enabled">true</setting>        <setting name="threshold">1 s</setting>    </event>    <event name="ru.krista.consolidation.interpreter.Section">        <setting name="enabled">true</setting>        <setting name="threshold">1 s</setting>    </event>

В дальнейшем для этой статьи использовался первый вариант конфигурации без порога по времени.

Анализ результатов в Java Mission Control

Как произвести запись JFR, подробно рассматривалось в предыдущей статье. Сейчас же рассмотрим, как можно проанализировать записанную информацию.

Настройка отображения событий

Прежде всего открываем файл с записью JFR в Java Mission Control:

  1. Переходим в окно Outline.

  2. Ищем в дереве пункт Event Browser.

  3. В основном окне появляется список всех возможных типов событий. Наши события оказываются в самом низу.

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

Для этого щелкаем правой кнопкой мыши по узлу consolidation и из контекстного меню выбираем пункт Create a new page using the selected event types:

Теперь разберёмся с получившимся новым представлением:

  1. Выбираем представление в дереве представлений.

  2. В открывшемся представлении виден раздел фильтра. Этот фильтр определяет набор типов событий, с которыми мы работаем. Видно, что добавились все три наших типа.

  3. Но сейчас нам этот фильтр только мешает, так что его можно убрать с помощью соответствующей кнопки.

  4. Внизу отображается таблица со всеми событиями выбранных типов.

  5. Но нам нужен не список, а графики, так что переключаемся на соответствующую вкладку.

В итоге мы увидим столбчатую диаграмму количества событий всех типов за равные промежутки времени (1). Такая диаграмма бесполезна, так что перенастроим всё под свои нужды. Прежде всего сгруппируем события по типам, нажав на кнопку группировки (2) и выбрав пункт Event Type.

В результате появится раздел группировки со списком отображаемых типов и количеством событий каждого типа в отдельности:

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

Далее уберём диаграмму количества событий, так как она нам не нужна: щёлкнем правой кнопкой по диаграмме и в подменю Show in Bar Chart уберём отметку с пункта Count:

А теперь самое главное: в том же контекстном меню выберем пункт Show Grouping as Span Chart. В результате сформируется диаграмма, показывающая временные отрезки, в течение которых выполнялось каждое отдельно взятое событие:

И вот с этим представлением уже очень удобно работать.

Анализ выполнения отдельного отчёта

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

И сразу правой кнопкой - по нему же (не снимая выделения), а затем выбрать пункт Zoom to Selected Range. Тогда график автоматически отмасштабируется, и станут видны детали в событиях выполнения секций и отдельных вычислений:

Теперь нам стало понятно, на какие вычисления стоит обратить внимание в первую очередь, так как они дольше всего выполняются, и к какой секции и какому отчёту они относятся.

Но не всегда так просто выделить отдельный отчёт, так как приложение многопоточное и множество вычислений может быть запущено одновременно. В результате на данном графике эти события будут накладываться одно на другое, и понять, что происходит, не получится.

В таком случае можно проделать следующее:

  1. Переключиться на табличное представление событий.

  2. В разделе группировки выделить только тип события отчётов. Тогда в таблице останутся события только этого типа.

  3. Далее дважды щёлкаем на столбце Duration, чтобы отсортировать события по убыванию по этому параметру.

  4. И выбираем самое первое событие как самое длительное, а значит, самое интересное для анализа.

  5. Далее - в окне Properties:

  6. Щёлкаем правой кнопкой мыши по пункту Event Thread

  7. И выбираем пункт меню Store and Set As Focused Selection

В результате все события в открытом файле JFR отфильтруются по данному потоку глобально во всех представлениях. Таким образом мы избавимся от перекрывающих друг друга событий и сможем нормально анализировать отдельно взятый отчёт.

Сопоставление с другими метриками JFR

Хорошо. Мы определили самые проблемные вычисления. Но как понять причину их долгого выполнения?

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

Для этого в браузере событий выберем два типа событий: Socket Read и Socket Write. Затем щёлкнем правой кнопкой мыши по выделенным пунктам и выберем Store Selection:

Далее переходим в наше отображение, открываем фильтр событий и из контекстного меню фильтра выбираем: Add Filter from Selection Event Types Tree Selection Event Type is included in set of 2:

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

Но это некорректный фильтр. По этому события надо перетащить в группу к остальным и удалить лишний OR. В результате на диаграмме мы увидим ещё и информацию о том, когда происходил активный сетевой обмен в процессе вычислений (что зачастую и является причиной долгого выполнения):

А так как события чтения и записи из сокета фиксируют ещё и стектрейс, то, выделив участок с большим количеством чтений, мы сможем узнать, какой именно код привёл к этим чтениям:

Часто этого достаточно, чтобы выявить первопричину неоптимального выполнения.

Заключение

В данной статье мы рассмотрели, как можно создавать свои типы событий JFR в приложении и использовать их для анализа поведения приложения. Но это только верхушка айсберга. Очень большое количество аспектов не было рассмотрено. Java Flight Recorder совместно с Java Mission Control представляют из себя весьма мощную пару инструментов, позволяющую анализировать и глубже понимать особенности выполнения приложений. А если приложить немного труда и добавить свои прикладные события, то это может дать безграничные возможности для анализа работы приложения как на тестовых стендах, так и на боевых серверах.

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru