maybe daily dev notes

私の開発日誌

AWSでn秒ごとのループ処理、どうする?

はじめに

サービスを開発していると、n秒ごとに何らかの処理を定期実行したい要件が見つかることがあります。 例えば、10秒ごとにあるAPIエンドポイントにアクセスして結果を保存したいなどです。この記事ではこのような機能の実装方法について考えます。

EventBridge + Lambda の限界

n > 60、つまり1分間以上の間隔の場合は、EventBridge + Lambdaによる処理が最適解となる場合が多いでしょう。

しかしながら、EventBridgeのスケジュール式の最低分解能は1分なので、それよりも短い間隔で呼び出すには適しません。

docs.aws.amazon.com

1分未満の間隔でループを回す場合は、次の方法を取ることが多いです。

方法1. ECSのサービスとして実装

ループ処理を適当な言語で実装し(以下はPythonの例)、ECSのサービスとして起動します。

from time import sleep

def main():
  # 10秒ごとにdoSomething関数を実行する
  while True:
    doSomething()
    sleep(10)

main()

この方法のメリット・デメリットは以下のようになるでしょう:

メリット

  • コンテナを1つ起動するだけなので、実装は直観的でシンプル
  • サーバーレスの基盤であるFargateを使えば運用もある程度楽
  • 処理が常時起動・自動復旧することをECSが保証してくれる

デメリット

  • ECSなのでVPCが必要になり、サーバーレス性はやや低い
  • sleep中もコンピュートリソースを消費するので、コストが高いかも?

類似の方法としてLambdaを使って同じことをする方法もあります。Lambdaは最大15分で強制終了されるため、定期的にLambdaを呼び出しつつ、呼び出したLambdaの中でsleepしながら処理をループする方法です。ただし、今回主眼としたいコスト面についてはECS Fargateを用いるこちらの方法と同等になると考えられるため、この記事では省略します。

方法2. Step Functionsのステートマシンとして実装

別の方法は、Step Functionsを使う方法。基本的にはWaitしながらLambdaを呼び出すことをループします。ただし、Step Functionsは1回のExecutionあたり履歴が25,000件を超えるとエラーになるので、適当な回数で再起動する必要があります。それらを考慮すると、以下のような定義になるしょう。

Lambda関数の中でループ回数をカウントアップして、カウントが一定数を超えたらステートマシンを再起動します。

def handler(event, context):
  count = event.get['count'] or 0
  doSomething()
  return { 'count' : count + 1 }

この方法のメリット・デメリットは以下のようになるでしょう:

メリット

  • VPCいらずで、よりサーバーレス

デメリット

  • マネージドにステートマシンの常時起動を保証できないので、別途監視する必要あり
  • Step Functionsのステート遷移がコスト高いかも?

ということで、2つの方法を紹介しました。

上記の方法1と2では、色々と違いはありますが、多くはケースバイケースの判断となるでしょう。そのような中でも絶対的な指標となる(低ければ低いほど良い)、コストについてまず注目してみます。

コストの比較

方法1と2のコスト主要因は、以下となります:

  • 方法1: ECS Fargateの利用時間
  • 方法2: Lambdaの呼び出し・利用時間、Step Functionsの状態遷移回数

具体的に見ていきましょう。ここで、諸々のパラメータを定義します :

  •  T : ループの処理間隔 [s]
  •  T_c : 1ループ当たりのLambda処理時間 [s]
  •  v_{cpu} : FargateのCPU数 [vCPU]
  •  v_{ram} : FargateのRAMサイズ [GB]
  •  l_{ram} : LambdaのRAMサイズ [GB]
  •  C_{fc} : Fargateの1vCPUあたりの料金 [USD/h]
  •  C_{fm} : Fargateの1GB RAMあたりの料金 [USD/h]
  •  C_{li} : Lambdaの呼び出しあたりの料金 [USD/invocation]
  •  C_{lt} : Lambdaの利用時間あたりの料金 [USD/GB/s]
  •  C_{s} : Step Functionsの状態遷移あたりの料金 [USD/遷移]

方法1のコストは非常に簡単です。一時間あたりの料金は以下になります。

 C_1 = v_{cpu}C_{fc} + v_{ram} C_{fm} [USD/h]

CPUとRAMの料金を足すだけです。単純ですね。

方法2の1時間あたりのコストは、以下です。

 C_2 = \frac{3600}{T}(C_{li}+ l_{ram}T_c C_{lt}   + 3C_{s}) [USD/h]

  \frac{3600}{T} は1時間あたりのループ回数です。1ループあたりはLambdaの料金に加えて、Step Functionsの遷移回数3回分 (上図を参照) の料金が課金されます。 また、厳密にはステートマシンの再起動による遷移料金も含まれますが、軽微なため無視しています。

では、各変数に2022/6現在の値を代入して、計算してみました。 ここからパラメータをいじることができます。

それぞれのコストは諸々のパラメータによるのですが、いくつかの例を見てみましょう。特定の例における比較があたかも一般論として独り歩きしてしまうのは避けたいです。以下はあくまでも特定の例における比較であることに注意してご覧ください。

一例

以下のようにパラメータを設定した場合です:

  •  T_c= 100 ms : 1ループ当たりのLambda処理時間 [s]
  •  v_{cpu}= 0.25 vCPU : FargateのCPU数 [vCPU]
  •  v_{ram}= 256 MB : FargateのRAMサイズ [GB]
  •  l_{ram}= 256 MB : LambdaのRAMサイズ [GB]

このときのグラフはこうなります。

青線が ECS Fargateのコスト ( C_1)、赤線がStep Functions + Lambdaのコスト ( C_2)です。ループの処理間隔  T を横軸の変数として、コストをプロットしています。

これより、処理間隔がおよそ20秒を超える場合は、Step Functionsのほうが安上がりなことが分かります。ちなみに緑のラインは、処理時間が1分を超える場合のみ利用できる、EventBridge + Lambdaの場合です。不必要なコンピュートリソースを使わない分、圧倒的に安いことが分かりますね。

繰り返しになりますが、これはあくまでも一例です。このページから、パラメータを色々と変更して動きを確認してみてください。また、計算式に間違いがある可能性もあるため、ご自身で検算した上で参考にしていただければ幸いです。

もう少し考慮点を

2つの方法では、コスト面で違いがあることが分かりました。特に処理間隔が10秒以下になる場合は、Step Functionsだとやや高くなる印象です。とはいえ、これ単一のコストは1時間で1円ちょっとなので、1ヶ月でも1000円程度でしょう。状況によっては、コストはこのアーキテクチャを決める支配的な要因にならないかもしれません。

その他のメリットデメリットとしては、上記に挙げたとおりです。特にVPCの有無や、Step Functionsで処理が実行されていることを監視する必要性などは、重要な考慮点となるでしょう。 許容される複雑度を考えながら、そのシステムに最適な技術選定をしてください。

まとめ

AWSで1分未満の間隔の定期的な処理を実行する方法について考えました。やはりいろいろな考慮点があるので、最適な技術選定をするためには、それぞれのメリットデメリットを把握する必要があります。

もし他にもこのような方法があるよ、このような考慮点があるよなどありましたら、教えていただければ幸いです。