 | Уровень сложности: средний Николас Уайтхед, старший технический архитектор, ADP
03.04.2009 В первой статье данной серии, посвященной мониторингу приложений Java™ на этапе их выполнения, основное внимание уделялось контролю состояния JVM и различным способам инструментирования исходного кода, позволяющим собирать показатели быстродействия. Во второй статье мы поговорим о вариантах инструментирования Java-классов и конструкций без внесения изменений в исходный код.
Введение
Как вы уже знаете из предыдущей статьи, мониторинг производительности и степени готовности Java-приложений, а также компонентов, от которых зависит их работа, очень важен для своевременного обнаружения и устранения проблем. Несмотря на то что инструментирование исходного кода классов, над которыми осуществляется мониторинг, имеет определенные преимущества (см. часть 1), в ряде случаев оно может оказаться невозможным или необоснованным. Например, компоненты, представляющие наибольший интерес, могут находиться в сторонних библиотеках с закрытым исходным кодом. В данной статье мы уделим основное внимание методам инструментирования Java-классов и ресурсов, не требующих редактирования исходного кода.
Существуют три основных варианта подобного инструментирования:
- перехват вызовов;
- создание классов-оберток;
- инструментирование байт-кода.
Ниже в статье будут приведены примеры, иллюстрирующие данные подходы. В примерах будет использоваться знакомый по первой статье интерфейс ITracer, служащий для трассировки показателей производительности.
Инструментирование Java-кода при помощи перехвата вызовов
Характерной чертой инструментирования при помощи перехватов является перенаправление вызовов таким образом, чтобы они проходили через объект-перехватчик, который собирает всю необходимую информацию как перед вызовом, так и непосредственно после него. Как правило, перехватчики реализуют следующие действия:
- засекают текущее время перед началом вызова;
- засекают текущее время сразу после окончания вызова;
- вычисляют время вызова как разницу между этими временными отсечками;
- передают вычисленное значение системе управления производительностью приложения (APM).
Схема работы показана на рисунке 1.
Рисунок 1. Базовая схема использования коллектора-перехватчика
 |
Граница между компилируемым кодом и конфигурацией
Приверженцы систем по управлению изменениями могут подвергнуть сомнению разницу между редактированием исходного кода и редактированием файлов настроек. По общему мнению, границы между такими терминами как "код", XML и "скрипт" становятся все более размытыми. Однако существует четкое разделение изменений на следующие две категории:
- изменения в исходном коде, требующие повторной компиляции, сборки и, во многих случаях, выполнения бесчисленных процедур, предваряющих ввод в эксплуатацию;
- изменения в ресурсах, внешних по отношению к бинарному (скомпилированному) коду, который остается неизменным.
Во многом данная классификация обусловлена разницей в простоте внесения и отмены изменений. Однако в некоторых случаях данного критерия оказывается недостаточно, чтобы преодолеть философские разногласия, к тому же часто недооценивается трудоемкость процесса либо строгость регламента по внесению изменений в различных средах. |
|
Многие инфраструктуры для создания приложений Java, например платформа Java Enterprise Edition (Java EE), предоставляют базовую поддержку для организации стека перехватчиков. В результате каждый вызов конечного сервиса проходит через цепочку компонентов, выполняющих предварительную и пост-обработку. Стек перехватчиков – это отличная возможность внедрения инструментирующего кода. Такой подход обладает двумя основными преимуществами: во-первых, нет необходимости изменять исходный код целевых классов. Во-вторых, внедрение перехватчика вызовов сводится к простому добавлению нужного класса в classpath JVM и к соответствующей модификации специального файла – дескриптора развертывания компонента.
Основные показатели, собираемые перехватчиком
Продолжительность вызова – это один из показателей, как правило, собираемых перехватчиками. Однако они могут использоваться для сбора и других полезных метрик, в частности тех, что перечислены ниже. Далее мы поговорим о двух новых аспектах интерфейса ITracer, которые понадобятся для сбора этих показателей.
Как правило, при использовании перехватчиков следует рассмотреть вариант сбора следующих метрик:
-
продолжительность вызова: среднее время выполнения вызываемой операции;
-
количество вызовов за промежуток времени: число вызовов метода за определенный временной интервал;
-
количество ответов за промежуток времени: число откликов вызываемого объекта за временной интервал;
-
количество сгенерированных исключений за промежуток времени: число вызовов за временной интервал, который привели к выбросу исключения;
-
степень параллелизма: число вызовов целевого объекта из параллельно выполняющихся потоков.
При помощи объекта ThreadMXBean можно также получить значения еще двух показателей, однако это связано с повышенными накладными расходами, и в целом они менее информативны:
-
время загрузки процессора: время (в наносекундах), в течение которого данный поток выполнялся на центральном процессоре (CPU). Степень загрузки процессора может показаться интересной, однако она не очень информативна. Полезными могут оказаться только тенденции изменения данного показателя. Кроме того, есть возможность вычислить примерный процент всех ресурсов процессора, занятых потоком в процессе выполнения, однако это потребует значительных накладных расходов;
-
счетчики и время, проведенное в состояниях ожидания и блокировки: под ожиданием понимается время, затраченное на планирование выполнения потоков. Заблокированное состояние, как правило, означает, что поток ждет, пока ему будет предоставлен доступ к определенному ресурсу. Например, поток блокируется на время ожидания запроса к удаленной базе данных через интерфейс JDBC (Java Database Connectivity). Данные показатели подробнее обсуждаются в разделе "Инструментирование JDBC".
Пример сбора показателей при помощи объекта ThreadMXBean показан в листинге 1, который также напомнит вам, что такое инструментирование исходного кода. В данном примере реализовано тяжеловесное инструментирование метода под названием heavilyInstrumentedMethod.
Листинг 1. Пример тяжеловесного инструментирования
protected static AtomicInteger concurrency = new AtomicInteger();
.
.
for(int x = 0; x < loops; x++) {
tracer.startThreadInfoCapture(CPU+BLOCK+WAIT);
int c = concurrency.incrementAndGet();
tracer.trace(c, "Source Instrumentation", "heavilyInstrumentedMethod",
"Concurrent Invocations");
try {
// ===================================
// Вызываемый метод
// ===================================
heavilyInstrumentedMethod(factor);
// ===================================
tracer.traceIncident("Source Instrumentation",
"heavilyInstrumentedMethod", "Responses");
} catch (Exception e) {
tracer.traceIncident("Source Instrumentation",
"heavilyInstrumentedMethod", "Exceptions");
} finally {
tracer.endThreadInfoCapture("Source Instrumentation",
"heavilyInstrumentedMethod");
c = concurrency.decrementAndGet();
tracer.trace(c, "Source Instrumentation",
"heavilyInstrumentedMethod", "Concurrent Invocations");
tracer.traceIncident("Source Instrumentation",
"heavilyInstrumentedMethod", "Invocations");
}
try { Thread.sleep(200); } catch (InterruptedException e) { }
}
|
В листинге 1 используется ряд новых конструкций, а именно:
-
методы
ThreadInfoCapture: вспомогательные методы, при помощи которых можно получить данные о продолжительности вызова, а также разницу (дельту) между показателями объекта ThreadInfoCapture – непосредственно до вызова и сразу после вызова. Метод startThreadInfoCapture засекает текущие значения показателей до вызова, а endThreadInfoCapture вычисляет и трассирует дельты. Данные метрики представляют собой монотонно возрастающие показатели, поэтому необходимо засекать базовые значения и вычислять разницу сразу после вызова. При этом нельзя использовать обычные методы трассировки дельт, так как абсолютные значения показателей будут различаться для каждого потока JVM, между которыми постоянно происходят переключения. Кроме того, обратите внимание, что поскольку трассировщик использует стек для хранения базовых значений, то с определенной осторожностью можно производить вложенные вызовы. Разумеется, сбор данных требует некоторых накладных расходов. На рисунке 2 показаны относительные значения длительности операций для получения различных показателей, предоставляемых ThreadMXBean.
Рисунок 2. Относительные затраты (в миллисекундах) на сбор различных данных через объект ThreadMXBean

Хотя при разумном использовании расходы на эти вызовы не катастрофичны, стоит руководствоваться теми же соображениями, что и при журналировании, в частности, не вызывать данные методы внутри "жестких циклов" (tight loops);
-
уровень параллелизма: для отслеживания числа потоков, выполняющих данный участок кода в каждый момент времени, необходимо создать счетчик, который, во-первых, будет потокобезопасным, а во-вторых – доступным для всех экземпляров целевого класса. В данном примере используется статическая переменная типа
AtomicInteger. При этом возможна печально известная ситуация, при которой класс загружается разными загрузчиками, что приводит к потере свойства атомарности и, как следствие, неверным и сбивающим с толку результатам. В качестве одного из вариантов решения данной проблемы можно предложить размещать счетчики параллелизма в гарантированно уникальном месте внутри JVM (например, в объекте MBean агента платформы).
Измерять уровень параллелизма можно лишь в тех случаях, когда методы целевого объекта способны выполняться в нескольких потоках, либо поддерживается пул объектов. В этих ситуациях данный показатель чрезвычайно полезен, как продемонстрировано ниже на примере перехватчиков вызовов объектов EJB (Enterprise JavaBean). С этих перехватчиков мы начнем серию примеров, иллюстрирующих инструментирование на основе перехвата вызовов. В примерах будут использоваться те же методы трассировки, что и в листинге 1.
Перехватчики EJB 3
После выпуска EJB 3 перехватчики стали стандартным компонентом архитектуры Java EE (некоторые серверы приложений Java включали поддержку EJB-перехватчиков еще до EJB 3). Несмотря на то что большинство серверов приложений Java EE предоставляют как минимум частичный доступ к показателям производительности основных компонентов, таких как EJB, может быть несколько причин, по которым вам может понадобиться собственное решение:
- необходимость контекстной трассировки или трассировки, основанной на диапазонах и пороговых значениях;
- необходимость консолидации всех метрик, в том числе показателей работы сервера приложений, внутри APM-системы;
- несоответствие стандартных метрик сервера приложений вашим требованиям.
Однако, даже создавая собственную схему сбора показателей, можно использовать некоторые стандартные возможности вашей APM-системы и реализации сервера приложений. Например, инфраструктура PMI сервера WebSphere® предоставляет доступ к серверным метрикам через интерфейс JMX (Java Management Extensions, см. Ресурсы). Даже если ваша APM-система неспособна автоматически считывать эти данные, прочитав эту статью, вы узнаете, как делать это самостоятельно.
В следующем примере мы внедрим перехватчик в контекст не обладающего состоянием сессионного компонента EJB под названием org.aa4h.ejb.HibernateService. Требования и зависимости перехватчиков EJB 3 достаточно скромны, необходимо выполнить лишь следующие условия:
-
интерфейс:
javax.interceptor.InvocationContext;
-
аннотация:
javax.interceptor.AroundInvoke;
-
целевой метод: имя метода может быть любым, а сигнатура должна быть следующей:
public Object anyName(InvocationContext ic).
Метод перехватчика вызовов к объекту EJB показан в листинге 2.
Листинг 2. Пример реализации метода объекта-перехватчика в EJB 3
@AroundInvoke
public Object trace(InvocationContext ctx) throws Exception {
Object returnValue = null;
int concur = concurrency.incrementAndGet();
tracer.trace(concur, "EJB Interceptors", ctx.getTarget().getClass()
.getName(), ctx.getMethod().getName(),
"Concurrent Invocations");
try {
tracer.startThreadInfoCapture(CPU + BLOCK + WAIT);
// ===================================
// Вызов целевого объекта
// ===================================
returnValue = ctx.proceed();
// ===================================
tracer.traceIncident("EJB Interceptors", ctx.getTarget().getClass()
.getName(), ctx.getMethod().getName(), "Responses");
concur = concurrency.decrementAndGet();
tracer.trace(concur, "EJB Interceptors", ctx.getTarget().getClass()
.getName(), ctx.getMethod().getName(),
"Concurrent Invocations");
return returnValue;
} catch (Exception e) {
tracer.traceIncident("EJB Interceptors", ctx.getTarget().getClass()
.getName(), ctx.getMethod().getName(), "Exceptions");
throw e;
} finally {
tracer.endThreadInfoCapture("EJB Interceptors", ctx.getTarget()
.getClass().getName(), ctx.getMethod().getName());
tracer.traceIncident("EJB Interceptors", ctx.getTarget().getClass()
.getName(), ctx.getMethod().getName(), "Invocations");
}
}
|
Как и ранее (см. листинг 1), в листинге 2 показан тяжеловесный вариант инструментирования, который обычно не рекомендуется использовать (здесь он приведен только в качестве примера). Основными моментами в листинге 2 являются следующие:
- аннотация
@AroundInvoke говорит о том, что данный метод должен вызываться в результате перехвата вызова объекта EJB;
- при вызове метода управление передается далее по стеку либо целевому объекту, либо следующему перехватчику. Базовые значения показателей засекаются перед передачей управления, а данные трассируются непосредственно после этого;
- объект типа
InvocationContext передается перехватчику в качестве параметра. Он содержит все необходимую метаинформацию о текущем вызове, в том числе:- ссылку на целевой объект;
- имя целевого метода;
- список параметров, передаваемых в целевой метод.
Этот параметр очень важен, так как перехватчик может быть внедрен в контекст многих объектов EJB, поэтому нельзя делать никаких предположений насчет того, какой именно вызов перехвачен в настоящий момент. Доступ к метаданным изнутри перехватчика совершенно незаменим, так как все метрики, какими бы полезными они ни были, окажутся совершенно неинформативными, если не указать, к каким операциям они относятся.
С точки зрения инструментирования в целом, наибольшим преимуществом подобных перехватчиков является то, что они могут внедряться путем редактирования только дескриптора развертывания. Пример такого дескриптора (файл ejb-jar.xml) для простого объекта EJB приведен в листинге 3.
Листинг 3. Описание перехватчика в дескрипторе развертывания EJB 3
<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd" version="3.0">
<interceptors>
<interceptor>
<interceptor-class>
org.runtimemonitoring.interceptors.ejb.EJBTracingInterceptor
</interceptor-class>
<around-invoke>
<method-name>trace</method-name>
</around-invoke>
</interceptor>
</interceptors>
<assembly-descriptor>
<interceptor-binding>
<ejb-name>AA4H-HibernateService</ejb-name>
<interceptor-class>
org.runtimemonitoring.interceptors.ejb.EJBTracingInterceptor
</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
|
Как уже упоминалось выше, инструментирование при помощи перехватчиков может быть полезно при реализации контекстной трассировки или трассировки на основе диапазонов и пороговых значений. При использовании EJB это преимущество становится еще более ощутимым благодаря доступу к значениям параметров, хранящихся в объекте InvocationContext. Эти значения могут пригодиться при трассировке составных имен диапазонов или дополнительной контекстной информации. В качестве примера рассмотрим вызов метода issueRemoteOperation(String region, Command command) EJB-объекта org.myco.regional.RemoteManagement. Данный объект получает на вход команду и выполняет удаленный вызов сервера, который идентифицируется при помощи параметра region. При такой схеме работы серверы могут быть широко распределены географически, причем вызов каждого сервера может иметь свои особенности, связанные с его сетью. Таким образом, ситуация чем-то напоминает задачу расчета заработной платы, рассмотренную в первой статье, так как для оценки продолжительности вызова данного объекта EJB необходимо знать, в каком регионе сервер был вызван в результате обработки команды. Логично предположить, что время вызова для серверов, расположенных на другом континенте, будет значительно превышать аналогичное время для серверов, находящихся в соседней комнате. Поскольку расположение сервера можно определить по значениям параметров в InvocationContext, коды регионов можно добавить к составному имени трассировки. В результате можно построить картину производительности в разрезе регионов, как показано в листинге 4.
Листинг 4. Пример реализации контекстной трассировки в перехватчике EJB 3
String[] prefix = null;
if(ctx.getTarget().getClass().getName()
.equals("org.myco.regional.RemoteManagement") &&
ctx.getMethod().getName().equals("issueRemoteOperation")) {
prefix = new String[]{"RemoteManagement",
ctx.getParameters()[0].toString(),
"issueRemoteOperation"};
}
// Теперь добавим префикс к составному имени трассировки
|
Сервлетные фильтры в качестве перехватчиков
Интерфейс Java-сервлетов поддерживает понятие фильтра, которое аналогично перехватчикам в EJB 3 в том смысле, что позволяет внедрять перехватчики без изменения исходного кода, а также предоставляет доступ к метаданным. В листинге 5 показан пример метода doFilter фильтра, содержащего упрощенный вариант инструментирования. Составные имена метрик при трассировке формируются на основе имени класса фильтра и универсального идентификатора ресурса (URI) в заголовке запроса.
Листинг 5. Метод сервлетного фильтра, перехватывающий вызов сервлета
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain filterChain) throws IOException, ServletException {
String uri = null;
try {
uri = ((HttpServletRequest)req).getRequestURI();
tracer.startThreadInfoCapture(CPU + BLOCK + WAIT);
// ===================================
// Вызов целевого сервлета
// ===================================
filterChain.doFilter(req, resp);
// ===================================
} catch (Exception e) {
} finally {
tracer.endThreadInfoCapture("Servlets", getClass().getName(), uri);
}
}
|
В листинге 6 приведен фрагмент дескриптора развертывания (файл web.xml), в котором конфигурируется фильтр, показанный в листинге 5.
Листинг 6. Описание фильтра в дескрипторе развертывания
<web-app >
<filter>
<filter-name>ITraceFilter</filter-name>
<display-name>ITraceFilter</display-name>
<filter-class>org.myco.http.ITraceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ITraceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
|
Клиентские EJB-перехватчики и передача контекста
Хотя в предыдущих примерах рассматривались только серверные компоненты, также существуют варианты инструментирования на стороне клиента, в том числе на основе перехватчиков. Клиенты Ajax могут регистрировать объекты-слушатели для измерения продолжительности вызовов через экземпляры XMLHttpRequest. Данные измерений (время и URI запроса, которое может использоваться в качестве составного имени при трассировке) в этом случае будут передаваться дополнительными параметрами при следующем запросе. Кроме того, некоторые серверы Java EE, в частности JBoss, позволяют создавать клиентские перехватчики, которые работают аналогично перехватчикам EJB 3. Собранные показатели передаются на сервер в следующем запросе.
Клиентскому коду часто уделяется мало внимания при мониторинге производительности. Тем не менее когда в следующий раз вы услышите жалобы пользователей на медленную работу вашего приложения, не торопитесь их игнорировать только потому, что средства серверного мониторинга не выявляют никаких проблем. Инструментирование клиентского кода позволяет производить мониторинг поведения приложения, непосредственно наблюдаемого пользователями. Эти данные не всегда совпадают с показателями, собранными на серверной стороне.
Клиентские перехватчики, поддерживаемые некоторыми реализациями Java EE, инстанциируются и связываются на клиентской стороне EJB. Другими словами, можно собирать показатели работы удаленного клиентского объекта, вызывающего серверный EJB-компонент, через протокол вызова удаленных методов (RMI). Более того, если внедрить перехватчики на обеих сторонах удаленного взаимодействия, то можно также обмениваться контекстной информацией, получая тем самым дополнительные данные о производительности.
В примере, приведенном ниже, показана пара взаимодействующих перехватчиков, вычисляющих продолжительность передачи данных (время, затрачиваемое на пересылку запросов и ответов). Кроме того, клиентский перехватчик собирает данные о том, с какой задержкой удаленный сервер генерирует ответы на запросы. Клиентский и серверный перехватчики вызовов EJB 3 созданы на основе проприетарных средств JBoss.
Перехватчики передают контекстные данные внутри EJB-вызовов вместе с остальной полезной информацией. Контекст состоит из следующих данных:
-
время отправки запроса клиентом: временная отметка, сделанная клиентским EJB-перехватчиком в момент отправки запроса клиентом;
-
время получения запроса сервером: временная отметка, сделанная серверным EJB-перехватчиком в момент получения запроса сервером;
-
время отправки ответа сервером: временная отметка, сделанная серверным EJB-перехватчиком в момент отправки ответа сервером.
Параметры вызовов обрабатываются в виде стека. Контекстные данные помещаются в стек клиентским перехватчиком (операция "push"), далее выбираются из стека серверным перехватчиком ("pop"), а затем передаются серверной EJB-заглушке. При ответе вся последовательность действий меняется на противоположную. Схема работы показана на рисунке 3.
Рисунок 3. Схема взаимодействия клиентского и серверного перехватчиков вызовов EJB
В данном примере клиентский и серверный перехватчики должны реализовывать интерфейс org.jboss.aop.advice.Interceptor, содержащий один важный метод:
public abstract java.lang.Object invoke(
org.jboss.aop.joinpoint.Invocation invocation) throws java.lang.Throwable |
Данный метод реализует идею, получившую название "инкапсуляция вызова". При таком подходе выполнение метода инкапсулируется внутри дискретного объекта, содержащего следующую информацию:
- целевой класс;
- имя вызываемого метода;
- информационное наполнение – данные, передаваемые в качестве параметров вызываемому методу.
Данный экземпляр может передаваться до тех пор, пока не поступит на вход вызывающего объекта, который выполнит демаршаллинг данных и вызовет нужный метод целевого класса.
Клиентский перехватчик помещает в контекст вызова время отправки запроса, а серверный – время получения запроса на обработку и время отправки ответа. При желании сервер может вычислить время передачи запроса, а клиент – время пересылки запроса и ответа. Для этого достаточно произвести следующие вычисления:
-
время на передачу запроса (клиентская сторона) равно
ServerSideReceivedTime (время получения запроса сервером) минус ClientSideRequestTime (время отправки запроса клиентом);
-
время на передачу ответа (клиентская сторона) равно
ClientSideReceivedTime (время получения ответа клиентом) минус ServerSideRespondTime (время отправки ответа сервером);
-
время на передачу запроса (серверная сторона) равно
ServerSideReceivedTime (время получения запроса сервером) минус ClientSideRequestTime (время отправки запроса клиентом).
Метод invoke клиентского перехватчика приведен в листинге 7.
Листинг 7. Метод invoke клиентского перехватчика
/**
* Вызываемый метод перехватчика
* @param invocation Инкапсулированный вызов.
* @return Результат вызова целевого метода.
* @throws Throwable
* @see org.jboss.aop.advice.Interceptor#invoke(org.jboss.aop.joinpoint.Invocation)
*/
public Object invoke(Invocation invocation) throws Throwable {
if(invocation instanceof MethodInvocation) {
getInvocationContext().put(CLIENT_REQUEST_TIME, System.currentTimeMillis());
Object returnValue = clientInvoke((MethodInvocation)invocation);
long clientResponseTime = System.currentTimeMillis();
Map<String, Serializable> context = getInvocationContext();
long clientRequestTime = (Long)context.get(CLIENT_REQUEST_TIME);
long serverReceiveTime = (Long)context.get(SERVER_RECEIVED_TIME);
long serverResponseTime = (Long)context.get(SERVER_RESPOND_TIME);
long transportUp = serverReceiveTime-clientRequestTime;
long transportDown = serverResponseTime-clientResponseTime;
long totalElapsed = clientResponseTime-clientRequestTime;
String methodName = ((MethodInvocation)invocation).getActualMethod().getName();
String className = ((MethodInvocation)invocation).getActualMethod()
.getDeclaringClass().getSimpleName();
ITracer tracer = TracerFactory.getInstance();
tracer.trace(transportUp, "EJB Client", className, methodName,
"Transport Up", transportUp);
tracer.trace(transportDown, "EJB Client", className, methodName,
"Transport Down", transportDown);
tracer.trace(totalElapsed, "EJB Client", className, methodName,
"Total Elapsed", totalElapsed);
return returnValue;
} else {
return invocation.invokeNext();
}
}
|
 |
Перехватчики EJB 3 в JBoss
Поддержка передачи произвольных данных в теле запросов была одной из возможностей архитектуры EJB 2 в JBoss. Однако, несмотря на старания разработчиков, при использовании этой функции в EJB 3 возникают проблемы. Вследствие этого перехватчики, показанные в данной статье, передают контекстную информацию в запросе в виде дополнительного параметра вызова. При маршаллинге объект-ответ сериализуется в массив из двух элементов (Object[2]), первым из которых является результат вызова, а вторым – контекст. Оба объекта восстанавливаются путем демаршаллинга перехватчиком на другой стороне вызова, поэтому клиент и сервер всегда получают ожидаемые типы объектов.
|
|
Серверный перехватчик работает аналогичным образом за тем исключением, что в целях упрощения данного примера он использует локальный поток для обнаружения случаев повторного входа (reentrancy). Данная ситуация означает, что поток, обрабатывающий запрос, вызывает один и тот же компонент EJB (и следовательно, перехватчик) более одного раза в течение одного удаленного вызова. В этом случае перехватчик анализирует контекст и выполняет трассировку только при первом вызове. Метод invoke серверного перехватчика показан в листинге 8.
Листинг 8. Метод invoke серверного перехватчика
/**
* Вызываемый метод перехватчика.
* @param invocation Инкапсулированный вызов.
* @return Результат вызова целевого метода.
* @throws Throwable
* @see org.jboss.aop.advice.Interceptor#invoke(org.jboss.aop.joinpoint.Invocation)
*/
public Object invoke(Invocation invocation) throws Throwable {
Boolean reentrant = reentrancy.get();
if((reentrant==null || reentrant==false)
&& invocation instanceof MethodInvocation) {
try {
long currentTime = System.currentTimeMillis();
MethodInvocation mi = (MethodInvocation)invocation;
reentrancy.set(true);
Map<String, Serializable> context = getInvocationContext(mi);
context.put(SERVER_RECEIVED_TIME, currentTime);
Object returnValue = serverInvoke((MethodInvocation)mi);
context.put(SERVER_RESPOND_TIME, System.currentTimeMillis());
return addContextReturnValue(returnValue);
} finally {
reentrancy.set(false);
}
} else {
return invocation.invokeNext();
}
}
|
Сервер JBoss внедряет перехватчиков, используя технологию аспектно-ориентированного программирования (AOP) (см. Ресурсы). Перехватчики устанавливаются на основе директив, описанных в файле ejb3-interceptors-aop.xml. Кроме того, AOP также используется в JBoss для применения базовых правил Java EE к классам EJB-компонентов во время выполнения приложения, поэтому вдобавок к перехватчикам, измеряющим производительность, данный файл содержит директивы, касающиеся управления транзакциями, безопасностью и хранением данных. Клиентские директивы достаточно просты. Они описываются при помощи XML-элемента stack name, содержащего набор имен классов, реализующих перехватчики. Каждый класс считается перехватчиком типа либо PER_VM, либо PER_INSTANCE, определяющим, что каждому EJB-компоненту должен соответствовать свой собственный перехватчик, либо один перехватчик может использоваться совместно несколькими компонентами. В ситуации, когда перехватчики используются для мониторинга производительности, выбор зависит от того, является ли код перехватчика потокобезопасным. Если является, т. е. перехватчик может безопасно выполняться в нескольких параллельных потоках, то в целях повышения эффективности можно использовать тип PER_VM. В противном случае следует назначать тип PER_INSTANCE.
Конфигурация серверных перехватчиков несколько более сложна. Они устанавливаются в соответствии с набором синтаксических образцов и фильтров, описанных в XML. В случае, если вызывается EJB-метод, имя которого удовлетворяет определенному образцу, срабатывают перехватчики, указанные для этого образца. Кроме того, при помощи дополнительных настроек можно устанавливать перехватчики только для некоторого подмножества развернутых компонентов EJB. На клиентской стороне для этого создается новый элемент stack name, относящийся к нужному компоненту. На серверной стороне для этого необходимо создать новый элемент domain. Содержимое клиентских элементов stack name или серверного domain может задаваться в аннотациях к классам EJB-объектов либо, если вы не хотите изменять исходный код, это можно указать в дескрипторе развертывания объектов EJB. Сокращенная версия файла ejb3-interceptors-aop.xml, используемого в нашем примере, приведена в листинге 9.
Листинг 9. Фрагмент конфигурационного файла настроек AOP в EJB 3
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE aop PUBLIC
"-//JBoss//DTD JBOSS AOP 1.0//EN"
"http://labs.jboss.com/portal/jbossaop/dtd/jboss-aop_1_0.dtd">
<aop>
.
.
<interceptor
class="org.runtimemonitoring.ejb.interceptors.ClientContextualInterceptor"
scope="PER_VM"/>
.
.
<stack name="StatelessSessionClientInterceptors">
<interceptor-ref
name="org.runtimemonitoring.ejb.interceptors.ClientContextualInterceptor"/>
.
.
</stack>
.
.
<interceptor
class="org.runtimemonitoring.ejb.interceptors.ServerContextualInterceptor"
scope="PER_VM"/>
.
.
<domain name="Stateless Bean">
<bind pointcut="execution(public * *->*(..))">
<interceptor-ref name="org.aa4h.ejb.interceptors.ServerContextualInterceptor"/>
.
.
</bind>
</domain>
</aop>
|
Данный способ измерения производительности позволяет одним махом убить двух зайцев. Во-первых, он показывает картину текущей производительности EJB-компонентов с точки зрения клиента. Во-вторых, с его помощью можно определить, является ли работа сети причиной падения быстродействия системы. На рисунке 4 показаны графики изменения общей длительности операций и времени передачи вызовов по сети. При этом быстродействие сети было искусственно занижено для иллюстрации влияния ее работы.
Рисунок 4. Контекстные показатели быстродействия (в миллисекундах), собранные клиентским перехватчиком
Для использования перехватчика на стороне клиента необходимо поместить его класс в classpath клиентского приложения либо загрузить его и все необходимые библиотеки с удаленного сервера при запуске. Учтите, что если системное время на клиентском компьютере не синхронизировано с серверным практически идеально, то результаты измерения могут выглядеть весьма странно, причем странные эффекты будут возрастать прямо пропорционально разнице во времени.
Перехватчики в Spring
Платформа Java EE предоставляет широкие возможности интегрирования перехватчиков прозрачным для приложения образом. Однако многие популярные контейнеры, не реализующие Java EE, также позволяют организовывать явный или неявный перехват вызовов. В данном случае контейнер обозначает некую инфраструктуру, использование которой снижает (или позволяет снизить) степень взаимных зависимостей между компонентами приложения. Отсутствие тесных связей значительно облегчает реализацию перехватчиков. Подобные типы инфраструктур часто называют архитектурами инъекции зависимостей (dependency injection) или инверсии управления (Inversion of Control – IoC). Они позволяют разработчикам "склеивать" компоненты между собой при помощи внешних файлов вместо того чтобы устанавливать все зависимости непосредственно в коде. В этом разделе, завершающем обсуждение перехватчиков, будут рассмотрены методы сбора показателей производительности приложения при помощи трассирующих перехватчиков в инфраструктуре Spring Framework (см. Ресурсы), представляющей собой один из популярных контейнеров IoC.
Spring Framework позволяет разрабатывать приложения на основе простых Java-объектов (Plain Old Java Objects – POJO). Сами объекты содержат только бизнес-логику, а все средства, необходимые для создания корпоративных приложений, предоставляются инфраструктурой. Многоуровневая архитектура Spring полезна не только при инструментировании, но и при первоначальной разработке. Приведение архитектуры приложения в соответствие с идеологией Spring не всегда представляет собой тривиальную задачу, но зато ее возможности по управлению объектами POJO, а также механизмы интеграции с Java EE и AOP позволяют развертывать в контейнере обыкновенные Java-классы. В то же время можно реализовать инструментирование на основе перехватчиков, не затрагивая исходный код целевых классов.
Spring часто называют IoC-контейнером, потому что она меняет традиционную топологию Java-приложений на противоположную. В традиционной модели центральная программа (поток управления) загружает все внутренние и внешние компоненты, от которых она зависит. При использовании IoC контейнер загружает несколько компонентов и управляет зависимостями между ними в соответствии с внешней конфигурацией. Подобное управление связями между объектами получило название "внедрение зависимостей", потому что каждый компонент получает ссылки на необходимые объекты, например, DataSource в JDBC, от контейнера, а не занимается их поиском самостоятельно. При реализации инструментирования можно изменить конфигурацию контейнера с целью внедрения перехватчиков непосредственно в связи между компонентами. Данный принцип иллюстрируется на рисунке 5.
Рисунок 5. Общая схема использования перехватчиков в Spring
Пришло время рассмотреть простой пример использования перехватчиков в Spring. Допустим есть класс EmpDAOImpl, представляющий собой базовый объект доступа к данным (DAO) и реализующий интерфейс DAO, в котором определен метод public Map<Integer, ? extends DAOManaged> get(Integer...pks). Данный метод принимает на вход массив первичных ключей, идентифицирующих объекты, и возвращает ассоциативный массив (Map) самих объектов. Разумеется, у подобного кода есть ряд недостатков, в частности, он никак не приспособлен для инструментирования, и в нем не используются средства объектно-реляционного отображения (ORM). Структура классов показана на рисунке 6. Ссылки на весь исходный код и остальные файлы данного примера содержатся в разделе Загрузка.
Рисунок 6. Классы EmpDAO
Экземпляр EmpDAOImpl развернут в контейнере Spring в соответствии с описанием в файле spring.xml. Фрагмент данного файла приведен в листинге 10.
Листинг 10. Пример конфигурационного файла в Spring
<beans>
<bean id="tracingInterceptor"
class="org.runtimemonitoring.spring.interceptors.SpringTracingInterceptor">
<property name="interceptorName" value="Intercepted DAO"/>
</bean>
<bean id="tracingOptimizedInterceptor"
class="org.runtimemonitoring.spring.interceptors.SpringTracingInterceptor">
<property name="interceptorName" value="Optimized Intercepted DAO"/>
</bean>
<bean id="DataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:url="jdbc:postgresql://DBSERVER:5432/runtime"
p:driverClassName="org.postgresql.Driver"
p:username="scott"
p:password="tiger"
p:initial-size="2"
p:max-active="5"
p:pool-prepared-statements="true"
p:validation-query="SELECT CURRENT_TIMESTAMP"
p:test-on-borrow="false"
p:test-while-idle="false"/>
<bean id="EmployeeDAO" class="org.runtimemonitoring.spring.EmpDAOImpl"
p:dataSource-ref="DataSource"/>
<bean id="empDao" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="org.runtimemonitoring.spring.DAO"/>
<property name="target" ref="EmployeeDAO"/>
<property name="interceptorNames">
<list>
<idref local="tracingInterceptor"/>
</list>
</property>
</bean>
<bean id="empDaoOptimized"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="EmployeeDAO"/>
<property name="optimize">
<value>true</value>
</property>
<property name="proxyTargetClass">
<value>true</value>
</property>
<property name="interceptorNames">
<list>
<idref local="tracingOptimizedInterceptor"/>
</list>
</property>
</bean>
</beans>
|
Кроме него будут развернуты еще несколько объектов. Они описываются в файле spring.xml (см. листинг 10) при помощи элементов bean. Для идентификации объектов служит атрибут bean id.
-
tracingInterceptor и tracingOptimizedInterceptor: оба этих объекта являются перехватчиками (экземплярами SpringTracingInterceptor). Этот класс передает данные системе APM через вызовы ITracer.
-
DataSource: JDBC-интерфейс, поддерживающий пул соединений с базой данных под названием runtime. Ссылка на экземпляр DataSource будет передана в объект EmpDAOImpl контейнером Spring.
-
EmployeeDAO: экземпляр EmpDAOImpl, методы которого будут вызываться в данном примере.
-
empDao и empDaoOptimized: эти два объекта являются экземплярами класса ProxyFactoryBean в Spring. Каждый из них является прокси-объектом для EmpDAOImpl и содержит ссылку на перехватчик. К объекту EmpDAOImpl можно обращаться напрямую, однако при помощи прокси можно вызывать перехватчиков и собирать данные о быстродействии. На примере данных прокси-объектов и двух перехватчиков, приведенных в листинге 10, показаны разные варианты реализации и конфигурирования (см. заметку "Оптимизированные перехватчики").
 |
Оптимизированные перехватчики
Разница в описании стандартного и оптимизированного перехватчиков в листинге 10 заключается в дополнительном свойстве оптимизированного прокси-объекта: <property name="optimize"><value>true</value></property>. При отсутствии данного свойства прокси-объект вызывает перехватчик через механизм рефлексии (интерфейс java.lang.reflect.Proxy). Если же свойство указано, то Spring динамически генерирует байт-код для прямого (без рефлексии) вызова. Это осуществляется при помощи библиотеки инструментирования байт-кода под названием CGLIB. Оптимизация на уровне байт-кода, как правило, позволяет повысить производительность по сравнению с динамическими прокси-объектами. Однако эта картина постепенно меняется, особенно вследствие серьезного ускорения работы механизма рефлексии в последних версиях JVM.
|
|
Контейнер Spring запускается при помощи класса SpringRunner. Он также запускает тестовый цикл, в котором вызывается метод DAO.get у следующих четырех объектов:
- объект
EmployeeDAO, представляющий собой простой неинструментированный объект DAO, управляемый Spring;
- объект
empDao, представляющий собой управляемый объект DAO c инструментированием в виде стандартного перехватчика Spring;
- объект
empDaoOptimized, представляющий собой управляемый объект DAO c инструментированием в виде оптимизированного перехватчика Spring;
- объект
EmpDAOImpl, не находящийся под управлением Spring. Он используется в примере для сравнения с управляемыми объектами.
В Spring эти типы перехватчиков реализуют интерфейс org.aopalliance.intercept.MethodInterceptor. Он содержит единственный метод: public Object invoke(MethodInvocation invocation) throws Throwable. Объект MethodInvocation выполняет две основные функции: во-первых, он содержит контекст вызова (а именно, имя перехваченного вызова), а во-вторых, предоставляет метод proceed, который передает управление целевому объекту.
В листинге 11 приведен метод invoke класса SpringTracingInterceptor. Хотя в данном конкретном случае свойство interceptorName не является обязательным, мы его добавим в целях создания дополнительного контекста. В системах, использующих множество различных перехватчиков, их имена добавляются в контекст трассировки, чтобы данные о работе методов можно было трассировать в разных пространствах имен APM.
Листинг 11. Метод invoke класса SpringTracingInterceptor
public Object invoke(MethodInvocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
tracer.startThreadInfoCapture(WAIT+BLOCK);
Object returnValue = invocation.proceed();
tracer.endThreadInfoCapture("Spring", "DAO",
interceptorName, methodName);
tracer.traceIncident("Spring", "DAO", interceptorName,
methodName, "Responses Per Interval");
return returnValue;
}
|
Точкой входа в данном примере служит класс SpringRunner. Он инициализирует фабрику объектов Spring, а затем запускает длинный цикл, в котором устанавливаются свойства каждого из объектов. Код цикла приведен в листинге 12. Обратите внимание, что поскольку объекты daoNoInterceptor и daoDirect не инструментированы перехватчиками Spring, инструментирование осуществляется непосредственно в цикле SpringRunner.
Листинг 12. Упрощенный вариант цикла SpringRunner
Map<Integer, ? extends DAOManaged> emps = null;
DAO daoIntercepted = (DAO) bf.getBean("empDao");
DAO daoOptimizedIntercepted = (DAO) bf.getBean("empDaoOptimized");
DAO daoNoInterceptor = (DAO) bf.getBean("EmployeeDAO");
DataSource dataSource = (DataSource) bf.getBean("DataSource");
DAO daoDirect = new EmpDAOImpl();
// Этот объект не инициализируется Spring, поэтому свойство
// устанавливается вручную
daoDirect.setDataSource(dataSource);
for(int i = 0; i < 100000; i++) {
emps = daoIntercepted.get(empIds);
log("(Interceptor) Acquired ", emps.size(), " Employees");
emps = daoOptimizedIntercepted.get(empIds);
log("(Optimized Interceptor) Acquired ", emps.size(), "
Employees");
tracer.startThreadInfoCapture(WAIT+BLOCK);
emps = daoNoInterceptor.get(empIds);
log("(Non Intercepted) Acquired ", emps.size(), " Employees");
tracer.endThreadInfoCapture("Spring", "DAO",
"No Interceptor DAO", "get");
tracer.traceIncident("Spring", "DAO",
"No Interceptor DAO", "get", "Responses Per Interval");
tracer.startThreadInfoCapture(WAIT+BLOCK);
emps = daoDirect.get(empIds);
log("(Direct) Acquired ", emps.size(), " Employees");
tracer.endThreadInfoCapture("Spring", "DAO",
"Direct", "get");
tracer.traceIncident("Spring", "DAO", "Direct",
"get", "Responses Per Interval");
}
|
В отчете, построенном APM по собранным данным, сопоставляются несколько показателей. В таблице 1 представлены данные о продолжительности вызовов методов каждого из объектов Spring.
Таблица 1. Результаты сравнения быстродействия перехватчиков Spring
| Объект Spring | Средняя продолжительность (мс) | Минимальная продолжительность (мс) | Максимальная продолжительность (мс) | Число вызовов |
|---|
| Прямой доступ | 145 | 124 | 906 | 5110 |
|---|
| Оптимизированный перехватчик | 145 | 125 | 906 | 5110 |
|---|
| Без перехватчика | 145 | 124 | 891 | 5110 |
|---|
| Обычный перехватчик | 155 | 125 | 952 | 5110 |
|---|
Дерево показателей, построенное APM для данного примера, показано на рисунке 7.
Рисунок 7. Дерево метрик, построенное APM по результатам тестирования перехватчиков Spring
В графическом виде данные представлены на рисунке 8.
Рисунок 8. Результаты (в миллисекундах) тестирования перехватчиков Spring
Несмотря на то, что все показатели достаточно близки, некоторые тенденции все же улавливаются. В частности, оптимизированный перехватчик оказывается чуть быстрее неоптимизированного. Однако данный пример не очень показателен ввиду однопоточности. В следующем разделе мы его расширим, реализовав вызов методов из нескольких параллельных потоков.
Инструментирование JDBC при помощи классов-оберток
Мой опыт подсказывает, что причина наиболее хронических проблем с производительностью в типовых корпоративных Java-приложениях как правило кроется в интерфейсах взаимодействия с базой данных. В этом нет ничего удивительного, так как обращения к базам данных через JDBC являются наиболее часто встречающимся вариантом вызова внешних сервисов с целью получения данных, недоступных внутри JVM. Логически, причиной неудовлетворительного быстродействия может быть поведение клиента, сервера базы данных, либо недостаточная степень совместимости между ними. Многие приложения, имеющие дело с базой данных, построенные по принципам "толстого" клиента, имеют следующие недостатки, негативно сказывающиеся на быстродействии:
- логически корректные, но медленно выполняющиеся SQL-запросы;
- слишком общие запросы, возвращающие значительно больше данных чем необходимо;
- многократная передача одних и тех же данных;
- неудачно выбранная гранулярность запросов, в результате чего происходит множество обращений к базе данных для выборки информации, относящейся к одной логической структуре. Лучшим вариантом было бы сократить число запросов, результатом которых был бы тот же набор данных (лично я предпочитаю делать один запрос, возвращающий большое число записей, а не множество запросов, результатами которых будут небольшие наборы данных). Данная ситуация часто встречается в приложениях, чья объектная модель содержит вложенные структуры классов, причем разработчик, пытаясь применить классические принципы инкапсуляции, возложил на каждый класс ответственность за сохранение и выборку своих экземпляров вместо того чтобы делегировать эти функции общему сервису по управлению данными.
Разумеется, я не собираюсь критиковать архитектуру и реализации всех клиентских приложений, более того, в третьей части серии мы обсудим методы мониторинга самой базы данных с целью сбора статистических данных о ее быстродействии. Однако в большинстве случаев проблема все же заключается в клиентах. Таким образом, главной целью мониторинга быстродействия кода, отвечающего за взаимодействие с базой данных, является JDBC.
Далее будет продемонстрирован подход к инструментированию JDBC-клиентов на основе так называемых классов-оберток. Основная идея заключается в следующем: целевой класс заключается в своего рода обертку из инструментирующего кода, которая ведет себя таким же образом, как и обернутый класс. Основная трудность в подобном сценарии заключается в том, чтобы поместить класс в обертку, не нарушив работу всех связанных с ним компонентов приложения.
В данном примере мы воспользуемся тем фактом, что JDBC представляет собой API, практически полностью основанный на интерфейсах. Его спецификация включает в себя лишь несколько классов, а архитектура исключает необходимость жесткого связывания кода приложения со специфическими классами, относящимися к конкретной СУБД. Подобные классы, составляющие реализацию JDBC для конкретной базы данных, загружаются неявным образом, и клиентский код практически никогда не обращается к ним напрямую. Таким образом, можно объявить новый JDBC-драйвер, все функции которого будут заключаться в том, чтобы делегировать запросы реальному драйверу JDBC, собирая при этом данные о быстродействии вызовов.
В нашем случае роль такого драйвера будет выполнять класс WrappingJDBCDriver. Его возможностей вполне хватит для демонстрации сбора данных о производительности JDBC при обращении к базе данных через класс EmployeeDAO (см. пример использования Spring выше). Схема работы класса WrappingJDBCDriver показана на рисунке 9.
Рисунок 9. Принцип работы WrappingJDBCDriver
Стандартный вариант загрузки драйвера JDBC подразумевает наличие двух вещей: имени класса драйвера и адресной строки JDBC, указывающей на базу данных, с которой должно быть установлено соединение. Класс драйвера загружается специальным загрузчиком, как правило, при помощи метода Class.forName(jdbcDriverClassName). Большинство драйверов JDBC после загрузки регистрируют себя в классе java.sql.DriverManager. Далее загрузчик передает драйверу URL (JDBC-адрес) базы данных, проверяя тем самым, является ли адрес корректным с точки зрения драйвера. Если является, то загрузчик вызывает метод connect драйвера и возвращает клиенту объект java.sql.Connection.
Функции драйвера-обертки выполняет класс org.runtimemonitoring.jdbc.WrappingJDBCDriver. После инстанциирования он загружает конфигурационный файл wrapped-driver.xml, находящийся в classpath приложения. Данный файл содержит приведенные ниже настройки инструментирования. В качестве индекса <Figurative Name> (символическое имя) выступает имя класса целевого JDBC-драйвера:
-
<Figurative Name>.driver.prefix: реальный префикс JDBC-адреса, передаваемого на вход целевому драйверу, например,
jdbc.postgresql:
-
<Figurative Name>.driver.class: имя класса целевого JDBC-драйвера, например,
org.postgresql.Driver.
-
<Figurative Name>.driver.class.path: разделяемый запятыми набор записей, указывающий на местоположение класса целевого JDBC-драйвера. Этот элемент необязателен - если он отсутствует, то класс
WrappingJDBCDriver будет пытаться загрузить класс целевого драйвера, используя собственный загрузчик классов.
-
<Figurative Name>.tracer.pattern.<Zero Based Index>: набор регулярных выражений, использующихся для выбора категории трассировки, соответствующей конкретной базе данных. Категории объединяются в иерархию в соответствии с очередностью выражений в наборе. Индексирование начинается с нуля.
Основная задача класса WrappingJDBCDriver заключается в настройке JDBC-клиента таким образом, чтобы он использовал специально видоизмененные JDBC-адреса, которые будут распознаваться исключительно инструментированным JDBC-драйвером (т.е. самим классом WrappingJDBCDriver). Данный класс, распознав специальный JDBC-адрес, будет загружать целевой JDBC-драйвер и передавать ему реальный адрес (URL) базы данных, предварительно выполнив обратное преобразование. Таким образом, реальная работа по установлению соединения с базой данных будет выполняться целевым драйвером. Соединение будет возвращено клиентскому приложению внутри еще одного класса-обертки – WrappingJDBCConnection. Алгоритм преобразования JDBC-адресов может быть очень прост; все, что от него требуется – это видоизменять адреса таким образом, чтобы они не могли быть распознаны целевыми драйверами (в противном случае управление может быть сразу передано реальному драйверу, минуя WrappingJDBCDriver). В данном примере JDBC-адрес jdbc:postgresql://DBSERVER:5432/runtime будет преобразован к виду jdbc:!itracer!wrapped:postgresql://DBSERVER:5432/runtime.
Имя класса целевого драйвера, а также необязательный параметр, указывающий на его classpath, используются WrappingJDBCDriver для поиска и загрузки драйвера. После этого WrappingJDBCDriver начинает выполнять роль класса-обертки и делегировать вызовы целевому драйверу. Шаблоны трассировки представляют собой набор регулярных выражений, при помощи которых WrappingJDBCDriver определяет пространство имен трассировки для конкретной целевой базы данных. Данные выражения, применяющиеся к реальным JDBC-адресам, необходимы для того, чтобы трассировщик мог передавать данные APM-системе с указанием того, к какой базе данных они относятся. Это полезно в случае, если jdbc:postgresql://DBSERVER:5432/runtime используется при обращении к нескольким базам данных (возможно разного типа), так как это позволяет группировать информацию по имени базы. Например, результатом применения регулярного выражения к JDBC-адресу jdbc:postgresql://DBSERVER:5432/runtime может быть пространство имен "postgresql, runtime".
В листинге 13 показано содержимое файла wrapped-driver.xml, в котором символическое имя postgres соответствует JDBC-драйверу СУБД PostgreSQL 8.3.
Листинг 13. Пример файла wrapped-driver.xml
<properties>
<entry key="postgres.driver.prefix">jdbc:postgresql:</entry>
<entry key="postgres.driver.class">org.postgresql.Driver</entry>
<entry key="postgres.driver.class.path">
C:\Postgres\psqlJDBC\postgresql-8.3-603.jdbc3.jar
</entry>
<entry key="postgres.tracer.pattern.0">:([a-zA-Z0-9]+):</entry>
<entry key="postgres.tracer.pattern.1">.*\/\/.*\/([\S]+)</entry>
</properties>
|
Частично идея подобной реализации была заимствована из проекта с открытым кодом под названием P6Spy (см. Ресурсы).
Для демонстрации работы с WrappingJDBCDriver мы несколько расширим предыдущий пример, иллюстрирующий работу с объектом EmpDAO через Spring. Новый конфигурационный файл Spring будет называться spring-jdbc-tracing.xml, а точкой входа будет класс SpringRunnerJDBC. Данный тест включает в себя ряд других аспектов, принципы именования были слегка изменены, чтобы избежать путаницы. Кроме того, тест будет выполняться в многопоточном режиме, что окажет интересное влияние на результаты измерений. Наконец, параметры вызова методов DAO будут меняться случайным образом, что добавит вариативности данному примеру.
Трассировка данных в новой версии примера была расширена следующим образом:
- определены два источника данных. Обращение к обоим источникам происходит через драйверы JDBC, один из которых инструментированный, а второй – нет;
- при необходимости к обоим источникам данных можно обращаться через прокси-объекты Spring. Код данных объектов инструментирован с целью измерения продолжительности операций по установке соединения;
- были расширены возможности перехватчиков вызовов DAO. Теперь они подсчитывают число параллельных потоков, вызывающих методы объекта DAO;
- для сбора статистики обращений к источникам данных используется дополнительный низкоприоритетный поток;
- все классы-обертки в данном примере, как правило, вызывают трассировщик при помощи методов базового класса, которым является
WrappingJDBCCore. Кроме предоставления удобного доступа к ITracer, этот класс также отвечает за трассировку суммарных показателей на уровне базы данных. Таким образом демонстрируется распространенный принцип работы APM-систем, при котором низкоуровневые и узкоспециализированные метрики могут многократно передаваться в пространства имен более высоких уровней, на которых происходит расчет суммарных показателей. Например, JDBC-вызовы со стороны всех объектов приложения агрегируются на уровне базы данных, на котором производится расчет среднего времени обработки запроса, а также общего числа обращений к базе.
В листинге 14 показаны описания новых объектов в файле spring-jdbc-tracing.xml file. Обратите внимание, что адресная строка JDBC в свойстве объекта InstrumentedJDBC.DataSource указана в видоизмененном формате.
Листинг 14. Фрагменты файла spring-jdbc-tracing.xml
<!-- Перехватчик обращений к DataSource -->
<bean id="InstrumentedJDBCDataSourceInterceptor"
class="org.runtimemonitoring.spring.interceptors.SpringDataSourceInterceptor">
<property name="interceptorName" value="InstrumentedJDBC.DataSource"/>
</bean>
<!-- DataSource для инструментированного JDBC-драйвера -->
<bean id="InstrumentedJDBC.DataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:url="jdbc:!itracer!wrapped:postgresql://DBSERVER:5432/runtime"
p:driverClassName="org.runtimemonitoring.jdbc.WrappingJDBCDriver"
p:username="scott"
p:password="tiger"
p:initial-size="2"
p:max-active="10"
p:pool-prepared-statements="true"
p:validation-query="SELECT CURRENT_TIMESTAMP"
p:test-on-borrow="false"
p:test-while-idle="false"/>
<!-- Прокси-объект для экземпляра DataSource -->
<bean id="InstrumentedJDBC.DataSource.Proxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="InstrumentedJDBC.DataSource"/>
<property name="optimize"><value>true</value></property>
<property name="proxyTargetClass"><value>true</value></property>
<property name="interceptorNames">
<list>
<idref local="InstrumentedJDBCDataSourceInterceptor"/>
</list>
</property>
</bean>
<!--
Прокси-объект для экземпляра DataSource. Данный прокси передается
объекту DAO вместо самого DataSource
-->
<bean id="InstrumentedJDBC.DataSource.Proxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="InstrumentedJDBC.DataSource"/>
<property name="optimize"><value>true</value></property>
<property name="proxyTargetClass"><value>true</value></property>
<property name="interceptorNames">
<list>
<idref local="InstrumentedJDBCDataSourceInterceptor"/>
</list>
</property>
</bean>
|
Дерево метрик, построенное APM по итогам работы данного примера, показано на рисунке 10.
Рисунок 10. Дерево результатов теста, включающего инструментированный JDBC-драйвер
На этом примере, благодаря увеличенной интенсивности работы с базой данных, становятся заметными некоторые конкретные причины перевода потоков в состояния BLOCK и WAIT. Объект SpringRunnerJDBC добавляет трассировку показателей ThreadInfoCapture(WAIT+BLOCK) непосредственно перед вызовом и сразу после вызова Thread.currentThread().join(100) в конце каждой итерации. Таким образом, APM-система сможет отображать среднее время ожидания для потоков, которое в этом примере составило 103 мс. Данный показатель отражает продолжительность времени, которое потоки проводят в ожидании наступления определенного события. Кроме времени ожидания также очевиден рост числа заблокированных потоков при выполнении метода DAO.get. Это происходит потому, что, запрашивая соединение с базой данных у объекта DataSource, каждому потоку приходится выдерживать жесткую конкуренцию со стороны большого числа других потоков, которым требуется тот же синхронизированный ресурс.
В примере используются несколько экземпляров DAO, отличающихся методом инструментирования источников данных. Сравнение результатов мониторинга при различных сценариях работы приведено в таблице 2.
Таблица 2. Сравнительные результаты теста, включающего инструментированный JDBC-драйвер
| Тест | Средняя продолжительность вызова (мс) | Минимальная
продолжительность вызова (мс)
| Максимальная
продолжительность вызова (мс) | Число вызовов |
|---|
| Прямой доступ, "чистый" JDBC | 5 | 0 | 78 | 12187 |
|---|
| Прямой доступ, инструментированный JDBC | 27 | 0 | 281 | 8509 |
|---|
| Объект Spring без перехватчиков, "чистый" JDBC | 15 | 0 | 125 | 12187 |
|---|
| Объект Spring без перехватчиков, инструментированный JDBC | 35 | 0 | 157 | 8511 |
|---|
| Инструментированный объект Spring, "чистый" JDBC | 16 | 0 | 125 | 12189 |
|---|
| Инструментированный объект Spring, инструментированный JDBC | 36 | 0 | 250 | 8511 |
|---|
| Инструментированный объект Spring с оптимизацией, "чистый" JDBC | 15 | 0 | 203 | 12188 |
|---|
| Инструментированный объект Spring с оптимизацией, инструментированный JDBC | 35 | 0 | 187 | 8511 |
|---|
По результатам, представленным в таблице 2, можно сделать ряд выводов, причем один из них совершенно очевиден: инструментированные источники данных JDBC работают значительно медленнее, чем неинструментированные. Всегда помните об этом эффекте и старайтесь максимально оптимизировать инструментирующий код. В данном примере падение быстродействия происходит по нескольким причинам, в том числе при трассировке, вызовах промежуточных объектов, а также при создании дополнительных объектов, необходимых для выполнения запросов. Разумеется, над этой реализацией пришлось бы серьезно поработать перед тем как использовать ее для мониторинга системы, для которой критична высокая производительность. Инструментирование экземпляров DAO также оказывает влияние на быстродействие, однако намного менее негативное. Оно также объясняется накладными расходами, связанными с использованием рефлексии, дополнительными вызовами и трассировкой. Вероятно, трассировку тоже можно оптимизировать, но, тем не менее, реальность заключается в том, что любой вариант инструментирования, так или иначе, снижает производительность. Диаграммы длительности вызовов, построенные по данным теста, показаны на рисунке 11.
Рисунок 11. Временные диаграммы (в миллисекундах), построенные по данным таблицы 2
Последним из интересных показателей является время, которое потоки провели в заблокированном состоянии. Данный показатель агрегируется на уровне базы данных. Подобная статистика накапливается для всех показателей с учетом всех обращений к базе данных за промежуток времени. Агрегирование для разных метрик выполняется по-разному, например, показатели продолжительности вызовов усредняются, а инцидентные метрики (число откликов, блокировок и ожиданий за интервал времени) суммируются. В данном случае среднее за интервал время блокировок равно нулю, однако, как видно из рисунка 12, некоторые средства визуализации APM также способны отображать максимальные и минимальные значения. На этом графике совмещаются среднее значение (горизонтальная линия на нулевом уровне), а также максимальные значения за каждый промежуток.
Рисунок 12. Агрегированные показатели продолжительности блокировок (в миллисекундах)
В заключительном разделе статьи будет рассказано о последнем варианте инструментирования Java-классов, не затрагивающем их исходный код. Этот подход заключается в инструментировании байт-кода.
Инструментирование байт-кода
Все ранее рассмотренные подходы к инструментированию заключались в использовании дополнительных объектов, что зачастую удлиняло последовательность вызовов даже без учета кода, выполняющего саму трассировку. Инструментирование байт-кода (BCI) представляет собой другой подход, при котором фрагменты байт-кода внедряются непосредственно в скомпилированные Java-классы с целью добавления функциональности, изначально данным классом не предусмотренной. Этот вариант инструментирования может быть полезен в ситуациях, когда разработчики стремятся либо изменить класс, не трогая его исходный код, либо динамически менять определение класса на этапе выполнения приложения. Ниже мы рассмотрим способы использования BCI с целью внедрения байт-кода, выполняющего мониторинг производительности.
Данная задача решается по-разному в зависимости от выбранной реализации BCI. В качестве простого варианта решения можно переименовывать целевой метод и добавлять в байт-код новый метод с исходной сигнатурой, который будет содержать код трассировки и вызовы основного (переименованного) метода. В открытой библиотеке BCI под названием JRat был предложен подход к инструментированию, специально ориентированный на вычисление продолжительности вызовов методов. Благодаря такой узкой специализации этот подход оказывается значительно компактнее, чем реализации BCI общего назначения, основанные на AOP (см. Ресурсы). В листинге 15 приведен адаптированный из проекта JRat пример подобного инструментирования.
Листинг 15. Пример инструментирования байт-кода метода
//////////////////////////////////////////////////////////////
// The Original Method
//////////////////////////////////////////////////////////////
public class MyClass {
public Object doSomething() {
// тело метода
}
}
//////////////////////////////////////////////////////////////
// Новый и старый методы
//////////////////////////////////////////////////////////////
public class MyClass {
private static final MethodHandler handler = HandlerFactory.getHandler(...);
// The instrumented method
public Object doSomething() {
handler.onMethodStart(this);
long startTime = Clock.getTime();
try {
Object result = real_renamed_doSomething(); // call your method
handler.onMethodFinish(this, Clock.getTime() - startTime, null);
} catch(Throwable e) {
handler.onMethodFinish(this, Clock.getTime() - startTime, e);
throw e;
}
}
// Исходный переименованный метод
public Object real_renamed_doSomething() {
// тело метода
}
}
|
Существуют две основные стратегии реализации BCI:
-
статическое инструментирование: после инструментирования копии Java-классов сохраняются там же, где и неинструментированные. Затем приложение развертывается, и эти классы загружаются точно так же, как и все остальные;
-
динамическое инструментирование: Java-классы инструментируются при их загрузке в JVM на этапе выполнения приложения. Таким образом, инструментированные версии классов существуют исключительно в памяти во время работы. После завершения работы JVM они уничтожаются.
Одним из основных преимуществ динамического подхода является его гибкость. Динамическое инструментирование, как правило, выполняется в соответствии с набором инструкций, обычно описываемых во внешнем файле, поэтому изменения в инструментирование можно внести путем простого редактирования файла форсирования сбора мусора в JVM (хотя далеко не все JVM поддерживают замену кода "на лету"). Тем не менее, сначала мы рассмотрим статический вариант инструментирования.
Статическое инструментирование байт-кода
В качестве первого примера рассмотрим статическое инструментирование байт-кода класса EmpDAOImpl. Для этого мы будем использовать открытую инфраструктуру BCI под названием JBoss AOP (см. Ресурсы).
Вначале необходимо создать класс-перехватчик, который будет заниматься сбором данных о производительности вызовов методов, поскольку именно он будет статическим образом внедряться в байт-код класса EmpDAOImpl. В данном примере интерфейс перехватчика JBoss идентичен перехватчикам Spring за исключением имени импортируемого класса. Функции перехватчика будет выполнять класс org.runtimemonitoring.aop.ITracerInterceptor. Затем следует создать файл jboss-aop.xml, используя тот же синтаксис, что и для описания перехватчиков вызовов EJB 3. Данный файл приведен в листинге 16.
Листинг 16. Конфигурирование статического инструментирования в файле jboss-aop.xml
<aop>
<interceptor class="org.runtimemonitoring.aop.ITracerInterceptor" scope="PER_VM"/>
<bind
pointcut="execution(public * $instanceof{org.runtimemonitoring.spring.DAO}->get(..))">
<interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
</bind>
</aop>
|
Сам процесс статического инструментирования выполняется при помощи специальной утилиты под названием "компилятор Aop" (aopc), входящей в состав JBoss. Легче всего это сделать, написав скрипт для Ant. Фрагмент задания Ant, а также выводимая компилятором информация, говорящая о том, что указанная точка соединения (pointcut) успешно связана с целевым классом, приведены в листинге 17.
Листинг 17. Задание aopc в Ant и результаты его выполнения
<target name="staticBCI" depends="compileSource">
<taskdef name="aopc" classname="org.jboss.aop.ant.AopC"
classpathref="aop.lib.classpath"/>
<path id="instrument.target.path">
<path location="${classes.dir}"/>
</path>
<aopc compilerclasspathref="aop.class.path" verbose="true">
<classpath path="instrument.target.path"/>
<src path="${classes.dir}"/>
<aoppath path="${conf.dir}/jboss-aop/jboss-aop.xml"/>
</aopc>
</target>
Вывод:
[aopc] [trying to transform] org.runtimemonitoring.spring.EmpDAOImpl
[aopc] [debug] javassist.CtMethod@955a8255[public transient get
([Ljava/lang/Integer;)Ljava/util/Map;] matches pointcut:
execution(public * $instanceof{org.runtimemonitoring.spring.DAO}->get(..))
|
Для описания точек соединения в файле in jboss-aop.xml, например как показано в листинге 16, используется специальный синтаксис AOP. Он был разработан с целью создания выразительного и гибкого языка, способного определять точки соединения конкретным или общим образом. Практически любой идентифицирующий атрибут метода может использоваться для связывания, начиная с имени класса или пакета и заканчивая аннотациями и типом возвращаемого значения. Например, в листинге 17 в качестве целевых задаются все открытые get-методы экземпляров org.runtimemonitoring.spring.DAO. Единственной реализацией данного интерфейса является класс org.runtimemonitoring.spring.EmpDAOImpl, поэтому только он будет подвергнут инструментированию.
На этом инструментирование заканчивается. Для запуска SpringRunner с таким типом инструментирования необходимо указать путь к файлу jboss-aop.xml при старте JVM. Это делается при помощи специального аргумента JVM: -Djboss.aop.path=[directory]/jboss-aop.xml. Таким образом, файл jboss-aop.xml используется дважды: сначала при описании статического инструментирования, а затем на этапе выполнения. Это позволяет инструментировать множество классов на этапе разработки, но активировать лишь необходимые из них при запуске приложения. Дерево метрик, построенное по результатам выполнения данного теста, показано на рисунке 13. Теперь оно содержит дополнительные показатели, касающиеся производительности методов класса EmpDAOImpl.
Рисунок 13. Дерево показателей, собранных при помощи статического инструментирования байт-кода
Как видите, статическое инструментирование обладает определенной гибкостью. Однако ее существенным ограничением является то, что для активации инструментированных классов необходима их статическая обработка, которая представляет собой достаточно трудоемкий процесс. Более того, статически инструментированные классы могут активироваться только перехватчиками, созданными в процессе инструментирования. Далее мы повторим тот же тест, но с использованием динамического инструментирования байт-кода.
Динамическое инструментирование байт-кода
Существует несколько способов реализации динамического инструментирования, но наиболее предпочительным из них является интерфейс javaagent в Java 1.5. Мы рассмотрим лишь основные его аспекты, за более подробным описанием имеет смысл обратиться к статье Эндрю Уилкокса (Andrew Wilcox) "Создание собственных средств профилирования" (см. Ресурсы).
Интерфейс javaagent содержит два основных компонента для выполнения динамического BCI. Во-первых, если JVM запускается с параметром -javaagent:a JAR file, в котором указанный JAR-файл содержит реализацию javaagent, то будет вызван метод public static void premain(String args, Instrumentation inst) класса, указанного в специальной секции манифеста. Как следует из названия premain, данный метод вызывается перед главной точкой входа в приложение Java, что предоставляет ему все возможности по инструментированию байт-кода загружаемых классов. Для этого он вначале регистрирует экземпляры класса, реализующего ClassTransformer – второго компонента динамического BCI. Экземпляры ClassTransformer перехватывают все вызовы загрузчиков и модифицируют байт-код загружаемых классов "на лету". Данный интерфейс содержит единственный метод transform , в который передается модифицируемый класс и его байт-код в виде массива байтов. Таким образом, метод может выполнять любое редактирование байт-кода, возвращая массив, представляющий собой код инструментированного класса. Эта схема работы позволяет реализовывать эффективное трансформирование классов, не требуя при этом (в отличие от некоторых ранее рассмотренных вариантов) использования компонентов, специфичных для конкретной платформы.
Реализация динамического BCI в тесте SpringRunner выполняется в два этапа. Вначале необходимо перекомпилировать класс org.runtimemonitoring.spring.EmpDAOImpl, удалив тем самым все следы статического инструментирования из предыдущего примера. Далее необходимо добавить строку -Djboss.aop.path=[directory]/jboss-aop.xml в список параметров запуска JVM. Наконец, необходимо добавить опцию javaagent следующим образом:
-javaagent:[directory name]/jboss-aop-jdk50.jar |
В листинге 18 показана новая версия файла jboss-aop.xml, иллюстрирующая преимущества динамического инструментирования байт-кода.
Листинг 18. Фрагмент файла jboss-aop.xml, описывающий динамическое инструментирование байт-кода
<interceptor class="org.runtimemonitoring.aop.ITracerInterceptor"
scope="PER_VM"/>
<interceptor class="org.runtimemonitoring.aop.PreparedStatementInterceptor"
scope="PER_VM"/>
<bind
pointcut="execution(public * $instanceof{org.runtimemonitoring.spring.DAO}->get(..))">
<interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
</bind>
<bind
pointcut="execution(public * $instanceof{java.sql.Connection}->prepareStatement(..))">
<interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
</bind>
pointcut="execution(public * $instanceof{java.sql.PreparedStatement}->executeQuery(..))">
<interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
</bind>
|
Одним из преимуществ является возможность инструментирования любых классов, в том числе представляющих сторонние библиотеки (например, в листинге 18 показан пример инструментирования всех экземпляров java.sql.Connection). Еще большее значение имеет возможность применения любых совместимых перехватчиков к любой указанной точке соединения. Примером может служить тривиальный перехватчик org.runtimemonitoring.aop.PreparedStatementInterceptor, слегка отличающийся от ITracerInterceptor. Таким образом могут создаваться целые библиотеки перехватчиков, часто называемые аспектами на жаргоне AOP. Некоторые из таких библиотек с открытым исходным кодом доступны уже сейчас. Эти аспектные библиотеки открывают широкий круг возможностей и могут быть в разной степени полезными, в зависимости от выбранного вами типа инструментирования, вида инструментируемых классов и различных комбинаций этих условий.
Дерево, содержащее дополнительные показатели, показано на рисунке 14. Обратите внимание, что некоторые классы реализуют интерфейсы из пакета java.sql при помощи провайдеров DataSource из проекта Jacarta Commons в Spring.
Рисунок 14. Дерево показателей, собранных при помощи динамического инструментирования байт-кода
Преимущество BCI-подхода в быстродействии особенно заметно при сравнении скорости работы драйверов WrappingJDBC и драйверов с инструментированным байт-кодом. Эта разница хорошо видна на рисунке 15, где показаны сравнительные результаты длительности выполнения метода PreparedStatement.executeQuery.
Рисунок 15. Сравнение быстройдействия BCI и подхода на основе классов-оберток (время в миллисекундах)
Заключение ко второй части
В этой статье рассказывалось о ряде способов инструментирования Java-приложений с целью передачи полезных данных о ее производительности системе APM. Все представленные методы не требуют внесения изменений в исходный код классов. Выбор подхода зависит от конкретной ситуации, однако очевидно, что лидерство принадлежит методам на основе BCI. Самодельные, с открытым кодом и коммерческие APM могут использовать данную технологию для мониторинга производительности Java-систем, для которых высокая степень готовности и производительность являются критически важными показателями.
В третьей – заключительной – статье серии речь пойдет о мониторинге ресурсов, внешних по отношению к JVM, в том числе хост-компьютеров и их операционных систем, а также удаленных сервисов, например баз данных и систем передачи сообщений. В завершение мы поговорим о таких аспектах управления производительностью как обработка данных, визуализация, построение отчетов и рассылка оповещений.
Переходите к чтению третьей статьи.
Загрузка | Описание | Имя | Размер | Метод загрузки |
|---|
| Исходный код примеров к статье | j-rtm2.zip | 316 KБ | HTTP |
|---|
Ресурсы Научиться
- Оригинал статьи: "Java run-time monitoring, Part 2: Postcompilation instrumentation and performance monitoring". (EN)
- Прочитайте другие статьи серии "Мониторинг выполнения Java-приложений".
- Обратитесь к статье "Создание PMI-приложений с использованием JMX" (Вэнь Чжан Кяо и Срини Рангасвами, developerWorks, февраль 2004 г.), в которой объясняется связь между такими инфраструктурами как WebSphere Performance Monitoring Infrastructure, объекты MBean в JMX и Java EE Performance Data Framework. (EN)
- Прочитайте статью "Введение в Spring Framework 2.5" (Род Джонсон, TheServerSide, октябрь 2007 г.), в которой Spring описывается ее создателем. (EN)
-
P6Spy: приложение для профилирования JDBC. (EN)
-
JRat: средство для инструментирования байт-кода. (EN)
-
JBoss AOP: инфраструктура AOP для приложений Java. (EN)
- Ознакомьтесь со статьей "Создание собственного приложения для профилирования" (Эндрю Уилкокс, developerWorks, март 2006 г.). В ней рассказывается о создании профайлера при помощи AOP и интерфейса javaagent в Java 5. (EN)
- Обратитесь к магазину технической литературы, в котором представлены книги на данную и другие темы. (EN)
- Сотни статей по всем аспектам программирования на Java можно найти на сайте developerWorks, в разделе Технология Java.
Получить продукты и технологии
Обсудить
Об авторе  | |  | Николас Уайтхед (Nicholas Whitehead) занимает должность старшего технического архитектора в подразделении ADP под названием Small Business Services в Флорхэм Парк, штат Нью-Джерси. Он имеет более чем десятилетний опыт разработки Java-приложений в таких областях, как инвестиционная банковская деятельность, электронная коммерция и коммерческое программное обеспечение. Накопленный опыт развертывания и поддержки приложений (в том числе созданных собственноручно) подтолкнул его к изучению и реализации систем мониторинга производительности. |
Выскажите мнение об этой странице
|  |