Spring Boot 3の新機能を使ってみよう! 2からアップグレードする手順、Observability機能、ネイティブイメージ化
Javaの開発フレームワークであるSpringの最新バージョンとして、Spring Boot 3が2022年11月にリリースされました。この記事ではSpring Boot 2で書かれたサンプルコードをSpring Boot 3にアップグレードしながら、考慮点や新機能を体感していただきます。ヴイエムウェア株式会社の星野真知さんによる解説です。
Javaのエコシステム、その中でも世界で一番の人気を誇るのが(JetBrains社の調査によると)Spring FrameworkおよびSpring Bootです。Spring Frameworkは軽量なJavaフレームワークとして2004年の1.0に始まり、2014年のSpring Boot 1.0のリリースを皮切りにクラウドネィティブへの対応がなされてきました。軽量なREST APIからエンタープライズ向けのバッチ処理まで、さまざまなシーンで使われています。
そんな中、2022年11月に大きな発表がありました。それがSpring Framework 6、そして若干のタイムラグの後に発表されたSpring Boot 3です。そのリリースノートには膨大な量の更新が記載されていました。
▶ Spring Boot 3.0 Release Notes · spring-projects/spring-boot Wiki
中でも大きな変更ポイントとなるのが、以下の4つです。
- ベースラインとしてJava 17を採用
- Jakarta EE 9のベースライン化およびEE 10のサポート
- 可観測性(Observability)の強化
- GraalVMによるネイティブイメージのサポート
この記事ではSpring Boot 2で動作するサンプルコードを用意して、Spring Boot 3にアップグレードする手順を紹介します。実際のアップグレード作業を通して、上記の変更がどう影響しているのか? どういった新しいことができるのか? などを紹介します。 できればSpring Boot 2の経験があるとよいでしょう。
- キューを読み書きするサンプルコードをSpring Boot 2で動作させる
- いざSpring Boot 3にアップデート
- Spring Boot MigratorでSpring Boot 3へ自動変換
- Spring Boot 3で強化されたObservability機能とは?
- Observability その1 ─ Spring Bootで有効化
- Observability その2 ─ RabbitMQで有効化
- Observability その3 ─ Micrometerによるトレースとの同時計測
- Observability その4 ─ Exemplarsによるスパンとの関連付け
- GraalVMを用いたネイティブイメージによる高速起動化
- まとめ
キューを読み書きするサンプルコードをSpring Boot 2で動作させる
以下のリポジトリに、本記事で使用するサンプルコードがあります。
手元のPCに適当な作業用のディレクトリを作成し、以下の手順でコードをダウンロードしてください。
$ git clone https://github.com/mhoshi-vm/sb3-demo
展開されるファイルはMaven Wrapper(mvnw)で起動するSpring Boot 2のプロジェクトとなっています。
なお、今回のサンプルを最後まで試すには、以下の環境が必要になります。
これらが揃っていればPCでもサーバーでも実施できます。
サンプルコードの構造
サンプルコードは、dataproducer(Data Producer)とdataconsumeandexpose(Data Consumer and Expose)という2つのWebサービスと、メッセージキュー(RabbitMQ)からなるシンプルな構造をしています。
- dataproducerとして、REST API経由で取得したデータを、RabbitMQのキューに保管
- dataconsumeandexposeが、RabbitMQ経由でデータを取得
- dataconsumeandexposeが、ローカルのデータベースへデータを保管
- dataconsumeandexposeが、REST API経由で全てのデータを表示
図式化すると以下のようになります。
RabbitMQの起動
サンプルコードを実行する前に、メッセージキュー(RabbitMQ)を起動しておく必要があります。
以降の解説では、RabbitMQが次の手順に従ってインストールされ、ユーザーとパスワードはデフォルトのまま(guest/guest)で起動されていることを想定しています。
▶ The Homebrew RabbitMQ Formula — RabbitMQ
RabbitMQを起動すると、デフォルトでは次のURLで管理ダッシュボードにアクセスできます。
http://localhost:15672
管理ダッシュボードはこの後の解説でも参照します。
データをキューに入れるdataproducerの動作
サンプルコードを動作させてみます。まず、dataproducerを起動します。
$ cd dataproducer
$ ./mvnw spring-boot:run
サービスがlocalhostの8082番ポートで立ち上がるので、次のようにcurlを使って簡単なデータをREST APIで入力してみます。
$ curl http://localhost:8082/produce -H "Content-Type: text/plain" -d 'aaa' -X POST
データが作成され、以下のように表示されます。
Data Produced
RabbitMQの管理ダッシュボードにログインすると、以下のように「demoQueue」というキューが生成されています。またcurlを何度か実行すると、その回数に応じてreadyがカウントアップされるでしょう。
データをキューから取り出すdataconsumerandexposeの動作
次に、dataconsumerandexposeを起動します。
$ cd dataconsumerandexpose
$ ./mvnw spring-boot:run
サービスがlocalhostの8083番ポートで立ち上がるので、以下のコマンドでデータを取得します。
$ curl http://localhost:8083/get
先ほどのようにdataproducerに対してcurlを実行した直後であれば、以下のように出力されます。
[{"id":1,"data":"aaa"}]
RabbitMQの管理ダッシュボードでも、readyの値が0になります。このようにcurlの実行に応じてデータ数などが変わります。
このシンプルなコードを例に、Spring Boot 3に上げてみたいと思います。
いざSpring Boot 3にアップデート
まずは深く考えずに、このサンプルコードをそのままSpring Boot 3に上げてみましょう。それぞれのサービスのPOMファイルを修正します。
ベースラインとしてJava 17を採用
まず、dataproducerをSpring Boot 3にします。dataproducer
ディレクトリにあるpom.xml
を次のように書き換えます。version
では、執筆時点でSpring Bootの最新バージョンである3.1.0を選択しています。
$ diff --git a/dataproducer/pom.xml b/dataproducer/pom.xml index 9d8662e..076b1f3 100644 --- a/dataproducer/pom.xml +++ b/dataproducer/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.7.12</version> + <version>3.1.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.engineerhub</groupId> @@ -14,7 +14,7 @@ <name>dataproducer</name> <description>dataproducer</description> <properties> - <java.version>11</java.version> + <java.version>17</java.version> </properties> <dependencies> <dependency>
ここで注意点として、Spring Boot 3のベースラインにはJava 17が採用されました。Spring Boot 2まではJava 8からサポートされていましたが、今後の技術追従を見込み、Spring Boot 3からはリリース時点のLTS(ロングタームサポート)であるJava 17が最低サポートバージョンとなっています。上記のjava.version
にはそれが反映されています。
さて、この変更のみで起動してみます。
$ ./mvnw spring-boot:run
おそらくこの時点では普通に起動する上に、curlのコマンドなども問題なく動作すると思います。「なんだ、アップグレードも簡単じゃないか」。そう思うかもしれません。
Jakarta EE 9のベースライン化およびEE 10のサポート
続いて、dataconsumerandexpose
ディレクトリでも同じようにpom.xml
を書き換えて、アップグレードしていきましょう。
$ diff --git a/dataconsumeandexpose/pom.xml b/dataconsumeandexpose/pom.xml index 1e853d9..fd1411b 100644 --- a/dataconsumeandexpose/pom.xml +++ b/dataconsumeandexpose/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.7.12</version> + <version>3.1.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.engineerhub</groupId> @@ -14,7 +14,7 @@ <name>dataconsumeandexpose</name> <description>dataconsumeandexpose</description> <properties> - <java.version>11</java.version> + <java.version>17</java.version> </properties> <dependencies> <dependency>
これもこのまま起動してみます。
$ ./mvnw spring-boot:run
正常に起動したdataproducerと異なり、以下のようなエラーが出るでしょう。
[INFO] ------------------------------------------------------------- [ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[3,25] package javax.persistence does not exist [ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[4,25] package javax.persistence does not exist [ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[5,25] package javax.persistence does not exist [ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[6,25] package javax.persistence does not exist
ポイントとなるのはjavax.persistence does not exist
というエラーです。この原因には、Spring Boot 3でJava EE 8のサポートを撤廃したことと、Jakarta EE 9/10のサポートが関わっています。
これはTomcat 10.1やJetty 11への技術追従(パッケージ名の変更を含む)を目的とした変更ですが、結果としてこれまでSpring Boot 2で使えていたパッケージが使えなくなるという大きな影響が出ました。今回のようにコード内でjavax
を指定しているものは使えなくなり、jakarta
への書き直しを余儀なくされます。
Spring Boot 3への変更に対応したマイグレーションの方法
ここまでに紹介した2つの大きな変更(Java 17とJakarta EE 9)については、公式ブログが2021年9月に取り上げています。
▶ A Java 17 and Jakarta EE 9 baseline for Spring Framework 6
またこれ以外にも、さまざまな細かいコードの変更の必要性が日々報告されています。 それに対して実際のプロジェクトでは、次のマイグレーションガイドをもとにアップグレード作業を行います。
▶ Spring Boot 3.0 Migration Guide · spring-projects/spring-boot Wiki
ただし、アップグレードはどうしても負荷が高い作業です。今回のようなシンプルなコードならまだ言われた通りに修正すればよいのですが、より複雑なコードを手動で変更していくには手間がかかります。
そこで、サンプルコードを機械的にSpring Boot 3へアップデートする仕組みを紹介します。 先ほど手動で行った変更は一度リセットし、Spring Boot 2へ戻しておきましょう。
$ git reset --hard origin/main
Spring Boot MigratorでSpring Boot 3へ自動変換
Spring Boot 2からSpring Boot 3への移行を支援するツールとして、Spring Boot Migratorがコミュニティから提供されています。
このツールは大規模なコードリファクタリングを支援するOpenRewriteをベースに、さまざまなフレームワークからSpring Bootへの移行に加えて、Spring Boot 3へのマイグレーションも行うことができます。
最新リリースのダウンロードと起動
次のページから、最新のspring-boot-migrator.jar
を任意のディレクトリへダウンロードします。
▶ Releases · spring-projects-experimental/spring-boot-migrator
さっそく起動してみましょう。
$ java -jar spring-boot-migrator.jar
すると以下のようなプロンプトが表示されます。
migrator:>
このプロンプトで、さまざまなコマンドを実行することができます。
dataproducerのコードをスキャンして変換する
まずこのプロンプトから、dataproducerのコードをスキャンします。
> scan sb3-demo/dataproducer
実行結果から、このコードに対する実行可能なさまざまなレシピが確認できます。
この中で一番シンプルな一括変換のレシピ(boot-2.7-3.0-dependency-version-update)を実行します。
> apply boot-2.7-3.0-dependency-version-update
これにより、先ほどの例で取り上げたJava 17へのアップグレードや、javax
からjakarta
パッケージへの変換だけでなく、コミュニティとして推奨されている変更まで含めて実施されます。
dataconsumeandexposeのコードもスキャンして変換する
続いてdataconsumeandexposeでも、同じようにスキャンと変換を実施します。
> scan sb3-demo/dataconsumeandexpose > apply boot-2.7-3.0-dependency-version-update
ところでサンプルコードのリポジトリには、解説のセクションごとにそれぞれブランチを用意しています。このSpring Boot Migratorによる自動変換の作業は、次のブランチにまとまっています。
▶ mhoshi-vm/sb3-demo at sb3migrate
例えば、上記のdataconsumeandexposeへのツールによる変換結果は、このコミットで参照できます。
このようなシンプルなコードではどうしても恩恵を感じにくいかもしれませんが、複雑なコードであればあるほど作業の簡略化が図れます。Spring Boot 3への移行を躊躇している場合は、強力なツールになり得ます。
とはいえ、執筆時点ではExperimentalなうえイシューも数多く上がっているので、自己責任で利用してください。新しい問題が見つかったときには、イシューを報告してコミュニティへ貢献するとよいでしょう。
さて、これでSpring Boot 3へのアップグレードが無事に完了しました。しかし、これだけではSpring Boot 3の新機能を実感しづらいでしょう。次のセクションから、Spring Boot 3ならではの機能を試していきます。
Spring Boot 3で強化されたObservability機能とは?
ここからは、アプリケーションの可観測性(Observability)を劇的に向上させる機能を紹介します。
Spring Boot 3から、Micrometerを中心としたこの機能が正式にGAされました。 これまでもSpring Actuatorによる監視が行えていましたが、Spring Boot 3になって以下の機能が強化されました。
- Micrometer Tracingによるトレーシング機能
- Micrometer Observabilityによるメトリクスとトレースの同時計測
- Exemplarsによるメトリクスとスパンの関連付け
それぞれの機能を見ていきましょう。
監視基盤の立ち上げ
機能を紹介する前に、まず監視基盤を立ち上げます。監視基盤にはさまざまなオプションがありますが、今回は以下を組み合わせて実施します。
まず作業ディレクトリにdocker-compose.yaml
を、以下のコンテンツで作成します。これは、先ほど挙げた3つのOSSを起動するDockerの設定ファイルです。
services: grafana: image: grafana/grafana extra_hosts: - host.docker.internal:host-gateway volumes: - ./docker/grafana/provisioning/datasources/:/etc/grafana/provisioning/datasources/:ro environment: - GF_AUTH_ANONYMOUS_ENABLED=true - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - GF_AUTH_DISABLE_LOGIN_FORM=true ports: - "3000:3000" prometheus: image: prom/prometheus extra_hosts: - host.docker.internal:host-gateway command: - --enable-feature=exemplar-storage - --config.file=/etc/prometheus/prometheus.yml volumes: - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: - "9090:9090" zipkin: image: openzipkin/zipkin-slim ports: - "9411:9411"
ここで重要なのが、Prometheusの構成ファイルに設定されている--enable-feature=exemplar-storage
です。これにより、後で取り上げるExemplarの取り扱いが可能になります。
Prometheusで監視対象を設定する
次に、./docker/prometheus/prometheus.yml
ファイルを作成します。
scrape_configs: - job_name: 'apps' scrape_interval: 10s metrics_path: '/actuator/prometheus' static_configs: - targets: ['host.docker.internal:8082','host.docker.internal:8083']
内容は極めてシンプルですが、次の箇所で監視対象(今回はdataproducerとdataconsumeandexpose)の宛先を指定しているので、ポート番号の変更などを行った場合にはあわせて修正してください。
targets: ['host.docker.internal:8082','host.docker.internal:8083']
Grafanaの設定
もうひとつ、./docker/grafana/provisioning/datasources/datasources.yaml
ファイルを作成してください。
apiVersion: 1 datasources: - name: Prometheus type: prometheus access: proxy url: http://host.docker.internal:9090 editable: false jsonData: exemplarTraceIdDestinations: - name: trace_id datasourceUid: zipkin - name: Zipkin type: zipkin uid: zipkin url: http://host.docker.internal:9411 readOnly: true
これもシンプルですが、Grafanaから見たPrometheusとZipkinの宛先を指定しているほか、後ほど説明するExemplar機能を使うためにexemplarTraceIdDestinations:
で設定しています。
Docker Composeで起動
用意ができたらDocker Compose経由で起動します。
$ docker-compose up -d
docker ps
コマンドで起動したことを確認します。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e83944f82a04 grafana/grafana "/run.sh" 14 seconds ago Up 10 seconds 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp sb3-demo-grafana-1 f0b47c0cfb9b openzipkin/zipkin-slim "start-zipkin" 8 minutes ago Up 7 minutes (healthy) 0.0.0.0:9411->9411/tcp, :::9411->9411/tcp sb3-demo-zipkin-1 b7f4e3b8b0f9 prom/prometheus "/bin/prometheus --e…" 8 minutes ago Up 7 minutes 0.0.0.0:9090->9090/tcp, :::9090->9090/tcp sb3-demo-prometheus-1
停止する際には、docker-compose down
を実行してください。
Observability その1 ─ Spring Bootで有効化
監視基盤が立ち上がったので、サンプルコードにおいてSpring Observabilityを有効化していきます。
dataproducerとdataconsumeandexposeのpom.xml
を開き、以下を追記します。
spring-boot-starter-actuator
... 監視の本体micrometer-tracing-bridge-brave
... トレース情報の送付micrometer-registry-prometheus
... メトリクスをPrometheusへ転送zipkin-reporter-brave
... Zipkinへのデータ転送
また、データベースへのトレースを有効化するため、以下も追加しています。
datasource-micrometer-spring-boot
dataproducerの設定変更
実際の変更後の差分イメージがこちらです。
$ diff --git a/dataproducer/pom.xml b/dataproducer/pom.xml index 076b1f3..3a7f447 100644 --- a/dataproducer/pom.xml +++ b/dataproducer/pom.xml @@ -25,6 +25,29 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-tracing-bridge-brave</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>io.zipkin.reporter2</groupId> + <artifactId>zipkin-reporter-brave</artifactId> + </dependency> + <dependency> + <groupId>net.ttddyy.observation</groupId> + <artifactId>datasource-micrometer-spring-boot</artifactId> + <version>1.0.2</version> + <scope>runtime</scope> + </dependency> <dependency> <groupId>org.springframework.boot</groupId>
次に、設定をapplication.properties
に追記していきます。
$ diff --git a/dataproducer/src/main/resources/application.properties b/dataproducer/src/main/resources/application.properties index 3f51428..1694b02 100644 --- a/dataproducer/src/main/resources/application.properties +++ b/dataproducer/src/main/resources/application.properties @@ -4,3 +4,8 @@ spring.application.name=producer logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SSS management.endpoints.jmx.exposure.include=* +management.tracing.sampling.probability=1.0 +management.endpoints.web.exposure.include=prometheus + +management.metrics.distribution.percentiles-histogram.http.server.requests=true +logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
dataproducerの起動と動作確認
この状態でdataproducer
を起動してみます。
$ cd dataproducer/
$ ./mvnw spring-boot:run
起動が完了したらcurlコマンドを実行して、アプリケーションに通信を発生させます。
$ curl http://localhost:8082/produce -H "Content-Type: text/plain" -d 'aaa' -X POST
そして次のURLからZipkinにログインします。
http://localhost:9411/
「Run Query」を行い、curlを実行した回数分のクエリがあることを確認します。
dataconsumeandexposeの設定変更と起動確認
dataconsumeandexposeでも同じように変更していきます。pom.xml
とapplication.properties
ともに同じ箇所を変更してください。
この状態で起動します。
$ cd dataconsumeandexpose/
$ ./mvnw spring-boot:run
再びZipkinにログインします。「Run Query」を行い、producerおよびconsumerともにデータが表示されていることを確認します。
最後に、次のURLでPrometheusにログインし、エンドポイントが2つとも起動していることを確認します。
http://localhost:9090/targets?search=
Observability その2 ─ RabbitMQで有効化
先ほどZipkinで確認したトレースですが、実は不完全です。 dataproducerとdataconsumeandexposeは本来なら関連したマイクロサービスのアプリケーションですが、Zipkinからは関連がないように見えています。
dataproducerとdataconsumeandexposeにコードを追加
これも若干のカスタマイズで、一連のマイクロサービスとして監視できます。 dataproducerに以下のコードを追加します。
dataproducer/src/main/java/com/engineerhub/dataproducer/ObservabilityCustomTemplate.java
package com.engineerhub.dataproducer; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.boot.autoconfigure.amqp.RabbitTemplateCustomizer; import org.springframework.stereotype.Component; @Component public class ObservabilityCustomTemplate implements RabbitTemplateCustomizer { @Override public void customize(RabbitTemplate rabbitTemplate) { rabbitTemplate.setObservationEnabled(true); } }
ポイントはRabbitTemplateCustomizer
を実装し、rabbitTemplate.setObservationEnabled(true);
を設定している点です。これによりrabbitTemplate
と呼ばれるRabbitMQのクライアントのMicrometer Observabilityを有効にします。
これにより送信側のRabbitMQのObservabilityが有効化されましたが、受信側も別途対応する必要があります。dataconsumeandexposeに、以下のコードを追加します。
dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java
package com.engineerhub.dataconsumeandexpose; import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) public class ObservabilityConfig { @Bean ContainerCustomizer<SimpleMessageListenerContainer> containerCustomizer() { return (container) -> container.setObservationEnabled(true); } }
これは、受け手側でのObservabilityを有効にし、具体的には@RabbitListner
アノテーションでくくられたコードを追跡するようになります。
この状態で2つのコードを立ち上げ直して、データを再度送ってみます。
一連の流れがつながって表示されることを確認
Zipkinにログインして、Dependenciesを見てください。「Run Query」すると、以下のようにつながった流れが見えるかと思います。
最新のトレースを「Show」してみてください。 すると、以下のように今回のアプリケーションの流れ(HTTPリクエスト → MQへの書き込み → MQの読み込み >→DBへの書き込み)を全て見ることができます。
今回はRabbitMQのトレースのカスタマイズ方法を紹介しましたが、Micrometerのその他のカスタマイズ方法は以下のサンプルに公開されています。ぜひ参考にしてください。
▶ micrometer-metrics/micrometer-samples: Sample apps to demo Micrometer features
Observability その3 ─ Micrometerによるトレースとの同時計測
Spring Boot 3のObservabilityの面白い機能の1つが、メトリクスとトレースを同時に取得ができるようになった点です。この機能も紹介していきます。
dataconsumeandexposeのコードの修正
まず、先ほど作成したdataconsumeandexposeのObservabilityConfig
に以下を追記します。
$ diff --git a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java index ae5845a..9f87ee6 100644 --- a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java +++ b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java @@ -1,5 +1,7 @@ package com.engineerhub.dataconsumeandexpose; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.aop.ObservedAspect; import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.context.annotation.Bean; @@ -7,6 +9,12 @@ import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) public class ObservabilityConfig { + + @Bean + ObservedAspect observedAspect(ObservationRegistry observationRegistry) { + return new ObservedAspect(observationRegistry); + } + @Bean ContainerCustomizer<SimpleMessageListenerContainer> containerCustomizer() { return (container) -> container.setObservationEnabled(true);
このBeanはこれから後に使う@Observed
アノテーションを有効化するAspectを追加するためのものです。なお、この必要性はこのイシューによっては変わるかもしれません。
次に、dataconsumeandexposeに以下のコードを追加します。
dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleLogic.java
package com.engineerhub.dataconsumeandexpose; import io.micrometer.observation.annotation.Observed; import org.springframework.stereotype.Component; @Component public class SimpleLogic { @Observed public void logic() throws InterruptedException { Thread.sleep(10); } }
見ての通り、Thread.sleep
による10ミリ秒の停止以外に何も入れていないシンプルなコードですが、@Observed
によって囲われたメソッドのメトリクスとトレースを同時に取得することが可能になります。
最後に、SimpleService
を以下のようにアップデートします。
$ diff --git a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java index 8a64670..cc785f8 100644 --- a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java +++ b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java @@ -11,8 +11,11 @@ public class SimpleService { private static final Logger log = LoggerFactory.getLogger(SimpleService.class); SimpleRepository simpleRepository; - public SimpleService(SimpleRepository simpleRepository) { + SimpleLogic simpleLogic; + + public SimpleService(SimpleRepository simpleRepository, SimpleLogic simpleLogic) { this.simpleRepository = simpleRepository; + this.simpleLogic = simpleLogic; } public Iterable<SimpleEntity> getAll(){ @@ -24,10 +27,11 @@ public class SimpleService { } @RabbitListener(queues = "demoQueue") - public void consume(String data){ + public void consume(String data) throws InterruptedException { log.info("new data from queue"); SimpleEntity simpleEntity = new SimpleEntity(); simpleEntity.setData(data); + simpleLogic.logic(); save(simpleEntity); } }
この状態でdataconsumeandexposeを再起動して、データを転送してみます。
PrometheusとZipkinで動作を確認
まず、以下のURLでPrometheusを見てみましょう。
http://localhost:9090/graph?g0.expr=method_observed_active_seconds_max&g0.tab=1&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h
すると@Observed
で囲ったメソッドのメトリクスが収集されていることが分かります。
さらにZipkinも見てみましょう。
同じく、メソッドと同じ名前のスパン情報が追加されています。さらに、Thread.sleep
で指定した10ミリ秒の時間もアプリケーションが停滞していることが分かります。
Observability その4 ─ Exemplarsによるスパンとの関連付け
Observabilityに関連して紹介する最後の機能は、Exemplarと呼ばれます。 この言葉にも馴染みがないかもしれませんが、OpenMetricsによって定義された概念であり、メトリクスとトレースをより関連付けることができる1つの機能です。
まず、以下のようなcurlを実行してみましょう。
$ curl -s 'http://localhost:8082/actuator/prometheus' | grep "trace_id"
特に何も表示されないと思います。次に以下を実行します。
$ curl -s 'http://localhost:8082/actuator/prometheus' -H 'Accept: application/openmetrics-text; version=1.0.0; charset=utf-8' | grep "trace_id"
すると、今度は多くの出力が得られます。以下は一例です。
http_server_requests_seconds_bucket{error="none",exception="none",method="POST",outcome="SUCCESS",status="200",uri="/produce",le="0.005592405"} 1.0 # {span_id="0789f112214e6089",trace_id="6470d1fc0ec0e0110789f112214e6089"} 0.005083116 1685115388.147
このメトリクス、厳密にはヒストグラムは、先ほどapplication.properties
に追加した次の設定によって参照が可能になったものです。
management.metrics.distribution.percentiles-histogram.http.server.requests=true
本来ヒストグラムは、指定されたバケットに複数のデータが入るものですが、ポイントとなるのがそのバケット内の代表点のspan_id
やtrace_id
、レスポンスタイムが記載されている点です。つまり上の結果では、/produce
にPOSTでリクエストしたレスポンスタイムのヒストグラムで0.005592405
[秒]以下のバケットにおける代表点(0.005083116
[秒])のtrace id
が6470d1fc0ec0e0110789f112214e6089
であることが分かります。このtracd id
を使うと、Zipkinで検索が可能になります。
Grafanaによる可視化
Grafanaでは、より面白い結果が見られます。以下の手順で実行していきます。
http://localhost:3000
にログイン- Explore機能を選択
- Prometheusを選択
- Metrics Browserで次を入力
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket
[$__rate_interval])) by (le))
- OptionsよりExemplarのトグルを有効化
- Run Queryの実行
- (若干見つけにくいですが)グラフ中のひし形にポインタを合わせる
すると以下の画面のようにメトリクスと連動したExemplarが確認できます。さらに「Query with Zipkin」を選択すると、Zipkin側のトレースも見ることができます。
先ほどの、Micrometer Observabilityとは違った角度とはいえ、両者共に、よりメトリクスとトレースを連動したものを作ろうとしているのが分かります。両者を連動させることでより関連性を明確にして、問題判別に役立てようという機能です。
以上が、Spring Boot 3で追加されたObservability機能の紹介です。
GraalVMを用いたネイティブイメージによる高速起動化
Spring Boot 3の解説もいよいよ佳境です。準備ができたこのJavaのコードをパッケージ化して世に公開する際に取ることができる1つの選択肢が、ネイティブイメージ化による高速起動化です。これまでネイティブイメージ対応はExperimental扱いでしたが、Spring Boot 3より正式にサポートされました。
ここまで修正したコードを、実際にネイティブイメージ化してみましょう。
GraalVMのインストールとネイティブ化のコンパイル
まず手順として、別途GraalVMを開発環境にインストールすることが必要です。執筆時点で最新のGraalVMを以下のコマンドでインストールできます。
$ sdk install java 22.3.r19-grl
インストールでき次第、以下のコマンドでdataproducerおよびdataconsumeandexposeをそれぞれパッケージ化します。
$ ./mvnw native:compile -Pnative
GraalVMによるネイティブイメージのコンパイルを初めて体験する方は驚くかもしれませんが、PCのスペックに依存するものの、今回のようなシンプルなコードでも5~10分近くのビルド時間が発生します。
完了後、以下の実行可能なファイルが生成されます。
./dataconsumeandexpose/target/dataconsumeandexpose ./dataproducer/target/dataproducer
そのままdataproducerを起動してみます。
$ ./dataproducer/target/dataproducer
通常のJVMによる起動に比べて、非常に高速に起動することが分かるかと思います。通常の起動だと3~5秒ほどかかるところが、以下の例では200ミリ秒以下で起動しています。
これがネイティブイメージ化の真髄であり、JVMによるアプリ起動と比較しても起動が速く、かつメモリの消費量も小さくなります。
さらにこの前章のObservabilityで紹介した監視がそのまま使えることも大きなポイントです。クラウドのサーバレス環境などでは、監視を設定する際に固有の設定を指示されるケースが多くあります。ネイティブイメージであれば、ローカル開発での監視の仕組みのまま、クラウドに持っていくことができます。
dataconsumeandexposeの起動エラーと対応
なお、執筆時点ではdataconsumeandexposeを起動しようとすると、以下のエラーになるはずです。
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory': No classes have been predefined during the image build to load from bytecodes at runtime. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1770) ~[dataconsumeandexpose:6.0.9] ...
このエラーには次のイシューが影響しています。
今回のコードのワークアラウンドとして、推奨はされませんが、以下のようにHibernateを一時的にダウングレードする必要があります。
$ diff --git a/dataconsumeandexpose/pom.xml b/dataconsumeandexpose/pom.xml index 1bdac74..40d544e 100644 --- a/dataconsumeandexpose/pom.xml +++ b/dataconsumeandexpose/pom.xml @@ -15,6 +15,7 @@ <description>dataconsumeandexpose</description> <properties> <java.version>17</java.version> + <hibernate.version>6.1.7.Final</hibernate.version> </properties> <dependencies> <dependency>
ある意味でこれがネイティブイメージ化の弱点でもあり、コードを変更しなければならない場合があります。
なお、Spring Framework 6でAOT(Ahead-of-Time Processing)対応が入り、ビルド時にネイティブイメージ化しやすいソースコード・バイトコードに事前コンパイルされています。とはいえ、コンパイルにおいてのAOT理解が必須になります。
ビルド時間が長いことも、体感いただいた通り、もう1つの考慮点です。
これらを加味した上で、ネイティブイメージ化はコンテナやFaaS(Function as a Service)などの用途には非常に適していることが次のドキュメントで分かります。
▶ GraalVM Native Image Support
なお、Spring Boot 3のネイティブイメージ対応は、可能な限りコード差異を意識することなくフレームワークレベルで抽象化されています。上のイシューがコミュニティですぐに修正されたように、最終形としては開発者がネイティブイメージにビルドするかどうかを意識することなく、開発に取り組めることが目標となっています。
まとめ
この記事では、Spring Boot 2で書かれたコードをアップグレードし、Spring Boot 3の新しい機能に触れていくということを解説しました。
前述したようにサンプルコードは以下のようなブランチに分かれていますので参考にしてください。
今回の記事が少しでもSpring Boot 3への興味や開発の意欲につながれば幸いです。
星野 真知(HOSHINO Machi)GitHub: mhoshi-vm
編集:中薗 昴
制作:はてな編集部