astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

GitLab CIを四倍速にした話

f:id:astamuse:20191220191229p:plain

chotaroです。豊洲PITでたくさんの人と頭の中のOMOIDEを共有してから一週間、興奮冷めやらぬ日々です。 astamuse lab年内最後の記事になります!

さて先日、ICPチームで利用しているGitLab CIの実行時間を4分の1にしました。 一度CIを動かすと1時間強待ちが発生する状況になっていたため、改善が急務でした。改善の結果と、どんなことをしたか自分の備忘も兼ねて書いておこうと思います。
悩める人の参考になれば嬉しいです。

before/after

10月に改善を実施したので、9月と11月の比較になります。

pipeline実行回数 合計実行時間(時間[分]) 平均実行時間
9月 164回 114時間[6880分] 0:41:57
11月 106回 15時間[945分] 0:08:55

1回のpipelineにかかる時間がなんと4分の1以下になりました(๑•̀ㅂ•́)و✧
せめて平均30分以内、を目指していたので大成功です!

改善ポイント

以下が主なポイントになります。

  • 無駄なjobは動かさないようにしましょう
  • 一度pullしたdocker imageを再利用するようにしましょう。
  • executorのスペックを見直しましょう。
  • 毎回利用する外部リソース(依存するjarとか)が存在する場合、cacheを活用して使いまわしましょう。

当たり前ですね!(※できてなかった

基本的には公式documentの記述をもとにそれぞれの環境に当てはめていけばいい感じになります。

それでは良いCIライフを!アデュー!

......というのもあんまりなのでかなり泥臭い内容ですが、私の闘いの記録を残します。

改善する前の状況

  • 言語は java / scala
  • マルチプロジェクトな構成のリポジトリ
  ~/pjroot
.
├── .git
├── .gitlab-ci.yml
├── api
│   ├── src
│   └── test
├── batch
│   ├── src
│   └── test
├── commonLibrary
│   ├── src
│   └── test
└── web
    ├── src
    └── test
  • 各PJで自動testを実施
  • どんな小さな変更でもすべてのテストを実施(README変更でもpipelineが実行されてしまう)
  • 一度のpipelineですべてのプロジェクトについて順次テストを実行
  • コミットのたび毎回全部のjobが実行される

->諸々の結果、一回あたり1時間強かかる上、pipelineの実行回数が無駄に多い

GitLab CIについて

利用方法

GitLab上で利用できるCIです。
ドキュメントはこちら
紙面の都合から詳細は省きますm( )m

ざっくり概要

  • ymlでjobや環境の設定を管理することができる
  • gitlab-runnerというCIランナーをgitlab上のGUIで管理して利用する。
  • executorがjobを実際に実行する。
    • executorには様々なものを設定できる。dockerや仮想マシン インスタンスなど。(インスタンスについてはawsやgcpが利用可能。)

我々の利用方法

  • GCPインスタンス上でgitlab-runnerが動いている
  • executorはdocker-machine形式により実行。
    • インスタンスを立ち上げ, dockerコンテナをそれ上で動かしてjobを実行する。

現状を確認してボトルネックを明確にする

jobの実行をgitlab上で観測する

まずどう動いているのかがわからなかったので、ひたすら眺めてみました。

  • gitlab-ciのjobのログをひたすら眺める
    • どこで詰まっているのかを調査するため、timeコマンドを挟むなどして実行時間を確認する
  • gitlab-runnerにモニタリングツールを挿してもらう
    • オートスケールの動き方など、どのようにexecutorがjobを処理していくのかをGUIで観測する

手元でrunnerを立てて観測してみる

リソースの状況などもっと詳細を確認したかったので手元でrunner、およびexecutorを立てて眺めてみました。

  • gitlab-runnerは手元で実行することが可能
    • gitlab-runner registerで対話形式で登録
  • executorをdockerにして実行
    • remote上で動かすときも実体はdockerコンテナ上でtestを実行しているだけなので、それをローカルで再現
    • 手元であればdocker psdocker statsで観測が可能

で、結局何がボトルネックになっていそうなのか

  • pipeline一本あたりに実行されるjobが多すぎる(再確認

複数PJが一つのリポジトリに乗っかっていて、一つのPJ修正でも全部のテストが起動している

  • 一度に並列に実行されるjobが少なく、待ちが発生している

jobが2並列しか実行できないのにpiplineの同じステージに5個jobが積まれている、、、

  • docker imageをpullするのに毎回時間がかかっている

前回実行時に取得したimageなどを使えないのか?

  • 依存するライブラリの取得に時間がかかっている

cacheで共有化できないか?

  • CPUの使用率がコンパイルのタイミングで跳ね上がる

最小のインスタンスを使っている現状ではスペック不足なのでは?

ボトルネックをそれぞれ対処してみる

pipelineで実行されるjobの制御

更新されているものが何か、によって実行JOBを制御します。

  • タイミングの制御:MRを立てている場合だけ実行する
  • 実行JOBの制御:該当directoryを変更している場合のみ実行する

各JOBについて、以下のようにonlyを記載することで制御します。

job:
    only:
        changes:
            - batch/**/*
        refs:
            - merge_request

並列実行数を増やす

これはgitlab-runnerのconcurrent設定で実現できます
ただし、このconcurrentを増やすほどexecutorが並列実行される=リソースを食い合うので、設定には注意しましょう

/[config_directory]>/config.toml

concurrent = 5

docker imageを使い回す

利用しようとしているimageがすでにpullされている場合、新たにpullするのをやめるようにします。
これはgitlab-runnerのpull_policy設定で実現できます

※公式documentに記載有り

/[config_directory]>/config.toml

[[runners]]
  [runners.docker]
    pull_policy = "if-not-present"

cacheの仕組みを利用して、一度取得したjarを別のpipelineでも使い回す

cacheを利用して、scalaの依存解決速度を爆速にします
※cacheについての公式ドキュメントはこちら

cacheの保存場所の設定を行わない場合、実行する環境でしかそのcacheを使い回せません。
そのため、gcs上にcacheを保存するbucketを作成して、そこに保存するようにします。runnerの設定にて指定することができます。

/[config_directory]>/config.toml

  [runners.cache]
    Type = "gcs"
    Path = "cache"
    Shared = true
      [runners.cache.gcs]
        CredentialsFile = "<credential fileを配置したpath>"
        BucketName = "<cacheを保存先のbucketのname>"

cacheの保存先の設定ができたら、次にcacheを利用するようjobの設定を行います。
ivyでの依存解決に時間がかかっていることがわかったので、.ivy2配下のcacheをpipelineを跨いで使い回すように設定します。

# 環境変数でsbtの参照先を上書きします
variables:
    JAVA_TOOL_OPTIONS: "-Dsbt.ivy.home=${CI_PROJECT_DIR}/.ivy2/ -Dsbt.global.base=./.sbtboot -Dsbt.boot.directory=./.boot"
    # coursierを利用する場合はこちらも指定してあげましょう
    COURSIER_CACHE: "${CI_PROJECT_DIR}/.cache"

...

job:
    cache:
        # jobの名前単位でkeyを設定することで、pipelineを跨いで同じjobでcacheを再利用させます
        key: app-dependencies-cache-${CI_JOB_NAME}
        paths:
            - ${CI_PROJECT_DIR}/.ivy2/

設定したら何度かjobを実行してみます。

cacheを利用できている場合、開始時にDownloading cache.zip from ~~, jobの終了時にUploading cache.zip to ~~~と表示されるので確認しましょう。

注意事項

cacheが効いてない?と感じる場合、以下のことを疑いましょう

  • cacheするディレクトリは間違っていませんか?
  • 使いまわしたいリソースを上書きしていませんか?
    • keyを元にpathを作成し、そのpathにzipファイルを保存しにいくため、同じkeyを用いてしまうと、cacheの内容が上書きされてしまいます
    • 改善策:jobごとにcache-keyを作成して、jobごとに保存するようにする(上記のkey設定を参考にしてください)

executorのスペックを見直してコンパイル速度を改善する 金で殴るともいう

ローカルでdockerに4コア使わせたところ、300%ほど利用していることがわかったので、スペックの調整をします。
executorが起動するインスタンスの設定を修正します。

docker-machineのオートスケールはDockerMachineの仕組みに依存するとのことなので、DockerMachineのgce向けのドキュメントを参考にインフラ担当に設定してもらいます(ありがとうございました!)

/[config_directory]>/config.toml

[[runners]]
    [runners.machine]
        MachineOptions = [
        ...

job設定に環境のスペックを確認するコマンドを入れるなどして反映されていることを確認しましょう。

まとめとして

これらの結果が前述の成果になります(再掲

pipeline実行回数 合計実行時間(時間[分]) 平均実行時間
9月 164回 114時間[6880分] 0:41:57
11月 106回 15時間[945分] 0:08:55

とにかく改善しないと開発のスピードがバリヤバいという危機感から、一つ一つ調べたり、仮説を立てて実践を繰り返しました。

一番効果があったのは何かと言われると明確ではないのですが、

  • executorのスペックを見直してコンパイル速度を改善する

が一つの鍵になり、そこ以外の地味な部分がすべて活用されるようになった結果劇的にjob時間が短くなった印象です。

成果だけみると月間100時間短縮して素晴らしい改善効果なんですが、一方で「改善をあとに回した結果、数十ないし数百時間を無駄にしてしまっていた」という事実も浮き彫りに。。。

もっと早く問題意識を持って改善すべきだった事案です。反省。

gitlab-ciはまだ不慣れな面も多々あるのですが、今回の件でだいたいお友達になれたような気がします。 来年はDeployフローの改善に着手したいところです。

次回更新は年明け予定です。それでは皆様、良いお年を!

Copyright © astamuse company, ltd. all rights reserved.