Cloud Run Jobを利用したAHCのローカルテスト実行
1.はじめに
先日、Atcoder Huristic Contest18に参加しました。その際、パラメータをチューニングするため、テストケースを3000件くらいに増やしてシリアルで実行したたら 1セットあたりの実行に時間が6〜7分かかるようになり、思ったより待ち時間が長くなってしまいました。クラウドで効率的に実行できないかなーと思って方法を探したところ、GCPのCloud Runで実現できそうだったので試してみました。
2.Cloud Run Jobとは
一言でいうとGCP上でコンテナアプリケーションを稼働させるPaaSのサービスです。
従来のCloud RunのようにHTTP(S)でリクエストを受け付けるWebアプリケーションやAPIサーバを実行させるのではなく、Jobとしてバッチ用のコンテナを実行する仕組みが2022年に追加になりました。(まだBetaの扱い)
詳しくは公式ページ参照
Cloud Run Jobだと何が嬉しいかというと、バッチ処理の並行実行が比較的簡単にできます。
具体的なイメージは以下の通りです。
3.試したこと
AHC18のシステムテストと同じ数の3000ケースを並行実行してみました。
概要
- Cloud Run用にLinux/AMD64用のイメージを用意する必要があるのですが、自宅PCがM1Macだったので、 Cloud Buildでイメージを作成しました。
- 評価用のローカルテスタはコンテナイメージのビルド時にコンパイルするようにしました。
- テスト対象のプログラムは、更新するたびにコンテナ再作成すると時間がかかるので、実行ファイルは外出しにしてCloud Storageからダウンロードして実行するようにしました。
処理結果の確認方法
タスク毎の結果はCloud Storageに保管し、最後に手動でCloud Storageからダウンロードして、タスク毎の結果をマージして参照するようにしました。
結果
AHC18のテストケース(3000ケース)を実行した結果は下記のとおりです。(3回実行した際の平均)
30タスクに分けて実行 | シリアル実行 |
---|---|
60秒 | 6分45秒 |
実行時間が約1/6になりました。
AHC18では自分のプログラムの場合、1回あたりの実行時間が50msくらいと短かかったのですが、 例えば焼きなまし法を行って1回数秒とかかだと、並行実行による時間削減効果は大きくなるので、うまくやれば活用できそうです。
4.手順(参考)
(0)事前準備
・GCPアカウント
・Google CLoud SDKのインストールとセットアップが済であること
※その他意図せず、料金を超過しないように利用料のアラートは設定しましょう。
(1)GCPのAPIを許可する
Cloud Run Jobなど必要なAPIの実行を許可します。
gcloud services enable \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com \
run.googleapis.com
(2)Cloud Storageの作成と実行ファイルの配置
テスト対象の実行ファイルはコンテナイメージではなくて、都度Cloud Storageからダウンロードする方式にしたいのでCloud Storageを新しく用意して、そこにテスト対象の実行ファイルをコピーして配置します。
- バケットを作成する
gsutil mb gs://{任意のバケット名}
- テスト対象の実行ファイルをCloud Storageに配置する
gsutil copy ./{アプリケーションの実行ファイル} gs://{作成したバケット}/
(3)Dockerイメージのビルドとレジストリへのpush
以下のファイルを同一ディレクトリに配置してビルドし、Container RegistryまたはArtifact Registryにpushします。(Cloud RunではContainer RegistryまたはArtifact Registryのいずれかを利用する必要があります)
- Dockerfile
- task.sh ※テスト実行用のラッパー
<Cloud Buildでのビルド実行例>
gcloud builds submit --project ${PROJECT_ID} --tag gcr.io/${PROJECT_ID}/ahctest
- Dockerfile
FROM ubuntu:20.04
# 追加パッケージのインストール
RUN apt-get update && apt-get install -y unzip vim curl python3 gcc
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | tee /usr/share/keyrings/cloud.google.gpg && apt-get update -y && apt-get install google-cloud-sdk -y
# workディレクトリ作成
RUN mkdir -p /App/in && mkdir -p /App/out
WORKDIR /App
# AHC018用
# rustをインストール
ENV HOME /App
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH $PATH:/App/.cargo/bin
RUN rustup update
# 入力ジェネレータをダウンロードして配置
ADD https://img.atcoder.jp/ahc018/6bada50282.zip /App
RUN unzip ./6bada50282.zip
WORKDIR /App/tools
# 入力ファイルのシードを作成
RUN python3 -c '[print(i) for i in range(3000)];' > seeds.txt
RUN cargo run --release --bin gen seeds.txt
# タスク実行用のシェルをコピー
COPY ./task.sh /App/tools/task.sh
CMD sh task.sh
- task.sh
#!/bin/bash
# 実行ファイルをダウンロードする
gsutil copy ${BUCKET_NAME}/${APP_NAME} ${APP_NAME}
chmod +x ${APP_NAME}
COUNT=${CLOUD_RUN_TASK_INDEX}
FROM=`expr ${COUNT}*${STEP}`
TO=`expr ${COUNT}*${STEP}+${STEP}`
RESULT_FILE="${JOB_NAME}_${COUNT}_result.txt"
mkdir -p ./out
rm -f out/${RESULT_FILE}
# 指定範囲のテストケースを順次実行する
for i in `python3 -c "[print(str(i).zfill(4)) for i in range(${FROM},${TO})];"`;do
IN_FILE=${i}.txt
OUT_FILE=${JOB_NAME}_${i}.txt
TMP=`cargo run --release --bin tester ./${APP_NAME} 2>&1 < in/${IN_FILE} >out/${OUT_FILE} | grep "Total Cost" `
COST=`echo ${TMP} | awk -F' ' '{print $4}'`
if [ -z ${COST} ]; then
COST="Error"
fi
echo ${i},${COST} >> out/${RESULT_FILE}
done
# 結果を出力してCloud Storageに保管する
cat out/${RESULT_FILE}
gsutil copy out/${RESULT_FILE} ${BUCKET_NAME}/${JOB_NAME}/${RESULT_FILE}
(4) サービスアカウントの作成
Cloud RunからCloud Storageにアクセスするため、サービスアカウントを作成し、Cloud Storageの権限を付与します。
ahs-saというサービスカウントを作成
gcloud iam service-accounts create ahc-sa --display-name="AHC service account"
ahs-saにストレージの管理権限を付与
gcloud projects add-iam-policy-binding $PROJECT_ID \
--role roles/storage.admin \
--member serviceAccount:ahc-sa@${PROJECT_ID}.iam.gserviceaccount.com
(5)Cloud Run Jobの作成
以下のコマンドを実行してjobを登録します。
gcloud beta run jobs create ${JOB_NAME} \
--image=$IMAGE_URL \
--tasks=30 \
--task-timeout=2m \
--set-env-vars=JOB_NAME=$JOB_NAME \
--set-env-vars=BUCKET_NAME=$BUCKET_NAME \
--set-env-vars=APP_NAME=$APP_NAME \
--set-env-vars=STEP=100 \
--service-account=ahc-sa@$PROJECT_ID.iam.gserviceaccount.com \
--max-retries 0 \
--cpu 1 \
--memory 2G
変数名 | 説明 |
---|---|
$IMAGE_URL | (3)で登録したコンテナイメージのURL。( gcr.io/$PROJECT_ID/ahctest:latest) |
$JOB_NAME | 任意のジョブ名 |
$BUCKET_NAME | (2)で作成したバケットのパス(例 gs://xxxx_job) |
$APP_NAME | (2)で配置した実行ファイル名 |
$STEP | タスク1回あたりに、タスク実行用シェル内で実行するテストケースです。tasks=30、STEP=100で30×100のテストを実行します。 |
以下で登録したJobの内容を確認できます。
gcloud beta run jobs describe ${JOB_NAME}
(6)Jobの実行と結果の確認
Jobを実行する際に「--wait」オプションをつけることでジョブ投入後にすぐに終了せず、ジョブの実行状況が表示しながらジョブの終了を待ちます。
- ジョブを実行して終了を待つ
gcloud beta run jobs execute $JOB_NAME --wait
- 結果ファイルをローカルにダウンロード
gsutil -m copy -r $BUCKET_NAME/$JOB_NAME/ .
cd ${JOB_NAME}
- ダウンロードしたファイルでジョブの実行結果を確認
#エラーがないかチェック
grep -i Err *
#awkでコストの合計、平均を算出
cat *.txt | awk -F ',' '{sum+=$2} END {print sum,sum/NR,NR}'
>521229949 173743 3000
Discussion