Spring Cloud OpenFeignによる宣言的HTTP通信とリトライ制御の実装

OpenFeignの基本概念とSpring Cloud統合

OpenFeignは、Netflix OSSのFeignをベースにSpring Cloudチームが拡張した宣言型Webサービスクライアントです。従来のHTTPクライアントライブラリが要求する冗長なテンプレートコードや、URL構築・シリアライゼーション処理の記述を廃し、JavaインターフェースとアノテーションのみでリモートAPI呼び出しを定義できます。

標準Feignからの主な拡張点

  • Spring Cloudネイティブな統合: サービスレジストリやクライアントサイドロードバランサーとの連携が標準で組み込まれており、サービス名のみでの動的ルーティングが可能です。
  • @FeignClientアノテーション: 宣言的インターフェースをIoCコンテナに自動登録するための専用アノテーションを導入し、設定の簡素化とスキャン機能を実現しています。
  • 高度なエラーハンドリング: ErrorDecoderによるHTTPステータスコードの解釈や、フォールバックメカニズムの組み込みにより、障害時の制御フローが柔軟になります。
  • 詳細な構成オプション: 接続タイムアウト、読み込みタイムアウト、リトライポリシーなど、通信層の挙動を外部設定ファイルで細かく制御できます。

環境構築とクライアント定義の手順

1. 依存モジュールの導入

サービスディスカバリとロードバランシング、OpenFeign本体を組み込む標準的な依存関係は以下の通りです。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. アプリケーション設定の定義

application.ymlにおいて、サービスディスカバリ先とFeignのデフォルト通信パラメータを指定します。

spring:
  application:
    name: order-processing-client
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.100:8848
        username: admin
        password: secure_pass
    openfeign:
      client:
        config:
          default:
            connect-timeout: 2000
            read-timeout: 3000
            retryer: com.example.cloud.feign.config.LinearBackoffRetryer
server:
  port: 9090

3. クライアントの活性化とインターフェース実装

エントリポイントクラスに@EnableFeignClientsを付与し、スキャン対象パッケージを登録します。

@SpringBootApplication
@EnableFeignClients(basePackages = "com.example.cloud.feign.api")
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

続いて、リモートサービスに対応するインターフェースを定義します。

@FeignClient(name = "inventory-service", path = "/api/v1")
public interface InventoryApiClient {
    @GetMapping("/stock/check")
    StockStatusResponse queryStockLevel(@RequestParam("skuCode") String skuCode);
}

定義したクライアントは、通常のSpringコンポーネントとしてインジェクション可能です。

@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
    private final InventoryApiClient inventoryClient;

    @PostMapping("/validate")
    public ResponseEntity<Boolean> validateOrder(@RequestBody OrderRequest request) {
        StockStatusResponse stock = inventoryClient.queryStockLevel(request.getSkuCode());
        return ResponseEntity.ok(stock.isAvailable());
    }
}

ネットワーク障害への耐性強化:リトライ機構

分散システムでは、ネットワーク分断、対象サービスのGC一時停止、データベースのロック競合などにより、一時的な通信遅延や切断が頻繁に発生します。OpenFeignのデフォルト設定ではリトライは無効化されており、初回リクエストが失敗した時点で即座に例外をスローします。信頼性を確保するには、以下の設定手順が必要です。

デフォルトリトライポリシーの登録

feign.Retryerインターフェースの実装クラスをSpring Beanとして登録することで、フレームワークが自動的に適用します。

@Configuration
public class FeignClientConfiguration {
    @Bean
    public Retryer standardRetryer() {
        // 初期待機時間500ms, 最大待機時間2000ms, 最大試行回数4回
        return new Retryer.Default(500L, 2000L, 4);
    }
}

カスタムリトライ戦略の構築

業務要件に応じて、固定間隔ではなく指数関数的バックオフやジッター(揺らぎ)を導入する場合は、Retryerインターフェースを直接実装します。特にcontinueOrPropagate(RetryableException e)メソッドのオーバーライドが制御の中心となります。

実装例:線形バックオフ付きリトライハンドラ

以下のコードでは、試行回数に応じて待機時間を線形に増加させ、スレッド割り込みが発生した場合は元の例外を再送出するロジックを採用しています。

package com.example.cloud.feign.config;

import feign.RetryableException;
import feign.Retryer;
import java.util.concurrent.atomic.AtomicInteger;

public class LinearBackoffRetryer implements Retryer {
    private static final int MAX_RETRIES = 5;
    private static final long BASE_DELAY_MS = 800L;

    private final AtomicInteger attemptCounter = new AtomicInteger(0);

    public LinearBackoffRetryer() {
        // デフォルトコンストラクタ
    }

    @Override
    public void continueOrPropagate(RetryableException exception) {
        int currentAttempt = attemptCounter.incrementAndGet();
        if (currentAttempt > MAX_RETRIES) {
            throw exception;
        }

        long delayMs = BASE_DELAY_MS * currentAttempt;
        try {
            Thread.sleep(delayMs);
        } catch (InterruptedException interruptedException) {
            Thread.currentThread().interrupt();
            throw exception;
        }
    }

    @Override
    public Retryer clone() {
        // 各リクエストごとに独立した状態を持つため、新しいインスタンスを返す
        return new LinearBackoffRetryer();
    }
}

設定ファイルへの適用

作成したクラスをapplication.ymlretryerプロパティに指定することで、デフォルトの挙動を上書きできます。

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            retryer: com.example.cloud.feign.config.LinearBackoffRetryer

フレームワーク内部の制御フロー

OpenFeignのリトライ処理は、プロキシ生成層とHTTP実行層の連携によって実現されています。内部的にはハンドラークラスが中心となり、以下のサイクルで実行されます。

  1. リクエストテンプレートの構築: メソッド引数からRequestTemplateを生成します。
  2. 実行と例外キャッチ: 設定されたHTTPクライアントを通じてリクエストを送信します。通信エラーや特定のエラーレスポンスが発生した場合、RetryableExceptionがスローされます。
  3. リトライ判定: 捕捉した例外を登録済みのRetryerインスタンスに渡します。この際、フレームワークは毎回retryer.clone()を呼び出し、試行状態が混線しないようにリクエストごとの独立したスコープを確保します。
  4. 待機または例外伝播: continueOrPropagate内で再試行が可能であれば指定時間スリープし、不可能であれば元例外を呼び出し元に伝播させます。

この動作原理を概念的に表現すると、以下の制御構造に集約されます。

public Object handleInvocation(Object[] args) {
    RequestTemplate template = buildTemplate(args);
    Retryer localRetryer = globalRetryer.clone(); // 状態の分離
    try {
        return executeRequest(template);
    } catch (RetryableException failure) {
        // リトライポリシーに委譲し、待機または例外の再スローを判断
        localRetryer.continueOrPropagate(failure);
        // 再帰的またはループ構造で再実行
        return handleInvocation(args);
    } catch (Exception other) {
        throw other;
    }
}

設計上の技術的留意点

マイクロサービス間の通信方式としてRestTemplateとOpenFeignが挙げられますが、前者はプログラム的なURL構築とレスポンスマッピングが必要であり、後者は契約駆動開発のアプローチを採用しているため、API定義と実装の分離が容易です。

リトライ設定においては、connect-timeout(TCPハンドシェイクの確立待ち時間)とread-timeout(レスポンスデータ到着待ち時間)の両方を業務のSLAに合わせて調整する必要があります。また、リトライ回数を無制限に設定すると、下流サービスへの過剰な負荷や循環参照によるスタックオーバーフローを招く可能性があるため、指数バックオフと最大試行回数の上限設定は必須の設計要素です。

タグ: spring-cloud-openfeign feign-retryer nacos-service-discovery java-microservices declarative-http-client

7月13日 20:44 投稿