Goマイクロサービスレジリエンス実践:サーキットブレーカーからレート制限までの5つのプロダクションパターン

云原生

カスケード障害が雪崩効果に出会う時:マイクロサービスの至暗の刻

金曜日のピーク時、決済サービスの応答が遅くなる。上流の注文サービスにはタイムアウト制御がなく、全リクエストが決済呼び出しでブロックされ、goroutineが50万に積み上がる。5分後、注文サービスがOOMでクラッシュし、ユーザーサービスも続いてダウン、コールチェーン全体が雪崩する。皮肉なことに、決済サービスは30秒後に復旧していた——しかし誰もアクセスできなくなっていた。

これは脅しではない。マイクロサービスアーキテクチャでは、1つの遅いノードがコールチェーン全体を引きずり下ろす。サーキットブレーカーで障害の波及を防ぎ、レート制限で下流を保護し、リトライで一時的な障害に対応し、タイムアウトで無限待機を避け、バルクヘッドで障害ドメインを分離する必要がある。本記事では5つのプロダクション級レジリエンスパターンを解説し、アンチフラジャイルなGoマイクロサービスの構築を支援する。


コア概念クイックリファレンス

パターン コア思想 主要ライブラリ 典型的なシナリオ
サーキットブレーカー 閾値到達で呼び出しを遮断、下流を保護 sony/gobreaker 下流サービス不可時に高速フェイル
レート制限 リクエストレートを制御、過負荷を防止 golang.org/x/time/rate APIレート制限、リソースクォータ制御
リトライ(バックオフ付き) 一時的障害の自動リトライ、指数バックオフでリトライストーム回避 カスタム実装 ネットワークジッター、一時的エラー
タイムアウト制御 1回の呼び出しの最大時間を制限 context.Context 全ての外部呼び出しにタイムアウト必須
バルクヘッド 異なる呼び出し先のリソースを分離、相互影響を防止 カスタム実装 複数下流呼び出しのリソース分離

目次

  1. マイクロサービスレジリエンスアーキテクチャ概要
  2. パターン1:サーキットブレーカー——sony/gobreaker実践
  3. パターン2:レート制限——トークンバケットとスライディングウィンドウ
  4. パターン3:リトライ——指数バックオフとジッター
  5. パターン4:タイムアウト制御——context.Context実践
  6. パターン5:バルクヘッド分離——リソースパーティショニング
  7. 5つのよくある落とし穴
  8. 10のエラートラブルシューティング
  9. 高度な最適化テクニック
  10. レジリエンスパターン比較
  11. おすすめツール
  12. まとめと参考資料

マイクロサービスレジリエンスアーキテクチャ概要

                    ┌─────────────────────────────────────────────┐
                    │           Client (HTTP/gRPC)                │
                    └──────────────────┬──────────────────────────┘
                                       │
                    ┌──────────────────▼──────────────────────────┐
                    │          API Gateway / BFF                  │
                    │  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
                    │  │ Rate     │  │ Circuit  │  │ Timeout  │ │
                    │  │ Limiter  │  │ Breaker  │  │ Control  │ │
                    │  └──────────┘  └──────────┘  └──────────┘ │
                    └──────────────────┬──────────────────────────┘
                                       │
              ┌────────────────────────┼────────────────────────┐
              │                        │                        │
    ┌─────────▼──────────┐  ┌─────────▼──────────┐  ┌─────────▼──────────┐
    │   Service A        │  │   Service B        │  │   Service C        │
    │  ┌──────────────┐  │  │  ┌──────────────┐  │  │  ┌──────────────┐  │
    │  │  Bulkhead    │  │  │  │  Bulkhead    │  │  │  │  Bulkhead    │  │
    │  │  ┌───┐ ┌───┐ │  │  │  │  ┌───┐ ┌───┐ │  │  │  │  ┌───┐ ┌───┐ │  │
    │  │  │ P1│ │ P2│ │  │  │  │  │ P1│ │ P2│ │  │  │  │  │ P1│ │ P2│ │  │
    │  │  └───┘ └───┘ │  │  │  │  └───┘ └───┘ │  │  │  │  └───┘ └───┘ │  │
    │  └──────────────┘  │  │  └──────────────┘  │  │  └──────────────┘  │
    │  ┌──────────────┐  │  │  ┌──────────────┐  │  │  ┌──────────────┐  │
    │  │  Retry with  │  │  │  │  Retry with  │  │  │  │  Retry with  │  │
    │  │  Backoff     │  │  │  │  Backoff     │  │  │  │  Backoff     │  │
    │  └──────────────┘  │  │  └──────────────┘  │  │  └──────────────┘  │
    └────────────────────┘  └────────────────────┘  └────────────────────┘
              │                        │                        │
    ┌─────────▼──────────┐  ┌─────────▼──────────┐  ┌─────────▼──────────┐
    │     Database       │  │    Cache (Redis)    │  │   Message Queue    │
    └────────────────────┘  └────────────────────┘  └────────────────────┘

レジリエンス4原則

  • フェイルファスト:サーキットブレーカーが開いたら即座にエラーを返し、タイムアウトを待たない
  • グレースフルデグラデーション:非コア機能の失敗時にフォールバックデータを返す
  • フォールトアイソレーション:バルクヘッドパターンで1つの障害が他の呼び出しに影響しないようにする
  • セルフヒーリング:サーキットブレーカーのハーフオープン状態で復旧をプローブ、リトライで一時的障害に対応

パターン1:サーキットブレーカー——sony/gobreaker実践

サーキットブレーカーはマイクロサービスレジリエンスの第一防線。下流サービスの障害率が閾値を超えると、ブレーカーが「跳閘」し、以降のリクエストは即座にエラーを返し、下流に回復時間を与える。

サーキットブレーカー状態遷移

         復旧率                    障害閾値
    ┌──────────────┐             ┌──────────────┐
    │              │             │              │
    │  Half-Open   │◄────────────│    Closed    │
    │  (プローブ)   │             │  (許可)      │
    │              │─────────────►│              │
    └──────┬───────┘  プローブOK  └──────────────┘
           │                       ▲
           │ プローブ失敗           │ タイムアウト後
           ▼                       │ 自動ハーフオープン
    ┌──────────────┐               │
    │              │───────────────┘
    │    Open      │
    │  (拒否)      │
    │              │
    └──────────────┘

完全実装

package circuitbreaker

import (
    "fmt"
    "time"

    "github.com/sony/gobreaker/v2"
)

type CircuitBreaker[T any] struct {
    cb *gobreaker.CircuitBreaker[T]
}

type Config struct {
    Name          string
    MaxRequests   uint32
    Interval      time.Duration
    Timeout       time.Duration
    FailThreshold uint32
    FailRatio     float64
}

func NewCircuitBreaker[T any](cfg Config) *CircuitBreaker[T] {
    settings := gobreaker.Settings{
        Name:        cfg.Name,
        MaxRequests: cfg.MaxRequests,
        Interval:    cfg.Interval,
        Timeout:     cfg.Timeout,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= cfg.FailThreshold && failureRatio >= cfg.FailRatio
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            fmt.Printf("CircuitBreaker '%s': %s -> %s\n", name, from, to)
        },
    }

    return &CircuitBreaker[T]{
        cb: gobreaker.NewCircuitBreaker[T](settings),
    }
}

func (cb *CircuitBreaker[T]) Execute(fn func() (T, error)) (T, error) {
    result, err := cb.cb.Execute(fn)
    if err != nil {
        var zero T
        if err == gobreaker.ErrOpenState {
            return zero, fmt.Errorf("circuit breaker open: %w", err)
        }
        if err == gobreaker.ErrTooManyRequests {
            return zero, fmt.Errorf("circuit breaker half-open: too many requests: %w", err)
        }
        return zero, err
    }
    return result, nil
}

func (cb *CircuitBreaker[T]) State() gobreaker.State {
    return cb.cb.State()
}

使用例——HTTPクライアントサーキットブレーカー

package httpclient

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/sony/gobreaker/v2"
)

type ResilientClient struct {
    client *http.Client
    cb     *gobreaker.CircuitBreaker[string]
}

func NewResilientClient() *ResilientClient {
    cb := gobreaker.NewCircuitBreaker[string](gobreaker.Settings{
        Name:        "http-client",
        MaxRequests: 3,
        Interval:    60 * time.Second,
        Timeout:     30 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 10 && failureRatio >= 0.6
        },
    })

    return &ResilientClient{
        client: &http.Client{Timeout: 10 * time.Second},
        cb:     cb,
    }
}

func (c *ResilientClient) Get(ctx context.Context, url string) (string, error) {
    result, err := c.cb.Execute(func() (string, error) {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return "", fmt.Errorf("create request: %w", err)
        }

        resp, err := c.client.Do(req)
        if err != nil {
            return "", fmt.Errorf("do request: %w", err)
        }
        defer resp.Body.Close()

        if resp.StatusCode >= 500 {
            return "", fmt.Errorf("server error: status %d", resp.StatusCode)
        }

        body, err := io.ReadAll(resp.Body)
        if err != nil {
            return "", fmt.Errorf("read body: %w", err)
        }
        return string(body), nil
    })

    if err != nil {
        return "", fmt.Errorf("resilient get %s: %w", url, err)
    }
    return result, nil
}

主要設計ポイント

  • ReadyToTripでトリップ条件をカスタマイズ:10リクエスト中60%失敗でトリップ
  • MaxRequestsでハーフオープン状態のプローブリクエスト数を制御
  • TimeoutでOpenからHalf-Openへの待機時間を制御
  • OnStateChangeコールバックでモニタリングとアラート
  • ジェネリクスサポート(Go 1.18+)、型安全な戻り値

パターン2:レート制限——トークンバケットとスライディングウィンドウ

レート制限は下流サービスを保護するコアメカニズム。golang.org/x/time/rateはトークンバケットアルゴリズムを実装——シンプルで効率的。

トークンバケットアルゴリズム

    リクエスト到着               トークンバケット
    ┌──────┐     ┌──────────────────────────┐
    │ Req1 │────►│  ● ● ● ● ● ○ ○ ○ ○ ○   │ ──► 許可(トークンあり)
    └──────┘     │  5/10 tokens             │
    ┌──────┐     │  r=10/s, burst=10        │
    │ Req6 │────►│  ● ● ● ● ● ○ ○ ○ ○ ○   │ ──► 許可(トークンあり)
    └──────┘     │  4/10 tokens             │
    ┌──────┐     │                          │
    │ Req11│────►│  ○ ○ ○ ○ ○ ○ ○ ○ ○ ○   │ ──► 拒否(トークンなし)
    └──────┘     │  0/10 tokens             │
                 └──────────────────────────┘
                      ▲
                      │ 毎秒10トークン補充
                      │ r = 10 tokens/sec
                      │ burst = 10 (バケット容量)

完全実装

package ratelimit

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/time/rate"
)

type RateLimiter struct {
    limiters sync.Map
    rate     rate.Limit
    burst    int
}

func NewRateLimiter(r rate.Limit, burst int) *RateLimiter {
    return &RateLimiter{
        rate:  r,
        burst: burst,
    }
}

func (rl *RateLimiter) getLimiter(key string) *rate.Limiter {
    if limiter, ok := rl.limiters.Load(key); ok {
        return limiter.(*rate.Limiter)
    }

    newLimiter := rate.NewLimiter(rl.rate, rl.burst)
    actual, _ := rl.limiters.LoadOrStore(key, newLimiter)
    return actual.(*rate.Limiter)
}

func (rl *RateLimiter) Allow(key string) bool {
    return rl.getLimiter(key).Allow()
}

func (rl *RateLimiter) Wait(ctx context.Context, key string) error {
    return rl.getLimiter(key).Wait(ctx)
}

func (rl *RateLimiter) Reserve(key string) (*rate.Reservation, error) {
    return rl.getLimiter(key).Reserve(), nil
}

HTTPミドルウェアレート制限

package middleware

import (
    "net/http"
    "time"

    "golang.org/x/time/rate"
)

type IPRateLimiter struct {
    limiters sync.Map
    rate     rate.Limit
    burst    int
}

func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter {
    return &IPRateLimiter{
        rate:  r,
        burst: burst,
    }
}

func (rl *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
    if limiter, ok := rl.limiters.Load(ip); ok {
        return limiter.(*rate.Limiter)
    }
    newLimiter := rate.NewLimiter(rl.rate, rl.burst)
    actual, _ := rl.limiters.LoadOrStore(ip, newLimiter)
    return actual.(*rate.Limiter)
}

func (rl *IPRateLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr

        limiter := rl.getLimiter(ip)
        if !limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }

        next.ServeHTTP(w, r)
    })
}

階層型レート制限

type TieredRateLimiter struct {
    tiers map[string]*rate.Limiter
}

func NewTieredRateLimiter() *TieredRateLimiter {
    return &TieredRateLimiter{
        tiers: map[string]*rate.Limiter{
            "free":      rate.NewLimiter(10, 20),
            "basic":     rate.NewLimiter(50, 100),
            "premium":   rate.NewLimiter(200, 400),
            "enterprise": rate.NewLimiter(1000, 2000),
        },
    }
}

func (trl *TieredRateLimiter) Allow(tier string) bool {
    limiter, ok := trl.tiers[tier]
    if !ok {
        limiter = trl.tiers["free"]
    }
    return limiter.Allow()
}

主要設計ポイント

  • rate.Limitは毎秒生成されるトークン数、burstはバケット容量
  • Allow()はノンブロッキング、Wait()はトークン取得までブロック
  • sync.Mapでper-keyリミッターを実装、自動スケール
  • 階層型レート制限でユーザーランクに対応
  • トークンバケットはバーストトラフィックを許可、長期平均レートは制御

パターン3:リトライ——指数バックオフとジッター

リトライは一時的障害に対応するが、戦略のないリトライは問題を悪化させる。指数バックオフ(Exponential Backoff)でリトライ間隔を段階的に増やし、ジッター(Jitter)でリトライストームを回避する。

リトライ戦略比較

    バックオフなし:  █ █ █ █ █ █ █ █ █ █    ← 全クライアントが同時リトライ
    固定間隔:       █   █   █   █   █      ← 同期リトライリスクあり
    指数バックオフ:  █   █     █         █   ← 間隔が段階的に増大
    指数+ジッター:  █  █    █       █      ← ランダム化でリトライストーム回避

完全実装

package retry

import (
    "context"
    "fmt"
    "math"
    "math/rand"
    "time"
)

type Config struct {
    MaxAttempts     int
    InitialInterval time.Duration
    MaxInterval     time.Duration
    Multiplier      float64
    Jitter          float64
}

func DefaultConfig() Config {
    return Config{
        MaxAttempts:     5,
        InitialInterval: 100 * time.Millisecond,
        MaxInterval:     30 * time.Second,
        Multiplier:      2.0,
        Jitter:          0.1,
    }
}

type RetryableError struct {
    Err error
}

func (e *RetryableError) Error() string {
    return fmt.Sprintf("retryable: %v", e.Err)
}

func IsRetryable(err error) bool {
    _, ok := err.(*RetryableError)
    return ok
}

func Do[T any](ctx context.Context, cfg Config, fn func() (T, error)) (T, error) {
    var lastErr error

    for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
        if attempt > 0 {
            delay := calculateDelay(attempt, cfg)
            select {
            case <-ctx.Done():
                var zero T
                return zero, fmt.Errorf("retry canceled: %w", ctx.Err())
            case <-time.After(delay):
            }
        }

        result, err := fn()
        if err == nil {
            return result, nil
        }

        lastErr = err
        if !IsRetryable(err) {
            var zero T
            return zero, fmt.Errorf("non-retryable error: %w", err)
        }
    }

    var zero T
    return zero, fmt.Errorf("max retries (%d) exceeded: %w", cfg.MaxAttempts, lastErr)
}

func calculateDelay(attempt int, cfg Config) time.Duration {
    delay := float64(cfg.InitialInterval) * math.Pow(cfg.Multiplier, float64(attempt-1))

    if delay > float64(cfg.MaxInterval) {
        delay = float64(cfg.MaxInterval)
    }

    jitter := delay * cfg.Jitter
    delay = delay - jitter + rand.Float64()*2*jitter

    return time.Duration(delay)
}

使用例——リトライ付きHTTP呼び出し

func fetchWithRetry(ctx context.Context, url string) (string, error) {
    cfg := retry.Config{
        MaxAttempts:     3,
        InitialInterval: 200 * time.Millisecond,
        MaxInterval:     10 * time.Second,
        Multiplier:      2.0,
        Jitter:          0.2,
    }

    return retry.Do(ctx, cfg, func() (string, error) {
        resp, err := http.Get(url)
        if err != nil {
            return "", &retry.RetryableError{Err: err}
        }
        defer resp.Body.Close()

        if resp.StatusCode >= 500 {
            return "", &retry.RetryableError{
                Err: fmt.Errorf("server error: %d", resp.StatusCode),
            }
        }

        if resp.StatusCode >= 400 && resp.StatusCode < 500 {
            return "", fmt.Errorf("client error: %d", resp.StatusCode)
        }

        body, err := io.ReadAll(resp.Body)
        if err != nil {
            return "", &retry.RetryableError{Err: err}
        }
        return string(body), nil
    })
}

サーキットブレーカー付きリトライ

func fetchResilient(ctx context.Context, url string, cb *CircuitBreaker[string]) (string, error) {
    cfg := retry.DefaultConfig()

    return retry.Do(ctx, cfg, func() (string, error) {
        result, err := cb.Execute(func() (string, error) {
            return doHTTPGet(ctx, url)
        })
        if err != nil {
            if errors.Is(err, gobreaker.ErrOpenState) {
                var zero string
                return zero, err
            }
            return zero, &retry.RetryableError{Err: err}
        }
        return result, nil
    })
}

主要設計ポイント

  • RetryableErrorでリトライ可能/不可能エラーを区別
  • 5xxはリトライ可能、4xxはリトライ不可
  • 指数バックオフ + ジッターでリトライストームを回避
  • contextキャンセルで即座にリトライ停止
  • サーキットブレーカーが開いている時はリトライしない(フェイルファスト優先)

パターン4:タイムアウト制御——context.Context実践

タイムアウトはマイクロサービスで最も基本的かつ重要なレジリエンス手段。全ての外部呼び出しにタイムアウトを設定する必要がある——タイムアウトのない呼び出しは時限爆弾。

タイムアウト階層設計

    リクエスト総タイムアウト: 30s
    ├── API Gateway: 28s (2s余裕)
    │   ├── Service A: 10s
    │   │   ├── DB Query: 5s
    │   │   └── Cache Get: 500ms
    │   ├── Service B: 15s
    │   │   ├── gRPC Call: 12s
    │   │   └── Redis Get: 1s
    │   └── Service C: 20s
    │       ├── HTTP Call: 15s
    │       └── MQ Publish: 3s
    └── レスポンス余裕: 2s

完全実装

package timeout

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

type TimeoutConfig struct {
    Connect    time.Duration
    Read       time.Duration
    Write      time.Duration
    Overall    time.Duration
    Graceful   time.Duration
}

func DefaultTimeoutConfig() TimeoutConfig {
    return TimeoutConfig{
        Connect:  5 * time.Second,
        Read:     10 * time.Second,
        Write:    10 * time.Second,
        Overall:  30 * time.Second,
        Graceful: 10 * time.Second,
    }
}

type CallResult struct {
    Data      string
    Duration  time.Duration
    TimedOut  bool
    Err       error
}

func CallWithTimeout(ctx context.Context, endpoint string, cfg TimeoutConfig) CallResult {
    start := time.Now()

    callCtx, cancel := context.WithTimeout(ctx, cfg.Overall)
    defer cancel()

    resultCh := make(chan CallResult, 1)

    go func() {
        data, err := doCall(callCtx, endpoint)
        elapsed := time.Since(start)

        resultCh <- CallResult{
            Data:     data,
            Duration: elapsed,
            Err:      err,
        }
    }()

    select {
    case result := <-resultCh:
        return result
    case <-callCtx.Done():
        return CallResult{
            Duration: time.Since(start),
            TimedOut: true,
            Err:      fmt.Errorf("call %s timed out after %v: %w", endpoint, cfg.Overall, callCtx.Err()),
        }
    }
}

func doCall(ctx context.Context, endpoint string) (string, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return "", fmt.Errorf("create request: %w", err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return "", fmt.Errorf("request deadline exceeded: %w", ctx.Err())
        }
        return "", fmt.Errorf("do request: %w", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", fmt.Errorf("read body: %w", err)
    }
    return string(body), nil
}

カスケードタイムアウト制御

func handleRequest(ctx context.Context, req *Request) (*Response, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    var (
        user    *User
        orders  []*Order
        profile *Profile
        err     error
    )

    g, ctx := errgroup.WithContext(ctx)

    g.Go(func() error {
        userCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
        defer cancel()
        user, err = fetchUser(userCtx, req.UserID)
        return err
    })

    g.Go(func() error {
        ordersCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
        defer cancel()
        orders, err = fetchOrders(ordersCtx, req.UserID)
        return err
    })

    g.Go(func() error {
        profileCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
        defer cancel()
        profile, err = fetchProfile(profileCtx, req.UserID)
        return err
    })

    if err := g.Wait(); err != nil {
        return nil, fmt.Errorf("handle request: %w", err)
    }

    return &Response{
        User:    user,
        Orders:  orders,
        Profile: profile,
    }, nil
}

グレースフルシャットダウン

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    server := &http.Server{Addr: ":8080"}

    go func() {
        <-ctx.Done()
        fmt.Println("shutting down...")

        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()

        if err := server.Shutdown(shutdownCtx); err != nil {
            fmt.Printf("shutdown error: %v\n", err)
        }
    }()

    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        fmt.Printf("server error: %v\n", err)
    }
}

主要設計ポイント

  • 各層のタイムアウトは上位層より小さく、余裕を残す
  • WithTimeouttime.Afterより安全(リソース自動解放)
  • errgroup + サブcontextで並行呼び出しの独立タイムアウトを実現
  • signal.NotifyContextでシステムシグナルを監視しグレースフルシャットダウン
  • defer cancel()でcontextリークを防止

パターン5:バルクヘッド分離——リソースパーティショニング

バルクヘッドパターンは船舶設計に由来:船体を複数の水密区画に分割し、1つの浸水が船全体を沈めないようにする。マイクロサービスでは、異なる下流呼び出しのリソース(接続プール、goroutine、セマフォ)を分離し、1つの遅い下流が全呼び出しを引きずり下ろすのを防ぐ。

バルクヘッドアーキテクチャ

    ┌─────────────────────────────────────────┐
    │              Service X                   │
    │                                         │
    │  ┌──────────────┐  ┌──────────────┐     │
    │  │  Bulkhead A  │  │  Bulkhead B  │     │
    │  │  Service A   │  │  Service B   │     │
    │  │  ┌──┐┌──┐┌──┐│  │  ┌──┐┌──┐   │     │
    │  │  │w1││w2││w3││  │  │w1││w2│   │     │
    │  │  └──┘└──┘└──┘│  │  └──┘└──┘   │     │
    │  │  pool: 3     │  │  pool: 2     │     │
    │  └──────┬───────┘  └──────┬───────┘     │
    │         │                 │              │
    └─────────┼─────────────────┼──────────────┘
              │                 │
    ┌─────────▼───────┐ ┌──────▼──────────┐
    │   Service A     │ │   Service B     │
    │   (応答遅延)     │ │   (正常)         │
    └─────────────────┘ └─────────────────┘

    Service Aの遅いリクエストがBulkhead Aを埋めても
    → Bulkhead BのService Bへの呼び出しに影響なし

完全実装

package bulkhead

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

type Bulkhead struct {
    name      string
    sem       *semaphore.Weighted
    timeout   time.Duration
    active    atomic.Int64
    rejected  atomic.Int64
    timedOut  atomic.Int64
}

type Config struct {
    Name          string
    MaxConcurrent int64
    Timeout       time.Duration
}

func New(cfg Config) *Bulkhead {
    return &Bulkhead{
        name:    cfg.Name,
        sem:     semaphore.NewWeighted(cfg.MaxConcurrent),
        timeout: cfg.Timeout,
    }
}

func (b *Bulkhead) Execute(ctx context.Context, fn func() error) error {
    acquireCtx, cancel := context.WithTimeout(ctx, b.timeout)
    defer cancel()

    if err := b.sem.Acquire(acquireCtx, 1); err != nil {
        b.rejected.Add(1)
        if ctx.Err() == context.DeadlineExceeded {
            b.timedOut.Add(1)
            return fmt.Errorf("bulkhead '%s' acquire timeout: %w", b.name, err)
        }
        return fmt.Errorf("bulkhead '%s' full: %w", b.name, err)
    }
    defer b.sem.Release(1)

    b.active.Add(1)
    defer b.active.Add(-1)

    return fn()
}

func (b *Bulkhead) Stats() BulkheadStats {
    return BulkheadStats{
        Name:     b.name,
        Active:   b.active.Load(),
        Rejected: b.rejected.Load(),
        TimedOut: b.timedOut.Load(),
    }
}

type BulkheadStats struct {
    Name     string
    Active   int64
    Rejected int64
    TimedOut int64
}

複数下流バルクヘッドマネージャー

type BulkheadManager struct {
    bulkheads sync.Map
}

func NewManager() *BulkheadManager {
    return &BulkheadManager{}
}

func (m *BulkheadManager) Register(name string, maxConcurrent int64, timeout time.Duration) {
    bh := New(Config{
        Name:          name,
        MaxConcurrent: maxConcurrent,
        Timeout:       timeout,
    })
    m.bulkheads.Store(name, bh)
}

func (m *BulkheadManager) Execute(ctx context.Context, service string, fn func() error) error {
    val, ok := m.bulkheads.Load(service)
    if !ok {
        return fmt.Errorf("bulkhead not found: %s", service)
    }
    return val.(*Bulkhead).Execute(ctx, fn)
}

func (m *BulkheadManager) Stats() map[string]BulkheadStats {
    stats := make(map[string]BulkheadStats)
    m.bulkheads.Range(func(key, value any) bool {
        stats[key.(string)] = value.(*Bulkhead).Stats()
        return true
    })
    return stats
}

使用例

func setupService() *BulkheadManager {
    mgr := NewManager()
    mgr.Register("user-service", 20, 5*time.Second)
    mgr.Register("order-service", 30, 8*time.Second)
    mgr.Register("payment-service", 10, 10*time.Second)
    mgr.Register("inventory-service", 15, 5*time.Second)
    return mgr
}

func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) error {
    var user *User
    var inventory *Inventory
    var payment *Payment

    g, ctx := errgroup.WithContext(ctx)

    g.Go(func() error {
        return s.bulkheads.Execute(ctx, "user-service", func() error {
            var err error
            user, err = s.userClient.Get(ctx, req.UserID)
            return err
        })
    })

    g.Go(func() error {
        return s.bulkheads.Execute(ctx, "inventory-service", func() error {
            var err error
            inventory, err = s.inventoryClient.Check(ctx, req.ProductID)
            return err
        })
    })

    if err := g.Wait(); err != nil {
        return fmt.Errorf("create order pre-check: %w", err)
    }

    return s.bulkheads.Execute(ctx, "payment-service", func() error {
        var err error
        payment, err = s.paymentClient.Charge(ctx, &ChargeRequest{
            UserID: user.ID,
            Amount: req.Amount,
        })
        return err
    })
}

主要設計ポイント

  • 各下流サービスに独立したセマフォプール、相互干渉なし
  • セマフォ取得にタイムアウト、無限待機を回避
  • active/rejected/timedOutカウントでモニタリング
  • errgroupと組み合わせて並行呼び出しを個別分離
  • sync.Mapで動的バルクヘッド登録をサポート

5つのよくある落とし穴と修正

落とし穴1:サーキットブレーカー閾値の不当な設定

❌ 誤った実装:

cb := gobreaker.NewCircuitBreaker[string](gobreaker.Settings{
    ReadyToTrip: func(counts gobreaker.Counts) bool {
        return counts.TotalFailures > 3
    },
})

3回の失敗でトリップ——低QPSシナリオでは敏感すぎる。1回のネットワークジッターでブレーカーが作動する可能性。

✅ 正しい実装:

cb := gobreaker.NewCircuitBreaker[string](gobreaker.Settings{
    ReadyToTrip: func(counts gobreaker.Counts) bool {
        failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
        return counts.Requests >= 10 && failureRatio >= 0.6
    },
})

落とし穴2:非冪等操作のリトライ

❌ 誤った実装:

func createOrder(ctx context.Context, req *OrderRequest) error {
    return retry.Do(ctx, cfg, func() error {
        return db.Insert(ctx, req)
    })
}

データベース挿入は冪等ではない——リトライで重複注文が作成される可能性。

✅ 正しい実装:

func createOrder(ctx context.Context, req *OrderRequest) error {
    return db.Insert(ctx, req)
}

func queryOrder(ctx context.Context, orderID string) (*Order, error) {
    return retry.Do(ctx, cfg, func() (*Order, error) {
        return db.FindByID(ctx, orderID)
    })
}

落とし穴3:contextタイムアウトの重ね掛け

❌ 誤った実装:

func handler(ctx context.Context) {
    ctx1, cancel1 := context.WithTimeout(ctx, 30*time.Second)
    defer cancel1()

    ctx2, cancel2 := context.WithTimeout(ctx1, 20*time.Second)
    defer cancel2()

    ctx3, cancel3 := context.WithTimeout(ctx2, 15*time.Second)
    defer cancel3()

    doWork(ctx3)
}

3層のタイムアウト重ね——有効タイムアウトは15秒だが、3つのタイマーを作成しリソースを浪費。

✅ 正しい実装:

func handler(ctx context.Context) {
    ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
    defer cancel()

    doWork(ctx)
}

落とし穴4:クライアント別リミッター未分離

❌ 誤った実装:

var limiter = rate.NewLimiter(100, 200)

func handler(w http.ResponseWriter, r *http.Request) {
    if !limiter.Allow() {
        http.Error(w, "too many requests", 429)
        return
    }
    handle(w, r)
}

グローバル共有リミッターでは、1つの高トラフィッククライアントが全クォータを使い切る。

✅ 正しい実装:

var limiters sync.Map

func getLimiter(clientID string) *rate.Limiter {
    if v, ok := limiters.Load(clientID); ok {
        return v.(*rate.Limiter)
    }
    l := rate.NewLimiter(100, 200)
    actual, _ := limiters.LoadOrStore(clientID, l)
    return actual.(*rate.Limiter)
}

落とし穴5:バルクヘッドセマフォリーク

❌ 誤った実装:

func (b *Bulkhead) Execute(fn func() error) error {
    if err := b.sem.Acquire(context.Background(), 1); err != nil {
        return err
    }

    result := fn()
    b.sem.Release(1)
    return result
}

fn()がpanicするとReleaseが呼ばれず、セマフォが永久に減少。

✅ 正しい実装:

func (b *Bulkhead) Execute(ctx context.Context, fn func() error) error {
    if err := b.sem.Acquire(ctx, 1); err != nil {
        return err
    }
    defer b.sem.Release(1)

    return fn()
}

エラートラブルシューティングクイックリファレンス

エラー現象 可能な原因 調査方法 解決策
circuit breaker openが頻発 下流障害または閾値が低すぎる 下流の健全性確認、ブレーカー統計の確認 ReadyToTrip閾値の調整、Timeout待機時間の増加
too many requests 429エラー リミッターのburstが小さい、rateが低い トークン消費レートの監視、クライアント同時接続数の確認 rateburstの増加、クライアント別リミッター分離
リトライでリクエスト量が倍増 リトライ回数過多、バックオフ戦略なし リトライ設定の確認、リトライリクエスト比率の監視 リトライ回数の削減、指数バックオフ+ジッターの使用
context deadline exceededが頻発 タイムアウトが短すぎる、下流が遅い P99レイテンシの確認、タイムアウトとの比較 タイムアウトをP99の2-3倍に設定、フォールバックロジックの追加
バルクヘッド拒否率が高い バルクヘッド容量が小さい、下流が遅い バルクヘッドStatsの確認、Active/Rejectedに注目 MaxConcurrentの増加、タイムアウトで長時間占有を防止
リトライストームが障害を悪化 ジッターなし、全クライアントが同時リトライ リトライリクエストの時間分布を監視 Jitterの追加、Retry-Afterヘッダーの使用
サーキットブレーカーが復旧しない Timeoutが長すぎる、ハーフオープンプローブなし ブレーカー状態変化ログの確認 Timeoutの短縮、MaxRequestsでプローブを許可
リミッターのメモリが増加 per-keyリミッターの期限切れキー未クリーン sync.Mapサイズの監視 非アクティブリミッターの定期クリーン、LRU淘汰の使用
タイムアウト後もgoroutineが実行中 contextはキャンセルしたがチェックしていない pprof goroutine profile 全goroutineがselectでctx.Done()をチェックすることを確認
バルクヘッド間のリソース競合 複数バルクヘッドが接続プールを共有 接続プール設定と待機時間の確認 バルクヘッドごとに独立した接続プール、または加重セマフォ

高度な最適化テクニック

最適化1:アダプティブサーキットブレーキング

リアルタイムメトリクスに基づいてサーキットブレーカー閾値を動的に調整:

package adaptive

import (
    "sync"
    "time"

    "github.com/sony/gobreaker/v2"
)

type AdaptiveCircuitBreaker struct {
    cb         *gobreaker.CircuitBreaker[string]
    mu         sync.Mutex
    window     []time.Duration
    windowSize int
    threshold  time.Duration
}

func NewAdaptiveCircuitBreaker(windowSize int, threshold time.Duration) *AdaptiveCircuitBreaker {
    acb := &AdaptiveCircuitBreaker{
        window:     make([]time.Duration, 0, windowSize),
        windowSize: windowSize,
        threshold:  threshold,
    }

    acb.cb = gobreaker.NewCircuitBreaker[string](gobreaker.Settings{
        Name:        "adaptive",
        Interval:    10 * time.Second,
        Timeout:     30 * time.Second,
        MaxRequests: 5,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return acb.shouldTrip(counts)
        },
    })

    return acb
}

func (acb *AdaptiveCircuitBreaker) shouldTrip(counts gobreaker.Counts) bool {
    acb.mu.Lock()
    defer acb.mu.Unlock()

    if len(acb.window) < acb.windowSize {
        failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
        return counts.Requests >= 10 && failureRatio >= 0.5
    }

    avgLatency := acb.averageLatency()
    failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)

    if avgLatency > acb.threshold && failureRatio >= 0.3 {
        return true
    }

    return failureRatio >= 0.6
}

func (acb *AdaptiveCircuitBreaker) averageLatency() time.Duration {
    var total time.Duration
    for _, d := range acb.window {
        total += d
    }
    return total / time.Duration(len(acb.window))
}

func (acb *AdaptiveCircuitBreaker) RecordLatency(d time.Duration) {
    acb.mu.Lock()
    defer acb.mu.Unlock()

    acb.window = append(acb.window, d)
    if len(acb.window) > acb.windowSize {
        acb.window = acb.window[1:]
    }
}

最適化2:スライディングウィンドウレート制限

トークンバケットは平均レート制御に適しているが、スライディングウィンドウは時間ウィンドウ内のリクエスト数をより正確に制御:

package slidingwindow

import (
    "sync"
    "time"
)

type Window struct {
    mu      sync.Mutex
    size    time.Duration
    limit   int
    buckets []bucket
    count   int
}

type bucket struct {
    time  time.Time
    count int
}

func NewWindow(size time.Duration, limit int) *Window {
    return &Window{
        size:    size,
        limit:   limit,
        buckets: make([]bucket, 0),
    }
}

func (w *Window) Allow() bool {
    w.mu.Lock()
    defer w.mu.Unlock()

    now := time.Now()
    cutoff := now.Add(-w.size)

    i := 0
    for i < len(w.buckets) && w.buckets[i].time.Before(cutoff) {
        i++
    }
    w.buckets = w.buckets[i:]
    w.count = 0
    for _, b := range w.buckets {
        w.count += b.count
    }

    if w.count >= w.limit {
        return false
    }

    w.buckets = append(w.buckets, bucket{time: now, count: 1})
    w.count++
    return true
}

最適化3:レジリエンスミドルウェアチェーン

5つのレジリエンスパターンを再利用可能なミドルウェアチェーンとして組み合わせ:

package resilience

import (
    "context"
    "fmt"
    "time"

    "github.com/sony/gobreaker/v2"
    "golang.org/x/time/rate"
)

type Middleware func(ctx context.Context, req *Request) (*Response, error)

type Chain struct {
    middlewares []Middleware
}

func NewChain(middlewares ...Middleware) *Chain {
    return &Chain{middlewares: middlewares}
}

func (c *Chain) Then(final func(ctx context.Context, req *Request) (*Response, error)) Middleware {
    return func(ctx context.Context, req *Request) (*Response, error) {
        var handler Middleware = func(ctx context.Context, req *Request) (*Response, error) {
            return final(ctx, req)
        }

        for i := len(c.middlewares) - 1; i >= 0; i-- {
            handler = func(mw Middleware, h Middleware) Middleware {
                return func(ctx context.Context, req *Request) (*Response, error) {
                    return mw(ctx, req)
                }
            }(c.middlewares[i], handler)
        }

        return handler(ctx, req)
    }
}

func RateLimitMiddleware(limiter *rate.Limiter) Middleware {
    return func(ctx context.Context, req *Request) (*Response, error) {
        if !limiter.Allow() {
            return nil, fmt.Errorf("rate limit exceeded")
        }
        return nil, nil
    }
}

func CircuitBreakerMiddleware(cb *gobreaker.CircuitBreaker[string]) Middleware {
    return func(ctx context.Context, req *Request) (*Response, error) {
        _, err := cb.Execute(func() (string, error) {
            return "", nil
        })
        if err != nil {
            return nil, err
        }
        return nil, nil
    }
}

func TimeoutMiddleware(timeout time.Duration) Middleware {
    return func(ctx context.Context, req *Request) (*Response, error) {
        _, cancel := context.WithTimeout(ctx, timeout)
        defer cancel()
        return nil, nil
    }
}

レジリエンスパターン比較

特徴 サーキットブレーカー レート制限 リトライ タイムアウト バルクヘッド
コア目的 高速フェイル、下流保護 リクエストレート制御 一時的障害対応 待機時間制限 リソース分離
主要ライブラリ sony/gobreaker golang.org/x/time/rate カスタム context semaphore
状態管理 ステートフル(3状態) ステートフル(トークン数) ステートレス ステートレス ステートフル(セマフォ)
対象障害 下流完全不可 トラフィック過負荷 一時的ネットワークジッター 下流応答遅延 部分的下流障害
誤検知リスク 中(閾値不適切) 高(非冪等操作) 中(短すぎる)
組み合わせ推奨 リトライと組み合わせ バルクヘッドと組み合わせ サーキットブレーカーと組み合わせ 全シナリオで必須 レート制限と組み合わせ
プロダクション推奨度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
実装複雑度

おすすめツール

  • JSONフォーマッター — マイクロサービスJSONレスポンスのフォーマット、データ構造問題の迅速なデバッグ
  • ハッシュ計算ツール — リクエスト署名とデータチェックサムの計算、分散呼び出しのデータ一貫性を確保
  • HTTPステータスコード検索 — HTTPステータスコードの意味検索、429/503等のレジリエンス関連エラーの迅速な特定

まとめ

マイクロサービスレジリエンスは「サーキットブレーカーを追加すれば完了」ではない——5つの問いに答えることだ:いつリクエストを拒否すべきか?リクエストレートをどう制御するか?失敗時にリトライするか?どのくらいでタイムアウトするか?1つの障害が他の呼び出しを引きずり下ろすか? サーキットブレーカーは「いつ拒否するか」、レート制限は「レートをどう制御するか」、リトライは「リトライするか」、タイムアウトは「どのくらい待つか」、バルクヘッドは「どう分離するか」に答える。5つのパターンを組み合わせて初めて、真にアンチフラジャイルなGoマイクロサービスを構築できる。


参考資料

ブラウザローカルツールを無料で試す →

#Go微服务#熔断器#限流#重试#服务韧性#2026#云原生