feature image

2022年8月21日 | ブログ記事

actions/setup-goに実装されたキャッシュ機能を使ってみたらCI実行時間を80秒短縮できた(けど少しハマったのでメモ)

この記事は2022年の夏のブログリレー12日目の記事です。

こんにちは。20Bの@Rasです。初めましての方はぜひ過去のブログもご覧ください!

過去のブログ一覧

目次

概要

30秒でわかる今回の内容

今回はtraPtitech/traPortfolioのCI実行時間を短縮しました。

改善前 (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 and cache-dependency-path were added. The cache 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つのキーを用いて(キーの名称は簡略化のため適当に付けています)、

の順にキャッシュを処理していたのに対し、変更後は1つのキーを使って

の順にキャッシュを処理していました。これに数時間ハマっていたわけです。

結果、モジュールのダウンロードをbuild jobに統合することで解決しました。バイナリのキャッシュが正常に効いたおかげで、go buildの実行時間も17秒から1秒に大幅に短縮できています(冒頭のビルド時間内訳を参照)。
解決後のキャッシュ処理の流れは以下の通りです。

ちなみに、もともと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です。お楽しみに~

Ras icon
この記事を書いた人
Ras

20B。アライグマです。

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2022年9月16日
5日でゲームを作った #tararira
Komichi icon Komichi
2022年8月29日
ケモナー向け VRChatの始め方、歩き方。VR無くてもできる!
pikachu icon pikachu
2022年4月5日
アーキテクチャとディレクトリ構造
mazrean icon mazrean
記事一覧 タグ一覧 Google アナリティクスについて