フォールバック パターン: マイクロサービスでのグレースフル デグラデーションの設計

フォールバック パターン: マイクロサービスでのグレースフル デグラデーションの設計

マイクロサービス アーキテクチャでは、サービスは分散ネットワーク呼び出しの Web を形成します。これにより、チームはサービスを独立して構築および拡張できますが、システム全体の信頼性がその最も弱い部分と同じ程度にしか強くないことも意味します。重要なサービスがダウンしたり応答しなくなったりすると、アプリケーション全体が中断される連鎖的な障害が引き起こされる可能性があります。

単一の依存関係が失敗した瞬間に一般的な「500 Internal Server Error」または空白のページがユーザーに返されるのは、ユーザー エクスペリエンスが低下します。代わりに、回復力のあるシステムは、問題が発生した場合に正常に機能を低下させるように構築されています。

ここで、フォールバック パターンが登場します。プライマリ サービス呼び出しが失敗した場合に、安全な代替実行パスを定義することで、機能が低下した状態であってもアプリケーションの機能を維持できます。

このガイドでは、フォールバック パターン、フォールバック パターンを実装するための一般的な戦略、フォールバック パターンが他の復元パターンとどのように相互作用するか、Java (Resilience4j) と Go でフォールバック ロジックを記述する方法について説明します。


現実世界の例え: コーヒーショップのバックアップ計画

ラテを買うために地元のコーヒーショップに入ったと想像してください。バリスタが注文を入力しますが、カードをタップしようとすると、支払い端末が接続エラーを点滅させます。店のインターネットプロバイダーが停止しているためです。

コーヒーショップはすぐに照明を消し、ドアに鍵をかけて、すべての顧客を家に送り返しますか?

もちろん違います。彼らは フォールバック戦略 を実装しています。 ※現金を持っている場合は、現金で支払えるか尋ねられます。 ※常連のお客様の場合、バリスタがお客様のお名前と注文を台帳に記載し、次回来店時にお支払いをお願いする場合がございます。

  • カード トークンをローカルに保存し、後でインターネットが回復したときに支払いを処理するオフライン カード リーダーを使用する場合があります。
プライマリ サービスの障害と、キャッシュ/バックアップ データを取得するためのフォールバック ハンドラーへのルーティングを示すフォールバック パターン アーキテクチャ図

ソフトウェア設計では:

  • コーヒーの注文はクライアントのリクエストです。
  • カード ターミナルは、主要なダウンストリーム サービス (ペイメント ゲートウェイ API など) です。
  • インターネットの停止は、ネットワークのタイムアウトまたはサービスのクラッシュです。
  • レジャー/オフライン リーダーはフォールバック実行パスです。

一般的なフォールバック戦略

ビジネス ロジックと失敗したサービスの重要度に応じて、いくつかのフォールバック戦略から選択できます。

1. 静的なデフォルト値

最も単純な戦略は、事前に構成された安全な静的な値を返すことです。これは、空白またはデフォルトのデータの表示が許容される、重要ではない機能の場合に非常に効果的です。

  • : プロファイル パーソナライゼーション サービスが失敗した場合、デフォルトのアバター画像と一般的な挨拶が返されます。
  • : レコメンデーション サービスが失敗した場合は、エラーをスローするのではなく、空のリストまたはユニバーサル ベストセラーのハードコードされたリストを返します。

2. キャッシュされた応答 (再検証中に失効)

ライブ データが利用できない場合は、ローカル キャッシュまたは Redis などの高速分散メモリ ストアからの読み取り専用の古いデータにフォールバックできます。

  • : 製品在庫サービスがダウンした場合、5 分前からキャッシュされた在庫数量が表示され、データが完全に最新ではない可能性があることを示す微妙な UI メッセージが表示されます。
  • : ユーザー設定サービスが失敗した場合、ログイン フローをブロックする代わりに、キャッシュされたユーザー プロファイルをロードします。

3. 代替サービス (マルチプロバイダー)

成功する必要がある重要な操作を実行する場合、セカンダリ サービス プロバイダーをバックアップとして構成できます。

  • : プライマリ支払いゲートウェイ (Stripe など) が 5xx エラーを返すかタイムアウトした場合、フォールバック メカニズムはトランザクション リクエストを直ちにセカンダリ ゲートウェイ (PayPal や Adyen) にリダイレクトします。
  • : ジオコーディング API が失敗した場合は、セカンダリ マッピング プロバイダーにフォールバックします。

4. 後でキューに入れる (非同期バッファ)

即時の同期処理を必要としない書き込み操作の場合、フォールバックはリクエストをローカル キューまたはデータベースにバッファリングして、後で再試行できます。

  • : 電子メール通知サービスがダウンしている場合は、通知ペイロードを配信不能キューまたはローカル データベース テーブルに書き込みます。通知サービスが再び正常になると、バックグラウンド ワーカーがこのキューから読み取り、電子メールを配信します。

レジリエンス トリオ: 再試行 vs. サーキット ブレーカー vs. フォールバック

復元力の高いアーキテクチャを構築するには、フォールバック パターンを 再試行 および サーキット ブレーカー パターンと組み合わせる必要があります。これらは 3 層の防御線を形成します。

パターン 役割 アクション シナリオ
再試行パターン 短期間の一時的なグリッチを解決します。 短い遅延 (バックオフ + ジッター) の後にリクエストを繰り返します。 ネットワーク パケットが短時間ドロップされ、ソケットがリセットされます。
サーキットブレーカー リソースの枯渇を防ぎます。 トリップを開いて、障害が発生したサービスへの呼び出しをただちにブロックします (フェイルファスト)。 永続的なサービスのダウンタイム、データベースのデッドロック。
フォールバック パターン ユーザーエクスペリエンスを維持します。 プライマリ コールが失敗するかブロックされた場合に、代替アクションを実行します。 リトライ回数がなくなったとき、またはサーキットブレーカーが開いたとき。

連携方法

  1. 受信リクエストがサービス ゲートウェイに到達します。
  2. リクエストは サーキット ブレーカー を通過します。
  3. サーキット ブレーカーが閉じている場合、リクエストは Retry ラッパーに送られ、実際の呼び出しが行われます。
  4. 一時的なエラーが発生した場合、再試行メカニズムによって呼び出しが再度試行されます。
  5. すべての再試行が失敗した場合、またはサーキット ブレーカーがすでに開いていた場合 (リソースの保存にすぐに失敗した場合)、フォールバック ハンドラー が失敗をインターセプトし、劣化/キャッシュされた応答を返します。

コードの実装

Java と Go の両方でフォールバック ロジックを実装する方法を見てみましょう。

1. Java (Resilience4j)

Java では、Resilience4j がフォールト トレランスの業界標準です。フォールバック メソッドは、アノテーションを使用して宣言的に、またはプログラム的に定義できます。

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;

@Service
public class ProductService {

    private final InventoryClient inventoryClient;
    private final CacheManager cacheManager;

    public ProductService(InventoryClient inventoryClient, CacheManager cacheManager) {
        this.inventoryClient = inventoryClient;
        this.cacheManager = cacheManager;
    }

    // Bind this method to a circuit breaker. If it fails or is open, route to fallback
    @CircuitBreaker(name = "inventoryService", fallbackMethod = "getInventoryFallback")
    public List<String> getProductInventory(String category) {
        return inventoryClient.fetchStockByCategory(category);
    }

    // Fallback method must have the same return type and accept the same parameters,
    // plus a Throwable parameter containing the error that triggered it
    public List<String> getInventoryFallback(String category, Throwable throwable) {
        System.err.println("Primary inventory service failed: " + throwable.getMessage());
        
        // Attempt to fetch from local cache (Strategy 2)
        List<String> cachedStock = cacheManager.get("inventory:" + category, List.class);
        if (cachedStock != null) {
            return cachedStock;
        }

        // Return static empty list if cache is empty (Strategy 1)
        return Collections.emptyList();
    }
}

2. Go (Golang)

Go では、関数型プログラミング パターンを使用してクリーンなフォールバック デコレーターを作成できます。これにより、任意の実行ハンドラーを 2 番目のハンドラーでラップできます。

package main

import (
	"context"
	"errors"
	"fmt"
	"time"
)

// Request represents a simple payload
type Request struct {
	UserID string
}

// Response represents the returned data
type Response struct {
	Data   string
	Status string
}

// ServiceFunc represents our primary function signature
type ServiceFunc func(ctx context.Context, req Request) (Response, error)

// WithFallback wraps a service function with fallback logic
func WithFallback(primary ServiceFunc, fallback ServiceFunc) ServiceFunc {
	return func(ctx context.Context, req Request) (Response, error) {
		res, err := primary(ctx, req)
		if err != nil {
			fmt.Printf("[Warning] Primary call failed: %v. Running fallback...\n", err)
			return fallback(ctx, req)
		}
		return res, nil
	}
}

func main() {
	// 1. Define primary service that occasionally fails
	primaryService := func(ctx context.Context, req Request) (Response, error) {
		return Response{}, errors.New("database connection timeout (504)")
	}

	// 2. Define fallback service that retrieves cached data
	fallbackService := func(ctx context.Context, req Request) (Response, error) {
		// Simulating cached read
		return Response{
			Data:   fmt.Sprintf("Stale cached profile data for user %s", req.UserID),
			Status: "DEGRADED (CACHED)",
		}, nil
	}

	// 3. Wrap them together
	resilientService := WithFallback(primaryService, fallbackService)

	// 4. Execute
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	req := Request{UserID: "user_992"}
	response, err := resilientService(ctx, req)
	if err != nil {
		fmt.Printf("Operation completely failed: %v\n", err)
	} else {
		fmt.Printf("Response Received:\n - Status: %s\n - Data: %s\n", response.Status, response.Data)
	}
}

フォールバック パターンのベスト プラクティス

  1. フォールバック パスの依存関係をなくす: フォールバック ロジックは、障害が発生した同じダウンストリーム システムまたはインフラストラクチャに依存してはなりません。データベースがダウンしている場合、同じサーバー上の別のデータベース クエリにフォールバックすることも失敗する可能性があります。
  2. 高速実行: フォールバック ロジックは迅速に実行される必要があります。フォールバック パスでの複雑な計算や低速のネストされた呼び出しを避けてください。目標は、できるだけ早くユーザーに応答を返すことです。
  3. アラートと監視: フォールバック実行を常にログに記録し、テレメトリ カウンターを増加します。システムがフォールバック パスを実行している場合、システムが低下した状態で動作していることを意味します。フォールバック率が通常のしきい値を超えたときを知るためには、アラートが必要です。
  4. UI の回復力を高める: フロントエンド エンジニアと緊密に連携して、クライアント側のレイアウトを壊さずにフォールバック ペイロード (空のコレクションや部分的な状態など) を受け入れるように UI が設計されていることを確認します。

## 結論

フォールバック パターン は、マイクロサービスの信頼性を確保するための究極の保険です。障害を予測し、エレガントなフォールバック ロジックを提供することで、ハード クラッシュを微妙で管理可能な中断に変えることができます。このパターンを 再試行 および サーキット ブレーカー と組み合わせることで、アプリケーションはユーザーにサービスを提供し続けながら、大規模な外部停止に耐えることができます。