Goマイクロサービスレジリエンス実践:サーキットブレーカーからレート制限までの5つのプロダクションパターン
カスケード障害が雪崩効果に出会う時:マイクロサービスの至暗の刻
金曜日のピーク時、決済サービスの応答が遅くなる。上流の注文サービスにはタイムアウト制御がなく、全リクエストが決済呼び出しでブロックされ、goroutineが50万に積み上がる。5分後、注文サービスがOOMでクラッシュし、ユーザーサービスも続いてダウン、コールチェーン全体が雪崩する。皮肉なことに、決済サービスは30秒後に復旧していた——しかし誰もアクセスできなくなっていた。
これは脅しではない。マイクロサービスアーキテクチャでは、1つの遅いノードがコールチェーン全体を引きずり下ろす。サーキットブレーカーで障害の波及を防ぎ、レート制限で下流を保護し、リトライで一時的な障害に対応し、タイムアウトで無限待機を避け、バルクヘッドで障害ドメインを分離する必要がある。本記事では5つのプロダクション級レジリエンスパターンを解説し、アンチフラジャイルなGoマイクロサービスの構築を支援する。
コア概念クイックリファレンス
| パターン | コア思想 | 主要ライブラリ | 典型的なシナリオ |
|---|---|---|---|
| サーキットブレーカー | 閾値到達で呼び出しを遮断、下流を保護 | sony/gobreaker |
下流サービス不可時に高速フェイル |
| レート制限 | リクエストレートを制御、過負荷を防止 | golang.org/x/time/rate |
APIレート制限、リソースクォータ制御 |
| リトライ(バックオフ付き) | 一時的障害の自動リトライ、指数バックオフでリトライストーム回避 | カスタム実装 | ネットワークジッター、一時的エラー |
| タイムアウト制御 | 1回の呼び出しの最大時間を制限 | context.Context |
全ての外部呼び出しにタイムアウト必須 |
| バルクヘッド | 異なる呼び出し先のリソースを分離、相互影響を防止 | カスタム実装 | 複数下流呼び出しのリソース分離 |
目次
- マイクロサービスレジリエンスアーキテクチャ概要
- パターン1:サーキットブレーカー——sony/gobreaker実践
- パターン2:レート制限——トークンバケットとスライディングウィンドウ
- パターン3:リトライ——指数バックオフとジッター
- パターン4:タイムアウト制御——context.Context実践
- パターン5:バルクヘッド分離——リソースパーティショニング
- 5つのよくある落とし穴
- 10のエラートラブルシューティング
- 高度な最適化テクニック
- レジリエンスパターン比較
- おすすめツール
- まとめと参考資料
マイクロサービスレジリエンスアーキテクチャ概要
┌─────────────────────────────────────────────┐
│ 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)
}
}
主要設計ポイント:
- 各層のタイムアウトは上位層より小さく、余裕を残す
WithTimeoutはtime.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が低い | トークン消費レートの監視、クライアント同時接続数の確認 | rateとburstの増加、クライアント別リミッター分離 |
| リトライでリクエスト量が倍増 | リトライ回数過多、バックオフ戦略なし | リトライ設定の確認、リトライリクエスト比率の監視 | リトライ回数の削減、指数バックオフ+ジッターの使用 |
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マイクロサービスを構築できる。
参考資料
ブラウザローカルツールを無料で試す →