本記事は「AWS LambdaとServerless Advent Calendar 2024」15 日目の記事です。
以前から下記のブログに関心があったので、実際に試してみたり、自分なりに改良を加えてみたのでそれを記事にします。
目次
サーキットブレーカーとは
まず、サーキットブレーカーとは何かというところ確認しておきます。 アプリケーション開発の文脈でいうと、サーキットブレーカーとはサービス呼び出しが正常に行えない場合に、過剰にリクエストを送信しないための仕組みのことを指し、その仕組みの実装パターンを、サーキットブレーカーパターンと呼んでいます。
サーキットブレーカーパターンは、下記の AWS 規範ガイドにも説明がありますので目を通しておくと理解が深まります。
ちなみに「サーキットブレーカー」という言葉は、アプリケーション開発以外の場面でも使われます。 例えば、電流を制御する物理的な装置もサーキットブレーカーと呼ばれますし、株式の取引でも急激な株価の下降を止めるために取引を停止することをサーキットブレーカーと呼ぶこともあります。
サーキットブレーカーパターンの実装例
このサーキットブレーカーパターンの実装を紹介しているのが上で紹介した Using the circuit breaker pattern with AWS Step Functions and Amazon DynamoDB というブログ記事です。 そのブログ記事のサーキットブレーカーの動作を図にしてみました。
上の図では、サーキットブレーカーが Payment サービスが使用できない状態を「サーキット状態 = OPEN 」として保存しています。 サーキット状態が OPEN であれば、次のリクエストでは Payment サービスは呼び出さずエラーにしています。 ただし、Payment サービスが復旧することに備えて、この状態の保存には期限を設けることを前提としています。 これを実際に実装したイメージが下図になります。(ブログ記事からの抜粋した図です)
サーキット状態の保存には Amazon DynamoDB テーブルを使用していますが、項目の有効期限 (Time To Live) を設定しておきます。 これにより、Payment サービスが復旧した場合に、サーキット状態=OPEN という項目が残り続けてリクエストを拒否する動作を避けることができます。
実際に試してみましたが、この実装には下記の利点があると感じました。
- シンプルに実装できる
- サービスを呼び出す側にサーキットブレーカーの実装を含めなくてよく、サーキットブレーカーを独立させて汎用的に使用できます。
- サーキットブレーカーの動作や状態を可視化しやすい
サーキットブレーカーパターンの実装例を自分なりに改良してみる
上記で紹介したサーキットブレーカーパターンの実装は、サンプルとしては十分ですが、実際に使うとなった場合は、下記のようにいくつか機能面で不足していることに気づきました。
- Order サービスが、Payment サービスに Payload を渡せる実装になっていない
- サーキット状態が OPEN のため呼び出しを行わない場合、Payload を退避させる実装になっていない
上記の点を踏まえて、自分なりに改良を加えることにしました。 またブログ記事では ステートマシンから呼び出す Lambda 関数のランタイムが .NET C# ですが、Python を使う人にも理解しやすいように、Lambda 関数のランタイムを Pythonで実装しなおしました。 そのステートマシンのイメージが下図です。Update Circuit Status ステートの後に Amazon SQS のキューに Payload を送信するステートを追加しました。これにより、呼び出しが失敗した場合でも、その Payload の消失を防げます。
実際のコードや AWS SAM リソースはこちらになります。(あくまで個人のコードでありサンプルとして公開しています。)
では実行してみましょう。まずは呼び出し先の Payment サービスに問題がなく、正常に呼び出せるケースを試してみます。
上の図のように、最初はサーキット状態はクローズで、実際に Payment サービスも問題なく呼び出せるので Succeed ステートである Circuit Closed で終了します。
次は、呼び出し先のサービスに問題があり、正常に呼び出せないケースです。 このケースでは、AWS Lambda 関数で必ずタイムアウトエラーが発生する PaymentTimeout というサービスを呼び出します。 すると、Execute Lambda ステートでエラーが発生しますが、リトライを設定しているので、何度かリトライを行います。 ただし、リトライをしてもエラーになるので、最終的には Fail ステートである Circuit Opened で終了します。
この時、Amazon DynamoDB テーブルへ下記の項目を Put します。
ServiceName (Partition Key) |
ExpireTimeStamp (Sort Key) |
CircuitStatus |
---|---|---|
Payment | 現在のエポック秒へ 20 秒足した値 | OPEN |
またサービス呼び出し元から送信された Payload や Lambda 関数のエラーメッセージ(下記)を Amazon SQS のキューへ送信しています。
{ "executionId": "arn:aws:states:ap-northeast-1:000000000000:execution:circuitbreaker-statemachine:5464ba09-3c01-4f8a-9da3-8007d67225ed", "state": { "TargetLambda": "arn:aws:lambda:ap-northeast-1:000000000000:function:circuitbreaker-PaymentTimeout", "Payload": { "order": { "item_id": "Dummy02", "unit": 20 } }, "output": { "TargetLambda": "arn:aws:lambda:ap-northeast-1:000000000000:function:circuitbreaker-PaymentTimeout", "CircuitStatus": "" }, "taskresult": { "Error": "States.Timeout", "Cause": "" } } }
これでサーキット状態が OPEN になりました。 この状態は DynamoDB テーブルへ保存していますが、有効期限を 20 秒に設定していますので、20 秒以内に同じリクエストを再送すると下図のようになります。
サーキット状態が OPEN であれば、サービスを呼び出さず、すぐに Fail ステートでステートマシンを終了させていますね。
もし、20秒以上経過してからリクエストを再送した場合は、DynamoDB テーブルの項目は有効期限が経過しているので削除されているか、もしくは項目が残っていても Query で ExpireTimeStamp >現在エポック秒 の条件を指定してサーキット状態を取得しているので、項目は取得されずサーキット状態は OPEN ではないものと判断され、もう一度 Execute Lambda ステートを実行します。
その他のサーキットブレーカーパターンの実装例
サーキットブレーカーパターンの実装は他にもあります。例えば下記のブログ記事では、AWS Step Functions ではなく、AWS Lambda extensions を使用しています。
上記のブログ記事の実装例は、サービス呼び出しが同期呼び出しを前提としています。またサーキットブレーカーのロジックを呼び出し元のサービス内に埋め込む形となります。 呼び出すサービスの状態は Amazon DynamoDB のテーブルへ保存していますが、Extensions 側でキャッシュでも維持しているところが興味深いですね。 また、呼び出すサービスの状態を定期的にチェックするために、Amazon EventBridge やチェック用の Lambda 関数を別途用意している点も、AWS Step Functions を使用するパターンとは異なります。 これはこれで、一つの実装パターンですね。
最後に
この記事では AWS Step Functions をサーキットブレーカーパターンの実装に用いるトピックについて扱いましたが、AWS Step Functions は他にも様々な適用ができます。 例えば、AWS 規範ガイドにある Sage パターン (厳密にいうと Sage におけるオーケストレーションのパターン)でも AWS Step Functions を活用できます。
このパターンでも、AWS Step Functions を活用することで実装の容易性や可視性、トレース性を向上できているといえます。
このように、AWS Step Functions は様々なパターンの実装に適用できる「使いやすい」サービスだなと改めて感じました。
AWS Step Functions を活用した実装パターンについては、今後も探求し続けたいと思います!