Google Cloud 上の Spring Native

1. 概要

この Codelab では、Spring Native プロジェクトについて説明し、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 Native アプリを作成してデプロイする
  • このようなアプリを Cloud Run にデプロイする

必要なもの

アンケート

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

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

Java の使用経験をどのように評価されますか。

初心者 中級者 上級者

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

<ph type="x-smartling-placeholder"></ph> 初心者 中級 上達

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 は、高度に最適化されたオープンソースの JDK ディストリビューションです。非常に高速な起動、AOT ネイティブのイメージ コンパイル、開発者が複数の言語を 1 つのアプリケーションに統合できる多言語機能を備えています。

GraalVM は現在開発が盛んであり、新機能の追加や既存の機能の改善が常に行われています。デベロッパーには今後の情報にご注目ください。

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

  • 新しい、ユーザー フレンドリーなネイティブ イメージのビルド出力(2021-01-18
  • Java 17 のサポート(2022-01-18
  • 多言語でのコンパイル時間を短縮するために多層コンパイルをデフォルトで有効にしました(2021 年 4 月 20 日

Spring Native

簡単に言うと、Spring Native では GraalVM のネイティブ イメージ コンパイラを使用して 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 ネイティブ プロジェクトがサポートする最新バージョンです。

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 アプリケーションをネイティブ イメージとして実行するために必要です。注: 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>

小さなビルダー イメージは、いくつかのオプションの 1 つにすぎません。追加のライブラリやユーティリティがほとんどなく、攻撃対象領域を最小限に抑えることができるため、当社のユースケースに適している選択です。

たとえば、一般的な 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"

次に、新しいレジストリに push するために認証されていることを確認します。

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. 概要/クリーンアップ

これで、Spring Native アプリケーションを Google Cloud 上でビルドしてデプロイできました。

このチュートリアルが、Spring Native プロジェクトに対する理解を深める一助となれば幸いです。将来、Spring Native のプロジェクトのニーズを念頭に置いてください。

省略可: クリーンアップとサービスの無効化

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

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

8. 参考情報

Spring Native プロジェクトは現在、新しい実験的なプロジェクトですが、先行ユーザーが問題のトラブルシューティングや参加に役立つ優れたリソースがすでに豊富に用意されています。

参考情報

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

ライセンス

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