🐳

AHC用にCloud Run Jobsでの並列実行を試したが期待したとおりに動かなかった話

に公開

はじめに

先駆者の記事を読んで、Cloud Run JobsでAHCのテストケースの並列実行に挑戦してみました。
結果的には望んだ結果にならなかったのですが、備忘録的に残しておきます。

TL;DR

  • マルチステージビルドの導入や依存関係の削減を実施
  • Cloud Run Jobsのジョブ毎に15秒のオーバーヘッドがかかった
  • 100並列で1000テストケース行った結果の実行時間は150秒
    • オーバーヘッドを無視した場合13並列相当
    • 50並列や100並列を指定した場合の所要時間で、200並列を指定するとより悪化
  • ローカルでの並列実行数を下回ったためローカル実行の方がお得

やりたかったこと

AHCの配布ツールではデフォルトで100テストケースが含まれています。
テストケースを増やすと当然それらを走らせるのにかかる時間が増えていきます。
ローカルでもある程度は並列で実行できますが限度があります。
これをクラウド環境の利用によって大幅に短縮できると嬉しいので試してみました。
具体的には1000テストケースでも100並列できれば20秒ほどで完了するため、かなり気軽に回すことが出来ます。

やったこと

今回変更した部分は大きく以下の2つです。

  • マルチステージビルドへの変更
  • Python依存の排除

https://github.com/y-i/ahc-remote-run/tree/master

インタラクティブ問題かどうかで2種類のDockerfileを作りました。ビルド時にどちらかをDockerfileにシンボリックリンクを貼ることでビルド可能です。
内容は基本的にほとんど同じです。

Dockerfileの変更

今回Dockerfileに変更を加えました。
理由としては2点あり、1つは記事内のDockerfileそのままが動かなかったためです。
加えて、変更するならついでにと思い、マルチステージビルドにして容量の削減や安全性の強化を狙いました。
最終的にアップロード後のサイズが500MBを切り、後述する費用の面で嬉しい結果となりました。[1]

ビルド用のイメージにはRustの公式イメージを用いています。
これとPython依存を無くしたことで言語の追加インストールが不要になりました。その結果、追加はビジュアライザ展開に用いるunzipだけで済んでいます。ビルド時間短縮にも役立つと思います。
処理としてはスコア計算に用いるテスターまたはビジュアライザのビルドと入力ファイルを生成しています。

最終的に用いるイメージにはGoogle Cloud CLIの公式イメージを用いています。
こちらは追加のライブラリなどは一切無い状態で利用しています。
その環境でも動作するように、ビルド時に必要なファイルを作成するようにして、成果物だけをコピーしてきています。
手元でビルドした実行ファイルはGCSからダウンロードしてきて利用しますが、C++のファイルを普通にビルドして持ってくると、実行時にライブラリのエラーが発生しました。
そのため、ビルド時に--staticオプションを付けてビルドすることで問題を解消しました。

基本的にZIP_URLTIMESを上書きするなり設定で渡すなりする変更だけで済みます。

https://github.com/y-i/ahc-remote-run/blob/905b558f23d5f76014a26d03034f3960480573cb/interactive.Dockerfile

https://github.com/y-i/ahc-remote-run/blob/905b558f23d5f76014a26d03034f3960480573cb/noninteractive.Dockerfile

シェルスクリプトの変更

シェルスクリプトは以下のように処理しています。

  1. 実行ファイルをGCSからダウンロード
  2. 指定範囲の入力に対して出力ファイルの作成とスコア計算を実行
  3. 出力ファイルとスコアをGCSにアップロード

元のスクリプトと特に異なる部分は出力ファイルもGCSにアップロードする点です。
これをダウンロードすることでビジュアライザでの考察にも利用可能になります。
シェルスクリプト内で変更が必要な部分はコンテスト依存とコメントしてある部分です。
スコア計算をしている所の出力結果に合わせてgrepawkを調整してください。

https://github.com/y-i/ahc-remote-run/blob/905b558f23d5f76014a26d03034f3960480573cb/task-interactive.sh

https://github.com/y-i/ahc-remote-run/blob/905b558f23d5f76014a26d03034f3960480573cb/task-noninteractive.sh

ジョブの値の変更について

Cloud Run Jobsでジョブを登録した後にテストケースの数や並列数を変更したい場合があると思います。
この時、一度ジョブを削除し再登録する必要はありません。

gcloud run jobs create実行時に引数で渡している値は、gcloud run jobs update ${JOB_NAME} --tasks 10のようなコマンドで変更が可能です。
環境変数として渡している値は、gcloud run jobs update ${JOB_NAME} --update-env-vars KEY1=VALUE1,KEY2=VALUE2のようなコマンドで変更が可能です。
テストケース数を増やすと入力ファイルが足りずに困りますが、一時的に減らす分には簡単に実現できます。

うまくいかなかったこと

ビルドやジョブの登録が完了した後、実際に実行してみた結果想定通りに動いていないことが分かりました。
10タスクを実行した際には5並列程度だったものが、100タスクにすると20-40並列程度でしか動きませんでした。

何かしらのリソースが割り当て上限に引っかかっているのかと思い確認しましたが特に逼迫しているリソースは確認できませんでした。
parallelism引数を設定できるのですが、これを0にしても100にしても結果は同じでした。
説明にあるとおり最大並列数の設定のため、クラウド側の何かしらのロジックで実際の並列実行数が低くなってしまっているのかもしれません。

実行時間が数十秒で終わることを期待していたのですが、実際には150秒ほどかかってしまっていて期待通りにはなりませんでした。

費用について

今回はGoogle Cloudのリージョンはus-central1を選択しています。
理由としては無料枠の対象のリージョンであり、日本へのネットワークの料金がasiaと変わらなかったからです。

リージョン選択を確実にするため以下のコマンドを打っておくのがおすすめです。

gcloud config set run/region us-central1

Cloud Run

https://cloud.google.com/run/pricing?hl=ja

インスタンスベースとリクエストベースの課金方法があるのですが、Cloud Run Jobsはどちらなのかいまいち分からなかったので高い方で考えます。

  • 無料枠(us-central1):
    • CPU - 毎月最初の 240,000 vCPU秒は無料
    • RAM - 毎月最初の 450,000 GiB秒は無料
  • CPU(1 vCPU秒あたり)
    • $0.000024
  • メモリ(GiB秒あたり)
    • $0.0000025
  • リクエスト(100万件あたり)
    • $0.40

今回は1CPU、2GBメモリ、実行時間2秒、1000テストケースで行っています。
そのため1回当たり最低1000vCPU秒、4000GiB秒は必要になります。
0.000024 \times 1000 + 0.0000025 \times 4000 = 0.034となるので最低5円程です。[2]
実際には1タスクあたり15秒追加で実行されるという結果になっています。
この内訳はインスタンス初期化からテストケース実行までが10秒、実行終了からインスタンス終了までが5秒でした。

GCS

https://cloud.google.com/storage/pricing?hl=ja

今回GCSに置いたファイルは実行ファイルと出力結果のファイルです。
実行ファイルは2MBほど、出力結果は5kBでテストケース分あります。

ただ、料金ページから計算するとGCSでかかる費用は最大で月々数円なので省略します。
注意点として削除(復元可能)に設定されていると保持期間も料金がかかるので無効化しておくべきです。[3]

Artifact Registry

https://cloud.google.com/artifact-registry/pricing?hl=ja

0.5GB内に収めることで無料です。
0.5GBを超えるイメージでも数十円程度です。

Cloud Build

https://cloud.google.com/build/pricing

以下のように書いてあるため基本無料枠で収まると思います。

Each billing account comes with 2,500 free build-minutes per month.

費用計算

各項目で書いたとおりCloud Run以外の費用はほぼ0なので各実行時のCloud Runの費用だけ考えます。
今回は1000テストケースの実行を1タスク内で10ケース実行する100タスクに分割してます。
この場合、CPU時間は(2\times10+15)\times100=3500秒、メモリ秒はその4倍の14000秒です。
よって、0.000024 \times 4000 + 0.0000025 \times 14000 = 0.119となり1実行当たり20円弱です。

タスク数を増やすと理想的には全テストケースを終了するまでの時間が短縮されますが、タスク毎のオーバーヘッド分費用が増えるので注意が必要です。たとえば1テストケース実行を1000タスク実行すると、費用は4倍以上になります。
逆に100テストケースを10タスクにすると6割程度になります。

まとめ

まずスクリプト周りの変更し、依存関係やサイズの削減、安全性の強化を行いました。
その後、実際にタスクを実行して実行時間やちゃんと実行できるかを確認できました。
実行結果から実行にかかる費用を計算しました。

しかし、実行時間が想定の何倍にもなってしまいました。
150秒ほどという結果に対して、ローカルで実行したと仮定すると13並列相当の処理速度となります。
実際にローカルで実行するとこれ以上の速度でできるため、Cloud Run Jobsによってテストケースの実行を高速化はできませんでした。

脚注
  1. Alpineにするとより減るかもしれませんが動作するかは未検証です。 ↩︎

  2. リクエスト料金は他より小さいので無視します。 ↩︎

  3. デフォルトで有効らしいです。 ↩︎

Discussion