Содержание


Разработка приложений для Java

Часть 3. Cоздание, валидация и анализ Java-приложений, работающих в режиме реального времени

Узнайте о технологиях и методах проверки и повышения детерминированного качества сервиса

Comments

Серия контента:

Этот контент является частью # из серии # статей: Разработка приложений для Java

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Разработка приложений для Java

Следите за выходом новых статей этой серии.

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

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

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

Демонстрационное приложение

Далее нашей задачей будет создание Java-приложения для операционной системы Linux® с поддержкой режима реального времени, которое должно с низкой латентностью предоставлять температурные данные, полученные от специального устройства. При этом следует минимизировать отличия кода от способного выполняться на стандартной JVM, чтобы сохранить высокую переносимость приложения. Оно должно передавать температурные показатели устройства клиентским программам, которые управляют его охлаждением, поддерживая наиболее благоприятные условия эксплуатации. Основным требованием к приложению является то, что показатели должны быть доступны не более чем через 5 мс после их предоставления устройством. Даже короткие задержки могут привести к неоптимальному управлению устройством, что может снизить эффективность его работы.

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

Таблица 1. Различия между обычными JVM и JVM реального времени
Недостатки традиционных JVMРешения, предлагаемые в JVM реального времени
Задержки, связанные с загрузкой классовПредварительная загрузка всех необходимых классов
Задержки, связанные с JIT-компиляциейПредварительная (Ahead-of-time – AOT) или асинхронная компиляция
Ограниченная поддержка приоритетов потоков на уровне операционной системыСпецификация расширений реального времени для Java (RTSJ) гарантирует наличие как минимум 28 уровней приоритетов.
Отсутствие поддержки нестандартных режимов планирования потоковRTSJ позволяет разработчикам выбирать режим планирования, например, FIFO (first-in-first-out).
Длительные задержки, вызываемые работой сборщика мусора (GC)

RTSJ предоставляет специальные зоны памяти (области с ограниченным и неограниченным временем жизни) для потоков NoHeapRealtimeThread (NHRT), которым не может помешать сборщик мусора.

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

Задержки, вызываемые работой потоков ядра операционной системы и инверсией приоритетов Ядра операционных систем реального времени, например, Linux Real Time, были специально разработаны таким образом, чтобы избегать длительного выполнения потоков в режиме ядра и инверсии приоритетов. Кроме того, они полностью поддерживают вытеснение потоков (см. первую статью серии).

Выбор модели разработки Java-приложений реального времени

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

Класс RealtimeThread позволяет использовать расширения реального времени для Java, избегая при этом сложностей программирования NHRT, в частности, низкоуровневого управления памятью без помощи сборщика мусора. На практике при использовании NHRT приходится работать с областями памяти с ограниченным временем жизни, поскольку память с неограниченным временем жизни объектов никогда не освобождается, имеет конечный размер, а, следовательно, неизбежно закончится. Кроме того, существуют ограничения на классы, которые могут выполняться в NHRT-потоках: они не могут использовать или ссылаться на объекты, размещенные в динамической памяти (куче).

Кроме простоты реализации этот подход имеет следующие преимущества.

  • Повышенная производительность. Создание и обращение к объектам в куче выполняется быстрее, чем в специальных областях памяти, поскольку исключается ряд накладных расходов, например, барьер памяти (memory-barrier).
  • Упрощенная отладка. Поскольку NHRT не должны ссылаться на объекты в куче, приходится выявлять и отлаживать все исключения, связанные с попытками обращений к таким объектам. Кроме того, при использовании NHRT возможны ошибки, обусловленные недостатком памяти в областях памяти с ограниченным или неограниченным временем жизни.

Выбор JVM с поддержкой режима реального времени

Существуют два Java-пакета для разработки приложений реального времени, поддерживаемых JVM IBM® для Linux. Несмотря на похожие названия, они отличаются набором возможностей, а также балансом между производительностью и детерминизмом (см. раздел Ресурсы). Примеры исходного кода к этой статье необходимо выполнять в IBM WebSphere® Real Time для Linux RT. Эта JVM поддерживает все возможности RTSJ , такие как потоки RealtimeThread, таймер (который нам понадобится для реализации потока, считывающего температурные показатели), а также обеспечивает минимальные паузы при вызовах GC. Она выполняется на ядре Linux, поддерживающем режим реального времени, и специальном аппаратном обеспечении, которое необходимо приложениям с жесткими требованиями реального времени. Другой пакет, IBM WebSphere Real Time для Linux, следует использовать для создания приложений, работающих в мягком режиме реального времени. Он позволяет повысить их пропускную способность и масштабируемость, однако не включает библиотеки RTSJ. Кроме того, он не включает сборщик мусора Metronome, который способен сократить время работы GC примерно до 3 мс.

Архитектура демонстрационного приложения

Основным недостатком потоков RealtimeThread по сравнению с NHRT являются непродолжительные задержки, связанные с работой сборщика мусора реального времени. Используя сборщик мусора Metronome в WebSphere Real Time, вы можете быть уверены, что потоки вашего приложения будут прерываться примерно не более чем на 500 мс. Кроме того, как минимум 70% каждого цикла GC будет отведено на выполнение приложения. Другими словами, в течение 10 мс интервала времени сборщику мусора будет выделено не более 6 квантов продолжительностью 500 мкс каждый, промежутки между которыми будут отданы прикладным потокам. Эта модель будет детально проиллюстрирована ниже.

Демонстрационное приложение должно предоставлять температурные показатели с интервалом не более 5 мс. Мы будем считывать их каждые 2 мс, чтобы гарантировать выполнение этого требования даже в условиях пауз на работу GC.

Качество сервиса, обеспечиваемого приложением реального времени

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

В жестком режиме реального времени система обязана предоставлять клиентским потокам температурные показатели в течение 5 мс. Другими словами, задержки более 5 мс приравниваются к отказу системы. В свою очередь системы, работающие в мягком режиме реального времени, должны передавать практически все температурные данные в течение 5 мс. Таким образом, если в течение 5 мс передается 99.9% показателей, а в течение 200 мс – 99.999%, то поведение системы можно считать удовлетворительным.

Валидация систем реального времени

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

Например, из приведенного выше требования к системе, работающей в мягком режиме реального времени, следует, что необходимо выполнить как минимум 100,000 тестовых считываний показателей, чтобы убедиться, что 99.999% данных предоставляются в течение 200 мс. В случае жесткого режима реального времени все бремя тестирования ложится на плечи разработчиков, которые должны выполнить все необходимые проверки, чтобы быть уверенными, что система сможет предоставлять все температурные показатели клиентским потокам в течение 5 мс. Это часто делается при помощи длительного тестирования, а также статистического анализа распределения показателей производительности. Существуют специальные средства для подбора кривых, максимально точно отражающих полученные данные и способных предсказать число и вероятность появления аномалий. Как правило, у разработчиков не хватает времени на тестирование системы в течение промежутка времени, на который рассчитано ее использование, поэтому на практике сертификация делается путем экстраполяции кривой до худшего аномального значения.

Реализация демонстрационного приложения

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

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

За считывание показателей с температурного датчика устройства с интервалами 2 мс будет отвечать процесс Reader (считыватель). В данном случае в качестве датчика будет выступать генератор случайных чисел. Для выполнения действий с заданной частотой в RTSJ служит класс javax.realtime.PeriodicTimer, который будет использоваться процессом Reader для обращения к датчику. Сам Reader будет реализован в виде асинхронного обработчика событий (класс BoundAsyncEventHandler), что позволит минимизировать латентность. Reader будет создавать фрагмент XML в процессе каждого считывания, а затем передавать его клиентскому процессу Writer (писатель) через сетевой сокет. В случае, если продолжительность цикла, состоящего из считывания данных с датчика и их последующей записи, превысит 2 мс, будет зафиксирована ошибка.

Процесс Writer выполняется в отдельной JVM на том же компьютере, что и Reader, ожидая поступления температурных показателей через сокет. Для его реализации используется класс javax.realtime.RealtimeThread, что позволяет извлечь преимущества из планирования по принципу FIFO и гибкого управления приоритетами. Writer распаковывает полученные фрагменты XML, извлекает температурные показатели и записывает их в журнальный файл. Ему отводится 3 мс на запись данных на диск, чтобы они максимально быстро стали доступны клиентам. Если общий промежуток времени между считыванием данных и окончанием их записи превысит 5 мс, то будет зафиксирована ошибка.

Схема работы приложения приведена на рисунке 1.

Рисунок 1. Схема работы демонстрационного приложения
Data flow
Data flow

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

Запуск приложения

Если на вашем компьютере установлена среда WebSphere Real Time, то вы можете запустить демонстрационное приложение, выполнив следующие действия.

  1. Распакуйте исходный код приложения в любом каталоге компьютера с JVM WebSphere Real Time.
  2. Скомпилируйте приложение, выполнив приведенную ниже команду.
    PATH to WRT/bin/javac -Xrealtime *.java
  3. Запустите процесс Writer, передав ему в качестве параметров номер порта и имя журнального файла (пример показан ниже).
    sdk/jre/bin/java -Xrealtime Writer 8080 readings.txt
  4. Запустите процесс Reader, передав ему в качестве параметров имя хоста и номер порта, который прослушивает Writer (пример показан ниже).
    PATH to WRT/jre/bin/java -Xrealtime Reader localhost 8080

Вывод демонстрационного приложения

Приложение выдает данные о задержках в работе потоков Reader и Writer. Кроме того, Writer вычисляет общее время, прошедшее с момента начала считывания показателей до окончания их записи на диск. В соответствии с требованиями жесткого реального времени этот промежуток никогда не должен превышать 5 мс. В случае мягкого режима реального времени требование состоит из двух частей: 99.9% данных должны быть доступны клиентам в течение 5 мс, а 99.999% – в течение 200 мс.

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

Анализ Java-приложений реального времени

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

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

Трассировка процесса загрузки классов

Самое простое, что можно предпринять - это запустить приложение с опцией -verbose:class и получить информацию обо всех событиях по загрузке классов. Однако для того чтобы связать аномалии в работе приложения с выполнением вспомогательных операций (таких как загрузка классов), нам необходимы точные временные характеристики как аномалий, так и вероятных причин их появления. Для этого можно самостоятельно написать необходимую утилиту с использованием интерфейса JVMTI (Java Virtual Machine Tool Interface), позволяющего реагировать на загрузку классов (см. раздел Ресурсы). Вдобавок к этому следует инструментировать код самого приложения для выявления временных корелляций.

Трассировка процесса JIT-компиляции

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

Одной из таких опций является флаг -Xjit:compiling, который позволяет получать уведомления о первичной компиляции и повторной компиляции методов.

Инструментирование кода демонстрационного приложения

Допустим, мы установили опции verbose:class и -Xjit:compiling, после чего выяснилось, что нарушения требований происходят значительно позже окончания загрузки классов и даже позже стабилизации кода, сгенерированного JIT-компилятором. В этом случае необходимо глубже заглянуть в работу приложения, связав его с другими процессами, происходящими в JVM.

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

AbsoluteTime startTime1 = clock.getTime();
xmlSnippet.append("<reading><sensor id=\"");
AbsoluteTime startTime2 = clock.getTime();
RelativeTime timeTaken = startTime2.subtract(startTime1);
System.err.println("Time taken: " + timeTaken);

Этот метод позволяет найти медленные участки кода, однако он представляет собой трудоемкий итеративный процесс, а полученные данные оказываются несвязанными с другими событиями. Разумеется, можно выводить последовательность событий в консоль, параллельно используя такие опции, как verbose:gc и verbose:classloading, но есть значительно более удобный способ: трассировка при помощи Tuning Fork.

Трассировка при помощи Tuning Fork

Платформа визуализации Tuning Fork изначально проектировалась в целях упрощения разработки и отладки приложений, использующих сборщик мусора Metronome. Она представляет собой расширяемый модуль для Eclipse, который применяется, например, в инструментарии IBM для сбора данных и визуального анализа многоядерных систем (IBM Toolkit for Data Collection and Visual Analysis for Multi-Core Systems, см. раздел Ресурсы).

Эта платформа имеет следующие преимущества:

  • мощные возможности для визуализации и анализа данных;
  • доступ к данным о процессах, протекающих внутри JVM, таких как сборка мусора, загрузка классов, JIT-компиляция и т.д.;
  • возможность интеграции данных трассировки на уровне приложения и внутри JVM.

Архив с исходным кодом демонстрационного приложения содержит версии классов Reader и Writer (Reader.java.instrumented и Writer.java.instrumented), включающие трассировку при помощи Tuning Fork (ссылка приведена в разделе Загрузка).

Код несколько усложнился по сравнению с предыдущим примером, в котором полученные временные данные выводились в стандартный поток ошибок, однако вскоре вы убедитесь, что преимущества стоят затраченных усилий. Tuning Fork поддерживает два вида точек трассировки: первые служат для записи простых временных отсечек, а вторые – для журналирования данных в интересах разработчика. Оба типа событий фиксируются с использованием тех же компонентов, что и события, возникающие внутри JVM, такие как загрузка классов, JIT и GC. Важно то, что такой подход гарантирует, что трассировка как приложения, так и JVM выполняется синхронизованно. Другими словами, можно проводить параллели между событиями, используя единый набор временных отсечек, чтобы отследить, что происходит внутри приложения в каждый момент времени. Подобные попытки, как правило, приводят к существенным трудностям и ошибкам в случае, если данные трассировки были получены от нескольких несинхронизованных источников. Для нашего приложения будет достаточно только точек трассировки для фиксации временных отсечек, которые мы поместим в начало и конец интересующих фрагментов кода.

Код инструментирования будет отмечен символами-разделителями, как показано на примере ниже.

/*---------------------TF INSTRUMENTATION START ------------------------- */
		writerTimer.start();
/* ---------------------TF INSTRUMENTATION END ------------------------- */

Инструментированная версия приложения генерирует два бинарных файла с данными трассировки: один для JVM, в котором выполняется Reader (Reader.trace), и один для Writer (Writer.trace). Оба файла содержат данные о начале и окончании обработки всех сообщений с температурными показателями и могут быть проанализированы при помощи визуализатора Tuning Fork.

Инструментирование при помощи Tuning Fork затрагивает следующие области кода:

  • область импорта, в которую добавляют операторы для импорта классов из файла tuningForkTraceGeneration.jar, служащих для генерации файла трассировки;
  • область инициализации объекта-логгера, в который помещается код для журналирования, а также создания таймера и фидлета (feedlet) - канала обмена данными между таймером и логгером;
  • область инициализации инструментирования;
  • метод для связывания фидлета с текущим потоком.

Кроме этого нам понадобится всего один дополнительный фрагмент кода для измерения временных интервалов (пример приведен ниже).

/*---------------------TF INSTRUMENTATION START ------------------------- */
                    writerTimer.start();
/* ---------------------TF INSTRUMENTATION END ------------------------- */

                    AbsoluteTime startTime = clock.getTime();

                    код, время выполнения которого необходимо измерять

                    RelativeTime timeTaken = stopTime.subtract(startTime);

/* ---------------------TF INSTRUMENTATION START ------------------------- */
                    writerTimer.stop();
/* ---------------------TF INSTRUMENTATION END ------------------------- */

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

	AbsoluteTime startTime = clock.getTime();
               Code to be timed
      RelativeTime timeTaken = stopTime.subtract(startTime);

Для компиляции и запуска кода с трассировкой Tuning Fork достаточно просто добавить файл tuningForkTraceGeneration.jar в classpath приложения.

Данные трассировки событий внутри JVM, полученные при помощи Tuning Fork

Для включения журналирования событий, происходящих внутри JVM, служит опция командной строки -XXgc:perfTraceLog=имя_файла.trace.

Средство визуализации Tuning Fork представляет собой модуль Eclipse, способный работать в операционных системах Linux и Windows®. При этом нам понадобится дополнительный модуль для построения графиков по данным трассировки событий внутри JVM IBM WebSphere Real Time. Обратите внимание, что Tuning Fork является инфраструктурой общего назначения и может использоваться для анализа других приложений, написанных на Java, С  или С++.

Наиболее полезной функцией Tuning Fork для понимания процесса выполнения приложения является временная диаграмма возникающих внутри него событий. Tuning Fork включает ряд стандартных графиков для визуализации событий в WebSphere Real Time (см. раздел Ресурсы), позволяющих анализировать различные интересные комбинации данных JVM, например, сводный график производительности GC.

Кроме того, мы включили инструментирование Tuning Fork в демонстрационное приложение, а именно, добавили таймер, который запускается непосредственно перед, а останавливается сразу после выполнения существующего кода инструментирования. Это было сделано для проверки предельной продолжительности выполнения потоков: 2 мс для потока Reader и 3 мс для Writer. Рисунок 2 иллюстрирует графическое представление полученных данных в Tuning Fork, подтверждающее, что приложение выполняется надлежащим образом.

Рисунок 2. Данные трассировки выполнения демонстрационного приложения, полученные при помощи Tuning Fork
Tuning Fork trace - demo application code
Tuning Fork trace - demo application code

Из рисунка 2 следует, что поток Reader выполняется примерно 130 мкс, а Writer, который обрабатывает данные, полученные через сокет, – около 900 мкс (что неудивительно, поскольку он выполняет больше работы). Полный цикл передачи и обработки данных – с момента считывания с датчика и до окончания записи в файл – занимает чуть более 1 мс, т.е. приложение с запасом удовлетворяет требованию 5 мс. Кроме того, можно заметить, что поток Reader запускается регулярно с интервалами 2 мс.

Визуализатор Tuning Forks автоматически наносит данные, полученные из двух источников, на одну временную ось.

Возникает вопрос: какое влияние на эту временную диаграмму оказывают циклы работы сборщика мусора? Без Tuning Fork вы можете отслеживать только события на уровне приложения, в то время как его использование позволит значительно прояснить эффекты, обусловленные GC. Картина, отражающая циклы GC внутри JVM потока Writer, показана на рисунке 3.

Рисунок 3. Данные трассировки при помощи Tuning Fork с отмеченными квантами работы GC
Tuning Fork trace - GC slices
Tuning Fork trace - GC slices

На этом рисунке видно, что вызовы сборщика мусора отражаются на продолжительности работы потока Writer. Каждый единичный запуск Metronome, называемый квантом (slice в Tuning Force), длится около 500 мкс. В течение этого времени все прикладные потоки в JVM останавливаются, поэтому запуск GC в момент выполнения Writer приводит к тому, что оно затягивается с 900 мкс до 1.4 мс.

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

Поток Reader работает в отдельной JVM, и на него не оказывают влияния действия GC в JVM потока Writer, поскольку компьютер имеет четырехъядерный процессор и способен выполнять обе JVM параллельно (в JVM WebSphere Real Time по умолчанию используется один поток для сбора мусора).

Проанализировав все кванты GC в JVM Writer, можно заметить несколько аномально длинных – более 2 мс – запусков Writer. Более подробно эти запуски показаны на рисунке 4, из которого следует, что они были прерваны двумя квантами.

Рисунок 4. Иллюстрация прерывания работы Writer двумя квантами GC
Tuning Fork trace - two GC slices
Tuning Fork trace - two GC slices

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

Для того чтобы избежать прерывания двумя квантами GC, необходимо, чтобы суммарная длительность операции, выполняемой прикладным потоком, и одного кванта GC не превышала интервала между соседними квантами (который, как правило, составляет 1 мс). Это означает, что Writer должен сократить время своего выполнения с 900 мкс примерно до 500 мкс. Однако даже это не поможет со 100%-ной вероятностью избегать прерываний единичными квантами по следующим причинам.

  • Существуют небольшие колебания во времени начала квантов GC. Они являются результатом выполнения соглашения по загрузке приложения, в соответствии с которым 70% времени отводится прикладным потокам.
  • • На каждом ядре процессора, как правило, выполняются высокоприоритетные системные потоки, например, таймеры и обработчики прерываний. Они запускаются на исключительно короткие интервалы времени, но при этом могут иметь более высокий приоритет, чем потоки приложения или потоки внутри JVM, а, следовательно, могут оказывать влияние на их выполнение.
  • В JVM всегда присутствует один поток, приоритет которого выше любого прикладного или GC-потока, а именно, сигнальный поток GC. Он нужен для управления квантами и запускается на несколько микросекунд каждые 450 мкс. Если планировщик операционной системы запускает его на том же ядре, что и прикладной или GC-поток, то возникнет небольшая задержка.

Анализ выполнения приложения на микросекундном уровне позволит отметить некоторые редко встречающиеся эффекты от взаимодействия между потоками, ядрами и планировщиком. В ряде случаев может быть необходимо полное понимание этих видов взаимодействий, поэтому приходится принимать во внимание события, происходящие в операционной системе. Tuning Fork позволяет импортировать подобную информацию, предоставляемую утилитой Linux System Tap, а также представлять ее графически при помощи инструментария IBM для сбора данных и визуального анализа многоядерных систем (см. раздел Ресурсы), однако рассмотрение этих возможностей выходит за рамки данной статьи.

Остальные аномалии при работе Writer

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

Writer deadline missed after 0 good writes. Deadline was (3 ms, 0 ns), time taken was (48 
ms, 858000 ns)

Это сообщение гласит, что во время первого запуска потоку Writer потребовалось около 49 мс для записи данных в файл, т.е. намного больше времени, чем при последующих запусках, которые занимали около 1 мс. При этом известно, что причины этого никак не связаны с JIT-компиляцией методов, поскольку вся компиляция была выполнена предварительно, а компилятор был выключен. Другим возможным объяснением является загрузка классов, что подтверждается тем фактом, что данный эффект возникает при первом запуске. Можно ли проверить эту гипотезу при помощи Tuning Fork? Обратите внимание на рисунок 5, на котором показана картина первого запуска Writer.

Рисунок 5. Первый запуск Writer и загрузка классов
Tuning Fork trace - first Writer run and classloading
Tuning Fork trace - first Writer run and classloading

Как мы и подозревали, на рисунке заметна активная деятельность загрузчика классов непосредственно перед первым запуском Writer и в течение этого запуска. Это является очевидным подтверждением того, что именно загрузка классов несет ответственность за медленное выполнение потока. Последующие запуски Writer укладываются в 1 мс.

Методы, позволяющие избежать задержек такого рода, обсуждались в предыдущей статье серии. Обратите внимание, что Tuning Fork может помочь идентифицировать классы для предварительной загрузки при помощи масштабирования по оси времени. Эта возможность показана на рисунке 6, из которого видно, что класс org/apache/xerces/util/XMLChar загружается более 3 мс.

Рисунок 6. Выявление задержек, связанных с загрузкой классов
Tuning Fork trace - identifying slow class loading
Tuning Fork trace - identifying slow class loading

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

Комплексные аномалии

До этого момента мы изучали только аномалии в JVM потока Writer, в то время как требования к приложению гласят, что весь цикл обработки данных должен завершаться в течение 5 мс. График JVM потока Reader не отражает нарушений временных ограничений, причем даже его первый запуск занимает менее 1 мс, а последующие - около 140 мкс. Инструментирование кода при помощи Tuning Fork позволит получить статистические данные, показанные на рисунке 7. Цикл длительностью в 3 мс произошел в результате останова JVM комбинацией клавиш Ctrl-C, что привело к загрузке ряда классов, связанных с обработкой исключений. В предыдущей статье рассматривались проблемы, возникающие при выполнении редких участков кода, а также способы предварительной загрузки классов (очевидно, что в этих случаях прогревочного запуска приложения будет недостаточно).

Рисунок 7. Статистика работы потока Reader
Tuning Fork trace - Reader statistics
Tuning Fork trace - Reader statistics

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

Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(122 ms, 93000 ns)
Writer deadline missed after 0 good writes. Deadline was (3 ms, 0 ns), time taken was 
(48 ms, 858000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(122 ms, 517000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(121 ms, 567000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(120 ms, 541000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(119 ms, 525000 ns)

Подобное поведение продолжается некоторое время, но степень превышения лимита постепенно сокращается, как показано ниже.

Data deadline missed after 0 good transfers. Deadline was 
(5 ms, 0 ns), transit time was (10 ms, 585000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(9 ms, 588000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(8 ms, 531000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(7 ms, 469000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(6 ms, 398000 ns)
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was 
(5 ms, 518000 ns)
Writer deadline missed after 3087 good writes. Deadline was (3 ms, 0 ns), time taken was 
(3 ms, 316000 ns)

Итак, изначально приложение работало с задержками около 122 мс, которые постепенно сокращались, пока система не перешла в состояние, в котором потоки Reader и Writer лишь изредка нарушают временные ограничения. График времени доставки данных с начала работы системы показан на рисунке 8.

Рисунок 8. График времени передачи данных после запуска приложения
Data-transfer startup
Data-transfer startup

За исключением одного 48-миллисекундного запуска Writer, в течение первых примерно 120 медленных циклов передачи данных не было зафиксировано нарушений ограничений потоков Reader и Writer, поэтому не ясно, чем объясняются суммарные задержки. Как и ранее, причины могут быть выявлены при помощи Tuning Fork, позволяющей совмещать данные трассировки обоих потоков с данными трассировки их JVM, как показано на рисунке 9. Опция -verbose:gc позволяет убедиться, что сборщики мусора не активны ни в одной JVM, поэтому логично обратить внимание на загрузку классов.

Рисунок 9. Трассировка обеих JVM и обоих прикладных потоков при помощи Tuning Fork
Tuning Fork trace - both JVMs and application threads
Tuning Fork trace - both JVMs and application threads

Загрузка классов в JVM Reader показана в третьей строке на рисунке 9. Как и следовало ожидать, она заканчивается в течение первого запуска Reader. В нижней строке графика представлена загрузка классов в JVM Writer. Если навести курсор мыши на область между 20 и 70 по оси Х, то можно заметить, что практически вся загрузка классов имеет отношение к XML. Таким образом, медленное выполнение потоков во многом объясняется загрузкой классов. Наиболее длительная задержка, составляющая 122 мс, представляет собой интервал от первого красного столбца (поток Reader) до последнего зеленого столбца с меткой 49.88 мс (поток Writer). Второй цикл доставки данных протекает несколько быстрее, но общая картина сохраняется, поскольку потоку Writer приходится обрабатывать накопившиеся входные запросы, после чего продолжительность циклов входит в рамки 5 мс. Это объясняет множество нарушений временных ограничений в начале работы системы. Однако является ли это единственной причиной задержек? В частности, может ли оказывать свое влияние процесс передачи данных через сокет между двумя JVM?

Латентность сокета

Использование сокета для связи между двумя JVM, позволяющее выполнять приложение одновременно на двух компьютерах, может также быть источником задержек. Для того чтобы проверить эту гипотезу, мы внесли следующие изменения в код потоков Reader и Writer.

  • Отключение алгоритма Нагла. Алгоритм Нагла (Nagle algorithm, см. раздел Ресурсы) известен своей способностью приводить к задержкам в работе систем реального времени, поскольку он буферизует сетевые пакеты перед их отправкой. Java-приложения могут проверить, работает ли этот алгоритм, при помощи метода socket.getTcpNoDelay() и, если да, деактивизировать его вызовом setTcpNoDelay(true). Однако деактивизация алгоритма Нагла не поможет ускорить медленные циклы передачи данных сразу после запуска приложения.
  • Использование PerformancePreferences. Стандартный сокет можно настроить под нужды нашего приложения при помощи метода setPerformancePreferences(1, 2, 0). Это означает, что приложению наиболее важна низкая латентность сокета, затем - быстрое подключение, и только после этого - высокая пропускная способность. Такая настройка существенно снижает стартовые паузы в работе демонстрационного приложения (рисунок 10).
    Рисунок 10. Результаты трассировки Tuning Fork после настройки PerformancePreferences сокета
    Tuning Fork trace - with socket PerformancePreferences
    Tuning Fork trace - with socket PerformancePreferences

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

Заключение

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

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=521262
ArticleTitle=Разработка приложений для Java: Часть 3. Cоздание, валидация и анализ Java-приложений, работающих в режиме реального времени
publish-date=09122010