SpringBoot3 WebFlux 可観測ベストプラクティス¶
著者: 劉銳
リアクティブプログラミング Kotlin で、Spring Boot 3 WebFlux を利用し Micrometer で分散型トレーシングを実現する方法
分散型トレーシングは、可観測性ソフトウェアシステムにとって非常に優れたツールです。これにより、開発者はアプリケーション内およびアプリケーション間で異なるインタラクションが発生する時間、場所、方法を把握できるようになります。同時に、複雑なソフトウェアシステムの観測が容易になります。
Spring Boot 3 から、Spring Boot で分散型トレーシングに使用されていた従来の Spring Cloud Sleuth ソリューションは、新しい Micrometer Tracing ライブラリに置き換えられます。
Micrometer は、以前からプラットフォームに依存しないメトリクス公開と、Prometheus などの JVM ベースのマイクロサービスのモニタリングにおけるデフォルトソリューションとして使用されていたため、すでにご存知かもしれません。最新製品は、プラットフォームに依存しない分散型トレーシングソリューションにより、Micrometer エコシステムを拡張します。これにより、開発者は共通の API を使用してアプリケーションを計装し、さまざまな形式で Jaeger、Zipkin、OpenTelemetry などの分散型トレーシングコレクターにエクスポートできるようになります。
1. マイクロサービスのセットアップ¶
次に、外部のサードパーティサービスに内部でクエリを実行して情報を取得する、リアクティブな REST エンドポイントを提供するシンプルな Spring Boot マイクロサービスを作成します。目標は、2 つの操作のトレースをエクスポートすることです。
以下の Spring Boot Initializr プロジェクトから開始します。このプロジェクトはこちらにあります。Kotlin Gradle DSL を使用した Spring Boot 3.0.1、Spring Web Reactive (WebFlux)、Prometheus を使用した Spring Actuator が含まれています。以下のコードは主に Kotlin を使用していますが、Java でも問題ありません。ほとんどのメソッドは同じです。
Spring Initializr テンプレート: Webflux、Spring Actuator、Prometheus を含む Spring Boot 3 Kotlin テンプレート
エンドポイントの定義¶
まず、Spring WebClient を使用して外部 API を呼び出すテストエンドポイントを持つ、シンプルな REST コントローラークラスを追加します。Kotlin のコルーチンを利用するために、suspend キーワードを使用しています。これにより、Spring WebFlux のリアクティブストリームを活用しながら、命令型コードを記述できます。
以下の例では、Spring WebClient を使用して外部の TODO-API を呼び出し、JSON 文字列として TODO アイテムを返します。また、後で分散型トレーシング情報を含めるべきログメッセージも作成します。
@RestController
class Controller {
val log = LoggerFactory.getLogger(javaClass)
val webClient = WebClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
@GetMapping("/test")
suspend fun test(): String {
// 複雑な計算をシミュレート
delay(1.seconds)
log.info("test log with tracing info")
// WebClient で外部 API を呼び出す
val externalTodos = webClient.get()
.uri("/todos/1")
.retrieve()
.bodyToMono(String::class.java)
.awaitSingle()
return externalTodos
}
}
Micrometer Tracing の追加¶
次のステップで、Micrometer Tracing の依存関係を build.gradle.kts ファイルに追加します。Micrometer は異なる分散型トレーシングフォーマットとベンダーをサポートしているため、依存関係は分離されており、必要なものだけをインポートします。すべての依存関係を同期させるために、Micrometer Tracing BOM を使用します。さらに、コア依存関係とブリッジを追加し、Micrometer Tracing を OpenTelemetry フォーマットに変換します(他のフォーマットも利用可能です)。
implementation(platform("io.micrometer:micrometer-tracing-bom:1.0.0"))
implementation("io.micrometer:micrometer-tracing")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
また、作成されたトレースをエクスポートするためのエクスポーター依存関係も追加する必要があります。この例では、OpenTelemetry によってメンテナンスされ、Micrometer Tracing によってサポートされている Zipkin エクスポーターを使用します。
設定¶
設定は分散型トレーシングをセットアップするために不可欠なステップです。設定ファイル application.yaml は src/main/resources ディレクトリにあります。
まず、管理設定で分散型トレーシングを有効にする必要があります。また、トレースサンプリングレートを 1(デフォルトは 0.1)に設定し、サービスが受信するすべての呼び出しに対してトレースを作成します。リクエストが多い本番システムでは、一部の呼び出しチェーンのみをトレースしたい場合があります。さらに、Zipkin エクスポーターがトレースを送信するエンドポイント URL を定義できます。 最後に、デフォルトのログ記録パターンを更新して、トレース ID とスパン ID を含める必要があります。
management:
tracing:
enabled: true
sampling.probability: 1.0
zipkin.tracing.endpoint: http://localhost:9411/api/v2/spans
logging.pattern.level: "trace_id=%mdc{traceId} span_id=%mdc{spanId} trace_flags=%mdc{traceFlags} %p"
2. テスト¶
サービスのセットアップが完了したので、実行できます。アプリケーションを起動すると、デフォルトではサーバーはポート 8080 で起動します。その後、ブラウザを開いて作成したエンドポイント http://localhost:8080/test を呼び出すことができます。以下はリクエストのレスポンス内容です。
エンドポイント呼び出し時に作成された実際のトレースを表示するには、それらを収集して表示する必要があります。このチュートリアルでは、zipkin エクスポーターを使用してデータを Guance にエクスポートします。もちろん、Zipkin、Grafana Loki、Datadog などの他のシステムも使用できます。
これで、Spring Boot サービスのエンドポイントを再度呼び出すことができます。その後、Guance で任意のトレースを検索すると、エンドポイントリクエストの分散型トレーシング情報が見つかるはずです。
3. 問題点¶
一見すると、すべてがうまく動作しているように見えます。しかし、2 つの問題があります。
一部の Issue は解決されており、これらは Micrometer Tracing ドキュメント で確認できます。
ログにデータが欠落している¶
アプリケーションログを確認すると、エンドポイント呼び出し時に出力されたログメッセージが表示されます。
trace_id= span_id= trace_flags= INFO 43636 --- [DefaultExecutor] com.example.tracing.Controller : test log with tracing info
ご覧のとおり、trace_id と span_id が設定されていません。これは、Micrometer Tracing がリアクティブストリーム内の分散型トレーシングコンテキストをまだ簡単に処理できないためです。また、リアクティブストリームの Kotlin コルーチンラッパーがトレーシングコンテキストを隠してしまいます。そのため、現在のリアクティブストリームのコンテキストを延期して、トレーシング情報を取得する必要があります。実際には、次のようになります。
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
log.info("test log with tracing info")
Mono.empty<String>()
}
}.awaitSingleOrNull()
より実用的にするために、サンプルコードを別の関数に抽出できます。
@GetMapping("/test")
suspend fun test(): String {
// ...
observeCtx { log.info("test log with tracing info") }
// ...
}
suspend inline fun observeCtx(crossinline f: () -> Unit) {
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
f()
Mono.empty<Unit>()
}
}.awaitSingleOrNull()
}
アプリケーションを起動してエンドポイントを呼び出すと、ログに trace_id が表示されるようになります。
trace_id=6c0053eba01199f194f5f76ff8d61917 span_id=967d591266756905 trace_flags= INFO 45139 --- [DefaultExecutor] com.example.tracing.Controller : test log with tracing info
WebClient 呼び出しでトレース情報が生成されない¶
2 つ目の問題は、Guance でトレースを確認することで発見できます。エンドポイントの親トレースのみが表示され、WebClient 呼び出しの子スパンは表示されません。理論上、Spring WebClient および RestTemplate は Micrometer によって自動的に計装されます。しかし、コードを見ると、静的なビルダーメソッド WebClient を使用しています。WebClient から自動的な分散型トレーシングを取得するには、Spring フレームワークが提供するビルダー Bean を使用する必要があります。これは、コントローラーのコンストラクターを介して注入できます。
@RestController
class Controller(
webClientBuilder: WebClient.Builder
) {
val webClient = webClientBuilder // 注入されたビルダーを使用
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
// ...
}
WebClient のスパンが表示されるようになります。Micrometer Tracing は、trace_id を含むヘッダーも自動的に追加します。たとえば、トレーシング機能を持つ別のマイクロサービスを呼び出すと、そのマイクロサービスは ID を取得し、Guance に追加情報を送信できます。
4. 観測ガイド¶
Micrometer Tracing は、Spring 内で多くのことを自動的に行ってくれます。ただし、トレーススパンに特定の情報を追加したり、アプリケーション内の受信/送信呼び出し以外の特定の部分を観測したい場合もあります。
スパンタグの追加¶
カスタムタグを定義し、現在の観測に追加してトレースデータを強化できます。現在のトレースを取得するには、ObservationRegistry クラスの Bean を使用します。ログの問題と同様に、正しいコンテキストを取得するためにラッパー関数を使用する必要があります。
@GetMapping("/test")
suspend fun test(): String {
observeCtx {
val currentObservation = observationRegistry.currentObservation
currentObservation?.highCardinalityKeyValue("test_key", "test sample value")
}
// ...
}
このコードを追加後、Guance でカスタムタグとその値を確認できます。
カスタム観測¶
Micrometer API を使用してカスタム観測(スパン)を作成することは、通常は簡単です。ただし、リアクティブストリームとコルーチンを使用する場合、コンテキストのトレーシングを支援する必要があります。エンドポイントハンドラー内で新しい観測を作成すると、それは別個のトレースとして扱われます。コードを再利用可能にするために、新しい観測ポイントを作成するシンプルなラッパー関数を作成できます。これは、trace_id を使用するために以前作成した関数と同様に動作します。
suspend fun runObserved(
name: String,
observationRegistry: ObservationRegistry,
f: suspend () -> Unit
) {
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
val observation = Observation.start(name, observationRegistry)
Mono.just(observation).flatMap {
mono { f() }
}.doOnError {
observation.error(it)
observation.stop()
}.doOnSuccess {
observation.stop()
}
}
}.awaitSingleOrNull()
}
この関数を適用して、delay 関数の実行など、任意のコードを観測できるようになりました。
@GetMapping("/test")
suspend fun test(): String {
runObserved("delay", observationRegistry) {
delay(1.seconds)
}
// ....
}
5. データベースの分散型トレーシング¶
典型的な Spring Boot アプリケーションは、実際のアプリケーションでデータベースに接続することがよくあります。リアクティブ技術スタックを活用するには、JDBC ではなく R2DBC API を使用することをお勧めします。
Micrometer Tracing は比較的新しい技術であるため、現時点では利用可能な自動トレーシングはありません。しかし、Spring チームは自動設定の作成に取り組んでいます。実験的なリポジトリはこちらにあります。
現在のプロジェクトでは、以下の依存関係を build.gradle.kts に追加する必要があります。テストを容易にするために、実際のデータベースではなく H2 インメモリデータベース を使用します。
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
runtimeOnly("com.h2database:h2")
runtimeOnly("io.r2dbc:r2dbc-h2")
// R2DBC Micrometer 自動トレーシング
implementation("org.springframework.experimental:r2dbc-micrometer-spring-boot:1.0.2")
Kotlin コードでは、コルーチンをサポートするシンプルな CRUD リポジトリを追加します。以下に示します。
@Table("todo")
data class ToDo(
@Id
val id: Long = 0,
val title: String,
)
interface ToDoRepository : CoroutineCrudRepository<ToDo, Long>
@RestController
class Controller(
val todoRepo: ToDoRepository,
// ...
) {
@GetMapping("/test")
suspend fun test(): String {
// ...
// 保存
val entry = ToDo(0,"Springboot3 + WebFlux + Kotlin ")
todoRepo.save(entry)
// トレース対象の DB 呼び出しサンプル
val dbtodos = todoRepo.findAll().toList()
// ...
return "${dbtodos.size} $externalTodos"
}
}
エンドポイントを呼び出すと、さらに 1 つのスパンが追加されます。新しいスパンは query という名前で、Spring Data R2DBC によって実行された SQL クエリを含む複数のタグが含まれています。
結論¶
Micrometer と新しい分散型トレーシング拡張機能は、Spring Boot 3 以降の可観測性技術スタックを統一します。異なる企業やその技術スタックで使用されるさまざまな分散型トレーシングソリューションに対して、優れた抽象化を提供します。そのため、開発者の作業が簡素化されます。
Spring WebFlux のリアクティブプログラミング、特に Kotlin に関しては、まだ改善の余地があります。Micrometer チームは Project Reactor(Spring WebFlux が使用するリアクティブライブラリ)の背後にあるチームと積極的に協議を行い、リアクティブ技術スタックにおける Micrometer Tracing の使用を簡素化することを目指しています。




