分散トランザクションにおけるSagaパターン - Goでの例
マイクロサービスにおけるSagaパターンによるトランザクション
Saga パターンは、分散トランザクションを補完アクションを伴う一連のローカルトランザクションに分割することで、エレガントな解決策を提供します。
サービス全体にわたって操作をブロックする可能性のある分散ロックに依存するのではなく、Sagaは可逆的なステップのシーケンスを通じて最終整合性を実現し、長期間実行されるビジネスプロセスに理想的なものです。
マイクロサービスアーキテクチャにおいて、サービス間でデータの一貫性を維持することは最も困難な課題の一つです。操作が独立したデータベースを持つ複数のサービスにまたがる場合、従来のACIDトランザクションは機能せず、データ整合性を確保するための代替アプローチを開発者が模索する状況に置かれます。
このガイドでは、オーケストレーションとキョレオグラフィの両方のアプローチをカバーする実践的な例を用いて、GoにおけるSagaパターンの実装を示します。Goの基礎知識を素早く参照したい場合は、Goチートシートが有用な概要を提供しています。
この美しい画像は、AIモデルFlux 1 devによって生成されました。
Sagaパターンの理解
Sagaパターンは、1987年にHector Garcia-MolinaとKenneth Salemによって初めて記述されました。マイクロサービスの文脈において、それは各トランザクションが単一のサービス内のデータを更新する一連のローカルトランザクションです。どのステップでも失敗が発生した場合、補完トランザクションが実行され、先行するステップの影響を取り消します。
2フェーズコミット(2PC)を使用する従来の分散トランザクションとは異なり、Sagaはサービス全体にわたってロックを保持しないため、長期間実行されるビジネスプロセスに適しています。トレードオフは、強い整合性ではなく最終整合性です。
主要な特徴
- 分散ロックなし: 各サービスは独自のローカルトランザクションを管理します
- 補完アクション: すべての操作には対応するロールバックメカニズムがあります
- 最終整合性: システムは最終的に一貫した状態に達します
- 長期間実行: 秒、分、あるいは時間単位の処理に適しています
Sagaの実装アプローチ
Sagaパターンを実装するには、主に2つのアプローチがあります:オーケストレーションとキョレオグラフィです。
オーケストレーションパターン
オーケストレーションでは、中央のコーディネーター(オーケストレーター)がトランザクションフロー全体を管理します。オーケストレーターは以下の責任を負います:
- サービスを正しい順序で呼び出す
- 失敗を処理し、補完をトリガーする
- Sagaの状態を維持する
- リトライとタイムアウトを調整する
利点:
- 集中された制御と可視性
- 理解とデバッグが容易
- より優れたエラー処理とリカバリ
- 全体のフローのテストが単純
欠点:
- 単一障害点(ただし、これは緩和可能です)
- 追加のサービス維持が必要
- 複雑なフローではボトルネックになる可能性があります
Goでの例:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Step 1: Create order
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Step 2: Reserve inventory
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Compensate
return err
}
// Step 3: Process payment
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Compensate
o.orderService.Cancel(orderID) // Compensate
return err
}
// Step 4: Create shipment
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Compensate
o.inventoryService.Release(order.Items) // Compensate
o.orderService.Cancel(orderID) // Compensate
return err
}
return nil
}
キョレオグラフィパターン
キョレオグラフィでは、中央のコーディネーターはありません。各サービスは行うべきことを知り、イベントを通じて通信します。サービスはイベントをリスンし、それに応じて反応します。このイベント駆動型のアプローチは、マイクロサービス全体にわたるイベント配布のためのスケーラブルなインフラストラクチャを提供するAWS Kinesisなどのメッセージストリーミングプラットフォームと組み合わせることで、特に強力です。Kinesisを使用したイベント駆動型マイクロサービスの実装に関する包括的なガイドについては、以下を参照してください。 Building Event-Driven Microservices with AWS Kinesis.
利点:
- 分散型でスケーラブル
- 単一障害点がない
- サービスは疎結合のまま
- イベント駆動型アーキテクチャに自然に適合
欠点:
- 全体のフローを理解するのが難しい
- デバッグとトレースが困難
- 複雑なエラー処理
- 循環依存のリスク
イベント駆動型アーキテクチャの例:
// Order Service
type OrderService struct {
eventBus EventBus
repo OrderRepository
}
func (s *OrderService) CreateOrder(order Order) (string, error) {
orderID, err := s.repo.Save(order)
if err != nil {
return "", err
}
s.eventBus.Publish("OrderCreated", OrderCreatedEvent{
OrderID: orderID,
CustomerID: order.CustomerID,
Items: order.Items,
Total: order.Total,
})
return orderID, nil
}
func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
return s.repo.Cancel(event.OrderID) // Compensation
}
// Payment Service
type PaymentService struct {
eventBus EventBus
client PaymentClient
}
func (s *PaymentService) HandleOrderCreated(event OrderCreatedEvent) {
paymentID, err := s.client.Charge(event.CustomerID, event.Total)
if err != nil {
s.eventBus.Publish("PaymentFailed", PaymentFailedEvent{
OrderID: event.OrderID,
})
return
}
s.eventBus.Publish("PaymentSucceeded", PaymentSucceededEvent{
OrderID: event.OrderID,
PaymentID: paymentID,
})
}
func (s *PaymentService) HandleInventoryReservationFailed(event InventoryReservationFailedEvent) error {
// Compensation: refund payment
return s.client.Refund(event.PaymentID)
}
補完戦略
補完はSagaパターンの核心です。各操作には、その影響を逆転できる対応する補完が必要です。
補完の種類
-
可逆操作: 直接取り消すことができる操作
- 例:予約済み在庫の解放、支払いの返金
-
補完アクション: 逆の効果をもたらす異なる操作
- 例:削除するのではなく注文をキャンセルする
-
悲観的補完: 解放できるリソースを事前に割り当てる
- 例:支払いをチャージする前に在庫を予約する
-
楽観的補完: 操作を実行し、必要な場合に補完する
- 例:支払いを先にチャージし、在庫が利用できない場合は返金する
冪等性要件
すべての操作と補完は冪等である必要があります。これにより、失敗した操作を再試行しても重複した影響が発生しないことが保証されます。
func (s *PaymentService) Refund(paymentID string) error {
// Check if already refunded
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Already refunded, idempotent
}
// Process refund
return s.processRefund(paymentID)
}
ベストプラクティス
1. Saga状態管理
進捗を追跡し、リカバリを有効化するために、各Sagaインスタンスの状態を維持します。Saga状態をデータベースに永続化する際、適切なORMを選択することはパフォーマンスと保守性の観点から重要です。PostgreSQLベースの実装では、Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlcの比較を参照し、Saga状態ストレージのニーズに最も適したものを選択してください:
type SagaState struct {
ID string
Status SagaStatus
Steps []SagaStep
CurrentStep int
CreatedAt time.Time
UpdatedAt time.Time
}
type SagaStep struct {
Service string
Operation string
Status StepStatus
Compensated bool
Data map[string]interface{}
}
2. タイムアウト処理
Sagaが無限に停止しないように、各ステップのタイムアウトを実装します:
type SagaOrchestrator struct {
timeout time.Duration
}
func (o *SagaOrchestrator) ExecuteWithTimeout(step SagaStep) error {
ctx, cancel := context.WithTimeout(context.Background(), o.timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- step.Execute()
}()
select {
case err := <-done:
return err
case <-ctx.Done():
// Timeout occurred, compensate
if err := step.Compensate(); err != nil {
return fmt.Errorf("compensation failed: %w", err)
}
return fmt.Errorf("step %s timed out after %v", step.Name(), o.timeout)
}
}
3. リトライロジック
一時的な失敗に対して指数バックオフを実装します:
func retryWithBackoff(operation func() error, maxRetries int) error {
backoff := time.Second
for i := 0; i < maxRetries; i++ {
err := operation()
if err == nil {
return nil
}
if !isTransientError(err) {
return err
}
time.Sleep(backoff)
backoff *= 2
}
return fmt.Errorf("operation failed after %d retries", maxRetries)
}
4. Saga状態のためのイベントソーシング
完全な監査証跡を維持するためにイベントソーシングを使用します。イベントストアとリプレイメカニズムを実装する際、Goジェネリクスは型安全で再利用可能なイベント処理コードを作成するのに役立ちます。Goでのジェネリクスの高度なパターンについては、Go Generics: Use Cases and Patternsを参照してください。
type SagaEvent struct {
SagaID string
EventType string
Payload []byte
Timestamp time.Time
Version int64
}
type SagaEventStore struct {
store EventRepository
}
func (s *SagaEventStore) AppendEvent(sagaID string, eventType string, payload interface{}) error {
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("failed to get version: %w", err)
}
event := SagaEvent{
SagaID: sagaID,
EventType: eventType,
Payload: data,
Timestamp: time.Now(),
Version: version,
}
return s.store.Save(event)
}
func (s *SagaEventStore) ReplaySaga(sagaID string) (*Saga, error) {
events, err := s.store.GetEvents(sagaID)
if err != nil {
return nil, fmt.Errorf("failed to get events: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("failed to apply event: %w", err)
}
}
return saga, nil
}
5. モニタリングと可観測性
包括的なロギングとトレーシングを実装します:
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
span := tracer.StartSpan("saga.create_order")
defer span.Finish()
span.SetTag("saga.id", sagaID)
span.SetTag("order.id", order.ID)
logger.WithFields(log.Fields{
"saga_id": sagaID,
"order_id": order.ID,
"step": "create_order",
}).Info("Saga started")
// ... saga execution
return nil
}
一般的なパターンとアンチパターン
従うべきパターン
- Sagaコーディネーターパターン: オーケストレーションのために専用サービスを使用する
- Outboxパターン: 信頼性の高いイベント発行を確保する
- 冪等キー: すべての操作に一意のキーを使用する
- Sagaステートマシン: Sagaをステートマシンとしてモデル化する
避けるべきアンチパターン
- 同期的補完: 補完が完了するのを待たない
- ネストされたSaga: Sagaが他のSagaを呼び出すのを避ける(代わりにサブSagaを使用する)
- 共有状態: Sagaステップ間で状態を共有しない
- 長時間実行ステップ: 時間のかかるステップを分割する
ツールとフレームワーク
Sagaパターンを実装するのに役立ついくつかのフレームワークがあります:
- Temporal: 組み込みのSagaサポートを備えたワークフローオーケストレーションプラットフォーム
- Zeebe: マイクロサービスオーケストレーション用のワークフローエンジン
- Eventuate Tram: Spring Boot用のSagaフレームワーク
- AWS Step Functions: サーバーレスワークフローオーケストレーション
- Apache Camel: Sagaサポートを備えた統合フレームワーク
管理とモニタリングのためにCLIインターフェースが必要なオーケストレーターサービスの場合、Building CLI Applications in Go with Cobra & Viperは、Sagaオーケストレーターと対話するためのコマンドラインツールを作成するための優れたパターンを提供します。
KubernetesでSagaベースのマイクロサービスを展開する場合、サービスメッシュを実装することで、可観測性、セキュリティ、トラフィック管理を大幅に改善できます。Implementing Service Mesh with Istio and Linkerdでは、サービスメッシュが分散トレーシングやサーキットブレイキングなどの横断的関心事を提供することで、分散トランザクションパターンをどのように補完するかについて説明しています。
Sagaパターンを使用するタイミング
Sagaパターンは次の場合に使用します:
- ✅ 操作が複数のマイクロサービスにまたがる
- ✅ 長期間実行されるビジネスプロセス
- ✅ 最終整合性が許容される
- ✅ 分散ロックを回避する必要がある
- ✅ サービスに独立したデータベースがある
次の場合は避けます:
- ❌ 強い整合性が要求される
- ❌ 操作が単純で高速である
- ❌ すべてのサービスが同じデータベースを共有している
- ❌ 補完ロジックが複雑すぎる
結論
Sagaパターンは、マイクロサービスアーキテクチャにおける分散トランザクションの管理に不可欠です。複雑さを導入する一方で、サービス境界を跨ってデータの一貫性を維持するための実用的な解決策を提供します。より良い制御と可視性のためにオーケストレーションを選択するか、スケーラビリティと疎結合のためにキョレオグラフィを選択します。常に操作が冪等であることを確保し、適切な補完ロジックを実装し、包括的な可観測性を維持してください。
Saga実装の成功の鍵は、整合性要件を理解し、補完ロジックを慎重に設計し、ユースケースに適したアプローチを選択することです。適切な実装により、Sagaは分散システム全体でデータ整合性を維持する堅牢でスケーラブルなマイクロサービスを構築することを可能にします。
参考リンク
- Microservices Patterns by Chris Richardson
- Saga Pattern - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Documentation
- Go Cheat Sheet
- Go Generics: Use Cases and Patterns
- Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Implementing CQRS in Go
- Building CLI Applications in Go with Cobra & Viper
- Implementing Service Mesh with Istio and Linkerd
- Building Event-Driven Microservices with AWS Kinesis