🐙

Cloud Run Jobを利用したAHCのローカルテスト実行

2023/03/07に公開

1.はじめに

先日、Atcoder Huristic Contest18に参加しました。その際、パラメータをチューニングするため、テストケースを3000件くらいに増やしてシリアルで実行したたら 1セットあたりの実行に時間が6〜7分かかるようになり、思ったより待ち時間が長くなってしまいました。クラウドで効率的に実行できないかなーと思って方法を探したところ、GCPのCloud Runで実現できそうだったので試してみました。

https://atcoder.jp/contests/ahc018

2.Cloud Run Jobとは

一言でいうとGCP上でコンテナアプリケーションを稼働させるPaaSのサービスです。
従来のCloud RunのようにHTTP(S)でリクエストを受け付けるWebアプリケーションやAPIサーバを実行させるのではなく、Jobとしてバッチ用のコンテナを実行する仕組みが2022年に追加になりました。(まだBetaの扱い)

詳しくは公式ページ参照
https://cloud.google.com/run/docs/overview/what-is-cloud-run?hl=ja

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