Google Cloud 上の Spring Native

1. 概要

この Codelab では、Spring Native プロジェクトについて学習し、それを使用するアプリを構築して Google Cloud にデプロイします。

このセクションでは、そのコンポーネント、プロジェクトの最近の履歴、ユースケース、そしてもちろん、プロジェクトで使用するために必要な手順について説明します。

Spring Native プロジェクトは現在試験運用段階にあるため、開始するには特定の構成が必要です。ただし、SpringOne 2021 で発表されたように、Spring Native は ファーストクラスのサポートとともに Spring Framework 6.0 と Spring Boot 3.0 に統合される予定です。リリース数か月前の今こそ、このプロジェクトを詳しく見てみる絶好の機会です。

ジャストインタイム コンパイルは、長時間実行されるプロセスなどのために非常に最適化されていますが、事前コンパイルされたアプリケーションがさらに優れたパフォーマンスを発揮するユースケースもあります。この点については、この Codelab で説明します。

GCP コンソールの

  • Cloud Shell を使用する
  • Cloud Run API を有効にする
  • Spring ネイティブ アプリを作成してデプロイする
  • このようなアプリを Cloud Run にデプロイする

必要なもの

アンケート

このチュートリアルをどのように使用されますか?

通読するのみ 通読し、演習を行う

Java のご利用経験はどの程度ありますか?

初心者 中級者 上級者

Google Cloud サービスの使用経験はどの程度ありますか?

初心者 中級者 習熟者

2. 背景情報

Spring Native プロジェクトは、いくつかのテクノロジーを利用して、ネイティブ アプリケーションのパフォーマンスをデベロッパーに提供します。

Spring Native を完全に理解するには、これらのコンポーネント技術のいくつか、それらが実現する機能、それらがどのように連携して動作するかを理解しておくと役立ちます。

AOT コンパイル

デベロッパーがコンパイル時に javac を通常どおり実行すると、.java ソースコードがバイトコードで記述された .class ファイルにコンパイルされます。このバイトコードは Java 仮想マシンでのみ理解されることを想定しているため、コードを実行するには、JVM が他のマシンでこのコードを解釈する必要があります。

このプロセスにより、Java の特徴である移植性が実現し、「一度記述すればどこでも実行できる」ようになりますが、ネイティブ コードの実行と比較するとコストが高くなります。

幸いなことに、JVM のほとんどの実装では、ジャストインタイム コンパイルを使用してこの解釈コストを軽減しています。これは、関数の呼び出し回数をカウントし、しきい値(デフォルトでは 10,000 回)を超える頻度で呼び出された場合に、実行時にネイティブ コードにコンパイルして、それ以降のコストのかかる解釈を防ぐことで実現されます。

事前コンパイルは、コンパイル時に到達可能なすべてのコードをネイティブ実行可能ファイルにコンパイルするという逆のアプローチを取ります。これにより、移植性と引き換えに、実行時のメモリ効率やその他のパフォーマンスが向上します。

5042e8e62a05a27.png

もちろん、これはトレードオフであり、常に取る価値があるとは限りません。ただし、AOT コンパイルは次のような特定のユースケースで効果を発揮します。

  • 起動時間が重要な短命なアプリケーション
  • JIT のコストが高すぎる可能性があるメモリ制約の厳しい環境

豆知識として、AOT コンパイルは JDK 9 で試験運用版の機能として導入されましたが、この実装は維持にコストがかかり、普及しなかったため、Java 17 でひっそりと削除され、デベロッパーが GraalVM を使用するようになりました。

GraalVM

GraalVM は、起動時間の短縮、AOT ネイティブ イメージ コンパイル、複数の言語を 1 つのアプリケーションに混在させることができるポリグロット機能を備えた、高度に最適化されたオープンソースの JDK ディストリビューションです。

GraalVM は活発に開発されており、常に新しい機能が追加され、既存の機能が改善されています。デベロッパーの皆様は、ぜひご期待ください。

最近の主なマイルストーンは次のとおりです。

Spring Native

簡単に言うと、Spring Native を使用すると、GraalVM の native-image コンパイラを使用して Spring アプリケーションをネイティブ実行可能ファイルに変換できます。

このプロセスでは、コンパイル時にアプリケーションの静的解析を実行して、エントリ ポイントから到達可能なアプリケーション内のすべてのメソッドを検出します。

これにより、アプリケーションの「クローズド ワールド」の概念が作成されます。この概念では、すべてのコードがコンパイル時に既知であると想定され、実行時に新しいコードを読み込むことはできません。

ネイティブ イメージの生成は、通常のアプリケーションのコンパイルよりも時間がかかるメモリ集約型のプロセスであり、Java の特定の側面に制限が課されることに注意してください。

場合によっては、Spring Native でアプリケーションを動作させるためにコードを変更する必要はありません。ただし、状況によっては、正しく動作させるために特定のネイティブ構成が必要になることがあります。このような場合、Spring Native はこのプロセスを簡素化するために Native Hints を提供することがよくあります。

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-native プロジェクトがサポートしている最新バージョンです。

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 に次の要素を追加する必要があります。これは、任意のエディタで行うことができます。

次の repositories セクションと pluginRepositories セクションを pom に追加します。

<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 アプリケーションをネイティブ イメージとして実行するために必要な spring-native 依存関係を追加します。注: 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>

tiny builder イメージは、いくつかのオプションの 1 つにすぎません。追加のライブラリとユーティリティが非常に少ないため、攻撃対象領域を最小限に抑えることができ、このユースケースに適しています。

たとえば、一般的な C ライブラリへのアクセスを必要とするアプリをビルドする場合や、アプリの要件がまだ明確でない場合は、full-builder の方が適している可能性があります。

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 CLI を使用すると、このプロセスを大幅に簡素化できます。

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

2. Artifact Registry にイメージを push する

次に、イメージにタグを付けます。

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 を使用して Artifact Registry に送信します。

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

3. Cloud Run へのデプロイ

これで、Artifact Registry に保存したイメージを Cloud Run にデプロイする準備が整いました。

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

アプリをネイティブ イメージとしてビルドしてデプロイしたため、実行時にインフラストラクチャの費用を有効活用できます。

ベースライン アプリの起動時間とこの新しいネイティブ アプリの起動時間を比較してみてください。

6dde63d35959b1bb.png

7. 概要/クリーンアップ

Google Cloud で Spring ネイティブ アプリを構築してデプロイしました。おめでとうございます。

このチュートリアルが、Spring Native プロジェクトをより深く理解し、将来のニーズを満たす可能性があることを念頭に置くきっかけになれば幸いです。

省略可: サービスをクリーンアップまたは無効にする

この Codelab 用に Google Cloud プロジェクトを作成した場合でも、既存のプロジェクトを再利用する場合でも、使用したリソースから不要な料金が発生しないように注意してください。

作成した Cloud Run サービスを削除または無効にする、ホストしたイメージを削除する、プロジェクト全体をシャットダウンすることができます。

8. 参考情報

Spring Native プロジェクトは現在、新しい試験運用プロジェクトですが、アーリー アダプターが問題のトラブルシューティングを行い、プロジェクトに参加するのに役立つ優れたリソースがすでに豊富に用意されています。

参考情報

このチュートリアルに関連する可能性のあるオンライン リソースは次のとおりです。

ライセンス

この作業はクリエイティブ・コモンズの表示 2.0 汎用ライセンスにより使用許諾されています。