この記事は2022年の夏のブログリレー12日目の記事です。
こんにちは。20Bの@Rasです。初めましての方はぜひ過去のブログもご覧ください!
→ 過去のブログ一覧
目次
概要
30秒でわかる今回の内容
- Github ActionsでGo製アプリケーションのCIを回す際にactions/setup-go@v3を使っている
- v3.2.0がリリースされ、キャッシュ機能が実装された
- モジュールキャッシュとバイナリキャッシュの2種類のキャッシュに対応
- モジュールキャッシュ:
go.mod
で管理される依存モジュールのキャッシュ - バイナリキャッシュ: コンパイル後の実行バイナリのキャッシュ
- モジュールキャッシュ:
- これまではactions/cacheを併用していたが不要に
- 記述が簡潔になるだけでなく、CIの実行時間が結構短くなった
- モジュールキャッシュとバイナリキャッシュの2種類のキャッシュに対応
- ついでにworkflowを見直したら1分以上短縮できた
- 厳密にはsetup-goのキャッシュ機能以外にも改善を入れています🙏
- モジュールキャッシュは正常に動作するがバイナリキャッシュについては少し注意が必要
- バイナリキャッシュが効かず数時間ハマった
- おまけ: v3.1.0ではGoのバージョンをgo.modから取得できる機能が実装された
今回はtraPtitech/traPortfolioのCI実行時間を短縮しました。
- 各ジョブごとの実行時間内訳と
Build
ジョブの実行時間内訳を示しています。 - 改善前は単体テストと結合テストを同一ジョブ内で行っていますがテスト時間の差分(1m23s-46s=37s)を考慮しても約1分短縮できていることが分かると思います。
- なぜか改善前はバイナリキャッシュが効いているにもかかわらずビルドに17秒も要していますがまだ原因不明です、それも含めて直しました。
改善前 (3b63bb1
, 2m47s)
ビルドチェックの実行時間内訳
改善後 (a656de7
, 1m27s)
ビルドチェックの実行時間内訳
本編
何が実装されたの
Github ActionsでGoを使う際はGithub公式のactions/setup-goを使うのが一般的だと思いますが、先日v3.2.0がリリースされ、キャッシュ機能が実装されました。
This release introduces support for caching dependency files and compiler's build outputs #228. For that action uses @toolkit/cache library under the hood that in turn allows getting rid of configuring @actions/cache action separately and simplifies the whole workflow.
setup-goのキャッシュ機能は、同じくGithub公式のactions/cacheでも使われているtoolkit/cacheを組み込んで実装されているようです。
Such input parameters as
cache
andcache-dependency-path
were added. Thecache
input is optional, and caching is turned off by default,cache-dependency-path
is used to specify the path to a dependency file -go.sum
.
go.sumをハッシュ化した文字列をキーとしてキャッシュを内部で管理してくれます。
GitHub Docsにも使い方が記載されています。
どう書くの
ここではビルドのチェックを例とします。また、CIの発火設定などの記述は省略します。
改善後の設定ファイルは ココ
これまでは以下のように書くのが主流でした。
build:
name: Build
runs-on: ubuntu-latest
env:
GOCACHE: /tmp/go/cache
steps:
# 作業ディレクトリにcheckout
- uses: actions/checkout@v3
# Goの環境構築
- uses: actions/setup-go@v3
with:
go-version: 1.19 # 手動で設定 or 環境変数で管理
# モジュールキャッシュ (自分で設定する🥵)
- uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-gomod-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-gomod-
# バイナリキャッシュ (自分で設定する🥵)
- uses: actions/cache@v3
with:
path: /tmp/go/cache
key: ${{ runner.os }}-go-build-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-go-build-${{ github.ref }}-
${{ runner.os }}-go-build-
# モジュールのダウンロード
- run: go mod download
# ソースコードのビルド
# バイナリキャッシュがある場合、中間バイナリを使って実行時間を短縮できる
- run: go build -o myApp .
これからはこう書きます!!
build:
name: Build
runs-on: ubuntu-latest
steps:
# 作業ディレクトリにcheckout
- uses: actions/checkout@v3
# Goの環境構築 & キャッシュ (内部で設定してくれる😊)
- uses: actions/setup-go@v3
with:
go-version-file: ./go.mod # v3.1.0で実装された。後述
cache: true # デフォルトはfalseなのでtrueにする
# モジュールのダウンロード
- run: go mod download
# ソースコードのビルド
# バイナリキャッシュがある場合、再利用することで実行時間を短縮できる
- run: go build -o myApp .
コードを全く読まなくても記述量が全然違うのが一目でわかります。
キャッシュが効かなくてハマった話
当初、一番最初に紹介したフロー図のようにモジュールをダウンロードするジョブ(build jobとします)とバイナリをビルドするジョブ(mod job)が分かれていました。尚、build jobはmod jobに依存しており、mod jobが終了したのちbuild jobが開始します。察しの良い方はもう問題点に気づいたかもしれません。
当初、この2つのジョブを分けたままsetup-goのキャッシュ設定を進めていたのですが、モジュールキャッシュは正常に効いているのに対し(go mod download
が0~1秒で終わる)、バイナリキャッシュが何度再試行しても効いていませんでした(毎回go build
が15秒以上要する)。これで数時間ハマってしまったので、何故この問題が起きてしまったのか、また、どのように解決すれば良いかをメモとして残しておきます。
前章の設定ファイルでは、前者は自分でrestore-keys
を設定していますが後者ではsetup-goに任せています。そこで、内部実装を見ると以下のように設定されています。
const primaryKey = `setup-go-${platform}-go-${versionSpec}-${fileHash}`;
前者ではモジュールのキャッシュとバイナリのキャッシュのキーを分けていたため、go.mod
に変更があるたびにモジュールのキャッシュが更新され、コードをコミットするたびにバイナリのキャッシュが更新されていました。それに対し、後者はモジュール・バイナリともにgo.mod
の変更時のみ更新されています。
つまり、これまでは2つのキーを用いて(キーの名称は簡略化のため適当に付けています)、
go.mod
変更時- (ここからmod job)
key-mod
をキーとしてキャッシュがあればrestore go mod download
(キャッシュがあれば使う)- 1でキャッシュがなければ
key-mod
をキーとして実行結果をsave
- (ここからmod job)
- コミット毎
- (ここからbuild job)
key-build
を使ってキャッシュがあればrestore go build
(キャッシュがあれば使う)- 4でキャッシュがなければ
key-build
をキーとして実行結果をsave
- (ここからbuild job)
の順にキャッシュを処理していたのに対し、変更後は1つのキーを使って
go.mod
変更時- (ここからmod job)
key-both
をキーとしてキャッシュがあればrestore go mod download
(キャッシュがあれば使う)- 1でキャッシュがなければ
key-both
をキーとして実行結果をsave - (ここからbuild job)
key-both
を使ってキャッシュがあればrestore- 3でsaveしたばかりなので当然存在する
go build
(キャッシュがあれば使う)- バイナリキャッシュはないので使えない!😧
- 4でキャッシュがなければ
key-both
をキーとして実行結果をsave- 存在するのでsaveしない
- バイナリキャッシュがされない!😨
- 次回以降もバイナリキャッシュが使えない!🥶
- (ここからmod job)
の順にキャッシュを処理していました。これに数時間ハマっていたわけです。
結果、モジュールのダウンロードをbuild jobに統合することで解決しました。バイナリのキャッシュが正常に効いたおかげで、go build
の実行時間も17秒から1秒に大幅に短縮できています(冒頭のビルド時間内訳を参照)。
解決後のキャッシュ処理の流れは以下の通りです。
go.mod
変更時 (全てbuild job内)key-both
をキーとしてキャッシュがあればrestorego mod download
(キャッシュがあれば使う)go build
(キャッシュがあれば使う)- 4でキャッシュがなければ
key-both
をキーとして実行結果をsave- モジュールとバイナリを1つのキーでsaveすることができた!😊
ちなみに、もともとbuild jobとlinterを回すジョブ(lint job)がmod jobに依存していましたが、lint jobでは静的解析を行うためモジュールが必要ありません。そのため、mod jobを外しても問題なく、また余分なジョブを外したことで実行時間もかなり削減することができました。
どっちを使えばいいの
個人としては簡潔に書けるしsetup-goのキャッシュで十分だと思います。
go.mod
の変更時だけではなくコミットごとにバイナリをキャッシュしたい場合はactions/cacheを使うのもよいかもしれませんが、go build
コマンドにはソースコードのコンパイル以外にも依存パッケージのコンパイルやパッケージのリンクなどの処理が含まれます。ここでは扱いませんが、How “go build” Works.や、これの日本語訳である"go build"した時に何が起きているのか?(Qiita)が詳しいです。
また、go
コマンドのドキュメントのBuild and test cachingに
cleaning the cache explicitly should not be necessary in typical use. However, the build cache does not detect changes to C libraries imported with cgo.
とあるように、C言語のライブラリが変わったとき以外は手動でキャッシュを削除する必要がなく、毎回キャッシュを新しいものに置き換えなくても十分高速に動きます。
おまけ
setup-goのv3.2.0でキャッシュ機能が実装されましたが、v3.1.0ではGoのバージョンをgo.modから取得できる機能が実装されました。
- uses: actions/setup-go@v3
with:
go-version-file: ./go.mod
のように記述することでバージョンを環境変数などで定義する必要がなくなります。
また、traPortfolioのlint checkでも使用しているreviewdog/action-golangci-lintにもv2.2.0で同じような機能が実装されています(go-version-file
ではなくgo_version_file
なので注意)。
おわりに
以上、Github Actionsでactions/setup-goを使うときはキャッシュ機能を使うといいという話でした。参考になれば幸いです。
明日の担当は@Swan_417です。お楽しみに~