Опыт сопряжения Java, JavaScript, Ruby и Python в одном проекте посредством GraalVM

В прошлом месяце вышла стабильная LTS-версия многоязычной среды выполнения GraalVM 20.3.0 от корпорации Oracle и мне захотелось испробовать её для решения какой-нибудь интересной практической задачи. Для тех кто не в курсе, приведу краткое описание этой новой платформы. GraalVM позволяет использовать в едином окружении различные популярные языки программирования и обеспечивает их разностороннее взаимодействие в рамках некоторой общей среды выполнения.



Схематическое изображение архитектуры GraalVM из официальной документации.

Добавление новых языков в GraalVM осуществляется с помощью специального фреймворка Truffle, выполненного в виде библиотеки Java. Фреймворк предназначен для создания реализаций языков программирования в качестве интерпретаторов для самомодифицируемых абстрактных синтаксических деревьев (AST). При желании на его основе можно создать собственный язык, в официальных репозиториях GraalVM подробно рассмотрен пример реализации такого проекта под названием SimpleLanguage. Интерпретаторы, которые были написаны с использованием фреймворка Truffle, будут автоматически использовать GraalVM как JIT-компилятор непосредственно для самой реализации языка запускаемой на JVM-платформе и, соответственно, иметь возможность взаимодействия и двустороннего обмена данными в одном и том же пространстве памяти посредством специально разработанного протокола и программного интерфейса Polyglot API.

Платформа GraalVM вместе с исполняемой программой на смеси самых разных языков может быть представлена в виде автономного и самодостаточного исполняемого файла, либо работать поверх OpenJDK, Node.js или даже внутри Oracle Database.

На текущий момент сразу «из коробки» поддерживаются следующие языки программирования и технологии:

  • Java, Kotlin, Scala и другие языки JVM-платформы.
  • JavaScript вкупе с платформой Node.js и сопутствующим инструментарием.
  • C, C++, Rust и другие языки, которые могут быть скомпилированы в LLVM bitcode.

Помимо этого, с помощью встроенного пакетного менеджера в дистрибутив GraalVM можно добавить поддержку:

  • Python
  • Ruby
  • R
  • WebAssembly

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

Кроме того, посредством ahead-of-time (AOT) компиляции имеется возможность создавать автономные исполняемые файлы, называемые нативными образами, которые используют не классическую Java VM, а более специализированную Substrate VM, реализующую компактную среду выполнения. В итоге программы запускаются значительно быстрее и расходуют гораздо меньше оперативной памяти. Но при этом теряются некоторые преимущества just-in-time (JIT) компиляции, доступной на классических платформах. Для формирования подобных нативных образов в большинстве случаев требуются значительные ресурсы CPU и десятки гигабайт оперативной памяти, поэтому их создание лучше всего производить на каких-нибудь мощных сборочных серверах или рабочих станциях.

Корпорация Oracle в настоящее время позиционирует GraalVM как единую и идеальную платформу для создания различных микросервисов. При этом она уже имеет влияние на развитие классического OpenJDK. Например, встраиваемый движок JavaScript под названием Nashorn уже удалён из JDK 15, а в качестве его замены предлагается попробовать именно GraalVM. Неизвестно, как дальше будут развиваться события и будет ли GraalVM в будущем предлагаться в качестве рекомендуемой JVM-платформы по умолчанию, но судя по весьма активному развитию и маркетинговому продвижению в последнее время, Oracle настроен вполне серьёзно. Так что время покажет.

Для конечного использования предлагаются две редакции: бесплатная GraalVM Community и платная GraalVM Enterprise, отличия между которыми описаны на этой страничке официального сайта GraalVM. В основном они сводятся к обеспечению лучшей производительности, меньшего потребления оперативной памяти и официальной поддержке от специалистов корпорации Oracle в платной версии. В этой статье я буду ориентироваться только на возможности GraalVM Community, распространяемой свободно.

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

Содержание:

1. Подсветка синтаксиса фрагментов кода на стороне сервера
2. Создание простейшего прототипа
3. Установка GraalVM и сопутствующих библиотек, запуск прототипа
4. От прототипа к готовому сервису
5. Подведём итоги

1. Подсветка синтаксиса фрагментов кода на стороне сервера

Когда-то давно мне стало жутко интересно, какими технологиями крупные хостеры IT-проектов вроде GitHub, Gitorious и Bitbucket подсвечивают наш исходный код на своих серверах. Проведя некоторое исследование, я пришёл к следующим результатам:

  1. Bitbucket использует библиотеку Pygments на языке программирования Python.
  2. GitHub использует Albino, обёртку библиотеки Pygments для языка программирования Ruby.
  3. Gitorious использует Makeup, тоже по своей сути обёртку библиотеки Pygments для Ruby.

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

Шло время и недавно я решил снова перепроверить чем же сейчас пользуются крупные хостеры исходного кода. За это время Gitorious был куплен GitLab‘ом, который в итоге стал новым крупным и популярным сервисом. На сей раз получились такие результаты:

  1. Bitbucket как использовал библиотеку Pygments ранее, так и продолжает её использовать.
  2. GitHub отказался от использования обёртки Albino и перешёл на новую библиотеку Linguist. Однако, Linguist только детектирует язык в исходных файлах, а сам процесс подсветки выполняется какой-то другой библиотекой с закрытым исходным кодом, принадлежащей GitHub.
  3. GitLab применяет библиотеку Rouge на языке программирования Ruby, свободную от использования обёрток, но сохранившую некоторую совместимость с Pygments.

Видимо аудитория этих сервисов росла и использовать обёртки, запускающие Python из-под Ruby, стало несколько накладно и дорого. Если посмотреть на официальный сайт библиотеки Rouge, то одним из преимуществ там явно обозначают то, что теперь не требуется спавнить процессы интерпретатора Python, так как Rouge уже изначально написан на Ruby.

Особняком ещё стоит библиотека Highlight.js на языке программирования JavaScript, получившая огромную популярность и широкое распространение на самых разнообразных сайтах и сервисах. Её применяют в основном чтобы подсвечивать код на стороне клиента, но никто не запрещает использовать эту библиотеку и для подсветки на стороне сервера.

Если бы вы писали сайт на каком-либо языке JVM-стека вкупе с каким-нибудь популярным веб-фреймворком и перед вами бы стояла задача реализовать серверную подсветку синтаксиса различных фрагментов кода, то у вас бы испортилось настроение. К большому сожалению, JVM-платформа не обзавелась такими библиотеками, как Pygments, Rouge и Highlight.js, которые поддерживают сотни языков программирования. Все известные мне попытки портирования Pygments на Java на сегодня по сути заброшены и поэтому вам для выполнения этой задачи пришлось бы делать такие же обёртки над чужеродными библиотеками, которые были описаны выше.

Альтернатива видится в использовании Jython, JRuby или Nashorn, то бишь внешних реализаций Python, Ruby и JavaScript для платформы JVM. Но с ними не всё так гладко, как хотелось бы. Во-первых, размер вашего JAR-файла или WAR-файла и время его запуска существенно увеличится. Во-вторых, некоторые библиотеки предоставляют реализации версий языков далеко не первой свежести, например, Jython так и остался на Python 2, который уже устарел и новые версии Pygments на нём просто не работают. В-третьих, установка сторонних библиотек внутрь конечного файла для развёртывания в некоторых случаях далеко не так тривиальна и сопровождается грудой различных проблем и точек отказа.

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

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

<< Перейти к содержанию

2. Создание простейшего прототипа

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

  • Highlight.js на JavaScript, поддерживает ~190 языков программирования.
  • Rouge на Ruby, поддерживает ~205 языков программирования.
  • Pygments на Python, поддерживает ~500 языков программирования.

Их связующим звеном я решил выбрать Java 8, хотя с таким же успехом этот выбор мог быть сделан в сторону Kotlin, Scala или Java 11.

Итак, покопавшись по официальным документациям и гайдам этих библиотек и выяснив как именно подсвечивать с их помощью код, я написал такую вот простенькую консольную программку примерно из ста строк:

Как видно, в этом фрагменте смешаны четыре разных языка программирования. Утилита принимает на вход stdin в виде текста исходного файла, передаваемые аргументы определяют используемую библиотеку для подсветки и язык фрагмента, затем подсвечивается код и выводится готовый HTML на stdout терминала. Звучит просто и понятно, но для компиляции и запуска этой программы требуется установить GraalVM и сопутствующие пакеты нужных библиотек.

<< Перейти к содержанию

3. Установка GraalVM и сопутствующих библиотек, запуск прототипа

Для экспериментов я выбрал сервер с ванильным CentOS 7, на который без каких либо проблем разворачивается дистрибутив GraalVM.

Рецепт установки платформы у меня получился следующим:

Стоит заметить, что в настоящий момент времени GraalVM 20.3.0 поддерживает только LTS-версии Java 8 и Java 11, поэтому при создании проектов на этой платформе следует помнить об этом и пока забыть про вкусности новых версий Java и JVM-платформ. Итак, после выполнения этих команд в директории /opt/ у вас будет развёрнут GraalVM, но в него ещё нужно добавить поддержку дополнительных компонентов.

Рецепт установки языков программирования в GraalVM и требуемых библиотек подсветки кода таков:

Перед использованием инструментов из дистрибутива GraalVM следует подгрузить нужные нам переменные окружения. При желании их подгрузку можно сделать при старте сервера для определённого пользователя или для всей системы сразу. Утилита gu является неким пакетным менеджером и предназначена для установки, удаления и обновления компонентов GraalVM. Нативные образы я не стал пересобирать, поскольку это достаточно долгий и ресурсоёмкий процесс. Он потребуется только в том случае, если вы захотите сформировать автономный исполняемый файл вашей программы. Пост-установочный скрипт после процесса инсталляции поддержки языка Ruby я тоже не стал выполнять, поскольку библиотека Rouge никак не взаимодействует с сетью и интернетом. Для использования Highlight.js достаточно просто скачать минифицированный файл библиотеки, платформа Node.js здесь никак не задействуется.

Ситуацию с Pygments следует разобрать немного подробнее. В случае с Python, его менеджер пакетов pip доступен лишь в виртуальном окружении virtualenv и для упрощения я решил отказаться от изоляции и использовать способ установки библиотеки Pygments посредством инструмента setuptools и загрузки исходников. Стоит заметить, что постоянные холодные вызовы интерпретатора graalpython довольно ресурсоёмки и на выполнение этих команд потребуется некоторое время, однако после завершения процесса требуемая библиотека будет сразу нам доступна. Кстати, установку и удаление библиотек можно производить и обычным системным pip, который входит в поставку дистрибутива операционной системы. В моём случае использовался CentOS 7, где по умолчанию установлен лишь Python 2, а усложнять инструкцию установкой современного Python 3 мне не хотелось, тем более раз для GraalVM доступна его собственная реализация Python.

Рецепт сборки и запуска консольной программки-прототипа из предыдущего раздела:

Как видно, всё прекрасно работает! Только не стоит забывать что для Highlight.js требуется положить файлик highlight.min.js рядом с нашей программкой. Использование внутренних ресурсов JAR-файла усложнило бы этот пример, поэтому я решил выбрать именно такой простой способ.

При желании все эти рецепты можно аккуратно завернуть в контейнеры вроде Docker или Podman, кому что нравится. Официальные образы от Oracle с GraalVM вполне себе доступны на том же Docker Hub.

<< Перейти к содержанию

4. От прототипа к готовому сервису

Имея на руках готовый прототип я решил превратить его в простенький демонстрационный сервис, представляющий собой веб-сайт с возможностью сохранения фрагментов исходного кода и выбора библиотеки для запекания подсветки синтаксиса в HTML.

Для реализации этой задачи я взял известный в Java-экосистеме фреймворк Spring, который в настоящее время без особых нареканий работает на GraalVM и даже обозначен на официальном сайте платформы. В качестве проксирующего веб-сервера был выбран Nginx, а для базы данных я использовал PostgreSQL. Для front-end‘а мной был выбран несколько устаревший в современном мире, но всё ещё использующийся подход с рендерингом HTML на стороне сервера при помощи шаблонизатора Thymeleaf. В общем, я остановился на одном из самых популярных фреймворков и его сопутствующих инструментах вроде Spring Boot 2.4.1 или Spring Data JPA, которые сильно ускоряют разработку и сокращают рутинный код. Но при этом с GraalVM вы вольны выбирать другие и более легковесные Java-фреймворки, которые больше ориентированы на создание микросервисов. Официальный сайт рекомендует посмотреть на Micronaut и Quarkus, преимущество которых состоит в возможности более лёгкого создания нативных образов из-за меньшего потребления оперативной памяти при их формировании.

Отправку фрагментов кода на веб-сайт я стилизовал под популярный ныне на многих IT-ресурсах язык разметки Markdown:

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



Сервис Code Polyglot с тёмной темой.

Тёмная тема в хакерском стиле создавалась под впечатлением от интерфейса старых мобильных телефонов Motorola на P2K, вроде E398, которые оставили в моей юности приятные и тёплые воспоминания. Оболочка называлась «Techno» и выглядела следующим образом:



Оболочка «Techno» на Motorola E398.

Надеюсь, вы не станете меня проклинать за моё извращённое чувство прекрасного и безумные умения в вёрстке, ибо я не имею должного опыта веб-разработки. Но если всё-таки станете, то на сайте доступна более нейтральная светлая тема, простой дизайн которой я позаимствовал с похожего сервиса по обмену фрагментами кода: paste.org.ru:



Сервис Code Polyglot со светлой темой.

Итак, прототип потихоньку начал обрастать различной функциональностью для выхода в мир, которую описывать здесь особого смысла я не вижу, всё же эта статья не о том, как быстро сделать подобный веб-сайт с помощью фреймворка Spring Boot. Расскажу лучше о тех проблемах, с которыми мне пришлось столкнуться и которые я попытался преодолеть.

Фрагменты кода на веб-сайте я решил отображать в таблицах, как это делает GitHub, но оказалось что с этим не всё так гладко. Библиотека Highlight.js, например, позиционируется разработчиком как максимально простая и поэтому принципиально не умеет оборачивать фрагменты кода в табличные строки. С другой стороны, генерируемые таблицы Pygments и Rouge слабо совместимы между собой. Поэтому мне пришлось выключать табличное отображение вообще, просто подсвечивать фрагмент кода выбранной библиотекой и построчно оборачивать его в таблицу уже на стороне Java.

Подобный подход привёл ещё к одной проблеме, которую проще всего продемонстрировать на следующем примере:

Библиотеки Highlight.js и Rouge не оборачивали каждую линию кода в автономные HTML-теги, поэтому при генерации таблицы они разрывались и подсветка кода работала некорректно. Я постарался исправить эту проблему с помощью обычного стека из стандартной библиотеки структур данных в Java. Упрощённый алгоритм выглядит примерно следующим образом: проходим по строке, когда детектим открывающий тег подсветки, то кладём его на стек, а когда детектим закрывающий, то просто убираем последний элемент со стека. Если к концу строки стек не оказывается пустым, то закрываем все открытые теги в текущей строке, а в начале следующей строки открываем те теги, которые остались у нас на стеке. Решение получилось немного топорным, но вполне себе рабочим.

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

С последним препятствием я столкнулся уже на слабенькой VPS от Oracle. Оказалось, что экспериментальная поддержка языка программирования Python в GraalVM вкупе с библиотекой Pygments не влазят в мои скромные 1 GB оперативной памяти на виртуальном сервере. Да и сама реализация Python показалась мне ещё несколько сыроватой и медленной, например, иногда в процессе подсветки кода кое-где теряются пробелы и переводы строк, хотя в классическом системном Python всё работает отлично. Поэтому мне пришлось поменять сервер на другой, с более мощным железом и 4 GB RAM на борту.

<< Перейти к содержанию

5. Подведём итоги

В целом, я получил положительный опыт работы с GraalVM и его инструментарием, достаточно быстро смог решить поставленную перед собой задачу сопряжения полезных батареек из экосистем разных языков программирования в рамках одной общей платформы. Из недостатков могу лишь отметить несколько сыроватую и медленную реализацию Python, наверное как раз по этой причине его поддержка до сих пор является в большей степени экспериментальной. А вот с реализацией Ruby, которая тоже является экспериментальной, и с поддержкой JavaScript, который сразу входит в стандартную поставку GraalVM, я не заметил каких-либо проблем, да и работают они вполне себе быстро.

Все свои наработки, рецепты развёртывания и сборки, весь исходный код я разместил в репозитории на GitHub:

https://github.com/EXL/CodePolyglot

Сам демонстрационный сервис можно потыкать палочкой по этой ссылке:

https://code.exlmoto.ru

Примечание: из-за наплыва посетителей после публикации статьи может случиться так, что сервер станет недоступным, всё-таки там совсем слабенький канал. Так что прошу меня заранее простить за технические неполадки. Если вам интересен этот проект, то на своих рабочих станциях с кучей оперативной памяти вы можете легко его развернуть и поиграться.

Отдельно стоит поговорить про несколько неудачный опыт создания нативных образов. В тех случаях, когда используются и смешиваются несколько языков программирования, их формирование превращается в пытку. Забегая вперёд сообщу, что для маленького прототипа, который был описан в статье выше, в его нативном образе удалось заставить работать лишь библиотеку Highlight.js вкупе с JavaScript. При попытке добавления других языков программирования автономный исполняемый файл раздувался совсем уж до неприличных размеров в 500 MB и отказывался видеть не только сторонние батарейки, но и стандартные библиотеки Python и Ruby. Копание в переменных окружения и флажках сборки исправило пару проблем, но удобоваримого результата у меня так и не получилось. К тому же, нельзя не упомянуть продолжительное время AOT-компиляции (порядка 10-15 минут), значительное потребление RAM (порядка 20 GB) и ресурсов CPU в процессе сборки.

Если с такими проблемами создания нативного образа я столкнулся на простейшем прототипе, то что уж говорить о моих тщетных попытках преобразования всего сервиса в автономный исполняемый файл. В экосистеме Spring совсем недавно появился экспериментальный проект Spring Native 0.8.5, позволяющий формировать нативные образы сервисов использующих библиотеки Spring. Оригинальные версии Java-библиотек, которые не работают должным образом под Substrate VM, вырезаются или подменяются на патченные, например, в их число входит встраиваемый веб-сервер Tomcat. При этом при сборке нужно ещё сделать некоторые ухищрения и телодвижения вроде правильной настройки Hibernate, подгрузки конфигурационного JSON-файла с описанием некоторой рефлексии и расстановки обязательных флажков для утилиты native-image, которая формирует нативные образы. В качестве системы сборки в Spring Native используется Maven, так как поддержка Gradle находится ещё на начальном этапе развития. В общем, всё выглядит слишком экспериментальным и сырым. В итоге мне удалось добиться корректной сборки нативного образа, но без поддержки каких-либо сторонних языков программирования кроме тех, что доступны на JVM-платформе. При попытках их добавления процесс зависал и ничего полезного так и не происходило в течении 30 минут, после чего я просто останавливал задачу. Далее, при запуске исполняемого файла сыпались ошибки встраиваемого языка SpEL в шаблонах Thymeleaf и я пока отступил от решения этой проблемы.

Для себя я сделал вывод, что AOT-компиляция может быть интересной в том случае, если в проекте не задействован Polyglot API, не смешано множество языков программирования и сервис используется как back-end к чему либо. Вместо Spring для AOT пока лучше использовать более легковесный Quarkus, который как раз опирается на возможность формирования нативных образов в GraalVM. А для проектов, которые используют множество языков и серверные HTML-шаблонизаторы, неплохо работает традиционный подход с JIT-компиляцией и запуском на JVM.

Версии реализаций языков программирования и технологий доступных на платформе GraalVM 20.3.0:

  • Java 8 (1.8.0_272), Java 11 (11.0.9)
  • JavaScript ES2020 (ES11)
  • Ruby 2.6.6
  • Python 3.8.5
  • R 3.6.1
  • LLVM 10.0.0

Некоторые полезные ссылки:

  1. Платформа GraalVM.
  2. Документация GraalVM.
  3. Фреймворк Spring.
  4. Библиотека Pygments.
  5. Библиотека Rouge.
  6. Библиотека Highlight.js.
  7. Публикация этой статьи на ресурсе Habr.
  8. Опыт объединения Java, JavaScript, Python и Ruby с использованием GraalVM, тема на форуме LINUX.ORG.RU.

Надеюсь, мой опыт и эта статья будут полезны тем, кто когда-нибудь заинтересуется платформой GraalVM и её возможностями по использованию множества языков программирования в одном окружении.

P.S. Искренне поздравляю посетителей моего блога с наступающим 2021 годом, желаю вам ребята исполнения всех ваших желаний и крепкого сибирского здоровья!

P.P.S. Благодарю Zorge.R за поддержку! Без него эта статья никогда бы не появилась на свет.

Update 07-Jan-2021: В некоторых комментариях сотрудников GitHub нашлась интересная информация. Та проприетарная библиотека, которая вот уже шесть лет используется ими для подсветки синтаксиса, называется PrettyLights и её исходный код до сих пор не может быть открыт из-за проблем с лицензированием бюрократического толка, хотя работы в этом направлении ведутся. Возможно библиотека написана на каком-нибудь компилируемом языке программирования, вроде «C», дабы обеспечить высокую скорость обработки больших объёмов кода от огромного количества пользователей сервиса. Кроме того, имеется интересная информация о тщательном аудите библиотеки Pygments, которую они использовали ранее. Аудит не выявил никаких проблем с безопасностью. Подробнее можно почитать по этим ссылкам: [1], [2], [3].

<< Перейти к содержанию

Dev, Manuals, Others

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *