ИССЛЕДОВАНИЕ ЭФФЕКТИВНОСТИ ВИРТУАЛЬНЫХ ПОТОКОВ И ТРАДИЦИОННЫХ МОДЕЛЕЙ КОНКУРЕНТНОСТИ В СРЕДЕ ВЫПОЛНЕНИЯ JAVA
Конференция: XCIX Международная научно-практическая конференция «Научный форум: технические и физико-математические науки»
Секция: Информатика, вычислительная техника и управление

XCIX Международная научно-практическая конференция «Научный форум: технические и физико-математические науки»
ИССЛЕДОВАНИЕ ЭФФЕКТИВНОСТИ ВИРТУАЛЬНЫХ ПОТОКОВ И ТРАДИЦИОННЫХ МОДЕЛЕЙ КОНКУРЕНТНОСТИ В СРЕДЕ ВЫПОЛНЕНИЯ JAVA
Введение
На протяжении более 20 лет модель параллельности выполнения кода в Java базировалась на концепции использования платформенных потоков. В таком подходе, каждый поток приложения представляет собой нативный поток операционной системы. В этом и заключается основной недостаток такого решения. Платформенные потоки очень дороги в создании с точки зрения производительности и ресурсоемкости.
Усугубляет это положение набирающие популярность конкурирующие языки программирования. Ярким примером является Go с легковесными горутинами и автоматическими переключениями при блокирующих вызовах. Разработчики получили легковесную реализацию, позволяющую использовать простой код и обрабатывать тысячи потоков одновременно.
Очевидным шагом со стороны Java сообщества стало внедрение такой функциональности, т.к. при бездействии Java может потерять лидирующие позиции в сегменте высоконагруженных распределенных системах. Результатом ответа конкурентам стала технология виртуальных потоков представленная к широкому использованию в Java 21.
Цель данной статьи является анализ существующих решений параллельного выполнения кода и проведение сравнительного эксперимента для оценки их эффективности.
Анализ существующих решений
Платформенные потоки
Как уже было сказано, самый популярный подход параллельности кода это использование платформенных потоков. В этой модели взаимодействия каждый поток привязан 1:1 к потоку операционной системы.
Запрос о создании потока к операционной системе не бесплатен и требует как времени на это, так и памяти для поддержания работы. Эту проблему отчасти решают пулы платформенных потоков. Однако существует ограничение на количество потоков для одного интсанса. При превышении лимита новые запросы помещаются в очередь, что приводит к лавинообразному росту задержек и исчерпанию доступных соединений.
Также при ожидании ответа, например от базы данных, поток блокируется и не может приступать к новым задачам. В этом случае операционная система использует механизм переключений контекстов между потоками, что также не является быстрой операцией. Также требуется хранить состояние каждого заблокированного потока, что может составлять не только сотни, но и тысячи мегабайт.
Реактивный подход
Также существует кардинально другой подход к обслуживанию клиентов. Реактивный подход основан на противоположной парадигме - использование неблокирующего параллельного взаимодействия. Это возможно благодаря событийному циклу, в котором работает поток, что позволяет обрабатывать тысячи задач ввода/вывода на одном потоке.
Также так как используются количество потоков соответствующее количество потоков процессора, реактивный подход требует меньших затрат и ресурсов на поддержку чем использование пула платформенных потоков.
Главным недостатком подхода является высокий порог входа. Для реактивного программирования требуется полностью перестроить мышление разработчика, а ошибка в одном месте может привести к блокировке событийного цикла и падению всего приложения. Причем отладка такого кода может быть затруднительна.
Также требуется полная поддержка неблокирующих взаимодействий во всех используемых библиотеках, что затруднительно из-за сложности парадигмы.
Виртуальные потоки
Виртуальные потоки в Java 21 объединяют все лучшие стороны этих двух подходов. Java Virtual Machine все еще управляет пулом потоков, однако самим потоком операционная система больше не управляет. Вместо нее это делает Java Virtual Machine. Когда вызывается блокирующий метод, JVM сохраняет состояние потока в кучу и открепляет его от этой задачи для других задач.
Таким образом, виртуальные потоки сохраняют простой и понятный стиль написания кода, а также решают проблемы использования платформенных потоков.
Исследовательский эксперимент
Для оценки эффективности подходов, было решено провести нагрузочное тестирование со сравнением таких метрик как: пропускная способность, средняя задержка выполнения запроса, задержка выполнения запроса по 95-му и 99-му перцентилю и потребление ОЗУ. Тестируемый сценарий включал линейное повышение количества виртуальных пользователей до 10 000 и удержание данной нагрузки до 5 минут. Пауза между запросами составляла 0.5 секунд.
Было создано бэкенд приложение Java 21, которое по HTTP протоколу передает коллекцию, состоящую из 1000 объектов. Эти объекты получаются из удаленной базы данных MongoDB версии 3.8.7. В качестве фреймворка использовался Spring версии 4.1.0. Spring позволяет поддерживать одновременно синхронный и реактивный подход, что удобно для проведения исследования. В качестве синхронного сервера использовался Tomcat с пулом из 100 потоков, а в качестве реактивного сервера использовалась библиотека Netty с пулом из 8 потоков. Размер очереди составлял 100 запросов. При тестировании не использовались настройки JVM.
Тестирование проводилось на вычислительной машине с процессором AMD Ryzen 3 3100 (архитектура x86_64) и 16 ГБ оперативной памяти. В качестве инструмента тестирования использовалась Grafana K6.
Таблица 1.
Результаты нагрузочного тестирования
|
Метрики |
Платформенные потоки |
Реактивный подход |
Виртуальные потоки |
|
Пропускная способность, запросы в секунду |
3420 |
11580 |
7830 |
|
Средняя задержка выполнения запроса, мс. |
183 |
64 |
91 |
|
Задержка выполнения запроса по 95-му перцентилю, мс. |
464 |
133 |
217 |
|
Задержка выполнения запроса по 99-му перцентилю, мс. |
1327 |
210 |
418 |
|
Потребление ОЗУ, МБ |
4820 |
614 |
1340 |
По результатам тестирования реактивный подход обеспечил 11580 запросов в секунду, что примерно в 3 раза превышает результат платформенных и в 1.5 раза виртуальных потоков. Такое соотношение объясняется отсутствием необходимости переключения контекста операционной системы и необходимостью создания новых потоков. Разрыв между виртуальными потоками и реактивным подходом обуславливается отсутствием расходов на операции монтирования и размонтирования потоков.
Во всех метриках задержки реактивный подход держит уверенное превосходство. Наиболее показательно отражается деградация производительности у платформенных потоков на 99 перцентиле. Значение реактивного подхода в 6 раз меньше, а виртуальных потоков в 3 раза. Такая деградация объясняется накоплением запросов в очереди Apache Tomcat, при насыщении часть запросов вынуждена ожидать свободного потока, что приводит к каскадному росту времени ожидания. Что также подтверждается повышенным потреблением оперативной памяти.
Заключение
Результат проведенного исследования подтверждает, что классический синхронный платформенный подход имеет существенные ограничения при построении масштабируемых облачных приложений.
Новая технология виртуальных потоков является новым витком эволюции языка программирования. Они успешно решают проблему конкуренции с такими платформами, как Go, предоставляя легковесную конкурентность без необходимости изменения парадигмы программирования.
Стоит выделить, что виртуальные потоки не являются универсальным решением. Если значительная часть запросов выполняет длительные CPU-вычисления на процессоре, то создание виртуальных потоков не повысит пропускную способность и приведет к увеличению накладных расходов планировщика. Также использование большого количества виртуальных потоков увеличивает нагрузку на сборщик мусора из-за их хранения в куче JVM.


