[Перевод] Смешиваем OpenJDK и NodeJS: межъязыковые взаимодействия и вертикальная архитектура

Привет, Хабр!

Давно вынашивали мысль обсудить с вами тему GraalVM, откладывали, пока не нашли, наконец, сегодняшнюю статью, тематика которой серьезно выходит за рамки разбора конкретной виртуальной машины. Автор Майк Хёрн (Mike Hearn) ни много ни мало излагает целую парадигму межъязыковых взаимодействий и многоязычного программирования (polyglot programming). Далее — знаменитый пример вертикального масштабирования и весьма длинная статья под катом.

w8rnplm8qono1q-owxp4_dcmlxk.jpeg


Эта статья — о новаторском способе написания ПО, который, возможно, станет популярен в будущем, но, вероятно, не сейчас. В статье есть код, честное слово!

В глубокой древности, то есть, в 2015, я написал, почему моим следующим языком программирования будет Kotlin, а в 2016 написал о Graal и Truffle: двух радикальных исследовательских проектах, связанных с компиляторами, которые не только значительно ускоряют работу таких языков, как Ruby, но и воплощают в реальности бесшовные межъязыковые взаимодействия. В этих проектах динамический (JIT) компилятор или OpenJDK заменяется на новый, имеющий превращать аннотированные интерпретаторы в ультрасовременные JIT-компиляторы… автоматически.

Возвращаясь к этим темам в 2019, хотел бы показать вам три вещи:

  1. Как использовать маленькую библиотеку, которую я написал, чтобы почти бесшовно использовать NPM-модули из кода программ, написанных на Java или Kotlin.
  2. Объяснить все веские причины, по которым вам это может понадобиться, даже если вы полагаете, что JavaScript/Java — худшая на свете штука, не считая рыбьего жира.
  3. Кратко исследовать концепцию вертикальной архитектуры, которая конкурирует с микросервисно-ориентированным проектированием. Она находится на пересечении новейших версий GraalVM и OpenJDK, причем, требует самого современного аппаратного обеспечения.

Использование NPM-модулей из Java и Kotlin

Сделаем всего три простых шага:

  1. Берем GraalVM. Это набор патчей, надстраиваемый поверх OpenJDK, который появился как нельзя кстати: он может выполнять весь имеющийся у вас байт-код JVM.
  2. Берем с github мой инструментик NodeJVM и добавляем его в наш путь.
  3. Заменяем java в командной строке на nodejvm. Вот и все!

Ладно, ладно. Признаю, здесь я немного рисуюсь и утрирую, до конца статьи вам придется потерпеть такой стиль. Разумеется, все совсем не так просто: надо ведь еще взять модуль и использовать его.

Рассмотрим, как это выглядит:

f3ovxvzch5mpwwmhy8w7tqyyiaw.png

Пример кода, использующего NodeJVM

Внимательно рассмотрите эту картинку. Да, именно так все и выглядит: Kotlin со встроенной многострочной строкой, в которой происходит автозавершение JavaScript, после чего выполняется статический анализ JavaScript, и синтаксис правильно подсвечивается. Такие же операции срабатывают из Java или других языков для JVM, понятных IntelliJ. Чтобы получить такие возможности, нужно перещелкнуть переключатель в настройках IDE (почитайте в readme для NodeJVM, как это делается), но впоследствии эта функция будет работать автоматически. Если IntelliJ удастся выяснить путем анализа потока данных, что ваша строка в конце концов должна быть передана методам run или eval, то она будет обрабатываться как встроенный JS.
Здесь я собираюсь поговорить об API для Kotlin, поскольку он немного симпатичнее и удобнее, чем API на обычном Java, но все, что я опишу ниже, можно сделать и из Java.

В вышеприведенном коде обратите внимание на несколько следующих фич:

  • Для обращения к JavaScript приходится использовать блок nodejs {}. Дело в том, что JavaScript однопоточный и, соответственно, для запуска модулей NPM необходимо «войти в поток Node». Блок nodejs {} выполняет такую синхронизацию за нас, независимо от того, в каком потоке мы находимся. Итак, нужно постоянно помнить: для запуска какого-либо JS-кода в принципе, нужно находиться внутри такого блока. Повторно входить в него можно столько угодно раз, поэтому будет безопасно использовать такой блок в любом месте, где он нам понадобится. Любые обратные вызовы от JavaScript будут выполняться в потоке Node, и, таким образом, всем другим потокам будет закрыт доступ в блок nodejs, поэтому, если вас заботит производительность или гладкое отображение GUI, то избегайте выполнения долгоиграющих операций в обратных вызовах.
  • Синтаксис var x by bind(SomeObject()) доступен только в блоке nodejs и позволяет подключиться к одноименной переменной в глобальной области видимости JavaScript. При изменении x из Kotlin она изменится и в JS, и наоборот. Здесь я привязываю обычный объект Java File к миру JS.
  • Метод eval возвращает … то, что мы попросим его вернуть, однако, в манере статической типизации. Это обобщенная функция и, попросту указав тип той сущности, которую мы ей присваиваем, мы обеспечим, чтобы eval автоматически привел объект JavaScript к статически типизированному классу или интерфейсу Java/Kotlin/Scala/т.д. Хотя, выше это явно указано не было, MemoryUsage — простой интерфейсный тип, который я определил, и у него есть функции rss()и heapTotal(). Они отображаются на одноименные свойства JavaScript, применяя их к тому, что вы получите от API Node process.memoryUsage(). Большинство типов JS можно таким образом привести к «нормальным» типам Java; подробная документация о том, как что работает, доступна на сайте GraalVM. Результирующие объекты можно хранить где угодно, но вызов методов в них, естественно, необходимо делать в блоке nodejs.
  • Объекты JavaScript также можно считать простыми отображениями строки на объект, что во многом действительно соответствует их природе. В свою очередь, такие отображения строки на объект можно приводить обратно к некоторой сильнее типизированной вещи, что хорошо видно в обратном вызове. Пользуйтесь тем представлением, которое вам более по душе.
  • Можно использовать require — и он станет в обычном порядке искать модули в каталогах node_modules.


В приведенном выше фрагменте кода используется протокол DAT, позволяющий подключиться к пиринговой сети, отдаленно напоминающей BitTorrent, а потом выискивать пиры, на которых имеется нужный файл. Я использую в качестве примера именно DAT, так как он (a) децентрализованный и поэтому однозначно шикарный и (b) к добру или к худу, его справочная реализация написана на JavaScript. Это не та программа, которую я мог бы написать совершенно без применения JS за сколь-нибудь разумное время.

Это также можно сделать из Java:

import net.plan99.nodejs.NodeJS;

public class Demo {
    public static void main(String[] args) {
        int result = NodeJS.runJS(() ->
            NodeJS.eval("return 2 + 3 + 4").asInt()
        );
        System.out.println(result);
    }
}

Java API не предоставляет вам такого приятного связывания переменных и автоприведения, как Kotlin API, но вполне удобен в обращении. Здесь показано, как мы приводим результат к целочисленному типу Java (int) и возвращаем его «из» потока Node: в данном случае главный поток Java — не то же самое, что поток NodeJS, но мы переключаемся между этими потоками совершенно бесшовно.

NodeJVM — это очень, очень маленькая обертка поверх GraalVM. Она добавляет ничтожный объем кода, поэтому не беспокойтесь о том, что он может перестать поддерживаться или исчезнет: 99.99% всей тяжелой работы в данном случае выполняется командой GraalVM.

Вот несколько очевидных идей по поводу напрашивающихся улучшений:

  • Разрешить модулям JS импортировать модули Java по координате Maven.
  • Сформулировать какие-нибудь «наилучшие практики» по джавизации модулей NPM. Например, может ли JAR-файл содержать каталог node_modules (коротко: нет, поскольку NodeJS все равно по-своему организует файловый ввод/вывод и ничего не знает о зипах, длинно: да, если как следует постараться).
  • Больше языков: Python и Ruby не нуждаются в таком «клее» для синхронизации потоков, какой нужен в NodeJS, поэтому вы можете просто воспользоваться обычным GraalVM Polyglot API. Но пользователям Kotlin покажется, что методы приведения/расширения и API для связывания переменных хорошо было бы иметь в любом языке.
  • Поддержка Windows.
  • Плагин Gradle, чтобы в программах могли присутствовать списки зависимостей на смешанном наборе языков
  • Интеграция с инструментом native-image, т.н. SubstrateVM; так что, если вам не требуется полновесная производительность HotSpot во время исполнения, то можете поставлять небольшие статически связанные двоичные файлы, в стиле Golang.
  • Может быть, какой-нибудь конвертер для преобразования TypeScript в Java, чтобы можно было использовать DefinitelyTyped и быстрее окунуться в статический мир.


Патчи приветствуются.

Зачем вам это может понадобиться?

Возможно, вы уже думаете: «вау, JavaScript, мы, разрабы, теперь можем пребывать во взаимной любви, обоюдном уважении и гармонии!»

pghg_dqd6pgbortse6gaqdks-p8.gif

Идеализированная восторженная реакция

Весьма возможно, что вам окажется ближе такая точка зрения:

j9thg6dscg5ce13x_vtpajwvjmm.jpeg

JavaScript и Java — это не просто языки. Это культуры, а разработчикам ничто так не мило, как КУЛЬТУРНЫЕ ВОЙНЫ!

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

  • Если вы прежде всего Java-разработчик, то теперь вам открывается доступ к уникальным модулям JavaScript, у которых может не быть эквивалента в JVM (например, протокола DAT). Можете обожать это или ненавидеть, но факт остается фактом: многие пишут опенсорсные NPM-модули, и некоторые из этих модулей весьма хороши. Также можно переиспользовать код, работающий на ваших веб-фронтендах, не нуждаясь при этом в языковых транспиляторах. А если вы работаете с унаследованной базой кода на NodeJS, которую хотелось бы постепенно портировать на Java, то внезапно эта работа весьма упрощается.
  • Если вы прежде всего JavaScript-разработчик, то теперь вам открывается легкий доступ к уникальным библиотекам JVM, у которых в JavaScript может либо не быть прямого эквивалента (напр., Lucene, Chronicle Map) либо могут предлагаться лишь плохо документированные, незрелые или менее производительные аналоги. Если вы желаете обойтись в вашем следующем проекте без HTML, то можете исследовать GUI-фреймворк для белого человека. Также вы получаете доступ ко множеству других языков, например, Ruby и R. Объекты JVM можно разделять между работниками NodeJS, пользуясь преимуществами многопоточности в разделяемой памяти, если, по данным вашего профилировщика, этой возможностью можно воспользоваться. А если вы работаете с унаследованной базой кода на Java, которую хотелось бы постепенно портировать на NodeJS, то внезапно эта работа весьма упрощается.
  • Если вы учите все языки сразу, то можете заняться многоязычным программированием. Программисты-полиглоты — не хейтеры, наоборот, они умеют подружиться с самым лучшим имеющимся кодом, из какой бы культуры он ни происходил. Они как ренессансные студенты, изучавшие сразу английский, французский латынь… все эти языки для них одно. Они смешивают библиотеки Java, Kotlin, JavaScript, Scala, Python, Ruby, Lisp, R, Rust, Smalltalk, C/C++ и даже FORTRAN, чисто сшивая из них аккуратное целое поверх GraalVM.
  • Наконец, если вы счастливый пользователь NodeJS, и прочие языки вас вообще не волнуют, то, возможно, вам все равно захочется поэкспериментировать с GraalVM.

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

tn4y68r7cmq6z9tpheeuwafpncy.png

Просмотр кучи, в которой содержатся объекты Ruby

kovcjjmwvhnayjpfix25cs-rx48.png

Замер показателей CPU, доступный через HTTP

sbfqqy9mzuppcp-vnro8tdscc_o.png

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

Вертикальная архитектура

Мы подходим к последней теме, которую хотелось обсудить в этой статье.

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

Во-первых, да, иногда требуется гонять на множестве серверов много сервисов, которые должны взаимодействовать. Я более 7 лет проработал в Google, почти ежедневно имел дело с их оркестровщиком контейнеров Borg. Я писал «микросервисы», хотя, мы их так и не называли, и потреблял их. А иначе было никак не справиться, ведь наши рабочие нагрузки требовали участия тысяч машин!

Однако, за такие архитектуры приходится платить высокую цену:

  1. Сериализация. Она сразу оборачивается снижением производительности, но, что еще важнее, требует от вас постоянно выравнивать ваши хотя бы отчасти типизированные и оптимизированные структуры данных, превращая их в простые деревья. Прибегая к JSON, вы теряете возможность делать простейшие вещи, например, иметь множество мелких объектов, указывающих на несколько крупных объектов (чтобы избежать повторения, приходится использовать собственные индексы).
  2. Версионирование. Это сложно. В университетах зачастую не преподают этой тяжелой, но повседневной дисциплины из области программной инженерии и, даже если вам кажется, что вы досконально усвоили разницу между прямой и обратной совместимостью, даже если вы уверены, что понимаете, что такое многоэтапное выкатывание — можете ли вы гарантировать, что все это будет понимать человек, который придет вам на смену? Правильно ли вы выполняете интеграционное тестирование для различных комбинаций версий, которые могут складываться при неатомарном выкатывании? Я видел в распределенных архитектурах несколько подлинных катастроф, сводившихся к тому, что сбились версии.
  3. Согласованность. Проводить атомарные операции в пределах одного сервера довольно легко. Гораздо сложнее оказывается обеспечить, чтобы пользователи в любых ситуациях видели абсолютно согласованную картину, когда в работе системы участвует множество машин, в особенности, если между ними происходит шардирование данных. Вот почему исторически сложилось, что движки реляционных баз данных не могут похвастаться хорошей масштабируемостью. Подскажу: лучшие инженеры Google потратили десятилетия, стараясь упростить распределенное программирование для своих команд, стараясь выстроить его так, чтобы оно сильнее походило на традиционное.
  4. Повторная реализация. Поскольку вызовы удаленных процедур обходятся дорого, поэтому много таких вызовов не сделаешь, и для решения некоторых задач не остается ничего иного, кроме как повторно реализовать код. В Google сделаны кое-какие библиотеки для работы с несколькими языками сразу, предназначенных для выполнения вызовов удаленных процедур; есть и такие ситуации, в которых такой код приходится переписывать с нуля.

Итак, какова же альтернатива?

Проще говоря, очень много железа. Этот способ может показаться абсурдно дедовским, но учтите, что стоимость аппаратного обеспечения постоянно снижается, многие рабочие нагрузки не назвать «веб-глобальными», а ваша интуиция по поводу того, на что стоит потратиться, может вас подводить.

Вот относительно свежий прайс от одного канадского производителя:

_2koqq_9uanqjed7ebsfcwjzyqy.png

Сорокоядерная машина с терабайтом оперативной памяти и почти терабайтом на жестком диске сегодня стоит около $6k. Вообразите, сколько времени за весь срок существования проекта придется потратить вашей команде на улаживание проблем с распределенными системами, и сколько это будет вам стоить.

Да, но не все ли сегодняшние компании можно считать веб-глобальными?

Если коротко — нет.

В мире полно компаний, для которых справедливо следующее:

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

Хороший пример подобной компании — банк. Банки не испытывают «гиперроста», не становятся «вирусными». Их модель роста — умеренная и предсказуемая, если вообще предположить, что у них есть какой-то рост (банки являются региональными и обычно работают на насыщенных рынках). Клиентская база крупнейшего банка США составляет порядка 50 миллионов пользователей и, конечно, не удваивается каждые полгода. В данном случае ситуация совсем не такая, как с Инстаграмом. Поэтому, стоит ли удивляться, что в основе системы типичного банка по-прежнему располагается мейнфрейм? Разумеется, то же верно для логистических фирм, производственных фирм и т.д. Это хлеб и масло нашей экономики.

В подобных бизнесах вполне возможно, что потребности каждого конкретного приложения, относящегося к ним, можно будет всегда удовлетворить при помощи ресурсов всего одной крупной машины. Да что там, даже некоторые общедоступные сайты сегодня умещаются на единственной машине. В 2015 году Мацей Цегловский прочитал очень интересную лекцию «Кризис с ожирением сайтов» и отметил, что его собственный сайт с сервисом закладок был прибыльным, а вот его конкурент разместил такой же сайт на AWS — и проиграл, всего лишь из-за отличающихся затрат на оборудование и разных допущений о сложности. В исследовании о сравнении вертикального и горизонтального масштабирования было выяснено, что PlentyOfFish работает примерно на ~одном мегасервере (статья датируется 2009 годом, поэтому можете игнорировать приведенные там цены на оборудование). Автор делает некоторые расчеты и показывает, что один сервер — не такой глупый выход, как может показаться. Наконец, если вы подумываете о Hadoop и Big Data, почитайте вот эту исследовательскую статью Microsoft от 2013 года, демонстрирующую, что многие рабочие нагрузки Hadoop от Microsoft, Yahoo и Facebook фактически выполняются гораздо быстрее и эффективнее на одной большой машине, а не на кластере. И так было 6 лет назад! Вероятно, с тех пор акцент в пользу вертикального масштабирования стал еще более выраженным.

Однако, реальная экономия связана совсем не с оборудованием, а с оптимизацией чрезвычайно дорогого рабочего времени инженеров, которое тратится на создание кучи крошечных микросервисов, которые должны горизонтально масштабироваться при эластичном управлении спросом. Подобный подход к инженерии является рискованным и времязатратным, даже если вы пользуетесь самыми новенькими игрушками, доступными в облаке. Можно потерять SQL, SOLID-транзакции, унифицированное профилирование, а также вы определенно потеряете такую информацию, как кросс-системная трассировка стека. Типобезопасность будет пропадать всякий раз при выходе за границы сервера. Вы получите вызовы функций, по которым может наступить таймаут, избыточные издержки, связанные с динамической компиляции, неожиданные отказы обратного давления, сложные оркестрационные движки с причудливыми форматами конфигурации и… ох, в самом деле воспоминания нахлынули. Было интересно со всем этим работать, когда у меня в распоряжении была проприетарная архитектура Google и толстый инженерный бюджет, но сегодня я бы рискнул повторить подобное, только если бы у меня не было никакого другого выхода.

По опыту, невозможно работать с очень крупными серверами, на которых действует сборка мусора — все дело в том, что сборка мусора как таковая была плохо проработанной технологией, и поэтому данная тема долгое время оставалась чисто академической. Вам в любом случае приходилось гонять сразу несколько серверов. Однако, с пришествием ZGC и Shenandoah, использование терабайтных куч, работающих на 80 гиперядрах в рамках единственного процесса становится совершенно обычным делом — ваши клиенты не заметят никаких подвисаний. Купите пару больших коробок, в одной запускайте вашу бизнес-логику, в другой обустройте сервер базы данных — и посмотрите, как далеко вам удастся зайти.

Итак, в самом ли деле вам нужны все ваши микросервисы? Либо лучше провести тщательный анализ полезности и издержек — и он подскажет, какие элементы системы потенциально можно было бы упростить? Вертикальная архитектура — это своеобразное возрождение олдскула: экономим деньги, переиспользуем код и ускоряем работу команды, комбинируя новейшие межъязыковые технологии компиляции с новейшим оборудованием…, но в традиционном ключе.

Заключение

NodeJVM — скромное демо, позволяющее оценить возможности GraalVM. Он позволяет требовать модули NPM из кода на Java/Kotlin, приводить объекты JS к интерфейсам, связывать локальные переменные Kotlin с переменными JavaScript, использовать обратные вызовы и гонять JS на скоростях, сопоставимых с V8.

Можете свободно смешивать выбранные модули, портировать базы кода JS на Java, портировать базы кода Java на JS или просто затеять языковые холивары с коллегами по работе.

Языковая интероперабельность часто достигается при помощи микросервисных архитектур, но ведь можно достичь сборки мусора с паузами не более 4 миллисекунд, независимо от размера кучи — на поразительно дешевом аппаратном обеспечении — если признать, что вертикальное масштабирование послужит хорошим паттерном проектирования для многих распространенных рабочих нагрузок. Достигаемая при этом простота и выгоды с инструментальным оснащением могут оказаться весьма существенными.

© Habrahabr.ru