はじめに

弊社で開発している Lisket では外部APIに大量のリクエストを行っています。このたび、ビジネス的な要請からアプリケーションの要件を変更することになり、それを機に外部APIへの大量リクエストのインフラアーキテクチャもブラッシュアップすることになりました。 要件は以下です。

  • バッチ処理の主な振舞は外部APIへのリクエスト。
  • 単位時間あたりに要求されるバッチ処理は10,000件を超えることもあるし数件のこともある。
  • 外部APIは単位時間あたりのリクエスト数に上限を設けている。よってその範囲内でリクエスト数をコントロールしたい。
  • 最大件数が増加しても6時間以内くらいでは終わってほしい。
  • バッチ処理の所要時間については、ほとんどが2,3秒で終わる。稀に数分かかることもある。
  • バッチ処理は互いにステートレス。順番や依存はない。

AWS Batchでやってみる

AWS Batch とはその名の通り、AWSの提供するバッチ処理サービスです。多量のジョブをさばいてくれるため今回の要件と合致しています。AWS Batchはその実Amazon ECSのラッパーに加えてキューシステムが備わったもののようで、面倒なEC2インスタンスの管理もよしなにやってくれるようです。以前EC2ベースのAmazon ECSを触ったときにEC2インスタンスの取り回しに面倒さを感じていたので「フルマネージド型バッチ処理をあらゆる規模で行えます」の触れ込みは魅力的です。
なお、「SQS + Lambda」や「SQS + ECS」も検討の俎上にあがったのですが、当初はAWS Batchが最も要件に合致していると判断したためまずはこれを試してみました。

懸念点

AWS Batchの要件と私たちの要件は概ね合致しているように見えます。しかし、2点において乖離があることが懸念ではありました。

外部APIは単位時間あたりのリクエスト数に上限を設けているので、その範囲内でリクエスト数をコントロールしたい。

バッチ処理単体の時間は前述の通り想定ができます。よって並列数を設けることで単位時間あたりの処理数を管理できると考えました。
ジョブの並列数を制御する…これについてはAWS Batchとしては業務外のことのようです。そこで、AWSサポートの方に相談したところ、コンピューティング環境における上限vCPUを設けた上でジョブ定義でvCPUの設定を並列数から換算した値に設定することで実現してはどうか、とご提案をいただきました。
たとえば並列数を 5 としたい場合、コンピューティング環境における上限vCPUを 10 としてジョブ定義でのvCPUを 2 とすることで擬似的に並列制御を実現するというわけです。
なるほど、この仕組みで問題なさそうです。

最大件数が増加しても6時間以内くらいでは終わってほしい

AWS Batchはその性質上、時間を保証するサービスではありません。とはいえ今回の要件上、ジョブごとに所要時間の多寡はあれど2,3秒で終わるものがほとんどです。前述の「並列数」との兼ね合いもありますが、仮に 5(秒/ジョブ) x 10,000(ジョブ) x 10(ジョブ/並列) とした場合、1.4時間ですべてのジョブが完了します。
これも問題なさそうだと判断しました。この時点では…

数件のジョブを送信してみる

数件のジョブを「送信」すると、しばらくしてEC2インスタンスが立ち上がりジョブが処理され、しばらくするとEC2インスタンスが削除されます。
ここで、「しばらくすると」という表現をしたのは、AWS Batchのダッシュボードを見ている限りインスタンスの作成・削除の所要時間にばらつきがあるように感じたためです。このあたりの確証が欲しくこれまたサポートの方に伺ったのですが、コンピューティング環境のインスタンスを起動/停止するまでに必要な具体的な時間の目安については公開されていないとのことでした。
とはいえ、EC2インスタンスが立ち上がるまでに数時間かかることはないでしょうし、一度インスタンスが立ち上がってしまいさえすれば複数のジョブはその上でサクサクと処理されていくはずです。次の検証にいきます。

多量のジョブを送信してみる

10,000件を超えるようなジョブを送信してみました。なお「並列数」は 10 になるように設定しています。

経過観察

aws_batch_ _amazon_ecs

経過時間 n n+4分 n+6分 n+8分 n+10分 n+12分 n+14分 n+16分
RUNNABLE 1000+ 1000+ 1000+ 1000+ 1000+ 1000+ 1000+ 1000+
STARTING 6 0 3 0 7 2 3 0
RUNNING 2 4 2 5 0 0 3 5
SUCCEEDED 1000+ 1000+ 1000+ 1000+ 1000+ 1000+ 1000+ 1000+

時間の経過と各ステータスの推移を模式的に表すと上記のような感じです。

検証前には RUNNING が常に10になっているイメージでした。しかし、実際のところは10未満をうろうろしています。
またAWS Batch のジョブダッシュボードだけを見ても RUNNABLESUCCEEDED1000+ のためにジョブの流速がまったくわかりません。よってECSのタスクを見てみた(上述の画面キャプチャ)のですが STARTING (ECSのタスクとしては PENDING )のジョブが長く滞留しているように見受けられます。
とはいえ、このあたりをよしなにやってくれるのがAWS Batch。とりあえずしばらくしたら本気出してくれるかもしれないのでこのまま放置してみることにしました。

が、24時間経っても状況が変わらず RUNNABLE1000+ のままでした。
また、ジョブの流量をロギングしていたのですが、時間によって 2,000 -> 900 -> 900 -> 800 -> 150 -> 80 -> 90 -> 60 -> 70 -> 85 -> 40 と処理件数が低下・変化しました。

各種設定を変えてみる

ジョブ定義

AWS Batch ではコンピューティング環境にてリソースが確保されたことを確認してから、ジョブを実行します。これはラップしている ECS の要件からも腹に落ちるものです。
よって、ジョブ定義におけるメモリ設定を富豪的な値ではなく必要最小限であろうものに変更しました。
しかし流速にめざましい変化は見られませんでした。

「並列数」

コンピューティング環境のvCPUの最大値を14,30,60,100と上げてみました。
単位時間あたりの流量は増えました。しかしこの増分も線形であり、指数関数的に増えたわけではありませんでした。
つまり、ひとつのジョブが完遂するまでのEC2インスタンス専有時間は変わらなかったことになります。コンピューティング環境のvCPUの最大値は外部APIの要請から必ず頭打ちになりますので、このままでは根本解決になりません。仕事をせずにEC2インスタンスを専有している状況は変わらず、これではコストメリットがまったくなさそうです。

配列ジョブ

あるジョブキューにおけるすべてのジョブの基になるジョブ定義は同一のものでありコンテナイメージも同一のものです。よってコンテナイメージのpullにかかる時間やリソース確保の計算にそれほど時間がかかるとは思えませんでした。ただし、各々のジョブは互いにステートレスなのでたとえ絶対値が同じでもAWS Batchがリソースの確保時に最適化してくれているとの保証はまったくありません。よって、各ジョブの要求するリソースが同一のものであることをより意図的に採用するために配列ジョブにしてみました。

配列ジョブは、ジョブ定義、vCPU、メモリなどの共通パラメータを共有するジョブです。

配列ジョブ - AWS Batch

これも結果的には流速にめざましい変化は見られませんでした。
配列ジョブを採用しなかったときは RUNNABLE から STARTINGSTARTING から RUNNING への各遷移がとても遅かったのですが、配列ジョブを採用してからはRUNNABLE から STARTING への遷移が遅いような印象を受けました。

結論

AWS Batchは今回の要件に合致しませんでした。

数十万件のバッチコンピューティングジョブを

AWS Batch – 簡単に使えて効率的なバッチコンピューティング機能 – AWS

とドキュメントには記してあるのですが、残念ながら少なくとも私どもの作成した環境では思うようなパフォーマンスが出ませんでした。
もしかしたら設定を細かく分析・調整することで所望のパフォーマンスに近づくことはあったかもしれませんが「フルマネージド型」に魅力を感じていましたのでこれ以降のメンテナンスコストを考えてもこれ以上コストをかけたくありませんでした。
よって、都度AWSサポートの方にもアドバイスいただいたのですが、今回は AWS Batch の採用を見送りました。

AWS Fargate を採用しました

思うようにいかないラッパーを触るよりは多少面倒でも低レイヤーのものを直接触ればよいじゃん、ということで。
AWS BatchはEC2ベースのECSですが、今回はFargateベースのECSを採用することとしました。

  • SQSキューのメッセージをポーリングし自前でキューをさばく処理をECSタスクとして定義
  • このECSタスクをECSサービスに登録し、ECSサービスの「タスク数」を指定することで「並列数」を制御する

こうすることで結果的に1,000件を12分で処理することができました。

また、AWS Batch用の各種リソースは僅かな手を加えることでECS用に変更できました。

さいごに

AWS は多数のサービスが存在しており、また各サービスの要件が重なる面もあります。 ドキュメントだけではわからない勘所などもあり、とりあえずは触って試してみるというのは重要ですね。