четверг, 1 декабря 2016 г.

Сo-location как путь к высокой производительности Java EE приложений

Введение


Спецификация JDBC API, разработанная в рамках Java Community Process (JCP), определяет только лишь набор интерфейсов и базовых классов, которые в свою очередь должны быть реализованы разработчиками того или иного драйвера. Можно выделить четыре подхода к разработке драйверов JDBC:

  1. JDBC Driver - Type 1 (JDBC ODBC Bridge)

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

  2. JDBC Driver - Type 2 (Part Native Driver)

    Данный подход подразумевает, что код, написанный на языке Java, обращается к коду, реализованному производителем СУБД на нативном языке, который уже в свою очередь общается с базой данных.

  3. JDBC Driver - Type 3

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

  4. JDBC Driver - Type 4 (Thin Driver)

    Данный подход подразумевает, что реализованный производителем СУБД код драйвера, написанный на языке Java, напрямую взаимодействует с базой данных. Другими словами данный тип драйвера представляет собой чисто Java-библиотеку, транслирующую JDBC-запросы напрямую в специфичный протокол базы данных.

В мире распределенных систем наиболее используемым типом драйвера является JDBC Type 4, в то время как в мире мейнфреймов (IBM z Systems) принят другой подход. Целью данной статьи является продемонстрировать какие именно преимущества с точки зрения обеспечения высокой производительности можно получить, просто выбрав правильный тип драйвера.



Бенчмарк


Давайте рассмотрим простой бенчмарк, разработанный для сравнения следующих типов драйверов: JDBC Type 2 и JDBC Type 4. В качестве исследуемого параметра выберем среднее время отклика приложения, соответственно наименьшее значение является наилучшим. Для подачи нагрузки и измерения параметра воспользуемся утилитой, разработанной нашим соотечественником Алексеем Шипилёвым для построения, проведения и анализа нано-, микро-, мили- и макро-бенчмарков, JMH.

Рекомендованный способ запуска утилиты JMH - просто использовать Maven для сборки standalone-проекта, зависящего от jar-файлов вашего приложения. Данный подход предпочтителен для обеспечения корректного запуска бенчмарка и генерации надежных результатов.

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

$ java -jar benchmarks.jar "DB2ConnectionsBenchmark" -f 4 -t 4 -wi 10 -i 25 -si true -gc true 
-p server=mvz2 -p port=9081

В данном примере бенчмарк запускается на 6-ти процессорном (4 GCP + 2 zIIP) IBM zEnterprise Business Class 12 мейнфрейме в четыре потока. Клиентская JVM будет запущена четыре раза, при каждом запуске будет произведено 10 итераций для прогрева и 25 итераций для сбора данных.

Исходный код бенчмарка доступен на GitHub.

Профиль рабочей нагрузки



Бенчмарк представляет собой Java-класс, работающий как HTTP-клиент, т.е. вызывающий сервер и подсчитывающий среднее время отклика. Сервер - это Java-сервлет, развернутый на сервере приложений WebSphere Liberty Profile 9 Beta for z/OS, подключенном к запущенном на том же образе операционной системы экземпляру СУБД DB2 11 for z/OS. На сервере созданы два пула соединений с СУБД, каждый из которых настроен на использование своего типа драйвера: JDBC Type 2 и JDBC Type 4, соответственно. Конфигурация описана в файле server.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<server description="JDBC Type 2 vs Type 4 DataSource definition">
    <featureManager>
        <feature>jdbc-4.0</feature>
        <feature>zosTransaction-1.0</feature>
    </featureManager>

    <nativeTransactionManager shutdownTimeout="20s" 
                              resourceManagerNamePrefix="DEMOS"/>

    <library id="DB2T2LibRef">
        <fileset dir="/usr/lpp/db2b10/jdbc/classes"/>
        <fileset dir="/usr/lpp/db2b10/jdbc/lib"/>
    </library>

    <library id="DB2JCC4Lib">
        <fileset dir="/usr/lpp/db2b10/jdbc/classes" 
                 includes="db2jcc4.jar db2jcc_license_cisuz.jar"/>
    </library>

    <jdbcDriver id="DB2T2" libraryRef="DB2T2LibRef"/>

    <dataSource id="jdbc/t2/test" jndiName="jdbc/t2/test" jdbcDriverRef="DB2T2"
                isolationLevel="TRANSACTION_READ_COMMITTED"
                type="javax.sql.ConnectionPoolDataSource">
        <connectionManager maxPoolSize="20" minPoolSize="8" connectionTimeout="10s" agedTimeout="10m"/>
        <properties.db2.jcc driverType="2" databaseName="DB0A"/>
    </dataSource>

    <dataSource id="jdbc/t4/test" jndiName="jdbc/t4/test" isolationLevel="TRANSACTION_READ_COMMITTED">
        <jdbcDriver libraryRef="DB2JCC4Lib"/>
        <connectionManager maxPoolSize="20" minPoolSize="8" connectionTimeout="10s" agedTimeout="10m"/>
        <properties.db2.jcc databaseName="DB0A" serverName="localhost" portNumber="446"
                            user="pavel" password="xxx"/>
    </dataSource>
</server>

Сервлет инкапсулирует бизнес-логику тестового приложения. Логика довольно проста, из пула просто извлекается одно соединение с базой данных, с его помощью выполняется SQL-запрос, результаты которого обходятся в цикле, при этом вычисляется объем полученных данных, который возвращается в качестве HTTP-ответа. Соединение затем возвращается в пул. В каждом запросе передается ограничение на объем извлекаемых данных, данное ограничение подставляется в качестве параметра s.

public abstract class AbstractConnectionBenchmarkServlet extends HttpServlet {

    private static final long serialVersionUID = -7382506933509679639L;

    protected abstract DataSource getDS();
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {        
        
        int size = Integer.parseInt(req.getParameter("s"));
       
        try {            
            resp.setStatus(HttpServletResponse.SC_OK);
            int[] res = getRecordsCount(size);              
            resp.getWriter().write("<p>" + res[1] + " messages have been " + 
                    "retrieved, last hash: " + res[0] + "</p>");
        } catch (Exception ex) {
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            ex.printStackTrace(resp.getWriter());
        } finally {
            resp.getWriter().flush();
        }
    }
    
    private int[] getRecordsCount(int size) throws Exception {
        try (Connection conn = getDS().getConnection();
             PreparedStatement pstmt = conn.prepareStatement(
                     "SELECT * FROM ( " + 
                       "       SELECT ROW_NUMBER() OVER (ORDER BY empno) AS rownumber," + 
                       "       empno, first_name, last_name, salary " + 
                       "       FROM ROOT.EMP2" + 
                       ") AS EMPLOYEE " + 
                       "WHERE rownumber between ? and ? ")) {
            pstmt.setInt(1, 1);
            pstmt.setInt(2, size);
               
            try (ResultSet res = pstmt.executeQuery()) {
                int lastHash = 0; 
                int count = 0;
                while(res.next()) {
                    lastHash = res.getInt(1) * 1000 + res.getString(2).hashCode() 
                               + res.getString(3).hashCode() + res.getString(4).hashCode() 
                               + res.getBigDecimal(5).hashCode();
                    count++;
                }                   
                
                return new int[]{lastHash, count};
            } 
        }
    }
}

Чтобы избежать срабатывания оптимизации JIT-компилятора, которая называется dead code elimination, используется переменная lastHash.

На сервере приложений Liberty Profile развернуты две версии сервлета, каждая из которых является подклассом базового класса AbstractConnectionBenchmarkServlet и содержит одну и ту же бизнес-логику, но использует свой источник данных. Один источник данных настроен на использование JDBC Type 2, а другой - JDBC Type 4.

Данные


После успешного золотого прогона бенчмарка получены следующие результаты:

# Run complete. Total time: 00:27:32

Benchmark                        (port) (server)  (size)  (zdbc)  Mode  Cnt  Score   Error  Units
DB2ConnectionsBenchmark.message    9081  mvz2      10      t2     avgt  100  0.886 ± 0.010  ms/op
DB2ConnectionsBenchmark.message    9081  mvz2      10      t4     avgt  100  1.139 ± 0.019  ms/op
DB2ConnectionsBenchmark.message    9081  mvz2      25      t2     avgt  100  0.911 ± 0.011  ms/op
DB2ConnectionsBenchmark.message    9081  mvz2      25      t4     avgt  100  1.156 ± 0.009  ms/op
DB2ConnectionsBenchmark.message    9081  mvz2      50      t2     avgt  100  0.948 ± 0.016  ms/op
DB2ConnectionsBenchmark.message    9081  mvz2      50      t4     avgt  100  1.212 ± 0.007  ms/op
DB2ConnectionsBenchmark.message    9081  mvz2     100      t2     avgt  100  1.035 ± 0.007  ms/op
DB2ConnectionsBenchmark.message    9081  mvz2     100      t4     avgt  100  1.363 ± 0.029  ms/op

Здесь приведены две переменные, которые управляют поведением бенчмарка: zdbc определяет тип используемого драйвера, а size - количество записей, извлекаемых из базы данных на каждой итерации.

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


Видно, что даже на таком примитивном примере использование драйвера JDBC Type 2 уменьшает время отклика на примерно 30%, т.е. Java EE приложение обеспечивает до 30% большую производительность только за счет использования правильного драйвера для доступа к СУБД.

Не плохо!

Анализ


Для того чтобы заглянуть внутрь процесса работы бенчмарка, воспользуемся утилитой IBM Jinsight. Профайлер Jinsight состоит из трех частей: основанный на JVMTI агент, работающий в паре с приложением, утилита контроля, которая позволяет запускать и останавливать процесс профилирования, а так же графический пользовательский интерфейс JinsightLive. Агент и утилита контроля являются платформеннозависимыми компонентами, работающими только на операционных системах z/OS, Linux for z и Linux for x. Агент во время профилирования генерирует файлы дампов, которые затем можно скачать на клиентскую рабочую станцию и открыть с помощью JinsightLive. Данная утилита является кросс-платформенной и может работать на компьютере под управлением операционной системы Microsoft Windows, например.

Давайте посмотрим как именно работает приложение при использовании каждого из заявленных драйверов: JDBC T2 и T4. Путь исполнения при использовании драйвера T2 приведен на рисунке ниже:


Путь исполнения JDBC T4:


Как продемонстрировано на данных диаграммах горячим методом является 'executeQuery', который при использовании драйвера JDBC T4 осуществляет чтение данных из сокета, а при использовании JDBC T2 - вызывает операцию 'nativeOpen'.

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

Время исполнения для драйвера JDBC Type 2 при извлечении из базы данных десяти записей:


Таблица наглядно демонстрирует, что есть два горячих метода: 'executeQuery' и 'next'. При использовании JDBC Type 4 ситуация выглядит иначе: больше времени тратится на методе 'executeQuery' и чуть меньше на методе 'next':


Давайте разберемся как метод 'executeQuery' работает для JDBC Type 2:


Сердцем метода 'executeQuery' является метод 'nativeOpen'. Данный метод извлекает данные из базы данных, используя z/OS Cross Memory services. Сервер приложений WebSphere Application Server обращается к адресному пространству DB2 с помощью специальной инструкции program call (PC). Т.е. только одна аппаратная инструкция процессора используется для обращения программы к соседнему адресному пространству, в данном случае - к DB2.

В случае использования JDBC Type 4 метод 'executeQuery' содержит операцию 'socketRead'. Как и ожидается, драйвер JDBC Type 4 извлекает данные из базы данных, используя сетевое соединение, что требует несколько большего времени нежели программный вызов через cross memory services:


Другой горячий метод для драйвера JDBC Type 2 - метод 'next', содержащий две важные части: методы 'closeX' и 'nextX'. Метод 'closeX' вовлекает в работу сервис RRS, предназначенный для управления транзакциями и выполняет операции фиксации транзакции ('readLocalCommit'/'nativeCommit').


Львиная доля времени тратится на выполнение данных методов, фактически это является причиной того, почему использование драйвера JDBC Type 2 уменьшает время отклика приложения только на 30%, т.к. драйвер JDBC Type 4 свободен от выполнения данной работы. На других примерах, содержащих глобальные транзакции, исполняемые в режиме двухфазного коммита, полученные результаты могли быть еще лучше для драйвера JDBC Type 2, но это требует дополнительных исследований.


По-умолчанию драйвер JDBC Type 2 извлекает записи из базы данных по одной при каждом вызове метода 'ResultSet#next()' (см. диаграмму метода 'nativeFetch'). Хотя каждая итерация выполняется не так уж долго, при увеличении количества извлекаемых записей растет так же и время исполнения метода.


Существует опция в СУБД DB2, именуемая как Multi-row FETCH. Данный тип операций может обеспечить лучшую производительность нежели извлечение новой записи при каждом выполнении операции FETCH. Когда приложение извлекает данные из базы, драйвер IBM Data Server Driver for JDBC and SQLJ определяет включена ли опция multi-row FETCH, основываясь на следующих факторах:

  • выставлено ли свойство enableRowsetSupport;
  • выставлено ли свойство useRowsetCursor при соединении с DB2 for z/OS;
  • тип используемого соединения IBM Data Server Driver for JDBC and SQLJ;
  • версию драйвера IBM Data Server Driver for JDBC and SQLJ.

Больше информации об опции Multi-row FETCH может быть найдено на странице Multi-row SQL operations in JDBC applications в базе знаний IBM Knowledge Center.

С помощью Jinsight можно визуализировать исполнение запросов к базе данных при использовании данной опции:


Данные извлекаются из базы с помощью метода 'getNextRowset' при первом обращении к ResultSet#next(). Каждый следующий вызов операции 'next' просто проверяет относится ли строка к извлеченному из базы набору с помощью вызова метода 'rowIsInCurrentRowset'.

Потребление процессорных ресурсов


Давайте исследуем сколько процессорных ресурсов необходимо для поддержки каждого типа связи между сервером приложений и базой данных. Первый шаг данного анализа - просто получить информацию о потреблении ресурсов. Будем использовать классификацию z/OS Workload Manager для назначения уникальных классов отчетов (report class) для каждого сервлета, ассоциированного с соответствующим типом драйвера JDBC. Информация для классификации предоставляется сервером приложений WebSphere Liberty Profile и задается с помощью элемента wlmClassification файла конфигурации server.xml.

<?xml version="1.0" encoding="UTF-8" ?>
<server description="WLM configuration">
    <featureManager>
        <feature>zoswlm-1.0</feature>
    </featureManager>

    <wlmClassification>
        <httpClassification transactionClass="DBMTRAT2" resource="/sync-to-thread/dbtest/t2" />
        <httpClassification transactionClass="DBMTRAT4" resource="/sync-to-thread/dbtest/t4" />
        <httpClassification transactionClass="DBMDTRAN" />
    </wlmClassification>

    <zosWorkloadManager collectionName="CZSR01" />
</server>

Классы отчетов REPBMRT2 и REPBMRT4 назначаются для соответствующих классов транзакций (transaction classes), приведенных выше. Одинаковый сервисный класс (service class) назначается для обоих классов транзакций, следовательно одинаковые цели по производительности будут применимы к каждому сервлету, т.е. компонент WorkLoad Manager (WLM) операционной системы z/OS не будет вносить погрешность в результаты тестирования.


При использовании драйвера JDBC Type 2 единый WLM enclave ассоциируется с потоком, который обрабатывает HTTP-запрос, причем данный энклав будет распространен так же и на СУБД, т.е. отдельного энклава для исполнения запроса в СУБД создаваться не будет, вместо этого будет использоваться энклав, созданный сервером приложений. Соответственно отчеты будут содержать только общее время процессора, потраченное на обработку запроса и сервером приложений, и СУБД вместе.


Ситуация меняется при использовании драйвера JDBC Type 4. Компонент Distributed Data Facility (DDF) позволяет удаленному клиентскому приложению работать с данными, размещенными на сервере DB2. Собственное множество энклавов будет создано для данного компонента, таким образом процессорное время, потраченное на обработку запроса в СУБД, будет содержаться в отчете отдельно.


Отчеты PPXSRPTS генерируются с помощью RMF Postprocessor и используются как источник анализируемых данных. Данные отчеты собраны с интервалом измерения в 15 минут, в это время бенчмарк исполнялся в четыре потока и на каждой итерации из базы данных извлекалось по 50 записей. Ниже на графике приведены результаты измерения параметра APPL %, представленные в данном отчете.


На графике отчетливо видно, что использование драйвера JDBC Type 4 позволяет получить гораздо большую пользу от использования специализированных процессоров zIIP. Практически вся нагрузка, порождаемая сервером приложений WebSphere Liberty Profile исполняется на специализированных процессоров, в принципе как и большая часть нагрузки СУБД.

Полученные значения


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

Выводы


Похоже данный бенчмарк может быть аргументом при демонстрации преимуществ работы сервера приложений WebSphere Liberty Profile на z/OS. Предоставляемый данной операционной системой механизм межпроцессного взаимодействия, являющийся по сути лишь процессорной инструкцией Program Call (PC), демонстрирует свои преимущества перед соответствующими механизмами, используемыми другими операционными системами, а уж тем более перед любыми видами сетевого взаимодействия. До 30% выигрыша в производительности может быть получено просто настройкой соответствующего JDBC драйвера. С другой стороны данный тип драйвера мешает сполна воспользоваться преимуществами исполнения нагрузки на специализированных процессорах zIIP, за работу программного обеспечения на которых корпорация IBM не требует уплаты лицензионных отчислений. Системные архитекторы, разрабатывающие новое программное обеспечение на платформе Java для мейнфреймов, работающее под управлением сервера приложений WebSphere Liberty Profile как без участия монитора транзакций, так и внутри CICS, должны учитывать данный компромисс между достигаемой производительностью и уровнем потребления ресурсов.

Понравилось сообщение - подпишитесь на блог

Комментариев нет:

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!