Spring Native в Google Cloud

1. Обзор

В этой лабораторной работе мы узнаем о проекте Spring Native, создадим приложение, которое его использует, и развернем его в Google Cloud.

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

Проект Spring Native в настоящее время находится на экспериментальной стадии, поэтому для его запуска потребуется определенная настройка. Однако, как было объявлено на SpringOne 2021, Spring Native будет интегрирован в Spring Framework 6.0 и Spring Boot 3.0 с первоклассной поддержкой , так что сейчас идеальное время, чтобы поближе познакомиться с проектом за несколько месяцев до его выпуска.

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

Вы узнаете, как

  • Используйте Cloud Shell
  • Включите API Cloud Run
  • Создайте и разверните приложение Spring Native.
  • Разверните такое приложение в Cloud Run.

Что вам понадобится

Опрос

Как вы будете использовать этот урок?

Прочтите только до конца Прочитайте его и выполните упражнения.

Как бы вы оценили свой опыт работы с Java?

Новичок Средний Опытный

Как бы вы оценили свой опыт использования сервисов Google Cloud?

Новичок Средний Опытный

2. Предыстория

Проект Spring Native использует несколько технологий для обеспечения разработчикам производительности собственных приложений.

Чтобы полностью понять Spring Native, полезно понять некоторые из этих компонентных технологий, что они нам позволяют и как они здесь работают вместе.

AOT-компиляция

Когда разработчики обычно запускают javac во время компиляции, наш исходный код .java компилируется в файлы .class, которые записываются в байт-коде. Этот байт-код предназначен только для понимания виртуальной машины Java, поэтому JVM придется интерпретировать этот код на других машинах, чтобы мы могли запустить наш код.

Этот процесс дает нам характерную для Java переносимость — позволяет нам «написать один раз и запускать везде», но это дорого по сравнению с запуском собственного кода.

К счастью, большинство реализаций JVM используют JIT-компиляцию, чтобы снизить затраты на интерпретацию. Это достигается путем подсчета вызовов функции, и если она вызывается достаточно часто, чтобы преодолеть порог ( по умолчанию 10 000 ), она компилируется в машинный код во время выполнения, чтобы предотвратить дальнейшую дорогостоящую интерпретацию.

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

5042e8e62a05a27.png

Это, конечно, компромисс, и его не всегда стоит принимать. Однако компиляция AOT может оказаться полезной в определенных случаях использования, например:

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

Забавный факт: компиляция AOT была представлена ​​как экспериментальная функция в JDK 9, хотя поддерживать эту реализацию было дорого, и она так и не получила широкого распространения, поэтому в Java 17 она была незаметно удалена в пользу разработчиков, просто использующих GraalVM .

ГраальВМ

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

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

Некоторые недавние вехи:

  • Новый, удобный для пользователя вывод сборки собственного образа ( 18 января 2021 г. )
  • Поддержка Java 17 ( 18 января 2022 г. )
  • Включение многоуровневой компиляции по умолчанию для сокращения времени полиглотной компиляции ( 20 апреля 2021 г. )

Весенний родной

Проще говоря, Spring Native позволяет использовать компилятор собственных образов GraalVM для превращения приложений Spring в собственные исполняемые файлы.

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

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

Важно отметить, что генерация собственных изображений — это процесс с интенсивным использованием памяти, который занимает больше времени, чем компиляция обычного приложения, и накладывает ограничения на определенные аспекты Java.

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

3. Настройка/Предварительная работа

Прежде чем мы начнем внедрять Spring Native, нам необходимо создать и развернуть наше приложение, чтобы установить базовый уровень производительности, который мы сможем позже сравнить с нативной версией.

1. Создание проекта

Начнем с загрузки нашего приложения с start.spring.io:

curl https://start.spring.io/starter.zip -d dependencies=web \
           -d javaVersion=11 \
           -d bootVersion=2.6.4 -o io-native-starter.zip

В этом стартовом приложении используется Spring Boot 2.6.4, последняя версия, поддерживаемая проектом Spring на момент написания.

Обратите внимание, что с момента выпуска GraalVM 21.0.3 вы также можете использовать Java 17 для этого примера. В этом руководстве мы по-прежнему будем использовать Java 11, чтобы свести к минимуму требуемую конфигурацию.

Как только у нас есть zip-архив в командной строке, мы можем создать подкаталог для нашего проекта и разархивировать папку там:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. Изменения кода

Как только проект будет открыт, мы быстро добавим признаки жизни и продемонстрируем производительность Spring Native после его запуска.

Отредактируйте DemoApplication.java, чтобы он соответствовал этому:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.Instant;

@RestController
@SpringBootApplication
public class DemoApplication {
    private static Instant startTime;
    private static Instant readyTime;

    public static void main(String[] args) {
        startTime = Instant.now();
                SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/")
    public String index() {
        return "Time between start and ApplicationReadyEvent: "
                + Duration.between(startTime, readyTime).toMillis()
                + "ms";
    }

    @EventListener(ApplicationReadyEvent.class)
    public void ready() {
                readyTime = Instant.now();
    }
}

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

Чтобы построить наше изображение:

mvn spring-boot:build-image

Вы также можете использовать docker images demo , чтобы получить представление о размере базового изображения: 6ecb403e9af1475e.png

Чтобы запустить наше приложение:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3. Развертывание базового приложения

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

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

Однако, поскольку наш пример представляет собой очень простое и понятное веб-приложение, мы можем упростить задачу и положиться на Cloud Run.

Если вы выполняете инструкции на своем компьютере, обязательно установите и обновите инструмент gcloud CLI .

Если вы используете Cloud Shell, обо всем позаботятся, и вы можете просто запустить следующее в исходном каталоге:

gcloud run deploy

4. Конфигурация приложения

1. Настройка наших репозиториев Maven

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

Это потребует добавления следующих элементов в наш pom.xml, что вы можете сделать в любом редакторе.

Добавьте в наш pom следующие разделы репозиториев и плагиновRepositories:

<repositories>
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

2. Добавляем наши зависимости

Затем добавьте зависимость Spring-native, которая необходима для запуска приложения Spring в качестве собственного образа. Примечание. Этот шаг не обязателен, если вы используете Gradle.

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.11.2</version>
    </dependency>
</dependencies>

3. Добавление/включение наших плагинов

Теперь добавьте плагин AOT, чтобы улучшить совместимость собственных изображений и размер ( Подробнее ):

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-aot-maven-plugin</artifactId>
        <version>0.11.2</version>
        <executions>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

Теперь мы обновим плагин Spring-boot-maven-plugin, чтобы включить поддержку собственного образа, и воспользуемся компоновщиком paketo для создания собственного образа:

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                    <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
            </image>
        </configuration>
    </plugin>    
</plugins>

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

Если, например, вы создавали приложение, которому требовался доступ к некоторым распространенным библиотекам C, или вы еще не были уверены в требованиях вашего приложения, полный сборщик может подойти лучше.

5. Создайте и запустите собственное приложение

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

Прежде чем приступить к сборке, следует помнить о нескольких вещах:

  • Это займет больше времени, чем обычная сборка (несколько минут). d420322893640701.png
  • Этот процесс сборки может занять много памяти (несколько гигабайт). cda24e1eb11fdbea.png
  • Для этого процесса сборки требуется, чтобы демон Docker был доступен.
  • Хотя в этом примере мы выполняем этот процесс вручную, вы также можете настроить этапы сборки для автоматического запуска собственного профиля сборки .

Чтобы построить наше изображение:

mvn spring-boot:build-image

Как только оно будет готово, мы будем готовы увидеть нативное приложение в действии!

Чтобы запустить наше приложение:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

На данный момент у нас есть прекрасная возможность увидеть обе стороны уравнения нативного приложения.

Мы отказались от некоторого времени и дополнительного использования памяти во время компиляции, но взамен получили приложение, которое может запускаться гораздо быстрее и потреблять значительно меньше памяти (в зависимости от рабочей нагрузки).

Если мы запустим docker images demo , чтобы сравнить размер собственного образа с оригиналом, мы увидим резкое уменьшение:

e667f65a011c1328.png

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

6. Развертывание нашего собственного приложения

Чтобы развернуть наше приложение в Cloud Run, нам нужно поместить наш собственный образ в менеджер пакетов, например Artifact Registry .

1. Подготовка нашего репозитория Docker.

Мы можем начать этот процесс с создания репозитория:

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

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

Интерфейс командной строки gcloud может значительно упростить этот процесс:

gcloud auth configure-docker us-central1-docker.pkg.dev

2. Отправляем наш образ в реестр артефактов.

Далее мы пометим наше изображение:

export PROJECT_ID=$(gcloud config list --format 'value(core.project)')


docker tag  demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

И затем мы можем использовать docker push , чтобы отправить его в реестр артефактов:

docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

3. Развертывание в Cloud Run

Теперь мы готовы развернуть образ, хранящийся в реестре артефактов, в Cloud Run:

gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

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

Не стесняйтесь сами сравнивать время запуска нашего базового приложения с этим новым собственным приложением!

6dde63d35959b1bb.png

7. Подведение итогов/очистка

Поздравляем с созданием и развертыванием приложения Spring Native в Google Cloud!

Надеемся, что это руководство поможет вам лучше познакомиться с проектом Spring Native и помнить о нем, если он будет соответствовать вашим потребностям в будущем.

Необязательно: очистите и/или отключите службу.

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

Вы можете удалить или отключить созданные нами службы Cloud Run, удалить размещенный нами образ или закрыть весь проект.

8. Дополнительные ресурсы

Хотя проект Spring Native в настоящее время является новым и экспериментальным проектом, уже существует множество хороших ресурсов, которые помогут ранним пользователям устранять проблемы и принимать участие:

Дополнительные ресурсы

Ниже приведены онлайн-ресурсы, которые могут иметь отношение к этому руководству:

Лицензия

Эта работа распространяется под лицензией Creative Commons Attribution 2.0 Generic License.