Search Results for ""

CircleCI 最適化クックブック

CircleCI 最適化クックブックは、CircleCI のリソースを使用してさまざまな最適化タスクを行うための詳しい手順をユース ケースごとにまとめた「レシピ集」です。 このクックブックと関連セクションを参照することで、繰り返し実行可能な最適化タスクをすばやく簡単に CircleCI プラットフォームで実行できるようになります。

はじめに

CircleCI プラットフォームを使用しているときに、パイプライン パフォーマンスに予期せぬ遅れが発生し、構造化されている重要な動作の実行性能に悪影響を及ぼす場合があります。 こうしたパフォーマンスのボトルネックは、パフォーマンス全体に影響するだけでなく、ワークフローやビルドの失敗の原因にもなります。 一見、軽微なトラブルのように思えますが、それぞれのボトルネックを解消するためにクレジットやリソース、時間などを費やすことになり、コストがかさんでしまいます。

最適化レシピ

このガイドでは、以下のような最適化戦略を紹介します。これを実践すれば、パフォーマンスの潜在的なボトルネックを最小限に抑え、CircleCI を使用する際に最善のパフォーマンスを得ることができます。

メモ: このガイドは、常に最新の最適化戦略を紹介できるよう、継続的に改訂されます。随時このページを参照して、最新の内容をチェックしてください。

ジョブを順次実行して同時処理を回避する

CircleCI プラットフォームを使用するとき、多くのケースで検討しなくてはならないのが、システム タイムアウトが原因でワークフローが失敗しないよう、複数のジョブをいかにうまく同時に処理するかという点です。 これは、同じ環境で複数のコントリビューターとコミッターが作業している場合に特に重要になります。 CircleCI プラットフォームは、パフォーマンスの低下や待機時間を発生させることなく複数のタスクを同時に処理するように設計されています。そのため、多数のジョブがキューイングされ、前のジョブが完了するのを待ってから新しいジョブが開始されている場合に、システム タイムアウトの設定が短すぎると、同時処理が問題となることがあります。 そうした場合には、1 つのジョブは完了しますが、他のジョブはこのタイムアウト設定によって失敗します。

ワークフローとジョブを効果的に最適化し、同時処理とタイムアウトによる後続ジョブの失敗を回避するために、CircleCI では、この種のパフォーマンスの問題に特化したシングル スレッド (キューイング) Orb を開発しました。 この Orb を呼び出すことにより、ジョブとビルドの全体のパフォーマンスを大幅に改善し、同時処理を回避できます。

メモ: CircleCI のキューイング Orb の詳細については、以下のページを参照してください。

CircleCI プラットフォームと CircleCI Orbs を使用するための環境のセットアップと構成

CircleCI プラットフォームと CircleCI Orbs を構成するには、以下の手順を行います。

1) .circleci/config.yml の先頭で CircleCI のバージョンを version 2.1 と設定します。

version: 2.1

2) パイプラインを有効化していない場合は、[Project Settings (プロジェクト設定)] -> [Advanced Settings (詳細設定)] に移動し、パイプラインを有効化する必要があります。

3) バージョンの下に orbs スタンザを追加し、Orb を呼び出します。 以下に例を示します。

version: 2.1

orbs:
  queue: eddiewebb/queue@1.1.2

4) 既存のワークフローやジョブで queue 要素を使用します。

5) サードパーティ製 Orb を使用する場合は、組織の [Security Settings (セキュリティ設定)] ページでオプトインします。

ワークフローのブロック

ワークフローの同時処理により、ワークフローの 1 つが「ブロック」される問題が発生することがあります。 これは、あるワークフローが既に実行中で、別のワークフローがキュー内で待機状態となっている場合に発生します。 CircleCI はすべてのワークフローにとって最適なパフォーマンスを提供することを目指しているため、キュー内のワークフローが長時間にわたって待機状態となっている場合、CircleCI プラットフォームは他のワークフローが完了するまで、ワークフローの実行を「ブロック」します。 キューイング Orb でワークフローの同時処理を回避する簡単な方法は、先行するタイムスタンプを持つワークフローが存在する場合の「ブロック」を有効にすることです。 パラメーター block-workflow の値を true に設定すると、すべてのワークフローが強制的に、同時ではなく順次実行されるようになります。 これにより、キューイングされるワークフローの数が減り、 ワークフローが破棄されなくなると同時に、全体のパフォーマンスも向上します。

Version: 2.1
docker:
  - image: 'circleci/node:10'
parameters:
  block-workflow:
    default: true
    description: >-
      true の場合、先行するタイムスタンプを持つ他のワークフローの実行が完了するまで、
      このジョブはブロックされます。 通常、最初のジョブに使用します。
    type: boolean
  consider-branch:
    default: true
    description: 同じブランチで実行されているジョブのみを考慮する必要があるかどうか
    type: boolean
  consider-job:
    default: false
    description: 非推奨。 block-workflow を参照してください。
    type: boolean
  dont-quit:
    default: false
    description: >-
      中止は失敗に相当します。 ジョブを強制的に、失敗ではなく
      いったん期限切れにします
    type: boolean
  only-on-branch:
    default: '*'
    description: 特定のブランチのキューのみを対象にします
    type: string
  time:
    default: '10'
    description: 断念するまでの待ち時間
    type: string
  vcs-type:
    default: github
    description: 必要に応じて、VCS として 'bitbucket' を指定します
    type: string
steps:
  - until_front_of_line:
      consider-branch: <<parameters.consider-branch>>
      consider-job: <<parameters.consider-job>>
      dont-quit: <<parameters.dont-quit>>
      only-on-branch: <<parameters.only-on-branch>>
      time: <<parameters.time>>
      vcs-type: <<parameters.vcs-type>>

キャッシュを効果的に利用してビルドとワークフローを最適化する

ビルドとワークフローを最適化する一番の早道は、具体的なキャッシュ戦略を実践して、以前のビルドとワークフローで生成された既存のデータを利用できるようにすることです。 パッケージ管理アプリケーション (Yarn、Bundler など) を使用する場合でも、キャッシュ処理を手動で構成する場合でも、最も適正で効果的なキャッシュ戦略を用いることにより、パフォーマンス全体の向上を図ることができます。 このセクションでは、実際の環境に合った最適なキャッシュ方法を判断するうえで参考になるユース ケースを紹介します。

ジョブで任意の時点のデータを取得しているなら、キャッシュを活用できる可能性があります。 よく用いられているのが、パッケージ/依存関係マネージャーです。 たとえば、プロジェクトで Yarn、Bundler、Pip を利用すると、ジョブ中にダウンロードする依存関係は、ビルドごとに再ダウンロードされるのではなく、後で使用するためにキャッシュされます。 以下は、パッケージ マネージャーのキャッシュ機能を活用した例です。

version: 2
jobs:
  build:
    steps: # 'build' ジョブを構成する一連の実行可能コマンド
      - checkout # 作業ディレクトリにソース コードをプルします
      - restore_cache: # ** キー テンプレート Branch または requirements.txt ファイルが前回の実行時から変更されていない場合、保存されている依存関係のキャッシュを復元します **
          key: deps1-{{ .Branch }}-{{ checksum "requirements.txt" }}
      - run: # pip を使用して、仮想環境をインストールしてアクティブ化します
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r requirements.txt
      - save_cache: # ** 依存関係のキャッシュを保存する特別なステップ **
          key: deps1-{{ .Branch }}-{{ checksum "requirements.txt" }}
          paths:
            - "venv"

上記の例では、キャッシュ キーで checksum を使用しています。 これを使用すると、特定の依存関係管理ファイル (package.json、上記の requirements.txt など) に変更があるかどうかを判断でき、キャッシュはそれに応じて更新されます。 また上記の例では、restore_cache で補間によって動的な値をキャッシュ キーに挿入することで、キャッシュを更新する必要性を正確に捉えて細かい制御を行っています。

メモ: キャッシュのステップをワークフローに追加する前に、依存関係のインストールが成功していることを確認してください。 依存関係のステップで失敗したままキャッシュする場合は、キャッシュ キーを変更して、不良キャッシュによるビルドの失敗を回避する必要があります。

キャッシュはビルドとワークフローを最適化するための非常に重要な側面であるため、まずは次のページの説明をよく理解することをお勧めします。このページでは、キャッシングについて説明すると共に、設定ファイルを最適化するためのさまざまな戦略を紹介しています。

テスト パフォーマンスを改善する

CircleCI プラットフォームでテストを実行するには、テスト プロセスを最適化して、いかにクレジットの使用量を最小限に抑えながらテスト パフォーマンス全体と結果を改善するかを検討する必要があります。 テストによっては、非常に時間がかかったり、高いパフォーマンスが必要になったりします。そのため、テスト時間を短縮できれば、組織の目標達成に向けて大きな後押しとなります。

CircleCI プラットフォームでテストを行う際には、多様なテスト スイートやアプローチを採用できます。 CircleCI はテスト スイートに依存しませんが、以下の例 (こちらのブログ記事でこのテスト最適化ユース ケースについて説明している開発者から許可を得て改変) では、Django と CircleCI プラットフォームでテストを最適化する方法を説明します。

CircleCI プラットフォームでの Python Django プロジェクトのテストの最適化

一部の組織では、CircleCI を使用して、各変更をメイン ブランチにマージする前にテストを実行しています。 テストを高速化すると、フィードバック サイクルが速く回るようになり、自信を持ってコードを頻繁に配信できるようになります。 Python Django アプリケーションのワークフローの例を見てみましょう。CircleCI プラットフォームでテストを完了するのに 13 分以上かかっています。

テスト プロセスは以下のように表示されます。

最適化する前のテスト最適化プロセス

それでは、上図のテスト プロセスを細かく見て、テストの完了までにかかった時間を確認してみましょう。

テスト中には以下のステップが実行されました。

1) ビルド ジョブで、ランタイムの依存関係のみを含む Docker イメージを作成しました。 2) ビルド ジョブで、そのイメージを docker save でファイルにダンプし、ワークスペースに保存しました。 3) 2 つのテスト ジョブを実行し、ベース イメージをワークスペースから復元しました。 4) 各テスト ジョブで、このベース イメージを基にして、テストの実行に必要なすべての追加モジュールを含むイメージを作成しました。 5) 各テスト ジョブで、依存関係に従ってサービスをインストールし、最終的にテストを実行しました。

通常、セットアップを 1 回実行してからファンアウト ステップを実行することは、リソース使用量を削減する方法として従来から用いられています。ただしこの例では、次のようにファンアウト ステップで非常にコストがかかっていることが判明しました。

  • ビルド済みイメージをファイルにダンプする docker save の発行に、約 30 秒かかっている。
  • ワークスペースへのイメージの保存に、さらに 60 秒かかっている。
  • 次に、テスト ジョブでワークスペースをアタッチしてベース イメージを読み込む処理に、さらに 30 秒かかっている。
  • テスト ジョブで docker-compose を実行して依存関係に従ってサービス (Redis、Cassandra、PostgreSQL) を開始するときに Machine Executor を使用しており、 Docker Executor と比較して起動時間に 30 ~ 60 秒が余計にかかっている。
  • ビルド ジョブのベース イメージにはランタイムの依存関係のみが含まれていたため、これを拡張してテスト用の依存関係を追加して Docker イメージをビルドする必要があり、 この処理にさらに 70 秒かかっている。

このように、実質的にテストを実行していないセットアップの段階でかなりの時間がかかっています。 このプロセスでは、実際のテストが実行されるまでに 6.5 分を必要とし、さらにもう 1 つのテスト ジョブの実行までに 6.5 分を要していました。

テストの準備の最適化

このワークフローのステップの実行に 13 分は長すぎるため、以下のアプローチが採用されました。

CI テスト ワークフローの変更

ベース イメージのビルドを行わないように、CI テスト ワークフローを変更しました。 テスト ジョブも変更し、docker-compose を使用するのではなく、CircleCI の Docker Executor に備わっているサービス コンテナ サポートを使用して補助サービスを起動するようにしました。 さらに、メイン コンテナから tox を実行して、依存関係のインストールとテストの実行を行うようにすることで、イメージを保存してワークスペースから復元するのに要していた時間を削減しました。 これにより、Machine Executor の起動にかかる余分なコストも削減されました。

依存関係の変更

Dockerfile を使用するのではなく、CircleCI のプライマリ コンテナに依存関係をインストールすると、CircleCI のキャッシュ機能によって virtualenv の作成を高速化できる場合があります。

テスト実行の最適化

これでテストの準備時間が短縮されました。次は、実際のテストの実行を高速化することも可能です。 たとえば、テストの実行後にデータベースを保持する必要がない場合もあります。 テストに使用するデータベース イメージを、ディスクに保存しないメモリ内 Postgres イメージに置き換えることも、テストを高速化する 1 つの方法です。 別の方法として、テストを 1 つずつ実行するのではなく、並列に実行することも検討できます。

これらの変更によってワークフロー全体の時間がどれだけ短縮されたかは下図のとおりです。

最適化した後のテスト最適化プロセス

ここまで見てきたように、1 つの変更だけでワークフロー全体の時間を短縮したわけではありません。 たとえば、時間の大部分がテストの準備に費やされていたら、テストを並列実行してもそれほどメリットはなかったでしょう。 ローカルの環境ではなく CircleCI プラットフォームでテストを実行することの違いを認識し、テストの準備と実行にいくつかの変更を加えることでテストの実行時間を改善できます。

関連項目