プロジェクトのCIを20分弱早くした
はじめに
この時期だと ○○ Advent Calendar の N 日目です となりそうなんですが特に何もないです
担当しているプロジェクトの CI を早くする事が出来たので自慢したい備忘録の記事になります
どのぐらい早くなったのかというと
before
after
20 分程度早くなりました
CI が遅いと push してからテスト結果出るまで 20 分待たないといけないストレスはもちろんなんですが、
本番にリリースする際も CI を実行し、テストが成功してからコンテナのイメージを作成していたため、
本番で何か問題があった場合に修正してからリリースするまで 最低 20 分掛かかってしまうという状況でした
(幸いにも急いでリリースしないといけない状況にはなりませんでしたが)
やったこと の前に
どんな事をして早くしたのを紹介する前に、開発/CI を行っている環境を説明します
担当しているアプリケーションでは Google Cloud Spanner (以降 spanner)を DB に使用しています
spanner はマネージドサービスのためローカルでの開発/テストではhandy-spannerを使用し、
CI では 本番と同様の環境で確認したいので、テスト実行前に共用のインスタンスに DB を 1 つ作成し実行後に削除するという形をとっています
CI 環境は CircleCI になります
ということで、どのような対応をしたのかを紹介したいと思います
やったこと
勘が鋭い人の中には複数 DB を用意して並列に実行したんでしょ?と思う人がいるかもしれませんが、
今回は複数 DB 使用する方法は採用しませんでした
やったこととしては
- テストケース終了時のデータ初期化方法の変更
- CI で使用しているツールを cache
- 確率チェックをしているテストケースの並列実行
になります
それぞれ説明していきます
テストケース終了時のデータ初期化方法の変更
前述の通り、1 つの DB をテストケースごとに使いまわしています
そのため各テストケース終了後に後続のテストに影響が無いよう各テーブルのデータをリセットする必要があります
修正する前のリセット方法がこちらです
for _, table := range TestTables {
stmt := spanner.NewStatement(fmt.Sprintf("DELETE FROM %s WHERE true", table))
if _, err := c.spanner.PartitionedUpdate(ctx, stmt); err != nil {
return fmt.Errorf("error partition update %w", err)
}
}
(一部抜粋)
テーブルをパーティション化 DML で 1 個 1 個 テーブル分リセットしていました
パーティション化 DMLである理由を確認しましたが、テスト内で大量のデータを使用したテストケースも特になく不要であることが分かりました
なので、呼び出しを 1 回にまとめるよう修正しました
ms := []*spanner.Mutation{}
for _, table := range TestTables {
ms = append(ms, spanner.Delete(ctx, spanner.AllKeys()))
if _, err := c.spanner.Apply(ctx, ms); err != nil {
return fmt.Errorf("error apply %w", err)
}
}
(一部抜粋)
この修正で 15 分程テストが高速化しました
CI に必要なツールの cache
修正前は CI で使用する DB の migration ツール等を都度 go get でインストールするようになっていました
0 からインストールすると時間もかかるうえ、ダウンロードする度にバージョンが変わる可能性もあります
そのため、こちらを参考に使用している go 製のツールを go module で管理するように修正しました
併せて vendor および インストール済みのバイナリを cache することでインストールする時間を短縮しました
この修正で 2 分程早くなりました
確率チェックをしているテストケースの並列化
テストケースに確率をチェックするテストケースがありました
実際に数千回施行して結果をチェックしていましたが、DB に依存していなかったためt.Parallelを使用して並列で実行するように修正しました
この修正で 1 分程 CI が早くなりました
さいごに
- テストケース終了時のデータ初期化方法の変更
- CI で使用しているツールを cache
- 確率チェックをしているテストケースの並列実行
で 20 分程 CI を高速化することができました
修正してみると遅い原因は まぁそうだよねという物がほとんどですが、少しでも参考になればと思います
おまけ
DB のデータのリセット方法、loop があるとはいえ使う API でこんなに差が出るもんなのかなと疑問だったのでベンチマークをとってみました
手元の MacBook Pro から 共用インスタンスの特定の DB に対して行いました
検証用のコードはこちらで削除対象テーブルを 1,3,5,10 と増やしてベンチを取ってみました
検証用コード
type client struct {
spanner *spanner.Client
}
func New(ctx context.Context, database string, opts ...option.ClientOption) (*client, error) {
cli, err := spanner.NewClient(ctx, database, opts...)
if err != nil {
return nil, fmt.Errorf("error new spanner client %w", err)
}
return &client{
spanner: cli,
}, nil
}
func (c *client) ResetEach(ctx context.Context) error {
for _, table := range TestTables {
stmt := spanner.NewStatement(fmt.Sprintf("DELETE FROM %s WHERE true", table))
if _, err := c.spanner.PartitionedUpdate(ctx, stmt); err != nil {
return fmt.Errorf("error partition update %w", err)
}
}
return nil
}
func (c *client) ResetMulti(ctx context.Context) error {
ms := []*spanner.Mutation{}
for _, table := range TestTables {
ms = append(ms, spanner.Delete(table, spanner.AllKeys()))
}
if _, err := c.spanner.Apply(ctx, ms); err != nil {
return fmt.Errorf("error apply %w", err)
}
return nil
}
...
func BenchmarkResetEach(b *testing.B) {
b.ResetTimer()
ctx := context.Background()
cli := new(ctx, b)
for i := 0; i < b.N; i++ {
if err := cli.ResetEach(ctx); err != nil {
b.Error("error reset each", err)
}
}
}
func BenchmarkResetMulti(b *testing.B) {
b.ResetTimer()
ctx := context.Background()
cli := new(ctx, b)
for i := 0; i < b.N; i++ {
if err := cli.ResetMulti(ctx); err != nil {
b.Error("error reset multi", err)
}
}
}
(一部抜粋)
テーブル数: 1
goos: darwin
goarch: amd64
pkg: xxx
BenchmarkResetEach-16 68 1014858045 ns/op
BenchmarkResetMulti-16 6736 10033684 ns/op
PASS
ok xxx 207.788s
テーブル数: 3
goos: darwin
goarch: amd64
pkg: xxx
BenchmarkResetEach-16 22 3026885868 ns/op
BenchmarkResetMulti-16 6650 11231347 ns/op
PASS
ok xxx 246.167s
テーブル数: 5
goos: darwin
goarch: amd64
pkg: xxx
BenchmarkResetEach-16 12 5007657921 ns/op
BenchmarkResetMulti-16 4845 18559626 ns/op
PASS
ok xxx 200.698s
テーブル数: 10
goos: darwin
goarch: amd64
pkg: xxx
BenchmarkResetEach-16 6 10029466758 ns/op
BenchmarkResetMulti-16 1868 34271077 ns/op
PASS
ok xxx 140.096s
テーブル数が 1 つの場合でも 約 100 倍遅い
テーブル数が増えるとパーティション化 DML でないクエリでも遅くなるけど、100 倍近く遅いクエリがループクエリが増えていくので…
テーブル数が 10 の場合は 300 倍程遅くなってることが分かりますね
今回のケースのプロジェクトはテーブルが 10 ちょいあるから そりゃ 15 分も早くなるのも頷ける…という感じですね