ルール記述の課題

問題を報告 ソースを表示 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

このページでは、具体的な問題と課題の概要を説明します。 効率的な Bazel ルールを作成できます。

要件の概要

  • 前提条件: 正確性、スループット、使いやすさ、レイテンシを重視
  • 前提: 大規模なリポジトリ
  • 前提条件: BUILD に似た記述言語
  • 過去: 読み込み、分析、実行の間のハードな分離 古いが、引き続き API に影響する
  • 本質的: リモート実行とキャッシュ保存は難しい
  • 本質的: 正確で高速な増分ビルドに変更情報を使用するには、通常とは異なるコーディング パターンが必要
  • 本質的: 二次時間とメモリ消費の回避は困難

前提条件

ビルドシステムには、次のような前提条件があります。 正確性、使いやすさ、スループット、大規模リポジトリの特徴です。以降のセクションでは、これらの前提条件について説明します。また、ルールを効果的に記述するためのガイドラインも示します。

正確性、スループット、使いやすさ、遅延

ビルドシステムは、何よりもまず パフォーマンスが向上します特定のソースツリーについて、 出力ツリーの外観に関係なく、同じビルドは常に同じである必要があります。 できます。最初の近似では、Bazel はすべてのノードを認識する必要がある 入力されたビルドステップがある場合にそのステップを再実行できるようにする 変化します。Bazel の正確さには限界があります。ビルドの日時などの情報が漏洩し、ファイル属性の変更などの特定のタイプの変更が無視されるためです。サンドボックス化 宣言されていない入力ファイルの読み取りを防ぐことで、正確性を確保できます。その他 システムの本質的な制限に加えて 正確性の問題が報告されています そのほとんどは Fileset ルールまたは C++ ルールに関連しており、 学習しますYouTube では、この問題の修正に長期的な取り組みを続けています。

ビルドシステムの 2 つ目の目標は、高スループットを実現することです。私たちは 現状で何ができるかの限界を恒久的に押し広げる リモート実行サービス用のマシン割り当ても行います。リモート実行が サービスが過負荷になり、誰も仕事を完了できなくなります。

次は使いやすさです。リモート実行サービスのフットプリントが同じ(または類似)の複数の正しいアプローチから、使いやすいアプローチを選択します。

レイテンシは、ビルドの開始から目的の結果が得られるまでの時間を表します。これは、成功または失敗したテストのテストログ、または BUILD ファイルに誤字脱字があるというエラー メッセージのいずれかです。

多くの場合、これらの目標は重複します。レイテンシはスループットの関数である リモート実行サービスの正確さに左右されます。

大規模なリポジトリ

ビルドシステムは、大規模なリポジトリの規模で動作する必要があります。大規模とは、単一のハードドライブに収まらないことを意味します。そのため、ほとんどすべてのデベロッパー マシンで完全なチェックアウトを行うことはできません。中規模ビルド 数万個の BUILD ファイルを読み取って解析し、 何十万もの glob があるとします。理論的には、1 台のマシンですべての BUILD ファイルを読み取ることは可能ですが、妥当な時間とメモリ内で読み取ることはまだできません。そのため、BUILD ファイルには必ず 個別に読み込んで解析できます

BUILD のような記述言語

このコンテキストでは、ライブラリとバイナリ ルールとそれらの相互依存関係の宣言で BUILD ファイルにほぼ類似した構成言語を前提としています。BUILD ファイルを個別に読み取り、解析できます。 可能な限りソースファイルを見ることも 避けています あります)。

歴史にゆかりがある場所

問題を引き起こす Bazel バージョン間の違いや、 その概要を次のセクションで説明します。

読み込み、分析、実行のハードな分離は時代遅れだが API に影響する

技術的には、アクションがリモート実行に送信される直前に、アクションの入力ファイルと出力ファイルをルールが把握していれば十分です。ただし、元の Bazel コードベースでは、パッケージの読み込み、構成(基本的にはコマンドライン フラグ)を使用したルールの分析、アクションの実行が厳密に分離されていました。この区別は、現在も Rules API に組み込まれています。 現在は、Bazel のコアでは不要になりました(詳細は下記を参照)。

つまり、Rules API ではルールの宣言型の記述が必要です。 インターフェース(どの属性、属性のタイプ)を表現することもできます。ただし、API で読み込みフェーズ中にカスタムコードを実行して、出力ファイルの暗黙的な名前と属性の暗黙的な値を計算できる例外もあります。対象 たとえば、「foo」という名前の java_library ルールが出力が暗黙的に生成され、 「libfoo.jar」。ビルドグラフ内の他のルールから参照できます。

また、ルールの分析では、ソースファイルの読み込みや アクションの出力その代わりに、部分有向二部生成関数を ビルドステップと、ルールによってのみ決定される出力ファイル名のグラフ 依存関係が存在します。

本質的

ルールの作成を困難にする固有のプロパティがいくつかあります。以降のセクションでは、最も一般的なプロパティについて説明します。

リモート実行とキャッシュ保存は難しい

リモート実行とキャッシュ保存は、大規模なリポジトリでのビルド時間を短縮します。 単一の Compute Engine で実行する場合と比べて、約 2 桁もの あります。ただし、実行する必要があるスケールは驚異的です。Google のリモート実行サービスは、1 秒あたり大量のリクエストを処理するように設計されており、プロトコルは不要なラウンドトリップやサービス側の不要な作業を慎重に回避します。

現時点では、このプロトコルでは、ビルドシステムが特定のアクションへのすべての入力を事前に把握している必要があります。ビルドシステムは、一意のアクション フィンガープリントを計算し、スケジューラにキャッシュヒットを要求します。キャッシュヒットが見つかると スケジューラは出力ファイルのダイジェストを返します。ファイル自体は 後ほど説明します。ただし、これにより Bazel の制約が すべての入力ファイルを事前に宣言する必要があります。

変更情報を使用して正確かつ高速な増分ビルドを行うには、通常とは異なるコーディング パターンが必要

上記では、Bazel が正確であるためには、すべての入力を Bazel が知っている必要があると主張しました。 そのビルドステップにそのビルドステップが実行されたかどうかを 維持します。パッケージの読み込みとルール分析についても同様です。 Skyframe でこれに対応するように設計されている 見ていきましょう。Skyframe は、目標ノード(「これらのオプションで //foo をビルド」など)を受け取り、それを構成要素に分解し、評価して組み合わせて結果を生成します。これはグラフ ライブラリと評価フレームワークです。このプロセスの一環として、Skyframe はパッケージを読み取り、ルールを分析し、 アクションを実行します。

Skyframe は各ノードで、特定のノードがコンピューティングに使用したノードを正確に追跡します。 目標ノードから入力ファイル( Skyframe ノードでもあります)。このグラフをメモリ内に明示的に表現することで、ビルドシステムは入力ファイルに対する特定の変更(入力ファイルの作成や削除を含む)によって影響を受けるノードを正確に特定し、出力ツリーを目的の状態に復元するために最小限の作業を行います。

このプロセスの一環として、各ノードは依存関係の検出プロセスを実行します。各 ノードは依存関係を宣言してから、それらの依存関係のコンテンツを使用して さらに依存関係を宣言します。原則的には ノードあたりのスレッド数です。しかし中規模のビルドには 現行の Java では簡単に実現できない数千の Skyframe ノード (歴史的理由から、現時点では Java の使用に縛られているため、 軽量のスレッドや継続は使用しません)。

Bazel は固定サイズのスレッドプールを使用します。ただし、つまりノードに まだ利用できない依存関係を宣言している場合、その依存関係を 依存関係の問題が解決されたときに(別のスレッドなどで)再起動する必要が できます。つまり、ノードはこれを過度に行わない必要があります。N 個の依存関係をシリアルで宣言するノードは、N 回再起動される可能性があり、O(N^2) の時間がかかります。代わりに、事前の一括申告を目指します。 コードの再編成や、コードの分割や依存関係の 1 つのノードを複数のノードに分割して再起動の回数を制限します。

なお、このテクノロジーは現在、rules API では使用できません。rules API は、読み込みフェーズ、分析フェーズ、実行フェーズの従来の概念を使用して定義されています。ただし、基本的な制限として、他のノードへのすべてのアクセスはフレームワークを経由して、対応する依存関係を追跡できるようにする必要があります。ビルドシステムの実装言語やルールの記述言語に関係なく(同じである必要はありません)、ルール作成者は Skyframe をバイパスする標準ライブラリやパターンを使用しないでください。Java の場合、java.io.File やあらゆる形式のリフレクション、およびどちらかを行うライブラリは使用しないことを意味します。依存関係をサポートするライブラリ そのような低レベル インターフェースの挿入も、例外なく正しく スカイフレーム。

ルール作成者を完全な言語ランタイムに公開しないようにすることを強く推奨します。 あります。このような API が誤って使用される危険性は非常に大きいです。過去に発生した Bazel のバグのいくつかは、Bazel チームや他のドメイン エキスパートによって記述されたルールであっても、安全でない API を使用したルールによって発生しています。

二次的な時間とメモリ使用量の回避が困難

さらに悪いことに、Skyframe によって課される要件とは別に、 従来の Java の制約、Rules API の時代遅れなど、 2 次的な時間またはメモリ消費量を誤って導入してしまうのは、 ビルドシステムで問題に注意する必要があります。2 つのモデル 2 次的なメモリ消費を引き起こすパターン(つまり、 二次時間消費)で表します。

  1. ライブラリ ルールの連鎖 - ライブラリ ルール A が B に依存し、C に依存し、 できます。次に、この推移的クロージングに対してなんらかのプロパティを計算します。 たとえば、Java ランタイム クラスパスや、Java 言語の C++ リンカー コマンドなど、 表示されます。単純に言えば、標準的なリストの実装を採用するかもしれません。ただし、 これにより、すでに二次的メモリ消費量が導入されています。 クラスパスにエントリが 1 つ、2 つ目のエントリが 2 つ、3 つ目のエントリが 3 つというように、 つまり、合計で 1+2+3+...+N = O(N^2) 個のエントリになります。

  2. 同じライブラリ規則に依存するバイナリルール - 同じライブラリに依存する一連のバイナリが (たとえば、同じテストを行う多数のテストルールがある場合や、 使用できます。N 個のルールのうち、半分のルールがバイナリルールで、残りの半分がライブラリルールであるとします。今度は、各バイナリがファイルのコピーを作成し、 ライブラリ ルールの推移的クロージャに対して計算されたプロパティ。 C++ リンカー コマンドラインで指定することもできます。たとえば、C++ リンク アクションのコマンドライン文字列表現を展開できます。該当なし N/2 要素のコピーは O(N^2)メモリです。

二次計算を回避するカスタム コレクション クラス

Bazel はこれらのシナリオの両方に大きく影響を受けるため、各ステップでのコピーを回避することでメモリ内の情報を効果的に圧縮する一連のカスタム コレクション クラスを導入しました。データ構造はほとんどすべて 意味があるので、これを depset (内部実装では NestedSet とも呼ばれます)。過去数年間に Bazel のメモリ消費量を削減するために行われた変更のほとんどは、以前に使用されていたものではなく depset を使用するように変更したものです。

残念ながら、depset を使用してもすべての問題が自動的に解決されるわけではありません。特に、各ルールで depset を反復処理するだけで、時間の消費が二次関数的に増加します。内部的には、NestedSets には、通常のコレクション クラスとの相互運用性を容易にするためのヘルパー メソッドもいくつかあります。残念ながら、これらのメソッドのいずれかに NestedSet を誤って渡すと、コピー動作が発生し、メモリ消費が再び二次関数的に増加します。