Cloud Runにおいて、動画中の物体検出フローを安定化させるまでに試行錯誤した話

こんにちは、ピリカ開発チームの九鬼です。

弊社が開発しているタカノメ自動車版では、クラウド上にて撮影動画中のごみを検知しています(下図例)。

本機能を安定化するにあたり、開発環境において試行錯誤した経緯を共有いたします。

車載動画からのごみの検出例。ペットボトルの場所に緑色の検出枠がある
車載動画からのごみの検出例。ペットボトルの場所に緑色の検出枠がある

タカノメ自動車版とは?

タカノメ自動車版は、内製のスマートフォンアプリを車両に搭載し、道路を走行することで各地のごみ分布を可視化するサービスです。得られたごみ分布の結果から、どこにごみが多いか、また清掃活動などを行った結果がどうだったかを定量的に判断することができるようになります。

本サービスの実現にあたり、プライバシー処理済みの撮影動画をアプリからGCPにアップロードします。GCP上で、学習済みの深層学習モデルを用いて動画内にあるポイ捨てごみを検出します。最後に、検出結果を基に散乱状況をマップ上に可視化します。

本サービスの詳細については、以下ページをご覧ください。

corp.pirika.org

1. 初期設計

最初、以下の構成でごみ検出を行う設計としました。まず、前日に撮影された動画一覧を取得し、全動画に対してバックグラウンドタスクを発行します。このタスクをCloud Run上のごみ検出処理用のコンテナインスタンスで処理するようにしました。この時点では、Cloud Runの設定はデフォルト値のままでした。

ごみ検出フロー 初期設計

発生した問題

Cloud Runのデフォルトスペック(1vCPU, 512MiB)では、メモリ容量が足りずクラッシュしていました。ごみ検出ではなく、ごみ検出のための深層学習モデルのロードの時点でメモリ不足に陥っていました。

2. backgroundサービスのvCPU, メモリ量見直し

いくつかの検証より、メモリ量は4GiB強だけ必要であることがわかりました。Cloud Runでは、メモリ量=8GiBに設定するにはvCPU≧2とする必要があります。そのため、

  • vCPU = 2
  • メモリ量 = 8GiB

とし、再度ごみ検出を試みました。

発生した問題

Cloud Runがデフォルトの5分でタイムアウトしました。このとき、Cloud Runの当該サービスの設定は

  • vCPU = 2
  • メモリ量 = 8GiB
  • コンテナあたりの最大受付リクエスト数(containerConcurrency) = 80 (デフォルト値)
  • 最大同時コンテナインスタンス数(maxScale) = 100

でした。処理負荷の1つのリクエストで1つの動画のごみ検出を行うのが限界でしたが、そこに最大80リクエストが殺到したためにタイムアウトが発生していました。

3. スケーリング設定見直し

各コンテナインスタンスに処理が集中しないよう、以下の通り設定し直しました。

backgroundサービスの設定

vCPU = 2なので、1コンテナインスタンスあたり2リクエストまで同時に受けるようにしています。また、コンテナインスタンスを200とし、同時に400動画分を捌けるようにしました。

発生した問題

本来の動画よりも1.5倍近くの量の動画が処理されていました。内訳を見ると、前日だけでなく前々日の動画も処理されていました。

発生した理由ですが、ローカルでは日本時間で前日を出していました。一方で、クラウド上ではUTCでの前日(=日本時間での2日前)となっていました。さらに、集計日時の終端を指定していなかったために、2日前のAM0:00 ~ 現在日時AM0:00までを余分に集計することになってしまいました。

    def __find_date_range(self) -> datetime:
        begin_date = datetime.combine(
            date.today() - timedelta(days=1), time())
        return begin_date

4. 前日の判定を修正

前述の問題を受けて、ごみの検出区間を下記の通り修正しました。

    def __find_date_range(self) -> tuple[datetime, datetime]:
        jst_now = datetime.now(pytz_timezone('Asia/Tokyo'))  # pytz_timezoneはpytzモジュールのtimezoneのエイリアス
        time_jst_midnight = time(tzinfo=jst_now.tzinfo)
        begin_date = datetime.combine(
            jst_now - timedelta(days=1), time_jst_midnight)
        end_date = datetime.combine(
            jst_now, time_jst_midnight)
        return begin_date, end_date

ごみ検出フロー 日時判定の修正
ごみ検出フロー 日時判定の修正

発生した問題

ごみ画像のCloud Storageへの保存時、ときより429エラーによりタスクが失敗することがありました。Cloud Storageでは、1プロジェクトあたり同時に1000件/秒だけファイルを書き込むことができるものの、それを越えて書き込みしていました。この原因として、画像作成の際にごみの数の分だけ余分に保存するバグが混在していました。

4. リトライ可能なエラーはリトライし、なおかつ画像は1フレームあたり1回だけ保存する

上記問題を踏まえ、以下構成の通り修正しました。

ごみ検出フロー 結果画像アップロードのリトライに対応

発生した問題

1日に撮影した動画が多い場合、ごみ検出タスク発行が10分以内に間に合わずタイムアウトしました。2タスクの発行ごとに約0.5~0.6秒を要しており、動画数が1000件を越えると600秒に間に合わなかったためです。

5. 動画ごとのタスク発行用に、日時を分割

最終的に以下の構成になりました。ごみの検出を行う期間を複数に等分割し、ごみ検出タスク発行を分散しました。これにより、ごみ検出タスク発行が複数のコンテナに分散され、タイムアウトが発生しないようになりました。

ごみ検出フロー タスク発行部を分割
ごみ検出フロー タスク発行部を分割

得られた知見

本件を通して、以下の知見が得られました。

  • 1つの実行インスタンスからタスクを発行する件数を一定以下に抑えるよう設計する
  • 複数タスクを同時に走らせるとき、Cloud Storageの書き込み制限数などのリクエスト上の制限を意識して設計する
  • Cloud Taskのキュー設定およびCloud Runなどの実行設定をみて、事前にどれくらいの件数を、どれくらいの期間で捌けるようにするかを設計する。
    • 本ケースのように重たい処理をクラウド上にて並列で走らせたいとき、CPUやメモリ負荷を見ながら受信リクエスト数を制限する

本ケースでは大量のデータを、1つ1つ高負荷な処理で加工する必要がありました。そのため、それに見合った処理フローの調整が必要でした。クラウドとローカルで時差が違ったり、計算リソースの不足があったりと様々な考えるべきポイントがあり、あらためてCloud RunやCloud Task等の公式リファレンスを読むのが大事でした。

以上、ご覧いただきありがとうございました!