Нативная компиляция в Quarkus – почему это важно

Всем привет! С вами второй пост из нашей серии по Quarkus — сегодня поговорим о нативной компиляции.

pouxirwqx8fafftuhwwcma0jw5q.jpeg

Quarkus — это Java-стек, заточенный под Kubernetes. И хотя здесь, конечно, многое еще предстоит сделать, мы хорошо проработали массу аспектов, включая оптимизацию JVM и целого ряда фреймворков. Одной из особенностей Quarkus, вызвавшей повышенный интерес со стороны разработчиков, стал комплексный бесшовный подход к превращению Java-кода в исполняемые файлы для конкретной операционной системы (так называемая «нативная компиляция») по аналогии с C и C ++, где такая компиляция обычно происходит в конце цикла, состоящего из сборки, тестирования и развертывания.
И хотя нативная компиляция, как мы покажем ниже, важна, надо отметить, что Quarkus реально хорошо работает и на самой обычной Java-машине OpenJDK Hotspot благодаря тем улучшениям производительности, которые мы реализовали по всему стеку. Поэтому нативную компиляцию стоит рассматривать как дополнительный бонус, который можно использовать по желанию или необходимости. На самом деле в том, что касается нативных образов, Quarkus в значительно мере опирается на OpenJDK. А тепло принятый разработчиками режим dev mode обеспечивает практически мгновенное тестирование изменений за счет развитых возможностей динамического выполнения кода, реализованных в Hotspot. Кроме того, при создании нативных образов GraalVM задействуется библиотека классов OpenJDK и возможности HotSpot.

Так зачем тогда нужна нативная компиляция, если всё и так отлично оптимизировано? На этот вопрос мы и постараемся ответить ниже.

Начнем с очевидного: Red Hat обладает большим опытом оптимизации JVM, стеков и фреймворков в ходе развития проекта JBoss, включая:

  • Первый сервер приложений для работы в облаке на платформе Red Hat OpenShift.
  • Первый сервер приложений для работы на компьютерах Plug PC.
  • Первый сервер приложений для работы на Raspberry Pi.
  • Целый ряд проектов, работающих на устройствах Android.


Мы уже много лет занимаемся проблемами запуска Java-приложений в облаке и на устройствах с ограниченными ресурсами (читай, IoT) и научились выжимать из JVM максимум в смысле производительности и оптимизации памяти. Как и многие другие, мы уже давно работаем с нативной компиляцией Java-приложений через GCJ, Avian, Excelsior JET и даже Dalvik и отлично осознаем плюсы и минусы такого подхода (например, дилемму выбора между универсальностью «build once — run-anywhere» и тем, что скомпилированные приложений имеют меньший размер и быстрее запускаются).

Почему так важно учитывать эти плюсы и минусы? Потому что в некоторых ситуациях их соотношение становится решающим:

  • Например, в serverless/управляемых событиями средах, где сервисы просто обязаны запускаться в режиме (жесткого или мягкого) реального времени, чтобы успевать реагировать на события. В отличие от долго живущих персистентных сервисов, здесь продолжительность холодного запуска критично увеличивает время ответа на запрос. На запуск JVM все еще уходит существенное время, и, хотя в некоторых случаях его можно сократить чисто аппаратными методами, разница между одной секундой и 5 миллисекундами может быть вопросом жизни и смерти. Да, здесь можно поиграться с созданием горячего резерва Java-машин (что мы, например, сделали при портировании OpenWhisk на Knative), но само по себе это не гарантирует достаточного для обработки запросов количества JVM по мере масштабирования нагрузки. Да и с экономической точки зрения это, наверняка, не самый правильный вариант.
  • Далее, есть еще такой часто всплывающий аспект, как мультитенантность. Несмотря на то, что JVM по своим возможностям сильно приблизились к операционным системам, они все еще не способны делать то, к чему мы так привыкли в том же Linux«е — изолировать процессы. Поэтому сбой одного потока может вывести из строя всю Java-машину. Многие пытаются обойти этот недостаток тем, что выделяют под приложения каждого пользователя отдельную JVM, чтобы минимизировать последствия сбоя. Это вполне логично, но плохо сочетается с масштабированием.
  • Кроме того, для облачно-ориентированных приложений важен такой показатель, как плотность сервисов на хосте. Переход на методологию 12 факторов приложения, микросервисы и Kubernetes увеличивает количество Java-машин на одно приложение. То есть, с одной стороны, все это дает эластичность и надежность, но одновременно растет и расход базовой памяти в пересчете на сервис, причем часть этих расходов далеко не всегда является строго необходимой. Статически скомпилированные исполняемые файлы выигрывают здесь за счет различных техник оптимизации, вроде низкоуровневого dead-code elimination, когда в итоговый образ включаются только те части фреймворков (включая и сам JDK), которые сервис реально использует. Поэтому нативная компиляция Quarkus помогает плотнее размещать экземпляры сервисов на хосте без ущерба для безопасности.


Собственно, приведенных выше доводов уже достаточно для того, чтобы понять оправданность нативной компиляции с точки зрения участников проекта Quarkus. Однако есть еще одна, не техническая, но тоже важная причина: в последние годы многие программисты и компании-разработчики отказались от Java в пользу новых языков программирования, посчитав, что Java вместе со своими JVM, стеками и фреймворками стала слишком прожорливой в плане памяти, чересчур медленной и т.д.

Однако привычка использовать один и тот же инструмент для решения любых задач — это не всегда правильно. Иногда лучше сделать шаг назад и поискать что-то другое. И если Quarkus заставляет людей сделать паузу и задуматься, то это хорошо для всей экосистемы Java. Quarkus воплощает собой новаторский взгляд на то, как создавать более эффективные приложения, делая Java более релевантной новым архитектурам приложений, вроде serverless. Кроме того, благодаря своей расширяемости, Quarkus, как мы надеемся, обзаведется целой экосистемой Java-расширений, значительно увеличив количество фреймворков, которые из коробки будут поддерживать нативную компиляцию в составе приложений.

© Habrahabr.ru