Search Unity

Unity Simulation を使うと、開発者、研究者、エンジニアは、パラメータ化された Unity ビルドの何千ものインスタンスをクラウド上のバッチで、スムーズかつ効率的に実行できる環境を手に入れることができます。Unity Simulation では、実行のたびに内容を変更できるように、Unity プロジェクトをパラメーター化することができます。また、機械学習のためのトレーニングデータの生成、AI アルゴリズムのテストと検証、モデル化されたシステムの評価と最適化など、目標とする応用先に必要なシミュレーション出力データを指定することができます。Unity Simulation を使用すると、ジョブの実行に使用するバッチコンピューティングソフトウェアやサーバークラスターをインストールして管理する必要がないため、結果の分析や問題解決に集中することができます。このブログ記事では、顧客のジョブが Unity Simulation 上で可能な限り高速かつ低コストで実行されるという目標のために、Unity のエンジニアがどのように新しい技術やアイデアを取り込んでいるかをご紹介します。

Unity Simulation と Kubernetes

Unity Simulation は、オープンソースのシステムである Kubernetes を活用して、適切な数とタイプのコンピュートインスタンスでシミュレーションジョブをコンテナ化し、スケジュールして実行します。Kubernetes を使って、シミュレーションの出力データをクラウドストレージの格納先に簡単にダウンロードし、設計、トレーニング、テストのワークフローに接続することができます。Kubernetes を活用することで、コンピュートリソースの割り当てやキャパシティプランニングを気にすることなく、一度に複数のシミュレーションを実行することができます。

以下に挙げる概念は、Unity Simulation と Kubernetes を理解する上での基本となるものです。

  • Run Definition:シミュレーションの名前と説明、シミュレーション実行のためのアプリケーションパラメーター、使用するコンピュートリソースを指定するシステムパラメーター、アップロードされた Unity 実行ファイルを参照する Unity Build の ID を指定します。
  • Kubernetes ジョブ:Kubernetes をデプロイすると、クラスターができます。Kubernetes クラスターは、コンテナ化されたアプリケーションを実行するノードと呼ばれるワーカーマシンのセットで構成されています。ワーカーノードは、アプリケーションワークロードのコンポーネントである Pod をホストします。Pod は Kubernetes システムの基本的な実行単位であり、クラスター上で実行されるプロセスを表します。Kubernetes ジョブは、一定時間実行して終了するバッチプロセスに参加している Pod を監視するシステムレベルのジョブコントローラーによって管理されています。このブログ記事で「ジョブ」という用語を使う場合、常に Kubernetes ジョブを指します。
  • Kubernetes コントローラーとオペレーター:Kubernetes コントローラーは、リソースの現在の状態を望ましい状態に向かって段階的に移行させる役割を持ちます。Kubernetes ジョブコントローラーは、1 つ以上の Pod を作成し、指定した数の Pod が正常に完了することを保証します。 Kubernetes オペレーターは、このパターンに沿ったコントローラーですが、ワークロードを実行するために必要な、具体的な運用上の知識を表現できるように拡張されています。Unity の Simulation Job Operator は、この記事で説明するように、Kubernetes のオートスケーラーを理解し、その動作が現在の状態に対して、望ましい状態との比較に基づき、どのように影響するかを理解しています。

Kubernetes ジョブは 1 つ以上の Pod を作成し、適切な数の Pod が正常に完了することを保証します。作業キューは通常、ジョブに割り当てられた Pod にタスクを分配するために使用されます。コンテナー内で実行されているアプリケーションプロセスは、必要に応じてキューからタスクを並列に、または個別に取り出すことができます。ジョブの並列性に関するパラメーターは、ジョブが同時に実行する並列 Pod の数(言い換えれば、同時実行される Run Execution のシミュレーションインスタンスの数)を決定するために使用されます。ジョブの完了数パラメーターは、正常に終了するべき Pod の数を決定します。

Unity Simulation スケジューラーは、Kubernetes ジョブとキューデザインパターンを使用して Run Execution をオーケストレーションします。スケジューラーは、ジョブを Kubernetes クラスターに投入する前に、各シミュレーションインスタンスへのメッセージを独立した Run Execution のキューに格納します。

以下の図は、ジョブを実行するための Pod にメッセージを配信するためにキューを使用している様子を示しています。この図は、並列度 4 のジョブを示しています。つまり、シミュレーション内で 4 つの Unity プロジェクトのインスタンスが実行されているということです。

問題点

Unity の Kubernetes アプリケーションは、バッチ処理とオートスケーリングを組み合わせているという点で、大多数のアプリケーションとは異なります。 私たちは、バッチ処理ジョブと Kubernetes Autoscaler の動作を組み合わせると予想外の相互作用が発生し、コンピュートリソースを大きく浪費し、ジョブの効率も大きく低下する結果になることを発見しました。Kubernetes オートスケーラーがクラスターのスケールアップとスケールダウンを交互に行い、ジョブコントローラーが誤った状態を報告してしまうのです。これは、ジョブ時間の過大な見積もり、ジョブ完了後の不正確なレポート、および全体的な CPU 効率の低下につながります。

ジョブのライフサイクルを通じて完了数は変わらないか、増加するですが、私たちのメトリクスでは、ジョブの完了数の減少が観測されました。 不正確な完了数のカウントにより、ジョブコントローラーは正常に完了しなければならない Pod の数を決定している完了カウントの要件を満たすために、より多くの Pod を作成しました。ジョブがより多くの Pod を要求したことが、Kubernetes スケーラーがクラスターにノードを追加する原因となりました。 新たに作成された Pod は、キューにタスクが残っていなかったため、作成後すぐに完了しました。また、Pod を完了した後に完了数のカウントが要件となっていた数に達したため、追加されたノードはすぐにアイドル状態になってしまいました。これにより、オートスケーラーによってクラスターからアイドル状態のノードが削除され、また Pod の完了数が減少しました。

このような動作は、次のような悪循環を引き起こします。

  1. より多くの Pod を実行するため、スケールアップする
  2. Pod がすぐに完了する
  3. ノードがアイドル状態になったためにスケールダウンする

問題のあるジョブがクラスターのリソースをすべて利用していたため、スケールアップによりクラスターが他の作業を実行できない状態になってしまいました。最善の場合でもリソースが無駄遣いされ、最悪の場合はサービスが利用できなくなります。

この問題について、順を追って詳しく説明します。

ステップ 1:

Unity Simulation はマルチクラウドソリューションであり、Google Cloud Platform 上で動作する Unity 製品には、マネージドな Kubernetes ソリューションである GKE を使用しています。GKE クラスターに、5 つの Pod をホスティングできる Node1 という 1 つの実行中のノードがあると仮定してみましょう。 新しいジョブは 15 個の Pod を必要とし、GKE クラスターは 15 個の Pod を実行するために 2 つのノード、Node2 と Node3 を追加してキャパシティを増加させます。

ステップ 2:

すべての Pod は GKE クラスター上で「アクティブ」になっています。

ステップ 3:

Node1 上の 5 つの Pod がジョブの「完了」状態(緑)になります。

ステップ 4:

Node1 の Pod が完了したので、アイドル状態になってスケールダウンされます。Node1 がクラスターから削除されると、Node1 が持っていた完了数 5 がカウントされなくなり、ジョブの完了数は本来 5 であるのに、0 とカウントされてしまいます。ジョブでは 15 個の Pod が存在すると期待されているのに、実際には Pod は 10 個しか存在しなくなり、これがエラー状態を誘発します。ジョブコントローラーが新しい Pod を 5 つ要求するため、オートスケーラーは再びクラスターにノードを追加します。

この問題をより深く理解し、Kubernetes のジョブコントローラーのソースコードを慎重に研究する必要があります。SyncJob 関数は、ジョブが管理している Pod の現在の状態に基づいてジョブの状態を同期させます。SyncJob 関数は getStatus 関数を呼び出して、ジョブで Phase が Succeeded の Pod と Failed の Pod の数を取得します。Pod は、getPodsForJob 関数のセレクターを使用してクラスター内に存在している Pod に対してクエリを行うことで取得されます。

残念ながら、ノードが Kubernetes クラスタから削除されると、そのノード上で動作していた Pod のメタデータが削除されてしまいます。ジョブコントローラーがジョブの Pod について Kubernetes にクエリを行うと、オートスケーラーがノードをダウンさせた後、ジョブコントローラが誤った完了数を受け取ってしまいます。この動作は、長いスリープコマンドを 1 回と、短いスリープコマンドを複数回、別々のタスクで実行するシンプルなジョブを作成することで、簡単に再現できました。

解決策

ジョブコントローラーのソースコードについての理解が深まったことで、Pod のステータスを持続させることでスケーリングの問題を修正できることに気づきました。これにより、Kubernetes クラスター内で Pod のメタデータが利用できない場合でも、メタデータをキャプチャできることが保証されます。また、シミュレーションを実行するためのオペレーターを開発することは、他の理由からも有益であることがわかりました。

私たちが実装したカスタムリソース定義とオペレーターは、オートスケーリングの問題を修正した現在の Kubernetes のジョブコントローラーと非常によく似ています。Unity のシミュレーションジョブ(SimJob)オペレーターは、SimJob オペレータの制御ループが実行されるたびに、Phase が Succeeded の Pod と Failed の Pod について、それぞれ重複のないリストを更新します。Pod の最新の状態によって、Kubernetes クラスター内の SimJob の最新の状態と、Phase が Succeeded の Pod と Failed の Pod の一意なセットを含むデータストアが決定されます。

次の図は、クラスターがスケールダウンされた場合でも、SimJob オペレーターがどのように正しい「完了」数を維持するかを示しています。

過去 2 か月にわたって、本番環境で SimJob オペレーターを実行させました。これまでに 1,000 以上のシミュレーションを合計で 50,000 近くの実行インスタンスで実行することに成功しています(ここでは実行インスタンスは Pod に相当しますが、1 つの Pod に複数のシミュレーションインスタンスが存在することもあります)。クラスターと Unity Simulation サービスの可用性を危険にさらすことなく、シミュレーション実行のオートスケールを安全に行うことができます。私たちはこの状況に非常に満足しており、今後も SimJob オペレーターの改善と新機能の追加を意欲的に続けていきたいと考えています。

まとめ

Unity Simulation は、機械学習のためのトレーニングデータの生成、AI アルゴリズムのテストと検証、モデル化されたシステムの評価と最適化など、データ駆動型人工知能の最前線にあります。 Unity のチームは、最高度に管理されたシミュレーションサービスのエコシステムを提供するために、日々新しい技術やアイデアを取り込み続けています。

私たちと一緒にエキサイティングな Unity Simulation と AI にまつわる課題に取り組みたい方は、現在募集中のポジションにご応募されることをぜひご検討ください。

Unity Simulation の詳細はこちらをご覧ください。