登壇者の自己紹介

星孝太郎氏:本日は「実機での評価を支える Self-hosted Environment」というタイトルで発表いたします。このパートでは、評価基盤のアーキテクチャに着目してお話ししたいと思います。

自分は、ティアフォーでWebのバックエンドエンジニアとして、遠隔運転システム、認証認可基盤の開発を経て、現在はCI/CDのクラウド基盤の開発を行っています。

シミュレーション環境としてクラウドと実機をサポートしている

本題に入る前に、前提となる部分を簡単に説明いたします。評価基盤が、膨大な数のシミュレーションをテスト項目として実行します。シミュレーションを実行する環境は、複数のコンピューティングノードで構成され、各コンピューティングノードで動作するROSアプリケーションが相互に通信を行います。

膨大な数のシミュレーションを短時間で実行するために、シミュレーション環境はできるだけクラウドでスケールさせることが望ましいです。しかし、マシンスペックやネットワーク帯域などが結果に影響する、タイミングにシビアなテストや、特殊なハードウェアに依存したコンポーネントを動かすテストには実機が必要です。そのため、評価基盤ではシミュレーション環境としてクラウドと実機の双方をサポートしています。

シミュレーション環境のオーケストレーション

これを実現するために、シミュレーション環境のオーケストレーションというものを、環境に依存しない抽象度でのオーケストレーションと、環境固有のオーケストレーションに分離することをコンセプトとしています。

環境に依存しない抽象度でのオーケストレーションは「Simulation Clusters」という名前のサービスが担います。Simulation ClustersのResource controllerは、シミュレーションで使用するコンピューティングノードを表現するSimulation nodeとそれらをまとめて1つのシミュレーション環境を表現するSimulation Clusterを、仮想的なリソースとして管理します。

環境固有のオーケストレーションは、Simulation cluster runnerというコンポーネントが担います。runnerは、Simulation ClustersがAPI経由で仮想リソースの状態を受信して、組み込まれた環境固有のドライバを使用して実リソースを操作します。

障害復旧フローなどを含めて、複雑なリソース制御はSimulation Clustersが仮想リソースの上で行い、runnerは仮想リソースと実リソースの状態差分を埋めることだけに専念するという考え方になっています。

タスクスケジューラーとしての機能も併せ持つ、Simulation Clusters

また、Simulation Clustersは、タスクスケジューラーとしての機能も併せ持ちます。タスクスケジューラーはキューに積まれたシミュレーションタスクを、どのクラスタに割り当てるかを決定します。クラスタに障害が発生した場合などには、その再スケジューリングも行います。

すべてのノード上で、Simulation node agentという環境に依存しないコンポーネントが動作しており、このエージェントは、自身が属するクラスタに割り当てられたタスクをSimulation ClustersからAPI経由で取得します。このように、リソースオーケストレーションのタスクスケジューリングの責務も可能な限り環境に依存しない部分に載せて、環境に依存する部分は最小限に抑えるというアーキテクチャになっています。

シミュレーション環境固有のドライバ「Kubernetes driver」「OTA driver」

runnerには、Kubernetes driverとOTA driverという2種類の環境固有ドライバが組み込まれています。Kubernetes driverはその名のとおり、Kubernetesのリソースを操作することでコンテナベースのシミュレーション環境を構築するためのドライバです。

特定のクラウドプロバイダの依存がなく、Kubernetesが動く環境であればオンプレミスを含めどこでも使用できますが、現在はAWS上でシミュレーション環境を大規模にスケールさせるために使用しています。

OTA driverは、テストベンチにシミュレーション環境を構築するためのドライバです。OTAはOver The Airの略で、実車に積載されたECUのファームウェアをOver The Airアップデート、日本語では無線更新ですね。(無線更新)する仕組みを利用しています。このドライバは、ファイルシステムやプロセスを操作してネイティブ環境でシミュレーションを実行することができます。

Kubernetes driverの説明

Kubernetes driverは、仮想リソースに対応するKubernetesリソースの仮想コントローラーです。シミュレーションノードは、KubernetesのPodに相当します。また、Simulation nodeの仕様にあたるSimulation node specというリソースは、KubernetesのデプロイメントのようなPod templateを含むカスタムリソースとして定義しています。

runnerは、プロビジョニングリクエストを受けるとnode specカスタムリソースを取得して、仕様を満たすPodを作成します。社内ではAmazon EKSというAWSのマネージドKubernetesを使用して運用しています。Kubernetes driverの責務は、Podを作成するところまでで、PodをスケジュールするためのEC2インスタンスのプロビジョニングは「Karpenter」というオープンソースのクラスタオートスケーラーに委譲しています。

このように、クラウドプロバイダに依存する部分は切り出されているため、GKEやAKSといった他のクラウドプロバイダでも動かすことができます。

OTA driverの説明

OTA driverでは先ほどお話ししたとおり、実車のECUのファームウェアを無線更新する仕組みを利用しています。この仕組みは弊社が内製しており、OSS (オープンソースソフトウェア))として公開されています。ここでは詳しい説明は省きますが、カーネルやデバイスドライバなどを含むrootファイルシステム全体を更新できるのが特徴です。

runnerは、プロビジョニングリクエストを受け取るとOTAクライアントに対してOTA Updateのリクエストを発行します。

OTAクライアントは、image registryから更新に必要なデータをダウンロードします。この時、差分があるファイルのみをダウンロードするため、データの転送量を削減できることもポイントになっています。エージェントやシミュレータなど、ノード内で動作するコンポーネントは、OTA driverによって直接実行されます。

また、プライマリECUの各コンポーネントが後段のECUのコンポーネントのプロキシになっており、直接インターネットに接続されていないセカンダリECU、ベンチ環境もサポートしています。

Simulation Clustersの非機能要件とは?

これまで説明してきたrunnerは、さまざまな場所で動作しています。一方、仮想リソースを制御するSimulation Clustersは、運用負荷の削減のため、マルチテナントサービスとしてすべてのrunnerのリソースを一元管理しています。そのためSimulation Clustersにはある程度のスケーラビリティが必要です。

さらに、クラスタをスケールさせて大量のタスクを流した瞬間にトラフィックが急上昇するため、高い弾力性能が求められます。コンテナオーケストレーションの代表格であるKubernetesのコントローラーで採用されているReconciliation Loopは、コントローラー自体にスケーラビリティや弾力性を担保することが難しいため、Simulation ClustersではKubernetesとは異なるアプローチを採用しています。

また、大量のコンピューティングリソースを扱うため、機能停止や誤作動によって莫大なクラウド利用料を発生させてしまうリスクをはらんでいます。ネットワークやデータベースなどで一時的な問題が発生した場合であっても、可能な限り影響を抑える障害耐性が求められます。サービスへの影響が抑えられないレベルでのインフラ障害が発生した場合は、フォールバックによって可能な限りコンピューティングリソースを解放することが求められます。

Simulation Clustersで採用しているアーキテクチャパターン

これらの非機能要件を満たすためにSimulation Clustersで採用しているアーキテクチャパターンを説明していきます。

Simulation Clustersでは、AWSのマネージドサービスを活用したサーバーレスアーキテクチャになっています。主に「Lambda」「DynamoDB」「DynamoDB Streams」「API Gateway」「Step Functions」といったフルマネージドサービスを活用しています。

それぞれのサービスを簡単に説明すると、Lambdaはコンピューティングサービスで、DynamoDBはNoSQLのデータベースサービスです。DynamoDB Streamsは、DynamoDBに書き込まれたデータの変更をストリーミングするサービスで、API Gatewayはその名のとおりAPI Gatewayのマネージドサービスで、REST APIとWebSocketをサポートしています。

Step Functionsは、分散アプリケーションのためのワークフローのサービスです。Step Functionsでは、ワークフローを「ステートマシン」と呼びます。

Simulation Clustersでは、外部からクラスタを作成するリクエストなどを受け取ると、API Gateway経由で起動したLambda関数がDynamoDBにデータを書き込みます。

データが書き込まれると、DynamoDB Streams経由で起動したLambda関数がStep Functionsのステートマシンを開始します。そして、Step Functionsで(スライドを示して)このようなさまざまなワークフローを実行していくというのが基本的なパターンになっています。

特徴その1 リソースの状態変化に伴うワークフローを一度だけ実行することを保証

このアーキテクチャパターンには次のような特徴があります。

1つ目は、リソースの状態変化に伴うワークフローを一度だけ実行することを保証できるという点です。2つ目は、リトライやフォールバックをマネージドサービスに委譲できるという点。3つ目は、依存するすべてのマネージドサービスが高いスケーラビリティや弾力性を持つという点です。

これらの特徴を1つずつ詳しく説明していきます。

DynamoDB StreamsとStep Functionsを組み合わせることによって、リソースの状態変化に伴うワークフローを確実に1回だけ実行することを保証できます。例えば、クラスタを作成するAPIはリクエストを受けると、クラスタの状態をDynamoDBに書き込んですぐにレスポンスを開始します。

その後、非同期でリソースを実際にプロビジョニングする必要があるのですが、そのためにはrunnerと通信しながら状態を遷移していくワークフローを実行する必要があります。DynamoDB Streamsを使用することで、クラスタの状態の書き込みイベントをトリガーとして、ダウンストリームのLambda関数を確実に起動することができます。この特性により、リソースの状態変化に伴うイベント駆動を容易に実現できます。

さらに、Lambda関数がステートマシンを開始する際に、クラスタのリソースIDなど、イベントを一意に識別できる値を使用することによって、ワークフローの実行の重複を排除できます。これにより、ワークフロー全体の冪等性を自前で担保する必要がなくなるため、実装のコストを削減できます。

特徴その2 リトライやフォールバックをマネージドサービスに委譲

リトライやフォールバックをマネージドサービスに委譲できるという点が、2つ目の特徴です。各ステートでリトライ可能なエラーが発生した場合、Step Functionsの制御の元リトライを行います。

リトライ可能なエラーはさまざまなものがありますが、Simulation Clustersでは、一時的なネットワーク障害、DynamoDBのシステムエラー、マネージドサービスへのリクエストのスロットリングが比較的多く発生しています。ほぼすべてのステートを冪等に設計し、リトライ戦略を適切に設定することで、全体設計やコーディング時には、これらのエラーを意識の外に追い出すことができます。

また、クラスタのプロビジョニングはシミュレーション環境のインフラストラクチャーに問題が生じるなど、さまざまな要因で失敗する可能性があります。プロビジョニングに失敗した場合は、Step Functionsでエラーをハンドリングし、コンピューティングリソースを解放するための処理を実行しています。また、タスクのライフサイクルを管理するステートマシンでは、タスクの完了を待機しているステートにタイムアウトを設定しています。

なんらかの障害が発生してタスクのタイムアウトまで完了しなかった場合、タスクの再スケジュールを行うフローに遷移したり、タスクの失敗を記録したりします。リトライやフォールバック機構をマネージドサービスに委譲することで、リトライ・フォールバックを実行している最中にコンピューティングインスタンスが突然中断する可能性などを考慮する必要がなくなり、フォールトトレラントな分散システムの設計の難易度を下げることができます。

特徴その3 すべてのマネージドサービスが、高いスケーラビリティと弾力性を持つ

依存するすべてのマネージドサービスが、高いスケーラビリティと弾力性を持つという点が3つ目の特徴です。ただし、当然、無制限にスケールするわけではなく、特に弾力性に関しては注意しなければならない点がいくつかあります。

Lambda関数インスタンスは、非常に高いレートでオートスケールしますが、Burst concurrencyというAWSアカウント単位のハードクォータが存在します。

これは1分間あたりに関数インスタンスがスケールできる数の上限です。Simulation Clustersでは、現状このクォータを超過していませんが、超過する場合は前段にキューを置いて流量を調整するなどの仕組みが必要です。

DynamoDBのオンデマンドキャパシティモードでは、過去のピークトラフィックの2倍までは瞬時にスケールすることができますが、テーブル単位にソフトクォータが存在しています。ある程度はexponential backoffで対応できますが、場合によってはソフトクォータの引き上げや、読み書きを行うLambda関数自体の並列実行数を制御してスパイクを制御することを検討します。

Step Functionsにもさまざまなアクションにトークンパケット方式のソフトクォータが存在します。ある程度はソフトクォータの引き上げで対応できますが、Expressタイプのワークフローを使用すると、より高レートでステートマシンを開始することができます。ただし、Expressタイプは実行の重複排除機能がないので、ワークフロー全体の冪等性を自前で担保する必要があります。

また、スタンダードタイプは状態遷移数が課金対象であるのに対し、Expressタイプは実行時間とメモリ時間の掛け合わせで課金されるので、実行時間の長いワークフローには不向きです。ワークフローの特徴に合わせて、スタンダードタイプとExpressタイプを使い分ける必要があります。

このように注意する点はいくつかありますが、一般的なコンテナとリレーショナルデータベースを組み合わせたアプリケーションを比較すると、比較的高い弾力性を持つアーキテクチャになっています。

仮想リソースと実リソースの状態差分を埋めるための仕組み

最後に、Simulation Clustersとrunnerが仮想リソースと実リソースの状態差分を埋めるためのリソース制御の仕組みについて説明します。

ここでは、状態差分を埋めるための操作のことをKubernetesに倣ってReconcileと表現します。リソース制御には、「イベント駆動」と「分散Reconciliation Loop」と呼ばれる2つの仕組みが存在します。

イベント駆動の仕組みは、runnerが実リソースの状態の変更を検知することをトリガーにしています。状態変更の検知方法はシミュレーション環境によって異なるのですが、この環境の差異はドライバによって吸収しています。runnerは、実リソースの最新状態をSimulation ClustersにAPI経由で通知すると、Simulation Clustersのリソースコントローラーは、Step Functionsのステートマシンを起動して、受け取った情報を基にReconcileを実行します。

もう1つの分散Reconciliation Loopは、各リソースのライフサイクルを制御するステートマシンの中で、一定時間ごとに実リソースの最新状態をrunnerに対してリクエストすることをトリガーとしています。リクエストを受信したrunnerは、ドライバ経由で実リソースの状態を取得し、状態をSimulation Clustersに通知します。

その後は、同様にReconcileを実行します。ループするステートマシンがリソースごとに並列に動作していることから、ここでは分散Reconciliation Loopと呼んでいます。イベント駆動の仕組みのほうが素早くReconcileを実行できるのですが、なんらかの原因でイベントを取りこぼしてしまう可能性があります。この問題を補うために分散Reconciliation Loopと組み合わせています。

また、runnerがダウンした場合、イベント駆動の仕組みは完全に機能しなくなります。一方、分散Reconciliation Loopはrunnerとの通信が途絶えてリクエストを送信できなくなった場合、フォールバックを実行することができます。

これらの仕組みは「Evaluator」というフロントエンドサービスを通じてブラウザから利用できます。Evaluatorでは一般的なCI/CDサービスのようなUIを持っていて、GitHubなどで管理しているソースコードをベースに評価ジョブを実行できます。テストの内容に応じて、マネージドなKubernetes環境やSelf-hosted Kubernetes環境、Self-hosted native環境を選択できるようになっており、統一されたUIで評価結果を管理できます。

発表は以上です。ご清聴ありがとうございました。