Мониторинг работы Java-приложений: Часть 1. Мониторинг производительности и степени готовности Java-систем

Подходы и принципы

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

Николас Уайтхед, старший технический архитектор, ADP

Николас Уайтхед (Nicholas Whitehead) занимает должность старшего технического архитектора в подразделении ADP под названием Small Business Services в Флорхэм Парк, штат Нью-Джерси. Он имеет более чем десятилетний опыт разработки Java-приложений в таких областях, как инвестиционная банковская деятельность, электронная коммерция и коммерческое программное обеспечение. Накопленный опыт развертывания и поддержки приложений (в том числе созданных собственноручно) подтолкнул его к изучению и реализации систем мониторинга производительности.



02.03.2009

Введение

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

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

В первой части:

  • рассказывается о свойствах систем для управления производительностью приложений (Application Performance Management Systems – APM);
  • описываются распространенные, но нежелательные подходы к мониторингу приложений;
  • описываются методы мониторинга производительности виртуальных Java-машин (JVM);
  • Предлагаются варианты эффективного инструментирования исходного кода приложения

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


Распространенные удачные и неудачные варианты реализации APM-систем

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

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

Распространенные недостатки мониторинга

Редкое приложение не обладает вообще никакими средствами мониторинга. Однако на практике часто встречаются следующие недостатки:

Проблемы фрагментированного мониторинга

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

Анализируя данные своей системы мониторинга, владельцы приложения видят, что со стороны их сервера открыто 100 соединений с базой данных. В свою очередь, администратор базы данных (DBA) через консоль администрирования видит, что со стороны данного компьютера открыто 120 соединений, причем их число стремительно возрастает. Для консолидированной APM-системы не должно составлять трудностей построить график, совмещающий обе эти картины. В момент, когда данные о числе соединений начинают расходиться, любой человек при взгляде на график должен, во-первых, немедленно понять, какое значение показателя является верным, а во-вторых, получить достаточно точное представление о причине проблемы.

  • Мертвые зоны: некоторые компоненты, от которых зависит приложение, не подвергаются мониторингу, либо данные мониторинга недоступны. Например, мониторинг может осуществляться над базой данных, а не над сетью, по которой происходит обращение. В этом случае может быть проблематично обнаружить сбои в работе сети, так как специалисты по устранению проблем будут заняты исследованием производительности базы данных и симптомов сервера приложений.
  • Черные ящики: само приложение либо один из внешних компонентов не позволяют осуществлять мониторинг своих внутренних процессов. Примером подобного черного ящика может служить JVM. Например, специалисты, исследующие причины необъяснимой латентности внутри JVM, которая предоставляет только статистику операционной системы (загрузку процессора или объем памяти, занимаемой процессом), могут иметь трудности при выявлении проблем, связанных со сборкой мусора или синхронизацией потоков.
  • Несовместимые или несвязные системы мониторинга: приложение может выполняться внутри крупного вычислительного центра, обращаясь к большому количеству общих ресурсов, таких как базы данных, высокоскоростные сети хранения данных (storage-area networks – SAN), а также системы передачи сообщений и различное промежуточное программное обеспечение. Кроме того, компании часто обладают сложной структурой, в которой каждая группа использует собственные системы мониторинга и APM (см. заметку «Проблемы фрагментированного мониторинга»). Если не обеспечить консолидированное представление каждого из внешних компонентов, то каждая группа будет иметь доступ только к небольшому фрагменту общей картины работы приложения.

    Сравнение консолидированных и фрагментированных ARM-систем представлено на рисунке 1.

    Рисунок 1. Сравнение фрагментированной и консолидированной APM-систем
    Siloed vs. consolidated APM systems
  • Построение отчетов и корреляций пост-фактум: в попытке решить проблемы, связанные с фрагментированным мониторингом, группа поддержки может периодически запускать процессы сбора данных от различных источников, консолидировать их, а затем строить сводные отчеты. Этот подход часто бывает неэффективен и непрактичен в условиях высокой частоты обновления данных, а отсутствие доступа к консолидированным данным в режиме реального времени снижает быстроту диагностирования проблем. Кроме того, данные, полученные в результате пост-фактум агрегирования, могут быть недостаточно детальными, из-за чего могут возникать трудности с выявлением важных зависимостей. Например, из отчета может следовать, что время обращения к некоторому сервису за прошедший день в среднем составляло 200 миллисекунд, но при этом ничто не будет говорить о том, что между 13:00 и 13:45 часами время вызова превышало 3500 миллисекунд.
  • Периодический мониторинг или мониторинг по требованию: существуют средства мониторинга, работа которых требует повышенных затрат ресурсов, поэтому они не могут (или не должны) выполняться постоянно. Таким образом, сбор данных происходит редко, либо только в случае обнаружения проблем. В результате ARM-система выполняет лишь минимальный базовый мониторинг и оказывается не в состоянии оповестить вас о проблеме, пока та не приведет к отказу системы, а также может сама ухудшить состояние системы.
  • Не сохранение данных мониторинга: многие средства мониторинга строят полезную картину производительности и степени готовности приложения, однако они либо не настроены, либо просто не поддерживают возможности сохранения данных для анализа за короткие и длительные промежутки времени. Зачастую данные о производительности, представленные вне временного контекста, оказываются малополезными (а то и вовсе бесполезными), так как непонятно, на основании чего можно судить о том, являются показатели хорошими, плохими или критически плохими. Например, допустим, что уровень загрузки процессора составляет 45%. Если не знать, какое число характерно для периодов высокой или низкой нагрузки на систему в прошлом, то данный показатель будет значительно менее информативен, чем при наличии данных о том, что типичное значение уровня загрузки – x%, а предельное, при котором обеспечивается допустимая производительность обслуживания клиентов – y%.
  • Привычка полагаться на предэксплуатационное моделирование: привычка полагаться на мониторинг и моделирование системы исключительно перед вводом ее в эксплуатацию с надеждой устранить все потенциальные проблемы перед развертыванием часто приводит к недостаточному мониторингу на этапе выполнения. Кроме того, подобные надежды разбиваются о всевозможные непредсказуемые явления и сбои в работе внешних компонентов, в результате чего специалистам по устранению проблем приходится работать в условиях отсутствия средств и данных мониторинга.

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

Атрибуты идеальной APM-системы

Отрицательным принципам, перечисленным выше, можно противопоставить набор следующих свойств, которыми обладает идеальная ARM-система, описываемая в данной статье:

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

Перед тем как углубиться в детали реализации, стоит обратиться к некоторым общим аспектам APM-систем.


Базовые понятия APM-систем

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

Источник данных о производительности

Источник данных о производительности (Performance Data Source - PDS) предоставляет информацию о быстродействии или уровне готовности компонента приложения, позволяющую делать выводы о его работоспособности. Например, сервисы JMX (Java Management Extensions), как правило, предоставляют обширный набор данных о работоспособности JVM. Подобная информация о большинстве реляционных баз данных доступна через SQL-интерфейс. Оба этих источника данных являются примерами непосредственных PDS, т.е., другими словами, данные передаются непосредственно через них. Другой разновидностью PDS являются косвенные источники, данные которых вычисляются на основе выполнения намеренного или случайного действия. Например, производительность сервера службы сообщений Java (JMS) может измеряться на основе времени отправки и получения периодически генерируемых тестовых сообщений.

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

Сбор данных и объекты-коллекторы

Сбор данных – это процесс получения информации о производительности или степени готовности приложения от PDS. При работе с непосредственным PDS объект-коллектор (далее просто коллектор), как правило, реализует програмнные интерфейсы, необходимые для доступа к данным. Например, для получения статистической информации с сетевого принтера коллектор должен использовать протоколы Telnet или SNMP (простой протокол управления сетью). В случае работы с косвенным PDS коллектор выполняет необходимые действия и производит измерения для получения необходимых данных.

Трассировка и трассировщики

Трассировка – это процесс передачи измерений от коллектора в ядро APM-системы. Многие коммерческие и открытые APM-системы предоставляют программные интерфейсы для этой цели. В качестве примеров к данной статье я реализовал интерфейс Java-трассировщика общего назначения, который мы более подробно рассмотрим в следующем разделе.

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

Рисунок 2. Место сбора и трассировки данных в общей архитектуре APM-системы
Collecting, tracing, and the APM system

На рисунке 2 также показана часть сервисов, которые, как правило, предоставляются APM-системами:

  • Динамическая визуализация: графики и диаграммы, отображающие динамику выбранных показателей в режиме реального времени.
  • Построение отчетов: генерирование отчетов об изменении показателей. Как правило, системы позволяют строить стандартные и специализированные отчеты, а также экспортировать данные для использования вне APM.
  • Хранение истории данных: поддержка хранилища данных, в котором содержится первичная или суммарная информация. Благодаря этому средства визуализации и построения отчетов могут выдавать результаты за конкретный временной интервал.
  • Система оповещений: сервис уведомления заинтересованных лиц или групп о выполнении определенного условия, которое проверяется на основе собранных показателей. Обычно подобные сервисы позволяют автоматически посылать сообщения по электронной почте, но также могут предоставлять интерфейсы, позволяющие специалистам службы поддержки передавать события в системы для их обработки.

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


Интерфейс для трассировки ITracer

Язык Java хорошо подходит для реализации коллекторов по следующим причинам:

  • Поддержка большого числа платформ. Java-классы коллекторов могут быть запущены без изменений на большинстве платформ. Благодаря этому архитектура системы мониторинга может выстраиваться таким образом, чтобы коллекторы располагались непосредственно рядом с источниками данных, сводя тем самым на нет необходимость удаленного сбора информации.
  • Отличная производительность в большинстве ситуаций (хотя она и зависит от располагаемых ресурсов).
  • Поддержка параллельного выполнения и асинхронных вызовов.
  • Поддержка большого набора коммуникационных протоколов.
  • Большое число сторонних API, таких как реализации JDBC, SNMP и проприетарных Java-интерфейсов, позволяющих создавать разнообразные коллекторы.
  • Поддержка со стороны активного сообщества разработчиков приложений с открытым исходным кодом, среди которых есть дополнительные средства и реализации Java-интерфейсов, позволяющих получать доступ к огромному количеству разнообразных источников данных.

При этом необходимо помнить, что ваши Java-коллекторы должны быть совместимы с программными интерфейсами трассировки, которые предоставляются вашей APM-системой. Некоторые из перечисленных выше факторов продолжают играть важную роль, даже если ваш механизм трассировки не предоставляет Java-интерфейсов. Однако в случае, если целевые источники данных выполнены в виде Java-сервисов (таких как, например, JMX), а целевое приложение написано не на Java, понадобятся промежуточные интерфейсы, такие как IKVM, который представляет собой компилятор Java-кода для платформы .NET (см. Ресурсы).

На данный момент не существует единого стандарта для API трассировки, поэтому все APM-системы предоставляют свои варианты интерфейса. В целях абстрагирования от конкретной реализации мы создадим Java-интерфейс общего назначения org.runtimemonitoring.tracing.ITracer. Интерфейс ITracer является оберткой над проприетарными интерфейсами трассировки, что, во-первых, помогает оградить исходный код коллекторов от последствий изменений в API, а во-вторых, позволяет реализовать дополнительные функции, не предоставляемые интерфейсами. В большинстве примеров к данной статье используются реализации самого интерфейса ITracer, а также поддерживаемых им концепций.

На рисунке 3 представлена UML-диаграмма классов, относящихся к интерфейсу org.runtimemonitoring.tracing.ITracer.

Рисунок 3. ИнтерфейсITracer и класс-фабрика
ITracer interface and factory class

Имена и категории при трассировке

Основной задачей ITracer является передача результатов измерений и ассоциированного с ними имени в центральную APM-систему. Это реализуется при помощи методов trace, каждый из которых соответствует своему типу измерений. Все методы принимают на вход параметр String[] name, в котором хранятся контекстные компоненты составного имени измерения. Структура данного имени определяется используемой APM-системой. В нем указывается как пространство имен, так и название самой метрики, значение которой передается APM-системе. Таким образом, структура составного имени, как правило, включает как минимум два компонента: корневую категорию и описание измерения. Класс, реализующий интерфейс ITracer, должен уметь преобразовывать переданный строковый массив в составное имя. Примеры двух соглашений о структуре составных имен приведены в таблице 1.

Таблица 1. Примеры составных имен
Структура имениПример составного имени
Простая со слэш-разделителямиHosts/SalesDatabaseServer/CPU Utilization/CPU3
JMX MBean ObjectNamecom.myco.datacenter.apm:type=Hosts,service=SalesDatabaseServer,group=CPU Utilization,instance=CPU3

Укороченный пример использования методов трассировки через данный интерфейс приведен в листинге 1.

Листинг 1. Пример вызова методов трассировки
ITracer simpleTracer = TracerFactory.getInstance(sprops);
ITracer jmxTracer = TracerFactory.getInstance(jprops);
.
.
simpleTracer.trace(37, "Hosts", "SalesDatabaseServer",
   "CPU Utilization", "CPU3", "Current Utilization %");
jmxTracer.trace(37, 
   "com.myco.datacenter.apm", 
   "type=Hosts", 
   "service=SalesDatabaseServer", 
   "group=CPU Utilization", 
   "instance=CPU3", "Current Utilization %");
);

Типы трассируемых данных

Данный интерфейс поддерживает трассировку результатов измерений, описываемых следующими типами данных:

  • int
  • long
  • java.util.Date
  • String

Сама APM-система может поддерживать другой набор типов данных для собираемой информации.

Режимы трассировки

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

Интервалы

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

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

  • Средняя длительность операции за интервал
  • Максимальная длительность операции за интервал
  • Минимальная длительность операции за интервал
  • Число выполнений за интервал
  • Точные временные границы интервала

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

В ITracer поддерживаются следующие режимы трассировки:

  • Усредненная в интервале: методы trace(long value, String[] name) и trace(int value, String[] name) служат для трассировки усредненных значений в заданном интервале (см. заметку "Интервалы"). Это означает, что каждое измерение учитывается при подсчете среднего значения для текущего интервала. При начале нового интервала текущее среднее значение сбрасывается в ноль.
  • Закрепленная (sticky): методы traceSticky(value long, String[] name) и traceSticky(value int, String[] name) служат для трассировки закрепленных значений. В отличие от усредненных метрик, при данной трассировке значения могут сохраняться в течение нескольких интервалов. Например, если вызвать метод для трассировки значения 5 и не вызывать его затем в течение суток, то значение метрики останется равным 5, пока не будут переданы новые данные.
  • Дельта-трассировка: методы дельта-трассировки получают на вход число, однако результатом, передаваемым в APM-систему, является разница (дельта) между текущим и предыдущим результатами измерения. Данный тип также называют скоростной трассировкой, что отражает область ее применения. Например, допустим, что подсчитывается общее число подтвержденных транзакций. Данный показатель монотонно возрастает и, скорее всего, его абсолютное значение не будет представлять интереса. Гораздо полезнее знать скорость, с которой он возрастает. Для этого показания об абсолютном числе транзакций снимаются с фиксированной частотой, при этом значения дельты соседних измерений характеризуют скорость возрастания. Дельта-трассировка делится на два подтипа: усредненная в интервале и закрепленная, хотя первая из них используется достаточно редко. При использовании дельта-трассировки важно различать показатели, которые могут только монотонно возрастать, от тех, которые могут убывать. Значения, которые оказываются меньше предыдущих, должны либо игнорироваться, либо приводить к сбросу дельты.
  • Инцидентная трассировка: данный режим представляет собой простую, неагрегируемую метрику, которая подсчитывает, сколько раз то или иное событие произошло за указанный интервал времени. Метод traceIncident(String[] name) не принимает на вход значение, поскольку и коллектор, и трассировщик могут не знать общее число возникновений в конкретный момент времени. Поэтому каждый вызов считается сигналом о единичном возникновении события. Кроме того, есть метод traceIncident(int value, String[] name), который увеличивает значение счетчика на значение параметра value. Таким образом, не приходится вызывать первый метод в цикле в случае, если надо зарегистрировать несколько событий.
  • Умная трассировка - это параметризованный вариант трассировки, позволяющий использовать трассировку в одном из перечисленных выше режимов в зависимости от строкового параметра, который может принимать значения, определенные в виде констант в интерфейсе. Значение измерения передается в метод дополнительным параметром. Этот вариант удобен в ситуации, когда коллектор не знает типа данных или трассировки для собираемой информации. В этом случае он просто передает трассировщику само значение и сконфигурированный заранее режим трассировки.

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

Принципы работы коллекторов

Коллекторы, как правило, работают по одной из трех схем, приведенных ниже. Выбранная схема оказывает влияние на выбор типа трассировки.

  • Опрашивание: коллектор вызывается через регулярные промежутки времени и опрашивает текущие значения метрики (или набора метрик), соответствующей данному PDS. Например, коллектор может вызываться ежеминутно для чтения показателей загрузки процессора или общего числа подтвержденных транзакций через JMX-интерфейс. Целью этой схемы является периодический подсчет значений целевой метрики. Таким образом, после вызова коллектора значение метрики передается в APM-систему, и оно считается неизменным до следующего опроса. Вследствие этого коллекторы, работающие по данной схеме, как правило, используют закрепленные виды трассировки. В результате в отчетах APM-системы данное значение будет показано как неизменное между вызовами коллектора. Этот принцип иллюстрируется на рисунке 4.
    Рисунок 4. Схема работы опрашивающего коллектора
    Polling collection pattern
  • Прослушивание: эта схема работы соответствует паттерну «наблюдатель». Коллектор регистрируется в PDS в качестве слушателя событий. При наступлении события выполняется метод коллектора, представляющий собой функцию обратного вызова. Результат работы данного метода будет передан трассировщику. Методы обратного вызова могут возвращать различные результаты, но в любом случае можно осуществлять инцидентную трассировку, подсчитывающую число наступлений события. Данная схема работы показана на рисунке 5.
    Рисунок 5. Схема работы коллектора-слушателя
    Listening collection pattern
  • Перехват: при данной схеме работы коллектор внедряется в виде перехватчика обращений вызывающей стороны к целевому объекту. Таким образом, каждый вызов перехватывается коллектором, который производит измерения и передает их трассировщику. В случае, если взаимодействие производится по схеме запрос/ответ, коллектор может измерять число запросов, время ответа, а также отслеживать информационное наполнение запросов или ответов. Примером такого коллектора может служить прокси-сервер HTTP, способный выполнять следующие измерения:
    • число запросов. При необходимости подсчет может вестись раздельно по типам (GET, POST и т.д.) или по универсальным идентификаторам ресурсов (URI);
    • время ответов на запросы;
    • размер запросов и ответов.
    Коллектор будет перехватывать все события, поэтому трассировка, как правило, должна осуществляться в режиме усреднения по интервалам. Если в интервале не зафиксировано ни одного события, то результатом усреднения будет ноль вне зависимости от того, каков был результат предыдущего интервала. Иллюстрация данной схемы работы коллектора показана на рисунке 6.
    Рисунок 6. Схема работы коллектора-перехватчика
    Intercepting collection pattern

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


Мониторинг JVM

JVM сама по себе является подходящей системой для мониторинга. Мы начнем с показателей производительности, характерных для всех JVM, а затем перейдем к внутренним компонентам JVM, которые, как правило, используются в корпоративных приложениях. За небольшим исключением, экземпляры Java-приложений представляют собой процессы, выполняющиеся внутри операционной системы (ОС), поэтому некоторые аспекты мониторинга JVM удобнее всего рассматривать именно в разрезе используемой ОС (эти вопросы будут освещены в третьей статье серии).

До выхода пятой версии стандартной редакции платформы Java (Java SE 5) стандартные средства JVM внутренней диагностики, способные эффективно и безотказно собирать данные, были достаточно ограничены. В настоящее время ряд возможностей мониторинга предоставляется через стандартный интерфейс java.lang.management, поддерживаемый всеми JVM, реализующими Java SE версии 5 и выше. Некоторые реализации JVM предоставляют дополнительные проприетарные метрики, но доступ к ним осуществляется аналогично стандартным интерфейсам. Ниже будут рассматриваться стандартные показатели производительности, доступные через объекты MXBeans – JMX-объекты MBeans (см. Ресурсы), размещенные внутри виртуальной машины и выставляющие наружу интерфейсы для управления и мониторинга. Основными из них являются следующие:

  • ClassLoadingMXBean: мониторинг системы загрузки классов.
  • CompilationMXBean: мониторинг системы компиляции.
  • GarbageCollectionMXBean: мониторинг работы сборщиков мусора JVM.
  • MemoryMXBean: мониторинг кучи и остальной памяти JVM.
  • MemoryPoolMXBean: мониторинг пулов памяти, выделенных JVM.
  • RuntimeMXBean: мониторинг системы времени выполнения. Данный объект предоставляет небольшое число метрик, однако, с его помощью можно получить информацию о параметрах запуска JVM, а также о времени запуска и продолжительности работы. Эти данные могут оказаться полезными при анализе других показателей.
  • ThreadMXBean: мониторинг системы управления потоками.

JMX-коллектор работает следующим образом: сначала он получает ссылку на объект MBeanServerConnection, который способен считывать значения атрибутов объектов MBeans, развернутых внутри JVM. Получив значения атрибутов, коллектор трассирует их при помощи реализации интерфейса ITracer. Главным моментом при использовании данного коллектора является вопрос о его размещении. Существуют два варианта: локальное развертывание и удаленное развертывание.

Ограничение прав доступа

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

При локальном развертывании как сам коллектор, так и планировщик, отвечающий за вызов коллектора, размещаются внутри целевой JVM. JMX-коллектор обращается к объектам MXBean через экземпляр PlatformMBeanServer, который представляет собой статический объект MBeanServerConnection внутри JVM. При удаленном развертывании коллектор запускается в отдельном процессе и соединяется с целевой JVM средствами JMX Remoting или его разновидности. Этот подход может быть менее эффективен, чем локальное развертывание, однако он не требует размещения дополнительных компонентов внутри JVM. Для соединения с JVM при помощи JMX Remoting достаточно развернуть в ней компонент RMIConnectorServer или включить поддержку внешних соединений (см. Ресурсы). Более подробное обсуждение средств JMX Remoting выходит за рамки этой статьи.

Пример JMX-коллектора

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

  • Получение экземпляра MBeanServerConnection для соединения с объектом MBeanServer, являющимся локальным для данной JVM. Соединение осуществляется при помощи метода java.lang.management.ManagementFactory.getPlatformMBeanServer().
  • Получение экземпляра MBeanServerConnection для соединения с вторичным объектом MBeanServer, также являющимся локальным для данной JVM. Соединение осуществляется при помощи метода javax.management.MBeanServerFactory.findMBeanServer(String agentId). Заметим, что в одной JVM могут быть размещены несколько объектов MBeanServer. Более сложные системы, например, серверы Java EE (Enterprise Edition), в дополнение к основному объекту MBeanServer, как правило, содержат дополнительный экземпляр, относящийся к конкретному приложению (см. заметку "Кросс-регистрация объектов MBean").
  • Получение ссылки на удаленный объект MBeanServerConnection при помощи стандартного интерфейса RMI (для адресации служит класс javax.management.remote.JMXServiceURL).

Сокращенный вариант метода collect() класса JMXCollector приведен в листинге 2. Метод служит для сбора и трассировки данных об управлении потоками и использует объект ThreadMXBean. Полная версия метода приведена здесь.

Листинг 2. Фрагмент метода collect() JMX-коллектора, в котором используется объект ThreadMXBean
.
.
objectNameCache.put(THREAD_MXBEAN_NAME, new ObjectName(THREAD_MXBEAN_NAME));
.
.
public void collect() {
   CompositeData compositeData = null;
   String type = null;
   try {
      log("Starting JMX Collection");
      long start = System.currentTimeMillis();
      ObjectName on = null;
.
.
      // Мониторинг потоков
      on = objectNameCache.get(THREAD_MXBEAN_NAME);
      tracer.traceDeltaSticky((Long)jmxServer.getAttribute(on,"TotalStartedThreadCount"), 
        hostName, "JMX", on.getKeyProperty("type"), "StartedThreadRate");
      tracer.traceSticky((Integer)jmxServer.getAttribute(on, "ThreadCount"), hostName, 
        "JMX", on.getKeyProperty("type"), "CurrentThreadCount");
.
.
      // Выполнено
      long elapsed = System.currentTimeMillis()-start;
      tracer.trace(elapsed, hostName, "JMX", "JMX Collector", 
         "Collection", "Last Elapsed Time");
      tracer.trace(new Date(), hostName, "JMX", "JMX Collector", 
         "Collection", "Last Collection");         
      log("Completed JMX Collection in ", elapsed, " ms.");         
   } catch (Exception e) {
      log("Failed:" + e);
      tracer.traceIncident(hostName, "JMX", "JMX Collector", 
         "Collection", "Collection Errors");
   }
}

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

Дерево метрик, построенное APM-системой по данным, собранным этим коллектором, показано на рисунке 7.

Рисунок 7. Отображение дерева показателей в интерфейсе APM-системы
JMX collector APM metric tree

Некоторые аспекты работы JMX-коллектора не показаны в листинге 2 (тем не менее они представлены в полной версии исходного кода). К ним относится регистрация планировщика, который каждые 10 секунд вызывает метод collect() коллектора.

В листинге 2 режим трассировки и типы трассируемых данных зависят от источника данных. Примеры приведены ниже.

  • Показатели TotalLoadedClasses (общее число загруженных классов) и UnloadedClassCount (число выгруженных классов) трассируются в режиме фиксированной дельта-трассировки, так как они монотонно возрастают, и дельты оказываются полезнее для мониторинга загрузки/выгрузки классов, чем абсолютные значения.
  • Показатель ThreadCount (число потоков) может как возрастать, так и уменьшаться, поэтому он трассируется в режиме закрепленной трассировки.
  • Показатель Collection Errors трассируется в режиме инцидентной интервальной трассировки. Он увеличивается каждый раз при возникновении исключения во время сборки мусора.

Имена целевых JMX-объектов MXBean (экземпляры ObjectName) не меняются в течение всего времени работы JVM, поэтому коллектор кэширует их в целях повышения эффективности, используя константы, определенные в классе ManagementFactory.

Точные имена двух объектов MXBean, а именно GarbageCollector и MemoryPool, в общем случае не могут быть известны заранее, но их можно определить по образцу. Для этого во время первого запуска коллектора выполняется запрос к MBeanServerConnection на получение всех объектов MBean, имена которых соответствуют образцу. Далее полученные таким образом объекты MBean сохраняются в кэше, что помогает избежать повторных запросов.

Атрибуты объектов MBean, опрашиваемые коллектором, не всегда бывают простых численных типов. Например, целевые атрибуты объектов MemoryMXBean и MemoryPoolMXBean имеют тип CompositeData, содержащий набор пар "ключ-значение". Объекты MXBean, доступные через интерфейс JVM java.lang.management, следуют модели открытых типов JMX, которая говорит о том, что атрибуты не должны иметь типы данных, специфичные для конкретного языка (т.е. в Java должны использоваться такие типы, как java.lang.Boolean или java.lang.Integer). Составные типы, подобные javax.management.openmbean.CompositeType, должны декомпозироваться на пары «ключ-значение», состоящи из данных простых типов. Полный перечень допустимых простых типов содержится в статическом поле javax.management.openmbean.OpenType.ALLOWED_CLASSNAMES. Таким образом, модель поддерживает определенный уровень типового абстрагирования, благодаря которому JMX-клиенты не зависят от нестандартных классов, а также могут быть разработаны не на Java, поскольку открытые типы сравнительно просты. Более подробная информация о модели открытых типов JMX (JMX Open Types) приведена в разделе Ресурсы.

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

Если коллектор открывает одно соединение и использует его при сборе всей необходимой информации, то следует предусмотреть возможность его обрыва. Другими словами, необходимы средства обнаружения обрыва соединения и его восстановления. Некоторые API для сбора данных предоставляют объекты-слушатели, которые могут уведомлять коллектор о необходимости прервать работу, освободить ресурсы, а затем открыть новое соединение. Также возможны ситуации, в которых коллектор пытается соединиться с источником данных, который недоступен (например, проводятся технические работы и т.д.). В таких случаях коллектор должен периодически опрашивать состояние источника. Кроме того, имеет смысл оценивать время установки соединения и сокращать интенсивность сбора данных при падении скорости отклика. Таким образом можно снизить нагрузку на целевую JVM, которая могла подвергаться чрезмерно активному мониторингу.

В данных примерах не реализованы два дополнительных способа повышения производительности JMX-коллектора и снижения нагрузки с его стороны на целевую JVM. Первый способ применим в ситуации, когда коллектор обращается к нескольким атрибутам одного объекта MBean. Вместо того, чтобы обращаться к ним поочередно, вызывая каждый раз метод getAttribute(ObjectName name, String attribute), можно получить значения сразу нескольких атрибутов при помощи метода getAttributes(ObjectName name, String[]. Разница может быть незаметна при локальном развертывании коллектора, но при удаленной работе расход ресурсов будет значительно снижен за счет сокращения числа сетевых обращений. Второй способ заключается в снижении нагрузки на пулы памяти, доступные через JMX, за счет замены схемы опрашивания на схему прослушивания. Объект MemoryPoolMXBean позволяет задать пороговое значение степени использования пула, после превышения которого он автоматически уведомит коллектор, который, в свою очередь, сможет передать значение трассировщику. Пороговое значение можно постепенно увеличивать по мере возрастания активности использования пула. У этого подхода есть один недостаток: детализация картины использования памяти может пострадать, если только пороговое значение не будет увеличиваться очень маленькими шагами. В противном случае можно потерять данные о том, как происходит обращение к памяти до превышения порога.

Наконец, в примере не показано, как можно измерять интервалы, а также полное время работы сборщика мусора. На основе этих двух показателей при помощи несложных арифметических подсчетов можно вывести продолжительность активности сборщика в процентном выражении. Это достаточно полезный показатель, так как сборка мусора неизбежна (по крайней мере, на текущий момент) для большинства приложений. Поскольку число и длительность работы коллектора известны, процент времени, в течение которого сборщик мусора был активен, может пролить свет на наличие или отсутствие проблем при работе JVM. Несмотря на то, что точный процент зависит от приложения, общее правило гласит, что периоды активности сборщика мусора не должны превышать 10% за 15-минутный интервал работы системы.

Внешнее конфигурирование коллекторов

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

  • директиву для получения ссылки на фабрику соединений с PDS. Коллектору должен быть предоставлен интерфейс и набор параметров для соединения с PDS;
  • частоту сбора данных;
  • частоту, с которой будут предприниматься попытки восстановления соединения;
  • целевой объект MBean для сбора данных, либо шаблон для подстановки имени объекта;
  • составное имя трассировки для каждого показател, либо указания о том, куда должны трассироваться данные. Кроме того, необходимо задавать тип трассируемых данных.

Пример фрагмента, содержащего настройки JMX-коллектора, приведен в листинге 3.

Листинг 3. Фрагмент внешнего файла, содержащего конфигурационную информацию для JMX-коллектора
<?xml version="1.0" encoding="UTF-8"?>
<JMXCollector>
   <attribute name="ConnectionFactoryClassName">
      collectors.jmx.RemoteRMIMBeanServerConnectionFactory
   </attribute>
   <attribute name="ConnectionFactoryProperties">
      jmx.rmi.url=service:jmx:rmi://127.0.0.1/jndi/rmi://127.0.0.1:1090/jmxconnector
   </attribute>
   <attribute name="NamePrefix">AppServer3.myco.org,JMX</attribute>
   <attribute name="PollFrequency">10000</attribute>
   <attribute name="TargetAttributes">
      <TargetAttributes>
         <TargetAttribute objectName="java.lang:type=Threading" 
            attributeName="ThreadCount" Category="Threading" 
            metricName="ThreadCount" type="SINT"/>
         <TargetAttribute objectName="java.lang:type=Compilation" 
            attributeName="TotalCompilationTime" Category="Compilation" 
            metricName="TotalCompilationTime" type="SDINT"/>
      </TargetAttributes>      
   </attribute>
</JMXCollector>

Обратите внимание, что каждый элемент TargetAttribute содержит атрибут type, в котором задается значение аргумента, передаваемого умному трассировщику. Значения SINT и SDINT соответственно означают обычную и дельта-трассировку данных типа int в закрепленном режиме.


Мониторинг ресурсов приложения через JMX

До этого момента мы рассматривали JMX-мониторинг только стандартных ресурсов JVM. Однако многие платформы, в частности Java EE, позволяют собирать показатели, относящиеся к конкретному приложению, через JMX. Набор показателей зависит от компании, которой принадлежит реализация платформы. Классическим примером может служить такой показатель как степень использования источника данных (DataSource). DataSource представляет собой сервис, поддерживающий пул соединений с внешним ресурсом (как правило, с базой данных) и ограничивающий число параллельных соединений с целью защиты ресурса от проблемных и перегруженных приложений. Мониторинг источников данных является критически важной частью общего плана мониторинга. Благодаря абстрактному слою JMX данный процесс оказывается аналогичным тому мониторингу ресурсов JVM, который мы рассматривали выше.

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

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

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

Листинг 4. Коллектор, собирающий данные об использовании DataSource
public void collect() {
   try {
      log("Starting DataSource Collection");
      long start = System.currentTimeMillis();
      ObjectName on = objectNameCache.get("DS_OBJ_NAME");
      AttributeList attributes  = jmxServer.getAttributes(on, new String[]{
            "AvailableConnectionCount", 
            "MaxConnectionsInUseCount",
            "InUseConnectionCount",
            "ConnectionCount",
            "ConnectionCreatedCount",
            "ConnectionDestroyedCount"
      });
      for(Attribute attribute: (List<Attribute>)attributes) {
         if(attribute.getName().equals("ConnectionCreatedCount") 
            || attribute.getName().equals("ConnectionDestroyedCount")) {
               tracer.traceDeltaSticky((Integer)attribute.getValue(), hostName, 
               "DataSource", on.getKeyProperty("name"), attribute.getName());
         } else {
            if(attribute.getValue() instanceof Long) {
               tracer.traceSticky((Long)attribute.getValue(), hostName, "DataSource", 
                  on.getKeyProperty("name"), attribute.getName());
            } else {
               tracer.traceSticky((Integer)attribute.getValue(), hostName, 
                  "DataSource",on.getKeyProperty("name"), attribute.getName());
            }
         }
      }
      // Выполнено
      long elapsed = System.currentTimeMillis()-start;
      tracer.trace(elapsed, hostName, "DataSource", "DataSource Collector", 
         "Collection", "Last Elapsed Time");
      tracer.trace(new Date(), hostName, "DataSource", "DataSource Collector", 
         "Collection", "Last Collection");         
      log("Completed DataSource Collection in ", elapsed, " ms.");         
   } catch (Exception e) {
      log("Failed:" + e);
      tracer.traceIncident(hostName, "DataSource", "DataSource Collector", 
         "Collection", "Collection Errors");
   }      
}

Дерево показателей, построенное по результатам работы коллектора DataSource, показано на рисунке 8.

Рисунок 8. Дерево показателей использования DataSource
The DataSource collector metric tree

Мониторинг компонентов внутри JVM

Кросс-регистрация объектов MBean

Многие реализации поддерживают регистрацию объектов MBean в различных объектах MBeanServer внутри одной JVM. Например, объекты MXBean, относящиеся к java.lang MXBean, зарегистрированы в агенте платформы (или MBeanServer JVM), в то время как объекты, относящиеся к DataSource, в JBoss зарегистрированы в MBeanServer jboss. При удаленном мониторинге подобная ситуация может привести к увеличению накладных расходов и усложнить конфигурирование, так как придется устанавливать соединения с каждым объектом MBeanServer. Кроме того, предоставление самой возможности для соединения с объектами MBeanServer со стороны платформы также ведет к снижению производительности. В этих случаях, как правило, не представляет сложностей произвести кросс-регистрацию объектов MBean таким образом, чтобы осуществлять мониторинг всех необходимых объектов через единый интерфейс MBeanServer. В архиве с исходным кодом примеров к статье приводится скрипт (Bean shell) под названием map-platform-mxbeans.bsh. Если выполнить данный скрипт в сервере JBoss, то все платформенные объекты MXBean будут также зарегистрированы в объекте MBeanServerJBoss.

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

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

Благодаря возможностям объекта ThreadMXBean, которые поддерживаются некоторыми реализациями Java SE, начиная с версии 5, можно получать значения следующих показателей:

  • Системное и пользовательское время работы CPU: процессорное время, затраченное на вызов метода.
  • Число ждущих потоков и общее время ожидания: число потоков и общее время, которое потоки провели в режиме ожидания вызова метода или сервиса. Время ожидания отсчитывается с момента выставления флага состояния WAITING или TIMED_WAITING. Находясь в режиме ожидания, поток ожидает определенного действия со стороны параллельно выполняющегося потока.
  • Число заблокированных потоков и общее время блокировок: число потоков и общее время, за которое потоки были заблокированы (флаг состояния BLOCKED) при вызове метода или сервиса. Поток является заблокированным во время ожидания монитора либо ожидания входа (первоначального или повторного) в синхронизированную секцию кода.

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

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

  • Инструментирование исходного кода: наиболее простым вариантом является инструментирование на этапе написания исходного кода, чтобы инструментирующий код автоматически добавлялся в скомпилированные классы. В некоторых ситуациях такой подход имеет смысл, кроме того, существуют способы сглаживания возможных негативных последствий.
  • Перехват: мониторинг может осуществляться точно и эффективно путем внедрения перехватчика всех вызовов целевых классов. При этом удается избежать изменения как исходного, так и байт-кода классов. Данный подход часто оказывается пригодным к использованию благодаря тому, что множество платформ Java и Java EE обладают следующими свойствами:
    • поддерживают абстрагирование путем внешнего конфигурирования;
    • поддерживают внедрение классов и обращение через интерфейсы;
    • некоторые реализации позволяют создавать стеки перехватчиков. При этом управление поочередно передается объектам, находящимся в конфигурируемом стеке. Данные объекты решают, стоит ли принимать вызов, затем выполняют определенные действия и передают его далее по стеку.
  • Инструментирование байт-кода: это процесс внедрения байт-кода непосредственно в классы приложения. Встраиваемый байт-код выполняет сбор данных о производительности вызовов. Таким образом, вызываемый класс является по сути новым, инструментированным классом. К преимуществам данного подхода относится высокая эффективность, так как инструментирование заключается в добавлении полностью скомпилированного байт-кода, который при сборе показателей оказывает минимальное влияние на процесс вызовов методов. Еще одним плюсом является то, что не требуется модифицировать исходный код классов, достаточно лишь минимальных изменений в настройках среды. Наконец, в общем случае методы внедрения байт-кода позволяют инструментировать классы с закрытым исходным кодом, что является распространенной проблемой при использовании сторонних библиотек.
  • Оборачивание классов: это процесс создания классов-оберток над целевыми классами. Обертки реализуют ту же функциональность, что и сами классы, но при этом включают код для сбора показателей о производительности.

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

Асинхронное инструментирование

Асинхронность играет фундаментальную роль при инструментировании классов. В предыдущем разделе рассматривалась схема опрашивания для сбора данных. Качественно реализованный сбор данных при помощи опрашивания не должен добавлять накладных расходов и оказывать влияние на быстродействие приложения. Инструментирование же всегда приводит к изменению кода и, как следствие, влияет на его исполнение. Главным принципом любого типа инструментирования должно быть "не навреди". Другими словами, необходимо максимально снизить накладные расходы. В то время как минимальные потери, связанные непосредственно с измерениями, исключить практически невозможно, совершенно необходимо, чтобы последующие действия (трассировка) выполнялись асинхронно. Существует несколько распространенных вариантов реализации асинхронной трассировки. Общая схема приведена на рисунке 9.

Рисунок 9. Схема асинхронной трассировки данных
Asynchronous tracing

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


Инструментирование исходного кода Java-классов

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

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

  • Если есть возможность подвергнуть исходный код инструментированию, но невозможно управлять сбором данных при помощи внешних настроек, то необходимо использовать гибкий и конфигурируемый API трассировки.
  • Абстрактные API трассировки аналогичны API журналирования (logging), подобным log4j. И те, и другие предоставляют следующие возможности:
    • управление детальностью вывода на этапе выполнения приложения: уровни детальности классов log4j, добавляющих записи в журнал, задаются перед запуском приложения, но могут быть изменены во время выполнения. Аналогичным образом API трассировки должны предоставлять средства на основе иерархии наименований для управления трассируемыми показателями.
    • настройка вывода: журналирование данных в log4j реализуется через объекты-логгеры, которые, в свою очередь, передают данные объектам, осуществляющим непосредственно вывод (так называемым appender’ам). Последние могут быть настроены для записи данных в различные выходные потоки, в частности файлы, сокеты или сообщения e-mail. Хотя подобное разнообразие типов вывода не является обязательным для API трассировки, интерфейсы должны быть абстрагированы от проприетарных, либо специфичных для конкретной APM компонентов. Это позволяет избежать необходимости внесения изменений в исходный код при помощи управления настройками.
  • В некоторых ситуациях определенные значения могут трассироваться только путем инструментирования исходного кода. Это весьма распространенный случай при так называемой контекстной трассировке. Мы будем использовать этот термин для описания данных о производительности, которые не представляют первостепенной важности, однако выступают в роли контекстной информации для основных данных.

Контекстная трассировка

Контекстная трассировка в высокой степени определяется конкретным приложением. Мы рассмотрим упрощенный пример класса для обработки платежной ведомости и его метод processPayroll(long clientId). Задачей метода является расчет заработной платы для каждого сотрудника компании-клиента. Разумеется, существует ряд способов инструментирования данного метода, однако очевидно, что время его вызова будет возрастать в зависимости от числа сотрудников. Поэтому временные показатели работы метода processPayroll окажутся вырванными из контекста, если не знать, сколько сотрудников было обработано при каждом запуске. Допустим, за выбранный промежуток времени средняя продолжительность вызовов метода processPayroll составила x миллисекунд. Основываясь только на этом значении, невозможно утверждать, насколько этот показатель плох или хорош, так как число сотрудников неизвестно. Если заработная плата рассчитывалась только для одного сотрудника, то показатель, вполне вероятно, плох, а если для 150 – то он может быть просто великолепен. Упрощенный пример реализации данного метода приведен в листинге 5.

Листинг 5. Пример метода, требующего контекстной трассировки
public void processPayroll(long clientId) {
   Collection<Employee> employees = null;
   // Выбрать список сотрудников
   //...
   //...
   // Произвести расчет для каждого сотрудника
   for(Employee emp: employees) {
      processEmployee(emp.getEmployeeId(), clientId);
   }
}

Основной проблемой здесь является то, что все вызовы, происходящие внутри метода processPayroll(), недоступны для инструментирования. Таким образом, если вам удалось инструментировать метод processPayroll (и даже processEmployee), то все равно нет никакой возможности для трассировки числа сотрудников, т.е. показателя, который в этом случае является контекстом для данных о производительности метода. В листинге 6 показан грубый, жестко описанный в коде (а также весьма неэффективный) вариант того, как можно получить контекстные данные в этой ситуации.

Листинг 6. Пример реализации контекстной трассировки
public void processPayrollContextual(long clientId) {      
   Collection<Employee> employees = null;
   // Выбрать список сотрудников
   employees = popEmployees();
   // Произвести расчет для каждого сотрудника
   int empCount = 0;
   String rangeName = null;
   long start = System.currentTimeMillis();
   for(Employee emp: employees) {
      processEmployee(emp.getEmployeeId(), clientId);
      empCount++;
   }
   rangeName = tracer.lookupRange("Payroll Processing", empCount);
   long elapsed = System.currentTimeMillis()-start;
   tracer.trace(elapsed, "Payroll Processing", rangeName, "Elapsed Time (ms)");
   tracer.traceIncident("Payroll Processing", rangeName, "Payrolls Processed");
   log("Processed Client with " + empCount + " employees.");
}

Главную роль в листинге 6 играет метод tracer.lookupRange. Диапазоны (ranges) – это коллекции, которым присвоены строковые имена, соответствующие названиям численных интервалов. Каждая коллекция ассоциируется со своим интервалом. Таким образом, вместо простой трассировки продолжительности вызова метода в листинге 6 происходит поиск соответствующего интервала, в который попадает текущее число сотрудников. Далее результаты группируются по близким значениям числа сотрудников. Дерево показателей, построенное APM-системой по собранным данным, показано на рисунке 10.

Рисунок 10. Продолжительность расчета заработной платы в разрезе диапазонов
Payroll processing times grouped by range

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

Рисунок 11. Графики продолжительности расчета с учетом числа сотрудников
Payroll processing elapsed times by range

В конфигурации трассировщика можно задавать URL файла свойств, в котором определены диапазоны и пороговые значения (о последних речь пойдет ниже). Данные свойства обрабатываются при создании трассировщика. Они содержат данные, на которые опирается реализация метода tracer.lookupRange. Пример конфигурационного описания диапазонов под именем Payroll Processing показан в листинге 7. В данном примере используется XML-представление объекта java.util.Properties, поскольку оно более терпимо к использованию нестандартных символов.

Листинг 7. Пример конфигурирования диапазонов
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
   <comment>Payroll Process Range</comment>
   <entry key="L:Payroll Processing">181+ Emps,10:1-10 Emps,50:11-50 Emps,
      80:51-80 Emps,120:81-120 Emps,180:121-180 Emps</entry>
</properties>

Использование диапазонов, определенных во внешнем файле, исключает необходимость частого внесения изменений в исходный код приложения из-за меняющихся требований либо соглашений об уровне сервиса (service level agreements – SLA). Для того чтобы модифицировать используемые диапазоны и пороговые значения, достаточно просто отредактировать внешний файл, а не само приложение.


Контроль пороговых значений и соответствие SLA

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

В рамках той же системы расчета заработной платы рассмотрим внутреннюю систему оценки времени выполнения расчета. Все вызовы метода классифицируются по шкале Ok (нормальное выполнение), Warn (настораживающе медленное выполнение) и Critical (критически медленное выполнение) в соответствии с диапазонами, в которые попадает число обработанных при вызове сотрудников. Процесс подсчета вызовов, выходящих за пределы пороговых значений, концептуально просто. Все что нужно – это указать трассировщику предельные значения времени выполнения для каждого диапазона и каждого пункта в шкале оценок. Далее необходимо вызывать метод tracer.traceIncident для трассировки классифицированного времени выполнения расчета, а затем (для упрощения отчета) трассировать общее время. В таблице 2 показаны пороговые значения, установленные соглашениями SLA.

Таблица 2. Пороговые значения быстродействия расчета заработной платы
Число сотрудниковOk (мс)Warn (мс)Critical (мс)
1-10280400>400
11-508501200>1200
51-809001100>1100
81-12011001500>1500
121-18014002000>2000
181+20003000>3000

В API ITracer реализуется трассировка на основе пороговых значений, которые определены в том же XML-файле свойств, что и диапазоны. Пороговые значения отличаются от диапазонов в двух аспектах. Во-первых, ключевое значение, использующееся в определении порогового значения, представляет собой регулярное выражение. При трассировке численного значения ITracer проверяет соответствие данного выражения составному имени метрики. В случае соответствия результат измерения оценивается по шкале "Оk, Warn, Critical", и добавляется дополнительный вызов tracer.traceIncident. Во-вторых, описание порогов заключается в задании всего двух значений (оценку Critical получают измерения, превышающие пороговое значение Warn). В листинге 8 показано задание пороговых значений продолжительности расчета заработной платы, которые установлены описанными выше соглашениями SLA.

Листинг 8. Конфигурирование пороговых значений для процесса расчета заработной платы
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
   <!-- Payroll Processing Thresholds -->
   <entry key="Payroll Processing.*81-120 Emps.*Elapsed Time \(ms\)">1100,1500</entry>   
   <entry key="Payroll Processing.*1-10 Emps.*Elapsed Time \(ms\)">280,400</entry>   
   <entry key="Payroll Processing.*11-50 Emps.*Elapsed Time \(ms\)">850,1200</entry>   
   <entry key="Payroll Processing.*51-80 Emps.*Elapsed Time \(ms\)">900,1100</entry>      
   <entry key="Payroll Processing.*121-180 Emps.*Elapsed Time \(ms\)">1400,2000</entry>   
   <entry key="Payroll Processing.*181\+ Emps.*Elapsed Time \(ms\)">2000,3000</entry>   
</properties>

На рисунке 12 показано дерево показателей расчета в разрезе используемых пороговых значений.

Рисунок 12. Дерево показателей скорости расчета с учетом пороговых значений
Payroll processing metric tree with thresholds

Как видно из рисунка 13, собранные данные могут отображаться в виде круговой диаграммы.

Рисунок 13. SLA-статистика по быстродействию расчета заработной платы в диапазоне 1-10 сотрудников
SLA summary for payroll processing

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


Заключение к первой статье

В первой части серии были описаны некоторые из распространенных, но неудачных подходов к мониторингу, а также представлен ряд возможностей, которыми должны обладать качественные APM-системы. Кроме того, были рассмотрены базовые принципы сбора информации о производительности и предложен интерфейс ITracer, который будет использоваться и в следующих частях серии. В статье демонстрировались методы мониторинга состояния JVM и сбора общих показателей через интерфейс JMX. Наконец, были представлены эффективные варианты инструментирования исходного кода, минимизирующие необходимость внесения изменений в классы приложения. Вы увидели, как при помощи инструментирования можно собирать непосредственную статистику по быстродействию вызовов и контекстную статистику, а также как на основе этих данных строить отчеты о соответствии приложения соглашениям SLA. В следующей статье будут рассмотрены методы инструментирования Java-приложений, не требующие модифицирования исходного кода. Речь пойдет о перехватах вызовов, классах-обертках и динамическом инструментировании байт-кода.

Переходите к чтению второй статьи.


Загрузка

ОписаниеИмяРазмер
Исходный код примеров к статьеj-rtm1.zip316 KБ

Ресурсы

Научиться

Получить продукты и технологии

  • IKVM: виртуальная машина с открытым исходным кодом для компиляции байт-кода Java в байт-код .NET. С ее помощью упрощается использование JMX в .NET-языках. (EN)
  • Узнайте больше о системе IBM для мониторинга производительности, посетив информационный центр продукта IBM® Tivoli® Monitoring for Transaction Performance. (EN)
  • CA/Wily Introscope: коммерческая система для управления производительностью Java и Web-приложений. (EN)
  • JINSPIRED JXInsight: коммерческая система для мониторинга производительности, диагностики, анализа транзакций и общего управления приложениями. (EN)
  • PerformaSure: коммерческая система для диагностики проблем производительности и профилирования транзакций. (EN)
  • Скачайте ознакомительные версии продуктов IBM и опробуйте инструменты разработки приложений, а также связующее программного обеспечение IBM семейств DB2®, Lotus®, Rational®, Tivoli и WebSphere®. (EN)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=373588
ArticleTitle=Мониторинг работы Java-приложений: Часть 1. Мониторинг производительности и степени готовности Java-систем
publish-date=03022009