マイクロサービスアーキテクチャでは、1つのリクエストが複数のサービスを横断するため、問題発生時の原因特定が困難になります。分散トレーシングは、リクエストの流れを可視化し、パフォーマンスボトルネックやエラーの発生箇所を特定するための重要な手法です。本記事では、Spring Boot 3.xでMicrometer TracingとZipkinを使用した分散トレーシングの実装方法を解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Micrometer Tracing 1.4.x
Zipkin 3.x
ビルドツール Gradle
IDE VS Code(Extension Pack for Javaインストール済み)
Docker Docker Desktop(Zipkin起動用)

また、本記事はSpring Boot REST API入門 - @RestControllerでCRUDエンドポイントを実装するの続編として、基本的なSpring Boot REST APIの実装経験があることを前提としています。

期待される学習成果

本記事を読み終えると、以下のことができるようになります。

  • 分散トレーシングの概念(Trace ID、Span ID)を理解できる
  • Micrometer TracingとBraveを使用したトレーシング設定ができる
  • Zipkinにトレース情報を送信し、可視化できる
  • ログにTrace IDを自動付与し、ログとトレースを関連付けられる
  • RestClientを使用したサービス間呼び出しでトレースを伝播できる

分散トレーシングの基本概念

分散トレーシングとは

分散トレーシングは、分散システム内でリクエストがどのように処理されているかを追跡する技術です。従来のモノリシックアプリケーションでは、ログを時系列で追うことで処理の流れを把握できました。しかし、マイクロサービス環境では複数のサービスが連携するため、個別のログだけでは全体像を把握できません。

分散トレーシングでは、以下の2つの識別子を使用してリクエストを追跡します。

graph LR
    subgraph "Trace ID: abc123"
        A[Client] -->|Span A| B[API Gateway]
        B -->|Span B| C[User Service]
        B -->|Span C| D[Order Service]
        D -->|Span D| E[Database]
    end

Trace IDとSpan IDの仕組み

分散トレーシングでは、2つの重要な識別子を使用します。

識別子 説明
Trace ID リクエスト全体を一意に識別する32文字の16進数 803B448A0489F84084905D3093480352
Span ID 個別の処理単位を識別する16文字の16進数 3425F23BB2432450

Trace IDは最初のリクエストで生成され、後続のすべてのサービス呼び出しに伝播されます。各サービスは独自のSpan IDを生成し、処理の開始時刻、終了時刻、メタデータを記録します。

Micrometer Tracingの役割

Micrometer Tracingは、トレーシングのファサード(抽象化レイヤー)を提供するライブラリです。Spring Cloud Sleuthの後継として開発され、以下の特徴があります。

  • ベンダーロックインの回避(OpenTelemetry、Braveなど複数のトレーサーに対応)
  • Spring Boot Actuatorとの統合
  • Micrometer Observationとの連携によるメトリクスとトレースの統合

本記事では、トレーサー実装としてOpenZipkin Braveを使用し、バックエンドとしてZipkinを使用します。

Micrometer Tracingの設定

依存関係の追加

Spring Boot 3.xでMicrometer TracingとBraveを使用するには、以下の依存関係を追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// build.gradle
dependencies {
    // Spring Boot Actuator(必須)
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    
    // Micrometer Tracing Bridge for Brave
    implementation 'io.micrometer:micrometer-tracing-bridge-brave'
    
    // Zipkin Reporter for Brave
    implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
}

依存関係の構成を図で示すと以下のようになります。

graph TB
    A[Spring Boot Application] --> B[Micrometer Tracing]
    B --> C[micrometer-tracing-bridge-brave]
    C --> D[Brave Tracer]
    D --> E[zipkin-reporter-brave]
    E --> F[Zipkin Server]

アプリケーション設定

application.ymlでトレーシングの設定を行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# application.yml
spring:
  application:
    name: user-service  # サービス名(Zipkinで表示される)

management:
  tracing:
    sampling:
      probability: 1.0  # サンプリング率(1.0 = 100%、本番では0.1程度を推奨)
    export:
      zipkin:
        endpoint: http://localhost:9411/api/v2/spans  # Zipkinのエンドポイント
プロパティ 説明 推奨値
spring.application.name サービス識別名 サービスごとに一意の名前
management.tracing.sampling.probability トレースのサンプリング率 開発: 1.0、本番: 0.1
management.tracing.export.zipkin.endpoint Zipkinのエンドポイント Zipkinサーバーのアドレス

サンプリング率はパフォーマンスとトレース収集量のトレードオフです。開発環境ではすべてのリクエストを収集し、本番環境では10%程度に抑えることを推奨します。

Zipkinへのトレース情報送信

Zipkinサーバーの起動

Zipkinはトレース情報を収集・可視化するためのオープンソースツールです。Dockerを使用して簡単に起動できます。

1
2
# Zipkinサーバーの起動
docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin

起動後、ブラウザでhttp://localhost:9411にアクセスするとZipkin UIが表示されます。

サンプルAPIの実装

トレーシングの動作を確認するため、シンプルなREST APIを実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        logger.info("Getting user with id: {}", id);
        
        // 処理をシミュレート
        simulateProcessing();
        
        return new User(id, "John Doe", "john@example.com");
    }

    private void simulateProcessing() {
        try {
            Thread.sleep(100);  // 100msの処理時間をシミュレート
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
1
2
3
4
package com.example.demo.controller;

public record User(Long id, String name, String email) {
}

トレースの確認

アプリケーションを起動し、APIにリクエストを送信します。

1
curl http://localhost:8080/api/users/1

Zipkin UI(http://localhost:9411)で「Run Query」ボタンをクリックすると、収集されたトレースが表示されます。トレースをクリックすると、以下の情報を確認できます。

  • サービス名とオペレーション名
  • 処理時間
  • Span間の親子関係
  • タグ情報(HTTPメソッド、ステータスコードなど)

ログへのTrace ID自動付与

ログ相関の設定

Spring Boot 3.xでは、Micrometer Tracingを設定するとログにTrace IDとSpan IDが自動的に付与されます。デフォルトのログフォーマットでは[traceId-spanId]の形式で出力されます。

ログパターンをカスタマイズする場合は、application.ymlで設定します。

1
2
3
4
5
6
7
8
# application.yml
logging:
  pattern:
    correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "
  include-application-name: false
  level:
    root: INFO
    com.example.demo: DEBUG

この設定により、ログ出力は以下のような形式になります。

1
[user-service,803B448A0489F84084905D3093480352,3425F23BB2432450] INFO  c.e.demo.controller.UserController - Getting user with id: 1

Logback設定のカスタマイズ

より詳細なログフォーマットが必要な場合は、logback-spring.xmlを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!-- src/main/resources/logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
    <springProperty scope="context" name="appName" source="spring.application.name"/>
    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [${appName},%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

ログとトレースの関連付けによるメリット

ログにTrace IDを付与することで、以下のメリットが得られます。

  1. 問題の迅速な特定: エラーログからTrace IDを取得し、Zipkinで関連するすべてのサービスのトレースを確認できる
  2. ログ集約ツールとの連携: ELKスタックやDatadogなどのログ集約ツールでTrace IDによる検索が可能
  3. 監査証跡: 特定のリクエストに関するすべてのログを追跡可能

サービス間呼び出しのトレース伝播

RestClientを使用したトレース伝播

マイクロサービス間でトレース情報を伝播するには、Spring Bootが提供するRestClient.Builderを使用する必要があります。自動構成されたビルダーを使用することで、トレース情報が自動的にHTTPヘッダーに追加されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        // 自動構成されたBuilderを使用することが重要
        return builder
                .baseUrl("http://localhost:8081")
                .build();
    }
}

以下は、別サービスを呼び出すサービスクラスの実装例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.demo.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class OrderService {

    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
    private final RestClient restClient;

    public OrderService(RestClient restClient) {
        this.restClient = restClient;
    }

    public Order getOrderWithUserDetails(Long orderId) {
        logger.info("Fetching order with id: {}", orderId);
        
        // 外部サービス(User Service)を呼び出し
        // Trace IDが自動的にHTTPヘッダーに追加される
        User user = restClient.get()
                .uri("/api/users/{id}", 1L)
                .retrieve()
                .body(User.class);
        
        logger.info("Retrieved user: {}", user.name());
        
        return new Order(orderId, user, "COMPLETED");
    }
}
1
2
3
4
5
6
7
package com.example.demo.service;

public record Order(Long id, User user, String status) {
}

public record User(Long id, String name, String email) {
}

トレース伝播の仕組み

トレース情報は、W3C Trace Context標準に基づいてHTTPヘッダーで伝播されます。

sequenceDiagram
    participant C as Client
    participant A as Order Service
    participant B as User Service

    C->>A: GET /orders/1
    Note over A: Trace ID: abc123<br/>Span ID: span1 (新規生成)
    A->>B: GET /users/1<br/>traceparent: 00-abc123-span1-01
    Note over B: Trace ID: abc123 (継続)<br/>Span ID: span2 (新規生成)
    B-->>A: User Response
    A-->>C: Order Response
ヘッダー 説明
traceparent W3C Trace Context標準のヘッダー 00-abc123...-span1...-01
b3 B3形式のヘッダー(レガシー対応) abc123...-span1...-1

カスタムSpanの作成

特定の処理を個別のSpanとして追跡したい場合は、ObservationRegistryを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.demo.service;

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
    private final ObservationRegistry observationRegistry;

    public PaymentService(ObservationRegistry observationRegistry) {
        this.observationRegistry = observationRegistry;
    }

    public PaymentResult processPayment(Long orderId, Double amount) {
        // カスタムObservation(Span)の作成
        return Observation.createNotStarted("payment.process", observationRegistry)
                .lowCardinalityKeyValue("orderId", orderId.toString())
                .lowCardinalityKeyValue("paymentMethod", "CREDIT_CARD")
                .observe(() -> {
                    logger.info("Processing payment for order: {}", orderId);
                    
                    // 決済処理
                    validatePayment(amount);
                    executePayment(orderId, amount);
                    
                    return new PaymentResult(orderId, "SUCCESS", amount);
                });
    }

    private void validatePayment(Double amount) {
        Observation.createNotStarted("payment.validate", observationRegistry)
                .observe(() -> {
                    logger.info("Validating payment amount: {}", amount);
                    // バリデーション処理
                });
    }

    private void executePayment(Long orderId, Double amount) {
        Observation.createNotStarted("payment.execute", observationRegistry)
                .observe(() -> {
                    logger.info("Executing payment for order: {}", orderId);
                    // 決済実行処理
                    try {
                        Thread.sleep(200);  // 処理時間をシミュレート
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
    }
}
1
2
3
4
package com.example.demo.service;

public record PaymentResult(Long orderId, String status, Double amount) {
}

この実装により、Zipkinで以下のような階層構造のSpanが表示されます。

gantt
    title Payment Processing Trace
    dateFormat X
    axisFormat %L ms

    section Spans
    payment.process    :0, 300
    payment.validate   :10, 50
    payment.execute    :60, 260

本番環境での運用設定

サンプリング戦略

本番環境では、すべてのリクエストをトレースするとストレージコストとパフォーマンスに影響が出ます。適切なサンプリング戦略を設定しましょう。

1
2
3
4
5
# application-prod.yml
management:
  tracing:
    sampling:
      probability: 0.1  # 10%のリクエストのみトレース

エラーが発生したリクエストを必ずトレースしたい場合は、カスタムサンプラーを実装できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demo.config;

import brave.sampler.Sampler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class TracingConfig {

    @Bean
    @Profile("prod")
    public Sampler productionSampler() {
        // 本番環境では10%のリクエストをサンプリング
        return Sampler.create(0.1f);
    }

    @Bean
    @Profile("!prod")
    public Sampler developmentSampler() {
        // 開発環境ではすべてのリクエストをトレース
        return Sampler.ALWAYS_SAMPLE;
    }
}

機密情報のマスキング

HTTPヘッダーやパラメータに含まれる機密情報がトレースに記録されないよう注意が必要です。

1
2
3
4
5
6
7
8
9
# application.yml
management:
  tracing:
    baggage:
      remote-fields:
        - x-request-id  # 伝播するカスタムヘッダー
      correlation:
        fields:
          - x-request-id  # MDCに追加するフィールド

ヘルスチェックとの統合

Actuatorのヘルスチェックエンドポイントで、Zipkin接続状態を確認できます。

1
2
3
4
5
6
7
8
9
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  health:
    defaults:
      enabled: true

トラブルシューティング

よくある問題と解決策

問題 原因 解決策
トレースがZipkinに表示されない サンプリング率が0 management.tracing.sampling.probabilityを確認
Span IDがログに表示されない MDC設定の不備 ログパターンに%X{traceId}を追加
サービス間でTrace IDが伝播しない RestClientの設定不備 自動構成されたRestClient.Builderを使用
Zipkin接続エラー エンドポイントURL誤り management.tracing.export.zipkin.endpointを確認

デバッグログの有効化

トレーシングの問題を調査する際は、関連するデバッグログを有効化します。

1
2
3
4
5
6
# application.yml
logging:
  level:
    io.micrometer.tracing: DEBUG
    brave: DEBUG
    zipkin2: DEBUG

まとめ

本記事では、Spring Boot 3.xでMicrometer TracingとZipkinを使用した分散トレーシングの実装方法を解説しました。

主要なポイントを整理すると以下のとおりです。

  • 分散トレーシングの基本: Trace IDとSpan IDを使用してリクエストの流れを追跡する
  • Micrometer Tracingの設定: micrometer-tracing-bridge-bravezipkin-reporter-braveを依存関係に追加する
  • ログ相関: 自動的にTrace IDがログに付与され、ログとトレースを関連付けられる
  • トレース伝播: 自動構成されたRestClient.Builderを使用することで、サービス間でトレース情報が自動伝播される
  • カスタムSpan: ObservationRegistryを使用して、独自の処理単位をトレースできる

分散トレーシングを導入することで、マイクロサービス環境での問題調査が大幅に効率化されます。本番環境では適切なサンプリング率を設定し、パフォーマンスとトレーサビリティのバランスを取ることが重要です。

参考リンク