JEP 137: Diagnostic-Command Framework
Фреймворк диагностических команд
| Ответственный | Frederic Parain |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Обсуждение | serviceability dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | M |
| Рецензенты | Mikael Vidstedt |
| Одобрен | Brian Goetz, Paul Hohensee |
| Создан | 2011/11/29 20:00 |
| Обновлён | 2023/11/10 14:59 |
| Задача | 8046127 |
Аннотация
Определить фреймворк для отправки диагностических команд в JVM.
Цели
Предоставить фреймворк, упрощающий реализацию и вызов диагностических команд в процессе JVM. Диагностические команды — это действия, выполняемые внутри виртуальной машины Java, в основном для мониторинга или управления. Диагностические команды должны вызываться локально с помощью новой утилиты командной строки или удалённо через соединение JMX. Фреймворк должен давать диагностическим командам возможность описывать себя: свою семантику, синтаксис и опции.
Что не является целью
Этот JEP касается только фреймворка диагностических команд, а не реализации самих диагностических команд. Реализация новых диагностических команд будет отдельным проектом или будет связана с другими проектами, которые захотят использовать эту возможность.
Мотивация
Такая возможность уже реализована в инструменте JRockit Mission Control и успешно используется командой сопровождения JRockit.
Описание
Работа состоит из двух частей. Первая часть находится в виртуальной машине HotSpot и содержит сам фреймворк с двумя диагностическими командами. Вторая часть находится в JDK и содержит утилиту командной строки для вызова диагностических команд, а также дополнения к HotSpotDiagnosticMXBean, которые позволяют удалённому клиенту обнаруживать и вызывать диагностические команды через соединение JMX.
1 — Фреймворк диагностических команд
1-1 Обзор
Фреймворк диагностических команд полностью реализован в нативном коде и опирается на внутренний механизм исключений HotSpot. Реализация только на нативном коде выбрана для того, чтобы диагностические команды можно было выполнять даже в критических ситуациях, например при нехватке памяти. Все диагностические команды зарегистрированы в едином списке, и два флага управляют тем, как пользователь может с ними взаимодействовать. Флаг hidden (скрытая) не даёт диагностической команде появиться в списке доступных команд, который возвращает команда help. Тем не менее подробную справку по скрытой команде всё же можно получить с помощью синтаксиса help <command name>, но для этого нужно знать имя скрытой команды. Второй флаг — enabled (включена), он определяет, можно ли вызвать команду. При выводе списка командами help отключённые команды отображаются с пометкой [disabled] в описании. Если пользователь пытается вызвать отключённую команду, возвращается сообщение об ошибке, и команда не выполняется. Это сообщение об ошибке можно настроить для каждой команды отдельно. Фреймворк лишь предоставляет эти два флага с их семантикой; никакой политики или механизма для установки или изменения этих флагов он не предоставляет. Эти действия будут делегированы JVM или конкретным диагностическим командам.
1-2 Реализация
Все диагностические команды реализованы как подклассы класса DCmd, определённого в services/diagnosticFramework.hpp. Ниже приведена структура класса DCmd и список методов, которые новая команда должна определить или переопределить:
class DCmd {
DCmd(outputStream *output);
static const char *get_name();
static const char *get_description();
static const char *get_disabled_message();
static const char *get_impact();
static int get_num_arguments();
virtual void print_help(outputStream* out);
virtual void parse(CmdLine* line, char delim, TRAPS);
virtual void execute(TRAPS);
virtual void reset(TRAPS);
virtual void cleanup();
virtual GrowableArray<const char *>* get_argument_name_array();
virtual GrowableArray<DCmdArgumentInfo*>* get_argument_info_array();
}
Диагностическая команда всегда создаётся с параметром outputStream. Этот outputStream может указывать на файл, буфер или сокет (см. файл ostream.hpp).
Метод get_name() возвращает строку, которая идентифицирует команду (то есть строку, которую нужно указать в командной строке, чтобы её вызвать).
Метод get_description() возвращает общее описание команды.
Метод get_disabled_message() возвращает настраиваемое сообщение, которое выдаётся, когда команда отключена, без необходимости создавать экземпляр команды.
Метод get_impact() возвращает описание того, насколько сильно диагностическая команда вмешивается в поведение виртуальной машины Java. Этот метод нужен потому, что некоторые диагностические команды могут серьёзно нарушить поведение виртуальной машины Java (например, дамп потоков для приложения с несколькими десятками тысяч потоков или дамп кучи размером более 40 ГБ), тогда как другие диагностические команды не оказывают на JVM серьёзного влияния (например, получение аргументов командной строки или версии JVM). Рекомендуемый формат описания — <impact level>: [longer description], где уровень влияния выбирается из набора {low, medium, high}. Необязательное более длинное описание может содержать более конкретные подробности, например то, что влияние дампа потоков зависит от размера кучи.
Метод get_num_arguments() возвращает количество опций/аргументов, которые распознаёт диагностическая команда. Этот метод используется только поддержкой интерфейса JMX (см. ниже).
Метод print_help() выводит подробную справку в аргумент outputStream. Подробная справка содержит список всех поддерживаемых опций с их типами и описаниями.
Метод parse() отвечает за разбор аргументов команды. Каждая команда может реализовать собственный парсер аргументов. Тем не менее для упрощения реализации предоставляется фреймворк парсера аргументов (см. раздел 1-3), но использовать его не обязательно. Метод parse принимает в аргументе символ-разделитель, который обозначает границу между двумя аргументами. Как правило, при вызове из jcmd разделителем будет пробел, а при вызове из кода разбора командной строки JVM — запятая.
Метод execute(), естественно, вызывается для выполнения диагностической команды. Методы parse() и execute() разделены, поэтому можно выполнить разбор аргументов в одном потоке и делегировать выполнение другому потоку, если диагностическая команда не обращается к локальным переменным потока. Фреймворк позволяет параллельно выполнять несколько экземпляров одной и той же диагностической команды. Если по какой-то причине для данной диагностической команды параллельное выполнение недопустимо, соблюдение этого правила обеспечивает разработчик диагностической команды, например защищая тело метода execute() глобальной блокировкой.
Метод reset() используется для инициализации внутренних полей диагностической команды или для сброса внутренних полей к начальным значениям, чтобы можно было повторно использовать уже выделенный экземпляр диагностической команды.
Метод cleanup() используется для очистки, например для освобождения всей памяти, выделенной для хранения внутренних данных. Класс DCmd расширяет класс ResourceObj, поэтому при выделении в ResourceArea деструкторы нельзя использовать для очистки. Чтобы очистка выполнялась во всех случаях, рекомендуется создавать экземпляр DCmdMark для каждого экземпляра DCmd. DCmdMark — это объект, выделяемый на стеке, с указателем на экземпляр DCmd. Когда DCmdMark уничтожается, его деструктор вызывает метод cleanup() экземпляра DCmd, на который он указывает. Если экземпляр DCmd был выделен в C-куче, DCmdMark также освобождает память, выделенную для хранения экземпляра DCmd.
Методы get_argument_name_array() и get_argument_info_array() относятся к интерфейсу JMX фреймворка диагностических команд, поэтому они описаны в разделе 3.
1-3 Фреймворк DCmdParser
Класс DCmdParser — это необязательный фреймворк, помогающий разрабатывать парсеры аргументов. Он предоставляет многие возможности, необходимые фреймворку диагностических команд, например формирование справки или описаний аргументов для интерфейса JMX, но все эти возможности легко реализовать заново, если разработчик решит не использовать фреймворк DCmdParser.
Класс DCmdParser опирается на шаблон DCmdArgument. Этот шаблон нужно использовать для определения различных типов аргументов, которые должен обрабатывать парсер. При создании новой специализации шаблона нужно предоставить три метода:
void parse_value(const char *str,size_t len,TRAPS);
void init_value(TRAPS);
void destroy_value();
Метод parse_value() используется для преобразования строки в значение аргумента. Метод print_value() используется для вывода значения по умолчанию (для подробной справки). Метод init_value() используется для инициализации или сброса значения аргумента. Метод destroy_value() — это метод очистки, полезный, когда аргумент выделил память в C-куче для хранения своего значения и эту память нужно освободить до уничтожения экземпляра DCmdArgument.
DCmdParser различает опции и аргументы. Опции идентифицируются по имени ключа, который должен присутствовать в командной строке, а аргументы — только по своей позиции в командной строке. Опции используют синтаксис <key>=<value>. Для булевых опций часть синтаксиса =<value> можно опустить, чтобы установить опции значение true. Аргументы — это просто последовательности символов, разделённые символом-разделителем. Этот разделитель можно задать во время выполнения при вызове фреймворка диагностических команд. Если аргумент содержит символ, который может использоваться как разделитель, аргумент можно заключить в одинарные или двойные кавычки. Опции и аргументы создаются с помощью одного и того же класса DCmdArgument, но регистрируются в DCmdParser по-разному. Чтобы использовать DCmdParser, нужно объявить парсер и опции/аргументы как поля класса диагностической команды, который сам является подклассом класса DCmd, например так:
class EchoDCmd : public DCmd {
protected:
DCmdParser _dcmdparser;
DCmdArgument<jlong> _required;
DCmdArgument<jlong> _intval;
DCmdArgument<bool> _boolval;
DCmdArgument<char *> _stringval;
DCmdArgument<char *> _first_arg;
DCmdArgument<jlong> _second_arg;
DCmdArgument<char *> _optional_arg;
}
Парсер и опции/аргументы должны быть инициализированы раньше класса диагностической команды, а опции/аргументы нужно зарегистрировать в парсере, например так:
EchoDCmd(outputStream *output) : DCmd(output),
_stringval("-strval","a string argument","STRING",false),
_boolval("-boolval","a boolean argument","BOOLEAN",false),
_intval("-intval","an integer argument","INTEGER",false),
_required("-req","a mandatory integer argument","INTEGER",true),
_fist_arg("first argument","a string argument","STRING",true),
_second_arg("second argument,"an integer argument,"INTEGER",true),
_optional_arg("optional argument","an optional string argument",
"STRING","false")
{
_dcmdparser.add_dcmd_option(&_stringval);
_dcmdparser.add_dcmd_option(&_boolval);
_dcmdparser.add_dcmd_option(&_intval);
_dcmdparser.add_dcmd_option(&_required);
_dcmdparser.add_argument(&_first_arg);
_dcmdparser.add_argument(&_second_arg);
_dcmdparser.add_argument(&_optional_arg);
};
Метод add_dcmd_argument()/add_dcmd_option() используется для добавления аргумента/опции в парсер. Конструктор опции/аргумента принимает имя опции/аргумента, её описание, строку, описывающую её тип, и булево значение, указывающее, обязательна ли опция/аргумент. Парсер не поддерживает дубликаты опций/аргументов (с одинаковым именем), но код сейчас не проверяет наличие дубликатов. Порядок регистрации опций не влияет на парсер. Однако порядок регистрации аргументов крайне важен, потому что парсер будет использовать тот же порядок при разборе командной строки. В приведённом выше примере парсер ожидает первый аргумент типа STRING (разбирается с помощью _first_arg), затем второй аргумент типа INTEGER (разбирается с помощью _second_arg) и, необязательно, третий параметр типа STRING (разбирается с помощью _optional_arg). Обязательную опцию или аргумент нужно указывать при каждом вызове команды. Если она отсутствует, по окончании разбора выбрасывается исключение. Необязательные аргументы нужно регистрировать после обязательных. Необязательный аргумент будет учитываться при разборе, только если все аргументы перед ним, обязательные или нет, уже были использованы для разбора командной строки.
DCmdParser и его экземпляры DCmdArgument встроены в экземпляр DCmd. Такая архитектура выбрана, чтобы ограничить количество выделений памяти в C-куче, а также чтобы можно было заранее выделять экземпляры диагностических команд для критических ситуаций. Если процессу не хватает места в C-куче, создать новые диагностические команды для диагностики ситуации невозможно. Если заранее выделить некоторые диагностические команды, их можно будет выполнить даже в такой критической ситуации. Разумеется, сама диагностическая команда не должна пытаться выделять память во время выполнения; поэтому диагностическая команда не может использовать аргументы переменной длины, например строки. Заранее выделенные диагностические команды по своей природе рассчитаны на повторное использование; для этого и предназначен метод reset(), который возвращает все аргументы в состояние по умолчанию.
1-4 Внутренний вызов
Использовать диагностическую команду из самой JVM довольно просто: нужно создать экземпляр класса и вызвать метод parse(), а затем метод execute(). Экземпляр диагностической команды можно создать внутри JVM, даже если команда не зарегистрирована. В этом отличие от внешних вызовов (из jcmd или через JMX), для которых команда должна быть зарегистрирована.
2 — Утилита jcmd
Диагностические команды можно также вызывать извне процесса JVM с помощью новой утилиты jcmd. Программа jcmd использует attach API, чтобы подключаться к JVM, отправлять запросы и получать результаты. Утилиту jcmd нужно запускать на той же машине, на которой работает JVM. При запуске без аргументов jcmd выводит список всех JVM, работающих на машине. Исходный код jcmd находится в репозитории JDK, как и у других существующих инструментов j*.
Чтобы выполнить диагностическую команду в конкретной JVM, используется следующий общий синтаксис:
jcmd <pid_of_the_jvm> <command_name> [arguments]
attachListener изменён так, чтобы распознавать запросы jcmd. Когда запрос jcmd распознан, он разбирается, чтобы извлечь имя команды. JVM ищет эту команду в списке зарегистрированных команд. Чтобы диагностическую команду можно было выполнить по внешнему запросу, она должна быть зарегистрирована. Регистрация выполняется с помощью класса DCmdFactory (см. services/management.cpp).
3 — Интерфейс JMX
Фреймворк предоставляет интерфейс к диагностическим командам на основе JMX. Этот интерфейс позволяет удалённо вызывать диагностические команды через JMX-соединение.
3-1 Интерфейс
Информация о диагностических командах доступна через методы, добавленные в класс com.sun.management.HotspotDiagnosticMXBean:
public List<String> getDiagnosticCommands();
public DiagnosticCommandInfo getDiagnosticCommandInfo(String command);
public List<DiagnosticCommandInfo>
getDiagnosticCommandInfo(List<String> command);
public List<DiagnosticCommandInfo> getDiagnosticCommandInfo();
public String execute(String commandLine) throws IllegalArgumentException;
public String execute(String cmd, String ... arguments)
throws IllegalArgumentException;
Метод getDiagnosticCommands() возвращает массив с именами зарегистрированных нескрытых диагностических команд.
Три метода getDiagnosticCommandInfo() возвращают одно или несколько описаний диагностических команд в виде класса DiagnosticCommandInfo.
Два метода execute() позволяют пользователю вызывать диагностическую команду разными способами.
Класс DiagnosticCommandInfo описывает диагностическую команду с помощью следующей информации:
public class DiagnosticCommandInfo {
public String getName();
public String getDescription();
public String getImpact();
public boolean isEnabled();
public List<DiagnosticCommandArgumentInfo> getArgumentsInfo();
}
Метод getName() возвращает имя диагностической команды. Именно это имя нужно использовать в методах execute() для вызова диагностической команды.
Метод getDescription() возвращает общее описание диагностической команды.
Метод getImpact() возвращает описание степени вмешательства диагностической команды.
Метод isEnabled() возвращает true, если метод включён, и false, если он отключён. Отключённый метод выполнить нельзя.
getArgumentsInfo() возвращает список описаний опций или аргументов, которые распознаёт диагностическая команда. Каждая опция/аргумент описывается экземпляром DiagnosticCommandArgumentInfo:
public class DiagnosticCommandArgumentInfo {
public String getName();
public String getDescription();
public String getType();
public String getDefault();
public boolean isMandatory();
public boolean isOption();
public int getPosition();
}
Если экземпляр DiagnosticCommandArgumentInfo описывает опцию, isOption() возвращает true, а getPosition() возвращает -1. В противном случае, когда экземпляр DiagnosticCommandArgumentInfo описывает аргумент, isOption() возвращает false, а getPosition() возвращает ожидаемую позицию этого аргумента. Позиция аргумента определяется относительно всех аргументов, переданных в командной строке; опции при определении позиции аргумента не учитываются. Метод getDefault() возвращает значение аргумента по умолчанию, если оно было задано, иначе возвращает null.
3-2 Реализация
Фреймворк спроектирован так, чтобы разработчикам диагностических команд не приходилось заботиться об интерфейсе JMX. Помимо методов, описанных в разделе 1-2, разработчик диагностической команды должен предоставить три метода:
int get_num_arguments()
который возвращает количество опций и аргументов, поддерживаемых командой;
GrowableArray<const char *>* get_argument_name_array()
который предоставляет имена аргументов, поддерживаемых командой; и
GrowableArray<DCmdArgumentInfo*>* get_argument_info_array()
который предоставляет описание каждого аргумента в виде экземпляра DCmdArgumentInfo. DCmdArgumentInfo — это класс C++, с помощью которого фреймворк создаёт экземпляры sun.com.management.DcmdArgumentInfo. Это происходит автоматически, поэтому разработчику диагностической команды не нужно знать, как создавать Java-объекты из среды выполнения.
4 — Диагностические команды
Чтобы избежать конфликтов имён между диагностическими командами из разных проектов, следует избегать плоского пространства имён; рекомендуется более структурированная организация. Сам фреймворк от этой организации не зависит, поэтому она будет набором правил, задающих соглашение об именовании команд.
Диагностические команды легко организовать иерархически, поэтому шаблон имени команды может быть таким:
<domain>.[sub-domain.]<command>
Этот шаблон можно расширять подподдоменами и так далее.
Особый набор команд без доменов будет зарезервирован для команд, относящихся к самому диагностическому фреймворку, например для команды «help».
Альтернативы
В HotSpot уже есть ряд инструментов для диагностики проблем (jps, jstack, jinfo и т. д.), но большинство из них не поддерживаются и могут вызываться только локально. Этот фреймворк даёт возможность объединить все диагностические команды в едином фреймворке и добавить поддержку удалённого вызова.
Тестирование
Вместе с фреймворком поставляются две диагностические команды, которые будут использоваться для разработки модульных тестов (интегрированных в репозиторий JDK). Разрабатываются дополнительные тесты.