Статьи

Нативные микросервисы с SparkJava и Graal

Микросервисы, написанные с помощью SparkJava, представляют собой простой Java-код, использующий стандартную библиотеку Java. Никакой магии аннотаций, просто код. Преимущество этого простого стиля программирования в том, что он прост. Это так просто, что нативный компилятор Graal просто компилирует его, не мигая , что в настоящее время очень сложно для более сложных сред, таких как Spring, например.

Комбинация SparkJava / Graal интересна сама по себе, и опыт людей с ней начинает появляться . Кроме того, в качестве библиотеки Java должна быть возможность использовать ее из других языков, основанных на JVM, и мне было интересно, как Graal справится с этим. На самом деле все оказалось просто, и в этом посте мы увидим, как легко создавать нативные микросервисные двоичные файлы для Java, Kotlin и даже Clojure .

Начиная

Если вы не сталкивались с Graal до того, как я предлагаю вам зайти на их сайт и посмотреть, что он предлагает. Здесь мы используем встроенную функцию компиляции, но на самом деле это просто царапина на поверхности.

Чтобы использовать Graal, вам необходимо установить последнюю версию Graal SDK. На момент написания статьи это 1.0.0-rc9 . Я сделал это с помощью SdkMan :

1
sdk install java 1.0.0-rc9-graal

И с тех пор

1
sdk use java 1.0.0-rc9-graal

Затем создайте базовый проект Gradle и добавьте минимальные зависимости:

1
2
3
4
dependencies {
    compile "com.sparkjava:spark-core:2.7.2"
    compile "org.slf4j:slf4j-simple:1.7.13"
}

(Я предполагаю, что вы уже знакомы с Gradle, если вы предпочитаете, чтобы вы могли делать это с Maven . Обратите внимание, что важно, чтобы выбранная вами реализация Slf4j соответствовала версии, требуемой SparkJava.)

В SparkJava конечная точка микросервиса — это, по сути, привязка или route между путем и обратным вызовом в форме лямбда-выражения. Это стандартный пример «Привет, мир», который мы будем использовать в качестве основы. Реальные сервисы, конечно, будут использовать объекты запроса и ответа. Смотрите документацию для более подробной информации.

1
2
3
4
5
6
7
import static spark.Spark.*;
 
public class HelloWorld {
    public static void main(String[] args) {
        get("/sayHello", (req, res) -> "Hello world!");
    }
}

Чтобы запустить его как программу командной строки, удобно скопировать все зависимости вместе в один каталог. Мы также можем сделать это с Gradle.

1
2
3
4
5
6
7
task copyDependencies(type: Copy) {
    from configurations.default
    into 'build/libs'
    shouldRunAfter jar
}
 
assemble.dependsOn copyDependencies

Создайте сервис и запустите его, чтобы убедиться, что он работает.

1
> ./gradlew clean assemble
1
2
3
> java -cp "build/libs/*" HelloWorld
...
[Thread-0] INFO org.eclipse.jetty.server.Server - Started @363ms
1
2
> curl localhost:4567/sayHello
Hello World!

Давайте скомпилируем его в собственный двоичный файл, используя Graal. Команда, к счастью, очень похожа на команду java :

1
2
3
4
5
6
7
8
9
> native-image -cp "build/libs/*" HelloWorld
...
Build on Server(pid: 31197, port: 52737)*
[helloworld:31197]    classlist:   2,142.65 ms
[helloworld:31197]        (cap):   2,154.21 ms
...
...
[helloworld:31197]        write:     443.13 ms
[helloworld:31197]      [total]:  56,525.52 ms

Теперь мы должны иметь наш собственный двоичный файл в текущем каталоге. Давайте запустим это:

1
2
3
> ./helloworld
...
[Thread-2] INFO org.eclipse.jetty.server.Server - Started @2ms
1
2
> curl localhost:4567/sayHello
Hello World!

Исполняемый файл имеет размер 14 Мб, но посмотрите на это время запуска, 2 мс , в основном мгновенно! По памяти было бы неразумно уделять слишком много внимания top но очевидно, что удаление JVM из среды выполнения имеет свои преимущества. Это особенно важно в системах микросервисов, где развернуто большое количество независимых процессов.

Как насчет Котлина?

Kotlin — это язык JVM, который набирает обороты и не без причины. Сочетание функционального стиля и функций OO, бесшовная совместимость с Java и краткий синтаксис делают его хорошим языком для общего использования и очевидной заменой Java. Чтобы построить наш сервис с Kotlin, сначала мы добавим библиотеку Kotlin в Gradle (на момент написания версии v1.3.10).

1
2
3
4
dependencies {
...
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.10"
}

И использовать плагин компилятора Kotlin.

1
2
3
plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.3.10'
}

С Kotlin наш нелепо простой микросервис становится еще проще.

1
2
3
4
5
import spark.Spark.*
 
fun main(args: Array<String>) {
    get("/sayHello") { req, res -> "Hello World!" }
}

Создайте сервис и запустите его, чтобы убедиться, что он работает.

1
> ./gradlew clean assemble
1
2
3
> java -cp "build/libs/*" HelloWorldKt
...
[Thread-0] INFO org.eclipse.jetty.server.Server - Started @363ms
1
2
> curl localhost:4567/sayHello
Hello World!

Давайте скомпилируем это изначально. Поскольку это Java, команда почти идентична версии Java (компилятор Kotlin автоматически добавляет суффикс Kt к сгенерированным классам).

1
2
3
4
5
6
7
> native-image -cp "build/libs/*" HelloWorldKt
Build on Server(pid: 53242, port: 51191)
[helloworldkt:53242]    classlist:     783.03 ms
[helloworldkt:53242]        (cap):   2,139.45 ms
...
[helloworldkt:53242]        write:     591.88 ms
[helloworldkt:53242]      [total]:  53,074.15 ms

И запустите это:

1
2
3
> ./helloworldkt
...
[Thread-2] INFO org.eclipse.jetty.server.Server - Started @2ms
1
2
> curl localhost:4567/sayHello
Hello World!

Исполняемый файл почти идентичен по размеру и скорости запуска версии Java, как и следовало ожидать, поскольку по сути это тот же код.

Это базовый пример, но сочетание Kotlin для простоты реализации , SparkJava для простоты микросервиса и Graal для простоты развертывания является очень привлекательным предложением для разработки микросервиса.

Тем не менее, кроме более приятного синтаксиса, Kotlin очень похож на Java. Есть и другие языки JVM, которые мы можем использовать, что может продвинуть Graal дальше.

Потребность в Clojure

Использование Clojure для создания микросервисов — интересная идея. Сервисы по своей природе функциональны, фактически сервис это функция, и динамическая природа языка может сделать его идеальным в некоторых ситуациях, ориентированных на данные.

Вместо того, чтобы использовать Gradle, мы начнем с нового проекта Leiningen :

1
lein new hello-clojure

Зависимости находятся в основном файле project.clj а также в названии основного класса, который мы запустим для запуска сервера.

1
2
3
4
:dependencies [[org.clojure/clojure "1.9.0"]
                 [com.sparkjava/spark-core "2.7.2"]
                 [org.slf4j/slf4j-simple "1.7.13"]]
  :main hello_clojure.core)

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

01
02
03
04
05
06
07
08
09
10
11
(ns hello_clojure.core
  (:gen-class)
  (:import (spark Spark Response Request Route)))
 
(defn route [handler]
  (reify Route
    (handle [_ ^Request request ^Response response]
      (handler request response))))
 
(defn get [endpoint routefn]
  (Spark/get endpoint (route routefn)))

(Позже я нашел хорошую статью с полным сервисом, использующим Clojure и SparkJava. Их адаптеры были немного лучше, чем у меня, поэтому я включил некоторые идеи из этой статьи в последующее.)

Затем мы готовы создать контроллер, который мы делаем из метода main, чтобы его было легко вызвать из командной строки. Также обратите внимание, что выше мы использовали директиву gen-class чтобы гарантировать, что основной класс указан в Manifest:

1
2
(defn -main []
  (get "/sayHello" (fn [req resp] "Hello World!!")))

Чтобы упростить создание сервиса, мы можем создать автономную банку с использованием Leiningen.

1
> lein clean && lein uberjar

Как и прежде, мы сначала проверяем, что сервис работает как обычный Java:

1
2
3
$ java -cp target/hello-clojure-0.1.0-SNAPSHOT-standalone.jar hello_clojure.core
...
[Thread-0] INFO org.eclipse.jetty.server.Server - Started @1033ms
1
2
> curl localhost:4567/sayHello
Hello World!

Компиляция в нативный образ так же проста, как и в предыдущих примерах с Java и Kotlin.

1
2
3
4
5
6
7
> native-image -cp target/hello-clojure-0.1.0-SNAPSHOT-standalone.jar hello_clojure.core
Build on Server(pid: 35646, port: 53994)*
[hello_clojure.core:35646]    classlist:   2,704.82 ms
[hello_clojure.core:35646]        (cap):     909.58 ms
...
[hello_clojure.core:35646]        write:     647.23 ms
[hello_clojure.core:35646]      [total]:  54,900.61 ms

И запустите это:

1
2
3
> ./helloworld_clojure
...
[Thread-2] INFO org.eclipse.jetty.server.Server - Started @2ms
1
2
> curl localhost:4567/sayHello
Hello World!

Еще раз исходный двоичный файл составляет примерно 15 МБ, и снова время запуска почти мгновенно.

Вывод

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

Другое ограничение заключается в том, что многие библиотеки просто не работают с Graal. Это не совсем отрицательно, потому что это побудит нас вернуться к простым методам кодирования, однако у вас может быть зависимость, которую вы не можете изменить, и это может вызвать серьезные хлопоты. Я думаю, что основным недостатком изначально будет отображение, основанное на отражении, будь то сериализация или разновидности ORM. Уже приложено немало усилий, чтобы сделать многие библиотеки и фреймворки совместимыми с Graal, но это еще рано.

Третье, прежде всего практическое, соображение — чрезвычайно медленная компиляция нативного образа. Даже этот очень простой пример занимает почти минуту, чтобы построить. Конечно, вы можете выполнять разработку, компилируя только байт-код, но тогда могут возникнуть проблемы с совместимостью. Непрерывный конвейер сборки и всесторонние тесты были бы способом уменьшить этот риск.

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

Опубликовано на Java Code Geeks с разрешения Сандро Манкузо, партнера нашей программы JCG . Смотрите оригинальную статью здесь: нативные микросервисы с SparkJava и Graal

Мнения, высказанные участниками Java Code Geeks, являются их собственными.