DeLMO(identify)エンジニアブログ

DeLMO(identify株式会社)のエンジニア・デザイナーが色々書くブログです

モノレポ管理ツールを Nx から Turborepo へ移行した話

これは何

今回モノレポ管理ツールを Nx から Turborepo へと移行したのですが、ポリレポからモノレポへの移行に関する記事はあっても管理ツールのリプレイスというのはあまり見当たらなかったため、その知見を共有したいと思います。

弊社サービスとフロントエンドの構成について

identify株式会社で副業エンジニアとして主にフロントエンドをお手伝いしている @uekenu です。 弊社identify株式会社では動画素材サービスDeLMOの運営を行っています。DeLMOは動画をダウンロードする広告主様/代理店様と動画提供者のクリエイターに利用いただいており、そこに特権管理者である弊社のスタッフを加えるとユーザーの属性としては3種類に分けることができます。そして3種のユーザーにそれぞれ別々の環境の画面を用意しています。

アーキテクチャとしては、バックエンドに GraphQL サーバーがあり、そこを叩くフロントエンドが3環境あるという構成になっています。

  • 広告主様/代理店様向けのサービスサイト
  • クリエイター向けの管理画面
  • 社内向けの管理画面

これらはそれぞれ Next.js で別々のアプリケーションとして構築されており、 1つのリポジトリ内で3つのプロジェクトを管理するモノレポ構成を Nx によって実現しています。

フロントエンドのアーキテクチャ

Nx を採用した理由と現状の課題

モノレポを構築する際、各 app の依存関係やビルドコマンドの実行順などの管理をツールに任せたいと考えました。当時最も人気のあるツールは Lerna でしたが、その時点でほとんどメンテされてないという点と Nx が台頭してきているという点から Nx を採用しました。

しかし、運用していく上で以下に挙げるような問題が顕在化してきました。

バージョンアップがつらい

Nx は Next.js へのサポートをプラグインによって実現しているため、 Next.js をバージョンアップするためには Nx のプラグイン、ひいては本体も上げないといけないというケースがよくありました。 Next.js は積極的に新機能を打ち出してきますし、できるだけ追従していきたいところです。

しかし、 Nx のバージョンアップには様々な障壁がありました。例えばマイナーバージョンアップでビルドが通らなくなったりする問題が相当数報告されており、実際に弊社も何回か遭遇しその度に問題バージョンの特定を切り分けによっておこなったり公式リポジトリの issue を掘ったりしていました。 私たちもハマったものでいうと例えば以下のような issue が挙げられます。その度にメンテナが誠意ある対応をしてくれているのですが、私たちとしてはバージョンアップのたびに疲弊していたというのが正直なところです。

ビルドが不安定

本プロジェクトは Next.js を Vercel にホスティングしているのですが、ある時から Vercel 上でのビルドが以下のようなログとともに失敗する状況に陥りました。しかも成功する時と失敗する時が混在しているのです(体感1~2割程度で失敗)。

.
.
.
Vercel CLI 28.18.1
Installing dependencies...
yarn install v1.22.17
[1/4] Resolving packages...
success Already up-to-date.
Done in 0.83s.
Detected Next.js version: 13.1.1
Running "yarn nx run accounts:build --skip-nx-cache --verbose"
yarn run v1.22.17
$ nx run accounts:build --skip-nx-cache --verbose
error Command failed with signal "SIGTERM".
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Error: Command "yarn nx run accounts:build --skip-nx-cache --verbose" exited with 1
BUILD_UTILS_SPAWN_1: Command "yarn nx run accounts:build --skip-nx-cache --verbose" exited with 1

いまいちログの内容が薄いため原因の特定が難しく、 Vercel に問い合わせたところ Nx の問題で失敗しているらしいのでバージョンアップなどを試してほしいといったような回答をもらったのですが、バージョンアップしても改善せず、そのまま脱 Nx のきっかけとなりました。

単一の package.json

Nx はプロジェクトルートに単一の package.json を持ちます。最初の頃は全 app 共通で依存関係を管理できるのが楽でしたが、時間が経つにつれライブラリのリプレイスやバージョンアップ時に影響範囲を調べるのが大変になり、ビッグバンアップデートもせざるを得ない状況となっていきました。


こうした問題と向き合い、頑張って Nx を使いこなせるようになるよりも、他のツールに移行し、シンプルな構成にすることでモノレポの長期的な運用コストを下げたい、というものが今回の移行の背景となりました。

移行先の選択肢

Turborepo

https://turbo.build/repo

  • Vercel が提供
  • Workspaces ベース

Rush

https://rushjs.io/pages/contributing/

  • Microsoft が提供
  • ビルドコマンドやパッケージアップデートなどは全て rush コマンドでおこなう
  • Nx と同じ複雑性を抱えそう

yarn Workspaces

https://yarnpkg.com/features/workspaces

  • yarn 以外に特別なツールは必要ない

そもそもモノレポをやめる

app ごとにリポジトリを作成しポリレポ構成に戻すという選択肢もありましたが、

  • フロントエンドを触る際に社内向け画面とクリエイター画面など複数の環境を同時に触ることがあり、そのような修正を一つの Pull Request で行いたいケースが存在する
  • 1つのリポジトリにフロントエンド環境が全部揃っているとコードを参考にしやすく、コードを似せて書きやすくなる

といった事情から、基本的にはモノレポのままいく前提で考えました。

Turborepo にした理由

構成がシンプル

Turborepo は実質的に yarn workspace をラップしている程度の構成と認識しており、それに追加で必要な設定ファイルは turbo.json のみなので非常にシンプルです。将来的にやっぱり Turborepo をやめたいとなっても比較的容易に生の yarn Workspaces に戻せるため、同等の他のツールへの移行もしやすいのではないかと考えました。

Remote Caching

Turborepo には Remote Caching という機能があります。ローカルでのビルドをチームで共有できるもので、 CI やローカルでキャッシュヒットすればビルドを高速化させることができます。 同様の機能は Nx も提供していますが、 Nx Cloud というサービスに別途登録と課金が必要です。 Turborepo の Remote cache はホスティングサービスとしての Vercel が対応しており、 Team plan であれば無料なので、すでにそのプランである私たちにとってはそのまま使えました。

Vercel 社が出してる

身も蓋もありませんが、意思決定においてそれなりに大きい割合を占めました。 基本的にはベンダーロックインは忌み嫌われがちなものですが、私たちは Vercel 社に対してのロックインは例外的に許容しています。本記事の趣旨とずれるので割愛しますが、Next.js の使いやすさなどが理由です。Turborepo は Vercel 社が提供しているため、 Next.js やホスティングサービスとしての Vercel と当然相性がよく、特に後者はほぼゼロコンフィグでいい感じになりました。

補足: yarn Workspaces でいいのでは?

今回実現したかったことはパッケージ間の依存関係を管理できている状態は保ちつつシンプルな構成にすることだったので、 yarn Workspaces で十分要件は満たせそうではありました。

しかし、 Vercel は今後 Turbopack というバンドルツールと Turborepo を統合し Turbo という単一のツールチェーンの開発に力を入れていくようです。将来的に Webpack から Turbopack へ乗り換えることでビルド速度の向上も期待できそうなこともあり、このあたりのエコシステムに早めに乗っかっておこうという方針で Turborepo の導入を決めました。

手順

Turborepo は yarn / npm / pnpm の各パッケージマネージャが提供する Workspaces の上に構築されています。そのため、移行は以下のような手順で進めました。

  1. Nx を剥がし、単純に1つのリポジトリ内に3つの Next.js が同居している状態にする
  2. Workspaces を導入
  3. Turborepo を導入

Nx を剥がす

移行前の状態は Nx が示す規約に従い

/
├ apps
│ ├ accounts
│ │ └ tsconfig.json
│ ├ creator
│ │ └ tsconfig.json
│ └ admin
│   └ tsconfig.json
├ nx.json
├ package.json
└ tsconfig.base.json

このようにプロジェクトルートに1つの package.json を持っていました。ここから

/
└ apps
  ├ accounts
  │ ├ package.json
  │ └ tsconfig.json
  ├ creator
  │ ├ package.json
  │ └ tsconfig.json
  └ admin
    ├ package.json
    └ tsconfig.json

このように各 app がそれぞれ個別の package.json を持っているような状態を目指します。

まずは Nx 関連の依存を全て剥がしていきます。主にやることは .eslintrcnext.config.js などに入り込んだ各種プラグインの削除となります。その後 package.json から Nx 関連の dependencies を remove すればOKです。

Nx は tsconfig.base.json をプロジェクトルートに持ち、各 app がそれを extend するようにしており、 .eslintrc 等も同様に共通した設定はルートにベースとなる設定ファイルを置く構成となっています。そこで、 Workspace 化するまでは共通設定ファイルはルートにそのまま置いておき、各 app に個別に置いた設定ファイルから相対パスで参照し extend するようにしておきます。

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    .
    .
    .
  }
}

次に、各 app で yarn initpackage.json を設置します。

cd apps/accounts && yarn init -y
cd ../creator && yarn init -y
cd ../admin && yarn init -y

最後に、ルートの package.json 内の dependencies および devDependencies 群を各 app にコピーしてきます。また、この時 Next.js のデフォルトの scripts も記述しておきます。

{
  "name": "creator",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "private": true,
  "dependencies": {
    // ルートの package.json の dependencies
  },
  "devDependencies": {
    // ルートの  package.json の devDependencies
  }
}

ここまでくれば、ルートの package.json を削除できるかつ各 app のディレクトリ内で Next.js が動くようになります。

rm package.json

cd apps/creator
yarn
yarn dev

以上で、無事に Nx を脱却し、3つの素朴な Next.js アプリケーションが1リポジトリ内にただ同居している、という状態にすることができました。

Workspaces を導入

本プロジェクトはパッケージマネージャーに yarn を使っているため、 yarn の Workspaces を導入しました。

まずはプロジェクトルートに yarn init -ypackage.json を設置します。その後、 apps 以下のディレクトリを app として読み込むようにします。ついでに共通パッケージを置く場所として packages も追加します。

{
  "name": "delmo-frontend",
  "version": "1.0.0",
  "license": "MIT",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

共通パッケージの設定については Turborepo の example が非常に参考になると思います。

https://github.com/vercel/turbo/tree/main/examples/basic

簡単に説明すると、例えば tsconfig なら

  • packages/tsconfig で新規パッケージを作成
    • package.json の name は例えば @delmo/tsconfig としておく
  • プロジェクトルートに置いてあった tsconfig.base.json を作成したパッケージに移動
  • 利用したいパッケージ側の dependencies に "@delmo/tsconfig": "*" として追加
  • プロジェクトルートで yarn install

このようにすれば、パッケージ間で参照できるので以下のように extend を変更することができます。

{
  "extends": "@delmo/tsconfig/base.json",
  "compilerOptions": {
    .
    .
    .
  }
}

Turborepo を導入

ここまでくれば Turborepo の導入は以下の通りにすれば特に詰まることなくスッと入ると思います。

https://turbo.build/repo/docs/getting-started/existing-monorepo

Turborepo は global にインストールすることもできますが、メンバー間の環境差異を極力無くしたかったため local インストールとしました。

yarn add -D turbo

続いて turbo.json という名前で Turborepo の設定ファイルを作成します。

{
  "$schema": "https://turbo.build/schema.json"
}

turbo.json にはプロジェクトルートの package.json に記述された各スクリプトの依存関係やキャッシュを pipeline として定義します。

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
    },
    "lint": {},
    "deploy": {
      "dependsOn": ["build", "test", "lint"]
    }
  }
}

ここに定義したコマンドは各 app の package.json に同名のスクリプトとして定義し動くようにしておく必要があります。例えば lint は各 app 上で yarn lint として動くようにしておき、 turbo.json にも定義するとプロジェクトルート上で npx turbo run lint を叩くことができるようになり、各 app 上で eslint を走らせ実行結果をキャッシュしてくれます。

最後に Vercel でのビルドの設定を変更します。 Root Directory を各 app のディレクトリとするだけで大丈夫です。

Build & Development Settings

以前はプロジェクトルートに戻ってから単一 app のみのビルドをする必要があるため Build Commandcd ../.. && yarn build --filter=app-name と設定する必要があったようですが、今は Vercel がコマンドを推論してくれるようになっているため設定は不要です。

以上で、次回のビルドから Turborepo のビルドが走るはずです。

また、 app 内にコードの差分がなかった場合にビルドのスキップをしたい場合は、 こちらを参考に Settings -> GitIgnored Build Step を以下とします。

npx turbo-ignore

はまったポイント・うまくいったポイントなど

プロジェクトルートの package.json が読めずパッケージ間の参照ができない

開発中に以下のようなビルドエラーが発生しました。

これは、 Vercel の Project Settings のうち、以下の Include source files outside of the Root Directory in the Build Step. のチェックが外れており、プロジェクトルートの package.json が読めず workspace が無効になってしまっていたのが原因でした。

Root Directory Setting

上記のチェックを入れることで解決しました。

CI で node_modules のキャッシュが引けなくなった

Nx ベースだと node_modules が root にしかなかったため、 CI でキャッシュする際はそこだけで事足りました。 Turborepo にしてからは apps 以下にそれぞれ node_modules を作るようになったので、キャッシュの取り方を変更する必要がありました。

$ git diff
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 63542ce0..b3312acd 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,7 +21,7 @@ jobs:
       - name: Restore node_modules cache
         uses: actions/cache@v2.1.5
         with:
-          path: 'node_modules'
+          path: '**/node_modules'
           key: yarn-cache-${{ hashFiles('yarn.lock') }}
 
       - name: Install yarn

本番適用において Instant Rollback が精神的な支えとなった

移行の本質とはずれますが、 Vercel には Instant Rollback という機能があります。簡単に言うと以前のデプロイの状態に瞬時に戻せる機能です。

ツールの移行に伴い、 Vercel 上の設定や環境変数が大きく変更になるため、リリース後に本番環境において問題が生じないか精神的に不安な部分がありましたが、何か起きてもこの機能を使えばすぐにロールバックできるため、比較的安心して本番適用を進めることができました。

移行してみてどうなったか

Nx 時代のコードベースと比べると、 Turborepo にしてからは以下のような点で運用負荷が軽減されたように感じています。

  • 各 app をそれぞれ独立したアプリケーションと見なしやすくなったこと
  • ビルドの安定性が増したこと
  • ツール固有のプラグインがなくなり、設定も減ったこと

一方で、 Turborepo の強みの一つである Remote Caching はまだチームとして使えていません。

現状だと .gitignore に含まれていない全ファイルを巻き込んでハッシュにし、それをキャッシュキーにしているので、恩恵を受けることができるのが git clone 直後の初回のビルドくらいでは、と考えているためです。また、適切な設定をおこなわないと誤った環境変数がビルドキャッシュに含まれる可能性があるなど、ビルド時間を短縮するという目的にしては必要以上に複雑性を抱えそうな気がしています。

今後、コードの差分のみをビルド対象とし他の部分はキャッシュをいい感じに使ってくれるようになると嬉しいな、などど考えています。

最後に

弊社ではソフトウェアエンジニアを募集しております。 @suthio にDMしていただくか、下記での応募をお待ちしております。

https://www.wantedly.com/projects/1226647

Google Cloud Batchを使ってバッチの処理待ち時間を1/30以下にしたので紹介させて欲しい

この記事について

今回、Google CloudのBatchを利用した動画変換処理を実装したので、どのようにしたのか、どこにハマったのか(ハマっているのか)、その効果についてまとめました。端的に現状を3行でまとめると、以下のようになります。

  • 動画変換処理にGoogle CloudのBatchを使いました
  • GPUを利用することで処理時間を短くできました
  • コンテナでGPUをうまく使えずシェルスクリプトとGoで作ったバイナリを活用しています(コンテナ化の知見募集中です)

はじめに

はじめまして。identify株式会社 CTOの@suthioです。 弊社、identify株式会社では動画素材サービスDeLMOの運営を行っています。

service.delm0.jp

簡単に言うと、動画素材を提供するサービスとなります。

課題

弊社ではGoogle Cloud Platformをインフラとして利用しています。DeLMOというサービスは動画素材のプラットフォームという特性上、動画を変換する処理自体が事業のコアとなっており、GCEインスタンスにインストールしたffmpegを用いて動画の変換処理を行っていました。

サービス開始当初は特に問題なかったのですが、関わるクリエイターの方々が増えたことや日々アップロードされる動画素材数が格段に増えたこと、iPhoneの高解像度化により変換処理が滞ることになりました。

弊社の運用は下記となっています。

①動画アップロード→②変換→③審査→④公開

動画アップロードの数が増えたことにより、最大で30時間ほど変換処理待ちが発生していました。この影響により、審査に待ちが発生し動画素材を公開できないといった状態が定常的に発生していたので、こちらを解決するべく「動画変換高速化プロジェクト」を始動する運びとなりました。

旧動画バッチフロー

下記の画像は3月5日から3月13日までの変換サーバーのCPU使用率となります

変換サーバーCPU使用率

解決したいことの整理

  • 最大30時間もの変換処理待ち状態となるのを解決したい
    • 1動画変換あたりの時間を高速化
    • 複数台のVMやCPUを使用して並列で処理を行えるようにしたい
  • あわよくばこのタイミングでコスト削減もできたら理想
    • 変換処理が一番インフラコストが高くなっているかつコストが高くなると予想されるため

下記の資料の通り、スマートフォンの背面カメラは日々高解像度しており、今後も1動画あたりの変換コストが高くなる想定をして 3年後も耐えられるような変換処理を目指しました。(2023年現在では非常にオーバースペックな構成) www.counterpointresearch.com

並列・高速な変換処理の検討

特に動画変換においてはGPUが利用できれば時間の短縮が見込まれるため、GPUが利用でき、かつ並列でバッチ処理ができるサービスを検討し、Google Cloudのアーキテクチャレビューを使用してGoogle Cloudの方にも相談させていただいた結果、ちょうど昨年にGAしたばかりのBatchを採用することになりました。その他に検討したものは下記になります。

  1. Cloud Run jobs
  2. 動画変換サービス

1のCloud Run jobsについてはGPUが利用できないため今回は見送りました。2の動画変換サービスという観点では、Googleが提供しているTranscoder APIや AWSが提供しているAmazon Elastic Transcoder などがあります。よしなにやってくれるのはよいのですが、動画変換の要件に見合わないことや、動画変換時に必要なコストが非常に高くなってしまうことが見込まれるので採用を見送りました。※DeLMOの動画素材は30秒以下の動画が非常に多いのに対し、動画変換サービスは60秒がミニマムの課金だったので30秒の変換を行っても60秒の料金がかかるため高額になってしまうという予測でした。

※弊社はパフォーマンスや審査の最適化のために1つの動画につき10個以上の動画に変換を施しているので動画変換サービスと相性がそこまで良くなかったというのも独自でBatchを使用する要因になりました。

Batchについて

2022/10/11にGAとなった比較的新しいサービスです。Batchで実行してほしい処理をコンテナ化またはシェルスクリプトで記述しておき、どのような環境(主にコンピュートリソース)で実行するかを設定すればあとはよしなに適切なGCEインスタンスを起動し、処理を実行し、終わったらインスタンスを削除するところまで自動・並列で行ってくれます。

cloud.google.com

処理部分の実装

Batchに行わせたい処理としては以下になります。

  • Batchの呼び出し元から変換対象の動画IDを受けとる
  • 動画IDを使って変換対象の動画ファイルの格納場所(GCS)をDBから取得する
  • 変換対象の動画ファイルをGCSからダウンロードする
  • 変換処理を行う(ffmpeg)
  • 変換後の動画ファイルをGCSへアップロードする
  • DBにある動画のステータステーブルを更新する

処理としてはGCEで行っているものが既にあるため大きく変えず、ffmpegでの変換の際にGPUを利用するオプションを加えるなどしました。コンテナ化にあたりベースイメージにffmpegをインストールしておき、Goバイナリを注入してそこから呼び出すつくりとしました。

ハマったポイント1:コンテナからGPUが使えない

GPUを利用したジョブのドキュメントを見ながら、コンテナにGPUの諸々をマウントして、ffmpegのオプション(h264_nvenc)を設定して変換しようとすると必要なライブラリが見つからない旨のエラーがでました。

Cannot load libcuda.so.1

色々調べた結果、使っていたffmpegが参照しているライブラリディレクトリ(--libdir=/usr/lib/x86_64-linux-gnu)に上記のファイル(GPU用のライブラリファイル?)が存在しない、というふうに捉えました。ただ、単純にコンテナ内の上記ディレクトリにGPUのライブラリディレクトリをvolumeマウントしてしまうと、もともと入っていたライブラリが上書きされてしまって結局ffmpegが立ち上がらない、ということになりました。そこでカスタムVMを起動し、そこにGPUのライブラリも、ffmpegが標準で使うライブラリも、全部先に入れておいてそれをコンテナでマウントすればいいのではないか、と考えました。が、うまくいきませんでした。※詳細は後述します。

ハマったポイント2:自作のインスタンステンプレートを使うとGCEが立ち上がらない

別途GCEインスタンスを立ち上げ、ffmpegをビルドし、GPUのドライバもインストールしてからイメージおよびインスタンステンプレートを作成しました。ここで、用意したインスタンステンプレートでBatchを起動するとGCEが立ち上がってこないという現象に陥りました。サポート問い合わせすると以下のコマンドでインスタンステンプレートを作成してくださいと回答をいただきました。ポイントとしてはおそらく以下かと思います。

  • イメージ作成時にfamilyを指定する(--image-family)
  • 未検証ですがその他オプション(--maintenance-policy、--restart-on-failure)もあったほうがよさそう
gcloud compute instance-templates create xxxxxxxx \
    --machine-type n1-standard-2 \
    --boot-disk-size 250GB \
    --accelerator type=nvidia-tesla-t4,count=1 \
    --image-family xxxxxxxx \
    --image-project xxxxxxxx \
    --maintenance-policy TERMINATE \
    --network-interface=network-tier=PREMIUM,subnet=xxxxxxxx \
    --restart-on-failure

また、他プロジェクトで作成したイメージで作ったインスタンステンプレートを使おうとすると、CODE_GCE_PERMISSION_DENIEDとなりインスタンスが起動できない現象にもぶつかりました。ここでもサポート問い合わせを行い、Batchが利用するServiceAgent(service-PROJECT_NUMBER@gcp-sa-cloudbatch.iam.gserviceaccount.com)に、 利用するイメージプロジェクトでのイメージ利用者権限を付与すればOKということがわかりました。

コンテナを諦めてシェルスクリプトへ

ライブラリ全部入りのVMを立ち上げてコンテナにマウントする作戦も色々やってみたのですが

Cannot init CUDA

のエラーがどうしても倒せず、コンテナからGCE側にアタッチされたGPUを使うのは一旦諦めました。(なんとなくBatchの問題というよりはGPUをコンテナで扱う一般的な問題にあたっている気はするのですが。。。)幸い処理部分は弊社が利用している言語がGolangだったのでGoのバイナリを作って配置すればいいこと、GCEに直接sshした場合ではGPUオプションのffmpegが起動できたことからコンテナを利用せず、シェルスクリプトからGoのバイナリを呼び出しそこからインスタンスのffmpegを呼び出すことでGPUを利用した動画変換が実現できました。

最終的な構成

最終的には以下のような構成になりました。

バッチ起動用GCEイメージの下準備

VMのベースとして、 Debian 10 based Deep Learning VM for TensorFlow Enterprise 2.11 with CUDA 11.3 M104 を利用し、ここでh264_nvencコーデックを有効にしたffmpegをビルドしてイメージとして保存しておきます。(ファミリーを指定するのを忘れずに!)

インスタンステンプレートの作成

上記で記載したコマンドでイメージからインスタンステンプレートを作成します。

Batchの起動

以下の設定ファイルを使ってBatchを起動します。※実際にはGolangで書いているので cloud.google.com/go/batch を使って起動しています。 アプリケーションは事前にGCSに格納しておいて、直接マウントしつつ取り出してキックしています。

{
    "taskGroups": [
        {
            "taskSpec": {
                "runnables": [
                    {
                        "script": {
                            "text": "sudo /opt/deeplearning/install-driver.sh" // なぜか毎回初期化が必要
                        }
                    },
                    {
                        "script": {
                            "text": "cp /cmd/app /app && cp -r /cmd/resources/ /resources" // アプリケーションをマウント先からコピー
                        }
                    },
                    {
                        "script": {
                            "text": 
                                "/app" // 環境変数の値を使ってアプリケーションの実行
                        },
                        "environment": {
                            "variables": // 環境変数諸々
                        } 
                    }
                ],
                "volumes": [ 
                    { 
                        "gcs": { 
                            "remotePath": "xxxxxx" // Goバイナリが入っているGCS
                        }, 
                        "mountPath": "/cmd/"
                    } 
                ]
            },
        }
    ],
    "allocationPolicy": {
        "instances": [
            {
                "installGpuDrivers": false,
                "instanceTemplate": "xxxxx" // 作ったインスタンステンプレート
            }
        ]
    },
    "logsPolicy": {
        "destination": "CLOUD_LOGGING"
    }
}

結果

GPUで変換できるようになってから、試しに重めの変換処理を行ってみました。結果、CPU版では667秒(11分7秒)かかった処理がGPU版では141秒(2分21秒)となり、約5倍早くなりました。重い動画の処理ほど処理速度向上の恩恵を受けることができそうです。
※平均、4倍から5倍程度高速化に成功

元々問題となっていた処理待ちの事象もほぼなくなり、動画の変換待ち時間は今のところではありますが最大で10分程となりました。Batchの呼び出し元で動画IDを10個単位に分割して起動しているので、仮に100個の動画変換タスクがキューに積まれても10並列で変換することで待ち時間を短くすることに成功し、Batchの恩恵を実感しています。

料金的には処理時間が短くなること、Spot VMを使用していることから結果として80%ほど削減できる見込みです。

おまけのハマりポイント:gcsfuseのインストールに失敗してジョブがコケる

検証環境ではうまくいっていたのですが、本番環境で稼働させるようになってから ジョブが多数失敗するようになりました。

エラーログを見てみると、記述した処理が始まる前の、gcsfuseのインストールに失敗しているようでした。上記設定ファイルにあるように、GCSをマウントする設定を入れている場合は自動でgcsfuseがインストールされる仕組みになっているようです。それ自体は納得できるのですが、そこでエラーが出てしまうと本来の処理にいかずにジョブが終了してしまうため、変換処理が進みません。サポートに問い合わせつつ色々見てみると、カスタムイメージにもとから入っていたgcsfuseがどうやら悪さをしていたようで、こちらをアンインストールしたイメージを再作成すると失敗しなくなりました。あまり意識していなかったのですが、Google Cloud公式イメージには最初からgcsfuseが入っていることがある、ということで今後似たようなことがあれば気をつけたいと思います。

検索でたどり着いた人のためにエラーメッセージを貼っておきます。

Could not get lock /var/lib/dpkg/lock-frontend - open (11: Resource temporarily unavailable)
Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), is another process using it?

まとめ

Google CloudのBatchを使った動画変換処理を実装しました。結局当初想定していた、コンテナを使った実装はGPUをうまく扱えずに断念する結果となりました。(ここらへんの知見のある方、アドバイスいただけると嬉しいです。。。)ただ、少し遠回りではありつつも当初やりたかったことは実現でき、フルマネージドで並列、高速な変換処理を行えるBatchの魅力を感じています。 この記事がなにかの参考になれば幸いです。

また、今回副業として手伝ってもらっている@saramuneにBatchのインフラ周りの設定や調査、構築などを行ってもらっています。今回の構成に関する話や、コンテナでGPUをうまく使うやり方をご存知の方は@suthioか、@saramuneまでご連絡いただければ幸いです。

最後にいつものやつですが、弊社ではソフトウェアエンジニアを募集しております。 @suthioにDMしていただくか、下記での応募をお待ちしております。 www.wantedly.com