Agonesで作る機械学習アプリケーションのWorker

この記事は NTTコミュニケーションズ Advent Calendar 2023 の13日目の記事です。

こんにちは、イノベーションセンターの坂本です。 ソフトウェアエンジニアとしてノーコードAI開発ツール Node-AI の開発に取り組んでいます。

機械学習やその前処理などの計算にかかる時間はデータサイズや処理内容により大きく異なります。そのため機械学習やデータ分析に関するアプリケーションでは、冪等でない処理をイベント駆動型アーキテクチャ(EDA)で扱う難しさがあります。 今回は上記の課題とその解決策として採用している専用ゲームサーバ(DGS)用OSS Agonesを利用したEDAのWorkerについて紹介します。

Node-AIとは

Node-AIはブラウザから以下の図のようにカードを直感的につなげるだけで時系列データの前処理からAIモデルの学習・評価までの一連のパイプラインを作成・実行できるツールです。

各計算処理ステップ(例えば移動平均や正規化など)がカードとして独立し1つずつ実行できるため、視覚的にデータ処理の流れを追いやすくなっています。複数人での同時作業にも対応しているため個人だけでなくチーム単位で効率的なデータ分析やAIモデル開発ができます。もし詳しく知りたい方がいれば過去の関連記事を参照してください。

Node-AIにおけるEDA

Node-AIではこの各カードに応じた計算処理を非同期処理として専用のマイクロサービス(Worker)で行っています。 ユーザがカードを実行すると裏では大まかに次のような流れで計算処理されます。 なお1つの実行当たりのリソース上限を与えたいので、1つのWorkerが引き受けられるのは1つのmessageまでという作りにしています。

  1. ユーザがNode-AI上のカードを実行する
  2. Broker(Redis)にどのような計算処理かといったmessageが格納される
  3. WorkerがBrokerからmessageを取得してそれに従った計算処理を実行する

WorkerおよびEDAの課題/要件

Node-AIのような機械学習系のアプリケーションにおいて、Workerに関連する部分では以下のようなことが課題/要件として挙げられます。

  • 運用者目線
    • いつでもアップデートしたい
    • インフラ費用を下げたい
  • ユーザ目線
    • 計算処理が高速に実行されてほしい

まずアップデートについて問題になるのは内部処理の安全性担保です。例えばサービス更新によって処理実行中にWorkerが強制終了してしまうとプロダクトの信頼性に関わります。うまくBlue/Green Deploymentのような仕組みを導入できればよさそうですが、そのための開発やメンテナンスのコストを考えると複雑なものや独自のCustom Resource Definitionsは極力採用したくありません。

次にインフラ費用についてです。これはオートスケールやオンデマンドなリソース確保を実現すれば概ね問題にはならないでしょう。

最後にユーザ目線の高速であって欲しいということについてです。そもそもユーザが体感するWorkerに関する待ち時間は ネットワーク遅延 + Workerがmessageを取得するまでの時間 + 計算にかかる時間 + その他 であるとします。

Workerがmessageを取得するまでの時間について考えてみます。 これはmessageがキューに滞在する時間とも読み替えることができ、実態としてはノードのProvisioningやコンテナ作成、プロセス起動までにかかる時間があります。この滞在時間を小さくするためには、デメリットを無視すれば予めWorkerをたくさん用意したり高速なスケールアウトなどが実現できればよさそうです。

他にも検討した結果、Workerに求められることは以下の4つとなりました。

  • 処理開始までの時間
    • カードを実行するとすぐに計算処理が開始されて欲しい
  • マイクロサービス更新の安全性
    • マイクロサービスを更新する際に計算処理中のWorkerに影響を与えたくない
  • スケーリングの安全性
    • スケールインの際に計算処理中のWorkerに影響を与えたくない
  • インフラ費用面
    • コンピューティングリソースの維持費用を安く済ませたい

実装方式の比較

まずPodとJobでWorkerを実装した際に要件と合致するかどうかをみていきます。 なおNode-AIはデータ機密性に課題をもつお客さま向けにオンプレミス環境でも提供していたり機械学習を使う都合上柔軟なタイムアウトを設定する必要があるため、各クラウドベンダーのサーバレスサービスなどは選択肢から除外しています。

こちらが評価した結果です。

評価観点 Pod Job
処理開始までの時間 ✅ (条件付き)
マイクロサービス更新の安全性
スケーリングの安全性 -
インフラ費用面

Node-AIのリリース当初はWorkerをPodで構築していました。

いくつか抜粋すると、まず処理開始までの時間の問題は条件付きでクリアしています。 これはコンテナ作成や内部プロセス起動はPod作成時の一度きりで済むからです。一方でスケーリングを考えた際にはキューにあるmessage数をメトリクスとしてオートスケールさせるとよさそうですが、実際にやってみるとmessageが溜まってからスケーリングが開始するので待ち時間が発生しそうです。

また、マイクロサービス更新やスケールイン時の安全性ついてはterminationGracePeriodSecondscluster-autoscaler.kubernetes.io/safe-to-evictを駆使するといったことも考えられますが、ライフサイクルの観点から厳しかったりサービス更新には非対応だったりと完璧な解決策にはなり得ませんでした。

次にJobで実装してみました。

JobではPodでの課題をほぼ解決できそうでしたが、今度は処理開始までの時間が大きく低下してしまう結果となりました。これは各message単位で行われるJobの作成とコンテナ中のプロセス起動に大きく時間がかかることに起因します。 Node-AIで使用しているマイクロサービスの場合、image cacheが効いたノードでも上記のオーバーヘッドが約8秒も(つまりPodでの実装パターンよりもカード実行開始までに約8秒多くかかる)あり、UXを大幅に低下させてしまうため採用に至りませんでした。

AgonesをWorkerに利用する

これらの課題を鑑みた結果、DGS用OSS Agonesを用いてWorkerを構築しました。なお本記事では少し古いですがAgonesのバージョンは1.28.0の内容となっています。

オンラインゲームを例とすると、DGSは運営側でホストされプレイヤー間の状態を同期しています。ゲームにもよりますが、おおよそ数分から数時間に及ぶプレイ中の計算結果をメモリに保存するためステートフルな状態となります。そのため実行中のゲームサーバーの保護をする仕組みが必要となります。また利用状況によってはリソースのスケーリングも必要です。AgonesはこうしたDGSのホスト、実行、スケーリングをKubernetes上で容易に実現するものです。

本記事の内容で利用するAgonesのリソースは以下です。

  • GameServer
    • DGSそのもの、これをWorkerとして使います
  • Fleet
    • 割り当て可能なGameServerのセット
  • FleetAutoscaler
    • Fleetのスケーリングを行うもの

DGSと機械学習処理にはステートフルかつ冪等ではない処理を扱うという共通点があります。 そこで具体的にはAgonesの次の機能を利用します。

  • 常にStateがAllocatedなGameServerの数を維持するオートスケールアルゴリズムを有する
  • AllocatedなStateをGameServerに付与することでアップデートやスケールインの対象外にできる

1つめの特徴を利用することでmessageを引き受けられる状態のWorkerを常に指定数だけPoolしておくことがFleetAutoscalerを用いて実現できます。動画のように例えば4つ用意する設定の場合には、そのうちの1つがmessageを受け取って計算処理状態(Allocated)になると自動で1つWorkerを作成する挙動となります。これによってある程度経済的でユーザに遅延を感じさせないスケーリングを実現できます。

2つめの特徴としてAgonesではStateがAllocatedなものを保護してくれます。つまりWorkerがmessageを取得したタイミングでAllocatedにすれば処理中のWorkerに影響を与えることなく安全なアップデートが可能になります。

また、うまくState遷移を設計すると擬似的にOne-ShotのJobのようなライフサイクルを持つWorkerを作ることができます。Node-AIのWorkerではプロセスの状態に合わせて概ね以下のようなState遷移を行なっています。

これらの特徴から我々の課題を全て一定の水準で解決できることが分かりました。

評価観点 Pod Job Agones
処理開始までの時間 ✅ (条件付き)
マイクロサービス更新の安全性
スケーリングの安全性 -
インフラ費用面

Worker内のアーキテクチャ

Worker自体は下図のような構成をとっています。なおControllerとの通信部分の記述は省略しています。

AI/ML用コンテナはPythonで書いており、message取得や前処理/機械学習の処理を実行します。 SDK ServerはAgones APIとの通信やメタデータを提供しています。 制御用コンテナはGo言語で書いており、GameServerのlifetime管理や再スケジューリングを行います。(実装例)

GameServerを高頻度で作成&削除を繰り返すとFleetでspec.scheduling: Packとしていてもリソースが分散して配置されることがあります。またGameServerのPodにはcluster-autoscaler.kubernetes.io/safe-to-evict: falseが付与されるのでノードのスケールインが効かなくなります。つまり高頻度でWorkerが使用された後にしばらく使われなくなるとその期間は使用率の低いノードが存在してしまい、ノード単位で課金される料金体系では無駄にコストがかかってしまいます。そこで制御用コンテナを用いて一定時間経つと自己的にGameServerを削除し、最小のノードのセットに詰め込むように再スケジューリングさせる仕組みを導入しています。

また、ゾンビリソースの防止のためにAllocatedなものでも一定時間たったものは強制削除するようにしています。この時間は各カードによって異なる値をAI/MLコンテナからSDK Serverのメタデータとして設定し、例えば正規化の場合は10分+バッファ時間(5分)、学習では24時間+バッファ時間(5分)としています。ただしアプリケーション側にも処理のタイムアウトは入れているのでこれはあくまで保険的な使い方です。

他にもGameServerのhostPort機能などWorkerとして不要なものを無効化しています。

おわりに

本記事ではNode-AIで用いているWorkerについて紹介しました。 Node-AIはどなたでもこちらから無料で試用できるので、もしプロダクトや使われている技術に興味を持っていただけたらぜひ触ってみてください。

それでは、明日の記事もお楽しみに!

© NTT Communications Corporation All Rights Reserved.