この記事はLCL Advent Calendar 2021 - 1日目です。
LCLとRailsバッチジョブ
バックエンドエンジニアの星野です。今年のre:Inventは開催前の時点で大型のアップデート続いており本番で何が発表されるのか全く予想がつきません。
さて、LCLではこれまでRailsバッチジョブの実行基盤としてKuroko2を利用してきました。 Kuroko2については過去の記事を参照してください。
Kuroko2はRuby on Railsを開発の中心に据えている私たちにとって使い勝手が良い一方で、 運用面で大きく2つの課題がありました。
- バッチジョブの実行数や負荷状況に応じたEC2インスンタンスのオートスケーリングが出来ていない
- Kuroko2サーバーの自体のメンテナンスタスクが発生する
注: あくまでLCLでの運用ケースでKuroko2が上記の問題に対応できないということではありません。
利便性を損なわず上記課題を解決するために部分的にバッチ実行基盤の置き換えを進めており、 ある程度形になってきましたのでまとめます。
新しいバッチジョブ実行基盤に求めるもの
前述の課題を解決するために次のことを重視しました。
- サーバーレスによるキャパシティ・コストの最適化
- 前述の課題1
- マネージドサービスによる運用タスクの低減
- 前述の課題2
- コードによるバッチジョブの管理
- TerraformをIaCの標準ツールとして活用しているのでTerraformから扱いやすいと嬉しい
- 単一のDockerイメージから利用可能
- Dockerを全面的に採用しているので同一のイメージを使いまわせると嬉しい
- ジョブの実行結果をSlackに通知できる
- Kuroko2では設定が容易だったので引き続き利用できることが必須
試行錯誤の結果、バッチジョブの特性に応じて3つのサービスを使い分けることに落ち着きました。
- CodeBuildのカスタムイメージ
- Container on Lambda + Step Functions Express Workflow
- SQS + Active Job
CodeBuildのカスタムイメージ
CodeBuildというとDveloper Toolsに分類されるので一見するとバッチジョブとは関係ないように聞こえますが、 その実態はサーバレス単発Dockerコンテナ実行サービスです。 CodeBuildは実行環境として通常のAWSから提供されるDockerイメージの他に任意のカスタムイメージを選択することができます。 このカスタムイメージを使うことでECSのrunTaskのように単発でRails runnerなどバッチジョブのコマンドを実行できます。
類似のFargateと比較して次のメリットがあります。
- Developer Toolsゆえにコンソール画面が開発者フレンドリーなのでAWSに不慣れなメンバーでも扱いやすい
- ECSのログ画面と比較して実行ごとのログがみやすく完了ジョブも参照しやすい
- ジョブの成否をEventBridgeから通知しやすい
- バッチ実行ができるためCodeBuildのみで簡易的なワークフローを組める
一方デメリットもあります。
- 最小のマシンスペックが2vCPU3GBメモリ(x86)なためコスト効率はよくない
- Spotオプションなど割引オプションがない
- Fargateと同様に起動までに時間がかかる
使い所として実行頻度は少ないが実行時間のかかるバッチジョブが挙げられます。
Container on Lambda + Step Functions Express Workflow
前述のCodeBuildは起動時間が遅いため実行頻度の多いバッチジョブには向いていません。 それを補完するようにContainer on Lambdaを使います。
この記事中ではDockerイメージをAWS Lambdaで動かすことをContainer on Lambdaと呼ぶことにします。 同じコンテナの実行環境でもFargateやCodeBuildとは実行モデルが異なるため特性も異なります。
- イメージのキャッシュが効いている間は起動が高速
- Lambdaからアプリケーションコードを実行するエントリーポイントの追加
- ミリ秒単位の課金体系
- 最大15分の実行時間
さらにStep Functionsと組み合わせることで実行成否に応じたアクションを設定することができます。 Lambdaの場合はより安価に利用できるExpress Workflowと積み合わせることで真価を発揮すると感じています。 Express Workflowは標準Workflowで使えるwait for callbackなどで呼び出し側のサービスの最終的な実行成否を取得できないのですが、 Lambdaだけは実行の成否が取得できます。*1
Step Functionsのエラーハンドリングの機能を使うことで失敗時のリトライ処理やEventBridgeへの通知が容易に実装できます。 ただしExpress Workflowの最大実行時間は5分なのでLambdaの15分よりさらに短くなる点に注意です。
使い所として実行頻度が多いが実行時間の短いバッチジョブとCodeBuildの反対になります。
SQS + Active Job
LCLには非常に実行時間の長いバッチジョブがあり失敗時のリカバリに労力がかかっていました。 その課題を解消するためにAmazon SQSとActive Jobを採用しました。 実行順が重要でないバッチジョブを小さいジョブに分割してSQSにキューイングしてActive Jobで逐次実行して行きます。
SQSをActive Jobのアダプターとして使えるようにするGemはShoryukenが広く使われてきたと思いますが、 2020年の12月にAWS公式のRailsを拡張するgemであるaws-sdk-railsにもその機能が追加されたのでそちらを採用しました。 Shoryukenと詳細に比較検討したわけではありませんがドキュメント記載のベンチマークの実行結果とコードベースが小さく処理内容が追いやすい点で採用しました。
Active JobのプロセスはECSサービスとして実行しているのでコスト面が気になるところですが、CPU負荷に応じたオートスケーリングとタスクを全てFargate Spotで起動することでコストの問題を解決しています。*2 SQSはリトライ回数を設定できるため処理中のメッセージがSpotの中断で失敗してもリトライが容易です。
Container on Lambdaとの使い分けとしてはActive Jobのプロセスの起動数を制限、すなわちオートスケーリングの最大値を設定することでデータベースやAPIに対する負荷をコントロールしたい場合に有力です。
まとめ
バッチジョブの実行基盤の課題をAWSのサービスを使い分けることで解決している解説をしました。 この記事では概念の紹介にとどめサンプルコードなどは記載していないため分かりにくい箇所もあるかもしれません。 明日以降のアドベントカレンダーでサービス毎により詳細に解説した記事を投稿していきます。
おまけ
ここでは検討したものの採用に至らなかったサービスを少しだけ紹介します。
ECS runTask(Fargate)
CodeBuildの項でも触れましたがECSのrunTaskは終了したタスクの実行ログが追いにくかったりEventBridgeのフィルターがCodeBuildよりも使いにくいという点で採用しませんでした。
AWS Batch(Fargate)
AWS BatchのFargate対応に大きく期待したのですがLCLのユースケースではECSでの実行と大きく変わるところがなかったので採用しませんでした。
Prefect
PrefectはOSSで開発されているワークフローエンジンです。
AirflowのコントリビューターがAirflowのつらみを解決するために開発されました。 GUIなど優れている点が多く有力な移行先でしたがSaaS版の費用面で採用しませんでした。
Amazon Managed Workflows for Apache Airflow (MWAA)
AWSマネージドなApache AirflowですがLCLでのユースケースではDAGなどオーバーヘッドが大きく、費用面も安価に収まらない見積もりだったので採用しませんでした。
採用情報
LCLでは開発メンバーを募集しております!
もし興味をお持ちになりましたらお気軽に応募してください。
*1:詳細は公式ドキュメントを参照 https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/connect-to-services.html#connect-to-services-integration-patterns
*2:こちらもContainer on Lambdaで起動するオプションがあるのですが導入検討時にContainer on Lambdaの実行パフォーマンスが未知数だったこともあり今の所採用していません https://github.com/aws/aws-sdk-rails#serverless-workers-processing-activejobs-using-aws-lambda