Google Cloud 上の Spring Native

1. 概要

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

コンポーネント、プロジェクトの最近の歴史、ユースケース、そしてプロジェクトで使用する手順について説明します。

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

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

GCP コンソールの

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

必要なもの

アンケート

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

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

Java のご利用経験についてお答えください。

初心者 中級者 上級者

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

初心者 中級者 上級者

2. 背景情報

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

Spring Native を完全に理解するには、これらのコンポーネント テクノロジーのいくつか、それらが実現できること、それらがどのように連携するかを理解することが役に立ちます。

AOT コンパイル

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

このプロセスにより、Java のシグネチャのポータビリティが実現されます。これにより、「1 度記述してどこでも実行」が可能になりますが、ネイティブ コードの実行と比較するとコストが高くなります。

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

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

5042e8e62a05a27.png

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

  • 起動時間が重要な短時間のアプリケーション
  • メモリが厳しく制限されている環境で、JIT のコストが高すぎる可能性がある場合

興味深い事実として、AOT コンパイルは JDK 9 で試験運用版機能として導入されましたが、この実装はメンテナンスに費用がかかり、普及しなかったため、Java 17 で静かに削除され、デベロッパーは GraalVM のみを使用するようになりました。

GraalVM

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

GraalVM は現在も積極的に開発されており、新しい機能が追加され、既存の機能が改善されています。デベロッパーの皆様は、今後の進展にご注目ください。

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

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

Spring Native

簡単に言うと、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-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 ライブラリへのアクセスが必要なアプリをビルドする場合や、アプリの要件がまだ不明な場合は、フルビルダーの方が適している場合があります。

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 Native アプリケーションを構築してデプロイしました。

このチュートリアルで、Spring Native プロジェクトをより深く理解し、今後のニーズに応じて活用できるようになれば幸いです。

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

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

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

8. 参考情報

Spring Native プロジェクトは現在、新しい試験運用版のプロジェクトですが、早期導入者が問題のトラブルシューティングを行い、参加するための優れたリソースがすでに多数あります。

参考情報

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

ライセンス

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