Verteilte Builds

Bei einer großen Codebasis können die Abhängigkeitsketten sehr tief werden. Auch einfache Binärdateien können oft von Zehntausenden von Build-Zielen abhängen. In diesem Umfang ist es einfach nicht möglich, einen Build in einer angemessenen Zeit auf einer einzelnen Maschine abzuschließen: Ein Build-System kann die grundlegenden Physikgesetze nicht umgehen, die auf der Hardware einer Maschine gelten. Die einzige Möglichkeit hierfür ist ein Build-System, das verteilte Builds unterstützt, wobei die vom System auszuführenden Arbeitseinheiten auf eine beliebige und skalierbare Anzahl von Maschinen verteilt werden. Vorausgesetzt, wir haben die Systemarbeit in kleinere Einheiten aufgeteilt (mehr dazu später), können wir so viele Builds beliebiger Größe abschließen, wie wir zu zahlen bereit sind. Diese Skalierbarkeit ist das heilige Gral, in dem wir durch die Definition eines Artefakt-basierten Build-Systems gearbeitet haben.

Remote-Caching

Der einfachste Typ eines verteilten Builds ist nur das Remote-Caching, wie in Abbildung 1 dargestellt.

Verteilter Build mit Remote-Caching

Abbildung 1. Ein verteilter Build mit Remote-Caching

Jedes System, das Builds ausführt, einschließlich Entwickler-Workstations und Continuous Integration-Systemen, hat einen Verweis auf einen gemeinsamen Remote-Cache-Dienst. Dieser Dienst kann ein schnelles und lokales kurzfristiges Speichersystem wie Redis oder ein Cloud-Dienst wie Google Cloud Storage sein. Wenn ein Nutzer ein Artefakt erstellen muss, sei es direkt oder als Abhängigkeit, prüft das System zuerst beim Remote-Cache, ob dieses Artefakt bereits dort vorhanden ist. In diesem Fall kann das Artefakt heruntergeladen und nicht erstellt werden. Ist dies nicht der Fall, erstellt das System das Artefakt selbst und lädt das Ergebnis wieder in den Cache hoch. Das bedeutet, dass untergeordnete Abhängigkeiten, die sich nur selten ändern, einmal erstellt und von mehreren Nutzern verwendet werden können, anstatt von jedem einzelnen Nutzer neu erstellt werden zu müssen. Bei Google werden viele Artefakte aus einem Cache bereitgestellt, nicht von Grund auf neu erstellt. Dadurch werden die Kosten für die Ausführung unseres Build-Systems deutlich reduziert.

Damit ein Remote-Caching-System funktioniert, muss das Build-System garantieren, dass Builds vollständig reproduzierbar sind. Das bedeutet, dass für jedes Build-Ziel in der Lage sein muss, den Satz von Eingaben für dieses Ziel zu bestimmen, damit derselbe Satz von Eingaben genau die gleiche Ausgabe auf einer beliebigen Maschine liefert. Dies ist die einzige Möglichkeit, sicherzustellen, dass die Ergebnisse des Herunterladens eines Artefakts mit den Ergebnissen der Erstellung selbst übereinstimmen. Dabei muss jedes Artefakt im Cache sowohl für das Ziel als auch für einen Hash der Eingaben verschlüsselt werden. So können verschiedene Entwickler gleichzeitig verschiedene Änderungen am selben Ziel vornehmen. Der Remote-Cache würde alle resultierenden Artefakte speichern und sie ohne Konflikte entsprechend bereitstellen.

Natürlich muss das Herunterladen eines Artefakts schneller sein als das Erstellen eines Remote-Cache, um von Vorteil zu profitieren. Dies ist nicht immer der Fall, insbesondere wenn der Cache-Server von dem Computer, der den Build ausführt, weit entfernt ist. Das Netzwerk- und Build-System von Google ist sorgfältig optimiert, um Build-Ergebnisse schnell freigeben zu können.

Remote-Ausführung

Remote-Caching ist kein wirklich verteilter Build. Wenn der Cache verloren geht oder Sie eine einfache Änderung vornehmen, die alles neu erstellt werden muss, müssen Sie dennoch den gesamten Build lokal auf Ihrem Computer ausführen. Das eigentliche Ziel ist die Remote-Ausführung, bei der die eigentliche Arbeit des Builds auf eine beliebige Anzahl von Workern verteilt werden kann. Abbildung 2 zeigt ein Remote-Ausführungssystem.

Remote-Ausführungssystem

Abbildung 2. Ein Remote-Ausführungssystem

Das Build-Tool, das auf dem Computer jedes Nutzers ausgeführt wird (Nutzer sind entweder menschliche Ingenieure oder automatisierte Build-Systeme), sendet Anfragen an einen zentralen Build-Master. Der Build-Master unterteilt die Anfragen in ihre Komponentenaktionen und plant die Ausführung dieser Aktionen über einen skalierbaren Pool von Workern. Jeder Worker führt die angeforderten Aktionen mit den vom Nutzer angegebenen Eingaben aus und schreibt die resultierenden Artefakte aus. Diese Artefakte werden von den anderen Maschinen gemeinsam genutzt, auf denen Aktionen erforderlich sind, bis die endgültige Ausgabe erstellt und an den Nutzer gesendet wird.

Der schwierigste Teil bei der Implementierung eines solchen Systems ist die Verwaltung der Kommunikation zwischen den Workern, dem Master und dem lokalen Computer des Nutzers. Worker können von Zwischenartefakten abhängen, die von anderen Workern erstellt wurden. Die endgültige Ausgabe muss an den lokalen Computer des Nutzers zurückgesendet werden. Dazu können wir auf dem zuvor beschriebenen verteilten Cache aufbauen und jeden Worker seine Ergebnisse in den Cache schreiben und seine Abhängigkeiten aus dem Cache lesen lassen lassen. Der Master blockiert Worker, bis alles, was sie benötigen, abgeschlossen ist. In diesem Fall können sie ihre Eingaben aus dem Cache lesen. Das Endprodukt wird ebenfalls im Cache gespeichert, sodass es vom lokalen Computer heruntergeladen werden kann. Wir benötigen auch eine separate Methode, um die lokalen Änderungen in der Quellstruktur des Nutzers zu exportieren, damit Worker diese Änderungen vor dem Erstellen anwenden können.

Damit dies funktioniert, müssen alle zuvor beschriebenen Teile der Artefakt-basierten Build-Systeme zusammengeführt werden. Build-Umgebungen müssen vollständig selbstbeschreibend sein, damit wir Worker ohne menschliches Eingreifen starten können. Build-Prozesse selbst müssen vollständig eigenständig sein, da jeder Schritt möglicherweise auf einer anderen Maschine ausgeführt wird. Ausgaben müssen vollständig deterministisch sein, damit jeder Worker den Ergebnissen vertrauen kann, die er von anderen Workern erhält. Solche Garantien sind für ein aufgabenbasiertes System extrem schwierig zu implementieren, weshalb es unmöglich ist, auf einem zuverlässigen Remote-Ausführungssystem auf einem System aufzubauen.

Verteilte Builds bei Google

Seit 2008 nutzt Google ein verteiltes Build-System, das sowohl Remote-Caching als auch Remote-Ausführung verwendet, wie in Abbildung 3 dargestellt.

Gesamtes Build-System

Abbildung 3. Verteiltes Build-System von Google

Der Remote-Cache von Google heißt ObjFS. Es besteht aus einem Back-End, das Build-Ausgaben in Bigtables speichert, verteilt auf unsere gesamten Produktionsmaschinen, und einen Front-End-FUSE-Daemon mit dem Namen "objfsd", der auf jedem Entwicklercomputer ausgeführt wird. Mit dem FUSE-Daemon können Entwickler Build-Ausgaben so durchsuchen, als wären sie normale Dateien, die auf der Workstation gespeichert sind. Der Dateiinhalt wird jedoch nur bei wenigen Dateien, die direkt vom Nutzer angefordert werden, nach Bedarf heruntergeladen. aus. Die Bereitstellung von Dateiinhalten bei Bedarf reduziert sowohl die Netzwerk- als auch die Laufwerknutzung erheblich und das System kann im Vergleich zum Zeitpunkt, an dem wir alle Build-Ausgabe auf dem lokalen Laufwerk des Entwicklers gespeichert haben, doppelt so schnell erstellt werden.

Das Remote-Ausführungssystem von Google heißt Forge. Ein Forge-Client in Blaze (Bazels internes Äquivalent) namens triDistributor“ sendet Anfragen für jede Aktion an einen Job, der in unseren Rechenzentren ausgeführt wird und als Planer bezeichnet wird. Der Planer verwaltet einen Cache mit Aktionsergebnissen. Er kann sofort eine Antwort zurückgeben, wenn die Aktion bereits von einem anderen Nutzer des Systems erstellt wurde. Falls nicht, wird die Aktion in eine Warteschlange gestellt. Ein großer Pool von Executor-Jobs liest fortlaufend Aktionen aus dieser Warteschlange, führt sie aus und speichert die Ergebnisse direkt in den ObjFS-Bigtables. Diese Ergebnisse stehen den Executors für zukünftige Aktionen zur Verfügung oder können vom Endnutzer über objfsd heruntergeladen werden.

Das Endergebnis ist ein System, das so skaliert, dass alle bei Google ausgeführten Builds effizient unterstützt werden. Und die Skalierung von Google-Builds ist riesig: Google führt täglich Millionen von Builds aus, die Millionen von Testläufen ausführen und Petabyte an Build-Ausgaben aus Milliarden von Quellcodes erzeugen. Mit diesem System können unsere Entwickler nicht nur schnell komplexe Codebasen erstellen, sondern auch eine große Anzahl automatisierter Tools und Systeme implementieren, die auf unserem Build beruhen.