Persistente Worker

Auf dieser Seite wird erläutert, wie Sie nichtflüchtige Worker verwenden, welche Vorteile sie bieten und welche Anforderungen sie haben und wie sie in einer Sandbox-Umgebung ausgeführt werden.

Ein nichtflüchtiger Worker ist ein lang andauernder Prozess, der vom Istio-Server gestartet wird. Er arbeitet als Wrapper um das eigentliche Tool (normalerweise ein Compiler) oder ist das Tool selbst. Damit du von nichtflüchtigen Workern profitieren kannst, muss das Tool eine Sequenz von Zusammenstellungen unterstützen. Außerdem muss der Wrapper zwischen der API des Tools und dem unten beschriebenen Anfrage-/Antwortformat übersetzen. Dasselbe Worker kann mit und ohne das Flag --persistent_worker im selben Build aufgerufen werden. Es ist dafür verantwortlich, das Tool entsprechend zu starten und mit dem Tool zu sprechen sowie die Worker beim Beenden herunterzufahren. Jeder Worker-Instanz wird ein separates Arbeitsverzeichnis unter <outputBase>/bazel-workers zugewiesen, ist aber nicht gerootet.

Die Verwendung von persistenten Workern ist eine Ausführungsstrategie, bei der der Startaufwand reduziert wird, die mehr JIT-Kompilierung ermöglicht und das Abspeichern der abstrakten Syntaxbäume im Rahmen der Aktion ermöglicht. Diese Strategie wird umgesetzt, indem mehrere Anfragen an einen lang andauernden Prozess gesendet werden.

Persistente Worker werden für verschiedene Sprachen implementiert, darunter Java, Scala, Kotlin und weitere.

Programme, die eine NodeJS-Laufzeit verwenden, können die Worker-Bibliothek @bazel/worker zur Implementierung des Worker-Protokolls verwenden.

Persistente Worker verwenden

Bazel 0.27 und höher verwendet beim Ausführen von Builds standardmäßig persistente Worker, obwohl die Remote-Ausführung Vorrang hat. Für Aktionen, die persistente Worker nicht unterstützen, startet Baizel die Tool-Instanz für jede Aktion neu. Sie können Ihren Build explizit so einstellen, dass er dauerhafte Worker verwendet. Dazu legen Sie die Strategie worker für die entsprechenden Tool-Makros fest. In diesem Beispiel ist es sinnvoll, local als Fallback auf die Strategie worker anzugeben:

bazel build //my:target --strategy=Javac=worker,local

Die Verwendung der Worker-Strategie anstelle der lokalen Strategie kann je nach Implementierung die Kompilierungsgeschwindigkeit erheblich erhöhen. Bei Java können Builds 2- bis 4-mal schneller, manchmal mehr für die inkrementelle Kompilierung ausgeführt werden. Das Kompilieren von Istio ist etwa 2,5-mal schneller mit Workern. Weitere Informationen finden Sie im Abschnitt Anzahl der Worker auswählen.

Wenn Sie auch eine Remote-Build-Umgebung haben, die Ihrer lokalen Build-Umgebung entspricht, können Sie die experimentelle dynamische Strategie verwenden, die auf einer Remote-Ausführung und einer Worker-Ausführung basiert. Übergeben Sie das Flag --experimental_spawn_scheduler, um die dynamische Strategie zu aktivieren. Bei dieser Strategie werden Worker automatisch aktiviert. Sie müssen die Strategie worker also nicht angeben, Sie können aber weiterhin local oder sandboxed als Fallbacks verwenden.

Anzahl der Worker auswählen

Die Standardanzahl von Worker-Instanzen pro Memory ist 4, kann aber mit dem Flag worker_max_instances angepasst werden. Es gibt auch Kompromisse zwischen der Nutzung der verfügbaren CPUs und der Menge der JIT-Kompilierung und Cache-Treffer. Bei mehr Workern zahlen mehr Ziele für die Ausführung von Nicht-JITted-Code und das Erreichen von Kalt-Caches. Wenn du nur wenige Ziele für den Aufbau hast, kann ein einzelner Worker den besten Kompromiss zwischen Kompilierungsgeschwindigkeit und Ressourcennutzung bieten (z. B. Problem 8586). Das Flag worker_max_instances legt die maximale Anzahl von Worker-Instanzen pro Memory und Flag-Set (siehe unten) fest. In einem gemischten System kann es also zu einem großen Speicherplatzbedarf kommen, wenn Sie den Standardwert behalten. Die Vorteile mehrerer Worker-Instanzen in inkrementellen Builds sind sogar noch geringer.

Dieses Diagramm zeigt die Zusammenstellungszeiten für Istio (Ziel //src:bazel) auf einer 6-Kerne-Hyperthread-Workstation von Intel Xeon (3,5 GHz) mit 64 GB RAM. Für jede Worker-Konfiguration werden fünf Bereinigungsläufe ausgeführt, wobei der Durchschnitt der letzten vier Modelle abgerufen wird.

Grafik zur Leistungssteigerung bei sauberen Builds

Abbildung 1. Grafik der Leistungsverbesserungen bei sauberen Builds.

Bei dieser Konfiguration erhalten zwei Worker den schnellsten Kompilierungsprozess, allerdings nur eine Verbesserung von 14 % im Vergleich zu einem Worker. Ein Worker ist eine gute Option, wenn Sie weniger Arbeitsspeicher verwenden möchten.

Die zusätzliche Zusammenstellung bietet in der Regel noch mehr Vorteile. Saubere Builds sind relativ selten, aber Änderungen an einzelnen Compilern sind keine Seltenheit, vor allem bei testbasierter Entwicklung. Im Beispiel oben sind auch einige Verpackungsaktionen ohne Java enthalten, die die inkrementelle Kompilierungszeit überlagern können.

Nur durch die Kompilierung der Java-Quellen (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar) nach der Änderung einer internen Stringkonstante in SummaryContainerizingSandboxedSpawn.java wird das 3-Fache beschleunigt (durchschnittlich 20 Builds, nachdem ein Aufwärmaufbau verworfen wurde).

Grafik zur Leistungssteigerung von inkrementellen Builds

Abbildung 2. Grafik zur Leistungssteigerung von inkrementellen Builds

Die Beschleunigung hängt davon ab, welche Änderung vorgenommen wurde. Eine Beschleunigung von Faktor 6 wird in der oben genannten Situation gemessen, wenn häufig verwendete Konstanten geändert werden.

Persistente Worker ändern

Sie können das Flag --worker_extra_flag übergeben, um Worker-Start-Flags, angegeben durch mememonic, anzugeben. Durch die Übergabe von --worker_extra_flag=javac=--debug wird beispielsweise die Fehlerbehebung nur für Javac aktiviert. Pro Flag können Sie nur ein Worker-Flag festlegen und nur für ein Gedächtnis. Worker werden nicht nur für jede Gedächtniseinheit erstellt, sondern auch für Varianten in ihren Start-Flags. Jede Kombination aus Makro- und Start-Flags wird in einem WorkerKey kombiniert. Für jeden WorkerKey bis zu worker_max_instances Workern können erstellt werden. Im nächsten Abschnitt erfahren Sie, wie Sie mit der Aktionskonfiguration auch Flag für die Einrichtung angeben können.

Mit dem Flag --high_priority_workers können Sie ein Speicherelement festlegen, das vorrangig als DNA mit normaler Priorität ausgeführt werden soll. So können Aktionen, die immer im kritischen Pfad sind, priorisiert werden. Wenn zwei oder mehr Worker mit hoher Priorität Anfragen ausführen, wird die Ausführung aller anderen Worker verhindert. Dieses Flag kann mehrmals verwendet werden.

Wird das Flag --worker_sandboxing übergeben, wird für jede Worker-Anfrage für alle Eingaben ein separates Sandbox-Verzeichnis verwendet. Das Einrichten der Sandbox dauert etwas länger, insbesondere bei macOS, bietet jedoch eine bessere Genauigkeit.

Das Flag --worker_quit_after_build eignet sich hauptsächlich für das Debugging und die Profilerstellung. Dieses Flag erzwingt das Beenden aller Worker nach Abschluss eines Builds. Sie können auch --worker_verbose übergeben, um eine bessere Ausgabe zu erhalten. Dieses Flag wird im Feld verbosity in WorkRequest berücksichtigt. Damit sind Worker-Implementierungen auch ausführlicher.

Worker speichern ihre Logs im Verzeichnis <outputBase>/bazel-workers, z. B. /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log. Der Dateiname enthält die Worker-ID und die Übung. Da es mehr als ein WorkerKey pro Gedächtnispunkt geben kann, werden eventuell mehr als worker_max_instances Logdateien für ein bestimmtes Gedächtnis angezeigt.

Weitere Informationen zu Android-Builds findest du auf der Seite Android Build Performance.

Persistente Worker implementieren

Informationen zum Erstellen von Workern finden Sie auf der Seite Persistente Worker erstellen.

Dieses Beispiel zeigt eine Starlark-Konfiguration für einen Worker, der JSON verwendet:

args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
ctx.actions.write(
    output = args_file,
    content = "\n".join(["-g", "-source", "1.5"] + ctx.files.srcs),
)
ctx.actions.run(
    mnemonic = "SomeCompiler",
    executable = "bin/some_compiler_wrapper",
    inputs = inputs,
    outputs = outputs,
    arguments = [ "-max_mem=4G",  "@%s" % args_file.path],
    execution_requirements = {
        "supports-workers" : "1", "requires-worker-protocol" : "json" }
)

Bei dieser Definition würde die erste Verwendung dieser Aktion mit der Ausführung der Befehlszeile /bin/some_compiler -max_mem=4G --persistent_worker beginnen. Eine Anfrage zum Kompilieren von Foo.java sieht dann so aus:

arguments: [ "-g", "-source", "1.5", "Foo.java" ]
inputs: [
  {path: "symlinkfarm/input1" digest: "d49a..." },
  {path: "symlinkfarm/input2", digest: "093d..."},
]

Der Worker empfängt diese unter stdin im durch Zeilenumbruch getrennten JSON-Format (da requires-worker-protocol auf JSON gesetzt ist). Der Worker führt dann die Aktion aus und sendet an JSON einen im JSON-Format formatierten WorkResponse an stoutout. Diese Analyse wird dann von geparst und manuell in ein WorkResponse-Proto umgewandelt. Wenn Sie mit dem verknüpften Worker kommunizieren möchten, der über Binärcode und nicht über JSON codiert ist, würde requires-worker-protocol auf proto gesetzt werden:

  execution_requirements = {
    "supports-workers" : "1" ,
    "requires-worker-protocol" : "proto"
  }

Wenn requires-worker-protocol nicht in die Ausführungsanforderungen aufgenommen wird, wird die Kommunikation mit der Worker standardmäßig von Maschinentyp verwendet.

Istio leitet das WorkerKey-Element aus den Gedächtnis- und geteilten Flags ab. Wenn diese Konfiguration also das Ändern des max_mem-Parameters zulässt, wird für jeden verwendeten Wert ein separater Worker erstellt. Das kann zu übermäßigem Arbeitsspeicherverbrauch führen, wenn zu viele Varianten verwendet werden.

Jeder Worker kann derzeit nur jeweils eine Anfrage verarbeiten. Die experimentelle Multiplex-Worker-Funktion ermöglicht die Verwendung mehrerer Threads, wenn das zugrunde liegende Tool Multithread-Tool ist und der Wrapper so eingerichtet ist, dass dies interpretiert werden kann.

In diesem GitHub-Repository sehen Sie Beispiel-Worker-Wrapper, die in Java und in Python geschrieben wurden. Wenn Sie mit JavaScript oder TypeScript arbeiten, können das @bazel/worker-Paket und das CIDR-Worker-Beispiel hilfreich sein.

Wie wirken sich Worker auf das Sandboxing aus?

Bei der standardmäßigen Verwendung der Strategie worker wird die Aktion nicht in einer Sandbox ausgeführt, ähnlich wie bei der Strategie local. Mit dem Flag --worker_sandboxing kannst du alle Worker in Sandboxes ausführen, sodass bei jeder Ausführung des Tools nur die erforderlichen Dateien angezeigt werden. Allerdings können Daten zwischen den Anfragen weiterhin intern gesendet werden, z. B. über einen Cache. Bei Verwendung der Strategie dynamic müssen Worker in einer Sandbox ausgeführt werden.

Um die richtige Verwendung von Compiler-Caches mit Workern zu ermöglichen, wird zusammen mit jeder Eingabedatei ein Digest übergeben. Daher kann der Compiler oder der Wrapper prüfen, ob die Eingabe immer noch gültig ist, ohne die Datei lesen zu müssen.

Selbst bei der Verwendung der Eingabe-Digests werden Sandbox-Worker beim Schutz vor unerwünschtem Caching weniger streng als bei einer Sandbox, da das Tool möglicherweise einen anderen internen Zustand beibehält, der von früheren Anfragen betroffen war.

Multiplex-Worker können nur in einer Sandbox arbeiten, wenn sie von der Worker-Implementierung unterstützt werden. In der Sandboxing-Funktion muss das Flag --experimental_worker_multiplex_sandboxing separat aktiviert sein. Weitere Informationen finden Sie im Designdokument.

Weitere Informationen

Weitere Informationen zu nichtflüchtigen Workern finden Sie unter: