Новости

Методи і практики проектування web-додатків реального часу з використанням технології Java

  1. Автор: Лакомкін Єгор Дмитрович
  2. Основні джерела непередбачуваної поведінки Java-додатка
  3. Лістинг 1.
  4. Головні фактори, що впливають на загальну продуктивність
  5. Лістинг 2.
  6. Лістинг 3.
  7. Лістинг 4.
  8. Деякі популярні бібліотеки
  9. Лістинг 5.
  10. Застосування ядра реального часу для досягнення детермінізму
  11. висновок
  12. Список літератури
Автор: Лакомкін Єгор Дмитрович
Опубліковано: 20.04.2012
Виправлено: 23.05.2012
Версія тексту: 1.1

Передмова

Стаття присвячена важливим аспектам створення додатків на Java з надшвидким часом відгуку.У матеріалі представлені деякі рішення автора, використані при розробці успішно-діючої системи автоматичної торгівлі на ринку цінних паперів з жорсткою вимогою щодо скорочення затримок.Крім того, в статті систематизовано розрізнені напрацювання зарубіжних проектів та запропоновано рекомендації по налаштуванню Linux OS, які значно впливають на підсумкову результативність системи.У тому числі наводяться корисні джерела, що знайомлять читача з техніками написання швидких, стабільних додатків на Java.

Вступ

Проектування систем критичних до часу відгуку вважається прерогативою мови С і підмножини мови C ++, виправдано є стандартом де-факто в розробці програмного забезпечення реального часу. Втім, існують технології, які дозволять створювати аналогічні по характеристикам додатки на Java. Однак через не завжди виправданою критики платформи Java в повільності, громіздкість і непристосованості до детермінізму і роботі без затримок дане рішення рідко використовується для реалізації ПО реального часу. У статті наочно показано, що при максимальних обмежень у часі відгуку до 100 мікросекунд використання Java може бути за витратами ресурсів більш економічним рішенням.

На поточний момент Java є найпоширенішою мовою в світі [25]. Це доброзичливий і доступний для програмістів інструмент. Завдяки незліченної кількості пропонованих сервісів, бібліотек і рівнів абстракції, його поріг входження істотно нижче, ніж у визнаних мастодонтів C / C ++. Значить, і вартість реалізації систем з жорсткими вимогами по надшвидкої реакції на Java відчутно менше. Це обумовлено тим, що за умови однакового досвіду програмістів Java і C ++, перший, використовуючи пропоновані йому середовищем кошти, спроектує таку систему швидше. При цьому вибираючи рішення, які автор рекомендує в цій праці, підсумкове ПО на Java не тільки не буде поступатися, а й в деяких питаннях перевершить свій можливий аналог на С ++.

Основні джерела непередбачуваної поведінки Java-додатка

Деякі фахівці критикують рішення на Java за непередбачуване і часто неефективне використання системних ресурсів. Тому я пропоную спочатку більш детально зупиниться на поясненні причин такої поведінки і способи усунення негативних ситуацій.

Першим і найважливішим джерелом непередбачуваної поведінки в нашому хіт-параді є збирач сміття (garbage collector). З одного боку, технологія виглядає сущим благом для розробників, що рятує їх від необхідності управляти пам'яттю вручну, але в той же час вона вносить вариационную складову в виконання програми. На жаль, неможливо передбачити в який момент включиться механізм збору непотрібних об'єктів, і як довго він триватиме, що може також спричиняти блокування важливих робочих потоків.

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

Лістинг 1.

Iterator <TestClass> testClassIter = list.iterator (); while (cursor.hasNext ()) {TestClass testClass = testClassIter.next (); if (o.getID () == RARE_ID) {NeverLoadedClass o2 = new NeverLoadedClass (o); } Else {...}}

Уникнути озвученої вище ситуації можна приготувавши заздалегідь список класів, використовуваних додатком, і змусивши JVM штучно завантажити їх за допомогою наступного простого методу:

Iterator <String> classIt = listOfClassNamesToLoad.iterator (); while (classIt.hasNext ()) {String className = classIt.next (); try {Class clazz = Class.forName (className); String n = clazz.getName (); } Catch (Exception e) {System.err.println ( "Could not load class:" + className); System.err.println (e); }}

Just-in-Time компілятор, замислювався, як і Garbage Collector, для поліпшення роботи ПО, може в процесі налагодження програми в ряді випадків спричинити додатковий головний біль. Суттю JIT компіляції є переклад байткода, згенерованого java, в машинні інструкції процесора. На жаль така компіляція "на льоту" витрачає процесорний час і може вносити затримку під час виконання від декількох мілісекунд до секунди. Ця ситуація схожа з динамічної завантаженням класів. Так, наприклад, непублічний внутрішній метод balanceTree класу TreeMap може викликатися будучи не скомпільований і тим самим гальмувати виконання програми.

Хороший джерело додаткових знань про методи компіляції дається в статті від експертів IBM: Real-time Java, Part 2: Comparing compilation techniques [28]. Крім детального опису процесу компіляції в ній наводиться техніка Ahead-Of-Time compilation, яка як може заміщати повністю JIT, так і доповнювати її. Суттю AOT є предкомпіляція байткода перед виконанням програми. Також в разі використання JIT поширеною практикою є введення в роботу програми стадії "прогріву". Система працює в холостому ходу якийсь період часу (за рекомендаціями інженерів Oracle від 10 до 30 хвилин [29]) протягом якого додаток заходить в усі можливі гілки виконання коду і JIT компілює і оптимізує всі методи.

Четверте джерело затримок - управління потоками в Java. Так, призначаючи потоку максимальний пріоритет, ми не можемо бути впевнені, що він не буде перерваний в ході боротьби за ресурси потоком з більш низьким пріоритетом.

Крім того, непрямими і прямими джерелами затримок можуть послужити: апаратна складова (наприклад, кешування даних на жорсткий диск), операційна система (фонові потоки-демони), інші працюють програми, сам додаток або навіть бібліотеки, які воно використовує.

Давайте далі перейдемо до огляду способів боротьби з вищевказаними проблемами:

Головні фактори, що впливають на загальну продуктивність

Головним фактором, що впливає на продуктивність, є Java машина, на якій виконується додаток. Підходити до вибору потрібно відповідально, зважуючи вихідні вимоги до характеристик системи.

Перше що необхідно відзначити - це наявність специфікації RTSJ [1] і JVM, що реалізують її. Ось кілька з них: Sun Java Real-time [2], JamaicaVM [3], Aonix Perc [4], IBM WebSphere Real Time V3.0 [5].

Real-time в складі назви специфікації говорить про передбачуваність і надійність часу відгуку, а не про те, що програма має реагувати на події зовнішнього середовища з максимальною швидкістю. Таким чином, основною вимогою систем реального часу є незмінне дотримання дедлайнів. RTSJ надає кілька засобів, що дозволяють досягти цього. Класи RealtimeThread і NoHeapRealTimeThread надають підтримку пріоритетів, періодичного поведінки, організацію дедлайнів з обработчиками, викликає, коли дедлайн пропущений, а також області пам'яті, відмінні від стандартної heap. NoHeapRealTimeThread в принципі не має доступу до heap і, таким чином, не може бути перерваний складальником сміття, що дозволяє використовувати його в якості потоку найвищого пріоритету і критичності до дедлайнів. Говорячи про пам'ять, специфікація RealTime забезпечує розробника двома додатковими областями, непідвладними GC - immortal і scoped. Об'єкти, виділені в області immortal, доступні всім потокам і ніколи не видаляються, а scoped надає програмісту можливість вручну керувати часом життя об'єктів. Крім того, RTSJ гарантує, що потік з найвищим пріоритетом буде виконуватися до тих пір, поки сам не звільнить ресурси процесора, чи не буде витіснений потоком з більш високим пріоритетом. Більш докладно можна почитати про це тут [6]. Варто окремо відзначити JVM від Azul Systems - Zing [7]. Дана технологія дозволяє мати необмежений розмір купи і повністю виключити паузи збирача сміття. Розробники Oracle, крім стандартної Java-машини, створили продукт JRockit [8] - високопродуктивне рішення для Java-додатків, що має в своєму складі збирач сміття, що підтримує детерміністичного поведінку з обмеженням на максимальний час паузи до 1 мілісекунди.

Як вже зазначалося вище, одним з головних факторів, що впливають на загальну продуктивність, часто стає збирач сміття, що є центром екосистеми Java. Основне завдання збирача сміття - відстеження непотрібних об'єктів в пам'яті і їх своєчасне видалення, з метою недопущення ситуації, коли вільної пам'яті в heap вже немає. Класичним варіантом алгоритму GC є поділ області heap на 2 зони: в nursery потрапляють "молоді" об'єкти, в tenured просуваються об'єкти вже пережили процес складання.

У мережі є багато матеріалів з налаштування GC під конкретну реалізацію Java-машини для досягнення детермінізму і скорочення пауз. Існують приклади, в яких виконання стадії Full GC здійснюється раз на добу за рахунок збільшення областей heap і nursery. Наприклад, в системах автоматичної торгівлі, де будь-які зупинки алгоритму купівлі-продажу на 100мкс можуть мати погані наслідки і подія "stop-the-world" повинне бути виключено. У такій ситуації для реалізації логування подій в системі в реальному часі можна використовувати підхід, що нагадує концепцію реалізовану в фреймворку disruptor.

Лістинг 2.

public class BackgroundLogger implements Runnable {static final int ENTRIES = 64; static class LogEntry {long time; int level; double data; } Static class LogEntries {final LogEntry [] lines = new LogEntry [ENTRIES]; int used = 0; } Private final ExecutorService executor = Executors.newSingleThreadExecutor (); final Exchanger <LogEntries> logEntriesExchanger = new Exchanger <LogEntries> (); LogEntries entries = new LogEntries (); BackgroundLogger () {executor.submit (this); } Public void log (int level, double data) {try {if (entries.used == ENTRIES) entries = logEntriesExchanger.exchange (entries); LogEntry le = entries.lines [entries.used ++]; le.time = System.nanoTime (); le.level = level; le.data = data; return; } Catch (InterruptedException e) {throw new RuntimeException (e); }} Public void flush () throws InterruptedException {if (entries.used> 0) entries = logEntriesExchanger.exchange (entries); } Public void stop () {try {flush (); } Catch (InterruptedException e) {e.printStackTrace (); } Executor.shutdownNow (); } @Override public void run () {LogEntries entries = new LogEntries (); try {while (! Thread.interrupted ()) {entries = logEntriesExchanger.exchange (entries); for (int i = 0; i <entries.used; i ++) {bgLog (entries.lines [i]); } Entries.used = 0; }} Catch (InterruptedException ignored) {} ​​finally {System.out.println ( "Warn: logger stopping."); }} Private void bgLog (LogEntry line) {}}

Наступний важливий аспект роботи - колекції, що дозволяють зручно, швидко і ефективно зберігати і організовувати доступ до даних. На ринку, крім стандартної Java Collection FrameWork від Oracle, представлені реалізації від High Performance Computing Collections [6], Trove [7], Colt [8], PCJ [9]. Основна ідея полягає в неможливості використання примітивів в стандартних Java-колекціях, що тягне за собою генерування додаткового сміття у вигляді класів-обгорток, таких як Double і Long для типів double і long відповідно.

Ілюстрації, наведені нижче, показують різницю в запусках збирача сміття на двох прикладах: один реалізований на примітивах, а інший на wrapper'ах.

Лістинг 3.

Map <Integer, Integer> counters = new HashMap <Integer, Integer> (); int runs = 20 * 1000; for (Integer i = 0; i <runs; i ++) {Integer x = i% 12; Integer y = i / 12% 12; Integer times = x * y; Integer count = counters.get (times); if (count == null) counters.put (times, 1); else counters.put (times, count + 1); }


Мал. 1. Результат роботи програми, написаний за допомогою типів-обгорток (Integer). джерело vanillajava.blogspot.com

Той же приклад на примітивах:

Лістинг 4.

int [] counters = new int [144]; int runs = 20 * 1000; for (int i = 0; i <runs; i ++) {int x = i% 12; int y = i / 12% 12; int times = x * y; counters [times] ++; }


Мал. 2. Результат роботи програми, написаної на примітивних типах. джерело картинки vanillajava.blogspot.com

Деякі популярні бібліотеки

Бібліотека Javolution [18] складається з широкого спектру класів, які надають більш детерміністичного поведінку в порівнянні з аналогами Java Collection FrameWork. Так, наприклад, класи FastMap (аналог HashMap), FastTable (аналог ArrayList) і TextBuilder (аналог StringBuilder) показують плавне зростання, а не виконують дорогу операцію розширення місткості колекції по досягненню певного відсотка заповнювання або процедуру повного перехешірованія.

На наступному малюнку наведено приклади сплески в часі вставки запису в HashMap при досягненні певного розміру таблиці. FastMap позбавлений такої вади.

FastMap позбавлений такої вади

Мал. 3. Порівняння затримки вставки в FastMap і HashMap. джерело javolution.org

Окремо відзначу бібліотеку месседжінга ZeroMQ, розроблену фахівцями iMatix, що відповідає найжорсткішим вимогам до пропускної спроможності і затримок. Продукт виглядає вигідно на тлі конкурентів з-за низького порогу входження і простоти експлуатації. ZeroMQ дозволяє легко описувати складні мережеві топології, варіанти обміну даними між тред (in-process), процесами (inter-process), а також розподіленими додатками. Нижче наведено приклад publisher-сервера (Лістинг 4), який віддає передплатникам поновлення погоди, опубліковані в керівництві по використанню ZeroMQ [13]:

Лістинг 5.

import java.util.Random; import org.zeromq.ZMQ; public class wuserver {public static void main (String [] args) {ZMQ.Context context = ZMQ.context (1); ZMQ.Socket publisher = context.socket (ZMQ.PUB); publisher.bind ( "tcp: // *: 5556"); publisher.bind ( "ipc: // weather"); Random srandom = new Random (System.currentTimeMillis ()); while (true) {int zipcode, temperature, relhumidity; zipcode = srandom.nextInt (100000) + 1; temperature = srandom.nextInt (215) - 80 + 1; relhumidity = srandom.nextInt (50) + 10 + 1; String update = String.format ( "% 05d% d% d \ u0000", zipcode, temperature, relhumidity); publisher.send (update.getBytes (), 0); }}}

Говорячи про мережевих бібліотеках Java-світу, неодмінно варто виділити Netty [23], що зарекомендувала себе в багатьох проектах, наприклад Twitter [22]. Netty - оптимізований фреймворк для створення швидких і масштабованих мережних додатків, що підтримує велику кількість протоколів за умовчанням, а також надає кошти для простої реалізації власних протоколів поверх TCP / UDP. Єдиний момент, який перешкоджає використанню netty в системах реального часу з жорсткими вимогами по часу відгуку - це необхідність створення окремого буфера для кожного мережевого повідомлення, що підвищує навантаження на збирач сміття. Однак на даний момент ця характеристика вже виправляється інженерами Netty, і в 4 версії фреймворка буде доступна можливість використання пулу об'єктів.

У деяких ситуаціях існує необхідність прив'язати певний потік до певного процесу. Ця функція відсутнє в JDK, але існує бібліотека Java Thread Affinity [19,24], яка може виконати таку вимогу. У разі, якщо ви хочете досягти мінімальної затримки в обробці мережевих пакетів, запропонована функція надзвичайно корисна. Наприклад, можна змусити певний процесор обробляти переривання з мережевої картки (опис процесу можна знайти тут в разі ОС Linux [20]), і до цього ж процесору прив'язати потік, який використовує дані пакети з мережі. Таким чином, ми добиваємося мінімальної можливої ​​затримки, так як дані не залишають процесор.

Застосування ядра реального часу для досягнення детермінізму

Розглянемо на критичній важливості використання ядра реального часу [17] на результатах, отриманих творцями ZeroMQ [14]. Наприклад, SUSE Linux Enterprise Real Time Extensions [15] або Red Hat Enterprise MRG [16]. На малюнках 4 і 5 показані виміри часу відправлення повідомлення між двома серверами в локальній мережі. У першому варіанті на серверах встановлена ​​система SUSE Linux Enterprise, а в другому - Real Time Extension, трохи збільшує середній час, але зате виключає піки і наближає значення максимуму до середнього показника.

Таким чином, важливо усвідомлювати, що, навіть використовуючи добре написані бібліотеки, потрібно уважно ставитися до того, в якому оточенні запускається додаток - які операційна система, драйвери, демони-процеси, апаратна складова. Корисною практикою є виключення непотрібних сервісів: irq_balance і cpu_speed, які можуть збільшити вариационную складову виконання програми. Крім того, гарним документом по налаштуванню Linux для досягнення мінімальних затримок є праця інженерів IBM [21].

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


Мал. 4. Час передачі повідомлення на SUSE Linux Enterprise. джерело zeromq.org


Рис.5. Час передачі повідомлення на SUSE Linux Enterprise Real Time Extension.Істочнік картинки [14].

висновок

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

Список літератури

1. Real-Time Specification for Java http://www.rtsj.org/

2. http://www.atego.com/products/aonix-perc/

3. http://www.aicas.com/jamaica.html

4. Sun Java Real-Time System, http://java.sun.com/javase/technologies/realtime.jsp

5. IBM WebSphere Real Time V3.0, http://www-01.ibm.com/common/ssi/cgi-bin/ssialias?subtype=ca&infotype=an&appname=iSource&supplier=897&letternum=ENUS211-279

6.http: //www.ibm.com/developerworks/java/library/j-rtj1/

7.http: //www.azulsystems.com/

8. http://www.oracle.com/technetwork/middleware/jrockit/overview/index.html

9. http://acs.lbl.gov/software/colt/

10. http://pcj.sourceforge.net/

11. http://fastutil.dsi.unimi.it/

12.http: //www.zeromq.org/results: rt-tests-v031

13. http://zguide.zeromq.org/java:wuserver

14. http://www.zeromq.org/results:rt-tests-v031

15. http://www.suse.com/products/realtime/

16. http://www.redhat.com/products/mrg/

17. https://rt.wiki.kernel.org/

18. http://javolution.org/

19. http://vanillajava.blogspot.com/2011/12/thread-affinity-library-for-java.html

20. http://lserinol.blogspot.com/2009/02/irq-affinity-in-linux.html

21. http://publib.boulder.ibm.com/infocenter/lnxinfo/v3r0m0/topic/performance/rtbestp/rtbestp_pdf.pdf

22. http://engineering.twitter.com/2011/04/twitter-search-is-now-3x-faster_1656.html

23.http: //netty.io/

24. http://vanillajava.blogspot.com/2012/02/how-much-difference-can-thread-affinity.html

25. http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html

26. http://vanillajava.blogspot.com/2011/07/low-gc-in-java-use-primitives-instead.html

27. http://javolution.org/target/site/apidocs/javolution/util/FastMap.html

28.http: //www.ibm.com/developerworks/java/library/j-rtj2/

29. http://docs.oracle.com/cd/E13221_01/wlrt/docs30/intro_wlrt/tuning.html

Будь-який з матеріалів, опублікованих на цьому сервері, не може бути відтворений в якій би то не було формі і якими б то не було засобами без письмового дозволу власників авторських прав.
Com/common/ssi/cgi-bin/ssialias?

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

Или позвоните нам по телефонам: (048) 823-25-64

Организация (обязательно) *

Адрес доставки

Объем

Как с вами связаться:

Имя

Телефон (обязательно) *

Мобильный телефон

Ваш E-Mail

Дополнительная информация: