Abhängigkeitsmanagement

Wenn man sich die vorherigen Seiten ansieht, wird ein Thema immer wieder wiederholt: Die Verwaltung Ihres eigenen Codes ist relativ einfach, aber die Verwaltung ihrer Abhängigkeiten ist schwieriger. Es gibt alle Arten von Abhängigkeiten: Manchmal gibt es eine Abhängigkeit von einer Aufgabe (z. B. “Push die Dokumentation, bevor ich einen Release als abgeschlossen markiere“). Manchmal gibt es auch eine Abhängigkeit von einem Artefakt (z. B. II Sie benötigen die neueste Version der maschinellen Vision-Bibliothek, um meinen Code zu erstellen." Manchmal haben Sie interne Abhängigkeiten auf einem anderen Teil Ihrer Codebasis und manchmal gibt es externe Abhängigkeiten für Code oder Daten. Gehört einem anderen Team (entweder in Ihrer Organisation oder einem Drittanbieter). In jedem Fall ist das Konzept "Ich möchte, dass ich das habe," in der Entwicklung von Build-Systemen wiederholt und das Verwalten von Abhängigkeiten ist vermutlich der wichtigste Job eines Build-System.

Umgang mit Modulen und Abhängigkeiten

Projekte, die artefakte Build-Systeme wie Bazel verwenden, sind in mehrere Module unterteilt, wobei Module die Abhängigkeiten voneinander über BUILD-Dateien ausdrücken. Eine ordnungsgemäße Organisation dieser Module und Abhängigkeiten kann sich sowohl auf die Leistung des Build-Systems als auch auf den hohen Verwaltungsaufwand auswirken.

Verwendung detaillierter Module und die 1:1:1-Regel

Die erste Frage, die Sie beim Strukturieren eines Artefaktes erstellen müssen, ist die Entscheidung, wie viele Funktionen ein einzelnes Modul umfassen soll. In Bazel wird ein Modul durch ein Ziel dargestellt, das eine erstellbare Einheit wie java_library oder go_binary angibt. In einem extremen Fall könnte das gesamte Projekt in einem einzigen Modul enthalten sein. Platzieren Sie dazu eine BUILD-Datei im Stammverzeichnis und alle Quelldateien dieses Projekts rekursiv. Andererseits kann fast jede Quelldatei in ein eigenes Modul umgewandelt werden. Jede Datei muss dann praktisch immer in einer BUILD-Datei aufgelistet werden.

Die meisten Projekte liegen irgendwo zwischen diesen Extremen. Bei der Auswahl muss ein Kompromiss zwischen Leistung und Verwaltbarkeit gefunden werden. Wenn Sie ein einzelnes Modul für das gesamte Projekt verwenden, müssen Sie die Datei BUILD nur berühren, wenn Sie eine externe Abhängigkeit hinzufügen. Es bedeutet jedoch, dass das Build-System immer das gesamte Projekt erstellen muss. auf einmal planen. Das heißt, es kann keine Teile des Builds parallelisieren oder verteilen und Komponenten, die bereits erstellt wurden, nicht im Cache speichern. Gegenüber der Datei ist das Gegenteil möglich: Das Build-System bietet die maximale Flexibilität für das Caching und die Planung von Build-Schritten. Entwickler benötigen jedoch mehr Aufwand, um Listen mit Abhängigkeiten zu verwalten. welche Dateien auf welche verweisen.

Obwohl die genaue Granularität je nach Sprache (und oft sogar innerhalb der Sprache) variiert, tendiert Google dazu, deutlich kleinere Module zu bevorzugen, als man normalerweise in einem aufgabenbasierten Build-System schreibt. Eine typische Produktionsbinärzahl bei Google hängt oft von Zehntausenden Zielen ab. Sogar ein Team mittlerer Größe kann mehrere hundert Ziele innerhalb seiner Codebasis haben. Bei Sprachen wie Java, die ein überzeugendes integriertes Konzept für die Verpackung haben, enthält jedes Verzeichnis normalerweise ein einzelnes Paket, ein Ziel und eine BUILD-Datei (Hosen, einem anderen Build-System auf Basis von Bazel, heißt dies die 1:1:1-Regel. Sprachen mit schwächeren Paketkonventionen definieren häufig mehrere Ziele pro BUILD-Datei.

Die Vorteile von kleineren Build-Zielen werden tatsächlich in großem Maßstab angezeigt, da sie zu schneller verteilten Builds führen und das seltenere Erstellen von Zielen erforderlich ist. Die Vorteile werden noch überzeugender, wenn der Test einmal in den Anwendungsbereich geht, da präzisere Ziele dazu führen können, dass das Build-System viel intelligenter sein kann, wenn nur eine begrenzte Teilmenge von Tests ausgeführt wird, die von einem bestimmten Test betroffen sein könnten. ändern. Da Google an die systemischen Vorteile kleinerer Ziele glaubt, haben wir einige Nachteile gemindert, indem wir in Tools zur automatischen Verwaltung von BUILD-Dateien investiert haben, um Entwickler zu vermeiden.

Einige dieser Tools, z. B. buildifier und buildozer, sind mit Bazel im Verzeichnis buildtools verfügbar.

Sichtbarkeit des Moduls minimieren

Bazel und andere Build-Systeme ermöglichen es jedem Ziel, eine Sichtbarkeit festzulegen: ein Attribut, das angibt, welche anderen Ziele davon abhängen können. Ziele können öffentlich sein. In diesem Fall kann jedes beliebige Ziel im Arbeitsbereich darauf verweisen. Privat; in diesem Fall kann nur in derselben BUILD-Datei auf sie verwiesen werden oder sind nur für eine explizit definierte Liste anderer Ziele sichtbar. Eine Sichtbarkeit ist im Wesentlichen das Gegenteil einer Abhängigkeit: Wenn Ziel A von Ziel B abhängen soll, muss Ziel B sich für Ziel A sichtbar machen. Wie bei den meisten Programmiersprachen ist es am besten, die Sichtbarkeit so weit wie möglich zu minimieren. Im Allgemeinen machen Teams bei Google Ziele nur dann öffentlich zugänglich, wenn diese Ziele häufig verwendete Bibliotheken darstellen, die allen Teams bei Google zur Verfügung stehen. Teams, die andere Personen mit ihnen koordinieren müssen, bevor sie ihren Code verwenden, führen eine Zulassungsliste mit Kundenzielen als Sichtbarkeit ihres Ziels. Die internen Implementierungsziele jedes Teams sind auf Verzeichnisse beschränkt, die dem Team gehören, und die meisten BUILD-Dateien haben nur ein Ziel, das nicht privat ist.

Abhängigkeiten verwalten

Module müssen aufeinander verweisen können. Der Nachteil einer Codebasis in detaillierte Module besteht darin, dass Sie die Abhängigkeiten zwischen diesen Modulen verwalten müssen (Tools können Ihnen bei der Automatisierung helfen). Wenn Sie diese Abhängigkeiten angeben, entspricht das normalerweise dem Großteil der Inhalte in einer BUILD-Datei.

Interne Abhängigkeiten

In einem großen Projekt, das in detaillierte Module unterteilt ist, sind die meisten Abhängigkeiten wahrscheinlich intern. Das heißt, für ein anderes Ziel wurde ein Ziel definiert und im selben Quell-Repository erstellt. Interne Abhängigkeiten unterscheiden sich von externen Abhängigkeiten dadurch, dass sie aus der Quelle erstellt und nicht als vordefiniertes Artefakt heruntergeladen werden, während der Build ausgeführt wird. Das bedeutet auch, dass internalVersion“ nicht für interne Abhängigkeiten gilt: Ein Ziel und alle internen Abhängigkeiten werden immer vom gleichen Commit/Revision im Repository erstellt. Ein Problem, das im Hinblick auf interne Abhängigkeiten sorgfältig behandelt werden sollte, ist die Handhabung von transitiven Abhängigkeiten (Abbildung 1). Angenommen, Ziel hängt von Ziel B ab, das von einem gemeinsamen Bibliotheksziel C abhängt. Sollte Ziel A die in Ziel C definierten Klassen verwenden können?

Transitive Abhängigkeiten

Abbildung 1. Transitive Abhängigkeiten

In Bezug auf die zugrunde liegenden Tools gibt es dabei kein Problem. Sowohl B als auch C werden bei der Erstellung mit Ziel A verknüpft, sodass alle in C definierten Symbole A bekannt sind. Bazel ist seit vielen Jahren im Einsatz, aber als Google größer wurde, begannen wir auch mit Problemen. Angenommen, B wurde so refaktoriert, dass er nicht mehr von C abhängig sein muss. Wenn die Abhängigkeit von B dann von C entfernt wird, würden A und alle anderen Ziele, die C über eine Abhängigkeit von B verwendet haben, nicht mehr funktionieren. Die Abhängigkeiten eines Ziels wurden effektiv Teil des öffentlichen Vertrags und können nie sicher geändert werden. Das bedeutet, dass sich mit der Zeit immer mehr Abhängigkeiten angesammelt und Builds bei Google langsam langsamer geworden sind.

Google löste dieses Problem schließlich durch die Einführung eines "striktiven transitiven Abhängigkeitsmodus" in Bazel. In diesem Modus erkennt Bazel, ob ein Ziel versucht, direkt auf ein Symbol zu verweisen, und schlägt in diesem Fall mit einem Fehler und einem Shell-Befehl fehl, mit dem die Abhängigkeit automatisch eingefügt wird aus. Die Einführung dieser Änderung in der gesamten Codebasis von Google und die Refaktorierung jedes unserer Millionen von Build-Zielen, um ihre Abhängigkeiten explizit aufzulisten, war eine mehrjährige Maßnahme, aber es hat sich gelohnt. Unsere Builds sind jetzt viel schneller, da Ziele weniger unnötige Abhängigkeiten haben und Entwickler die Möglichkeit haben, nicht benötigte Abhängigkeiten zu entfernen, ohne sich Gedanken über problematische Abhängigkeiten machen zu müssen.

Wie immer erforderte die Durchsetzung strenger transitiver Abhängigkeiten einen Kompromiss. Dadurch werden Build-Dateien ausführlicher, da häufig verwendete Bibliotheken nun an vielen Stellen explizit aufgelistet werden müssen, anstatt zufällig geladen zu werden. Außerdem mussten die Ingenieure mehr Aufwand betreiben, um Abhängigkeiten zu BUILD-Dateien hinzuzufügen. aus. Seitdem haben wir Tools entwickelt, die diesen Arbeitsaufwand reduzieren, indem sie automatisch viele fehlende Abhängigkeiten erkennen und ohne Eingriff des Entwicklers zu einer BUILD-Datei hinzufügen. Aber selbst ohne solche Tools haben wir festgestellt, dass die Kompromisse lohnen, wenn die Codebasis skaliert wird: Das explizite Hinzufügen einer Abhängigkeit von der BUILD-Datei ist ein Einmalpreis, implizite transitive Abhängigkeiten können dauerhafte Probleme verursachen, solange das Build-Ziel vorhanden ist. Bazel erzwingt standardmäßig Java-Code für strikte transitive Abhängigkeiten.

Externe Abhängigkeiten

Wenn eine Abhängigkeit nicht intern ist, muss sie extern sein. Externe Abhängigkeiten sind diejenigen von Artefakten, die außerhalb des Build-Systems erstellt und gespeichert werden. Die Abhängigkeit wird direkt aus einem Artefakt-Repository importiert (normalerweise über das Internet aufgerufen) und unverändert genutzt, statt aus der Quelle erstellt zu werden. Einer der größten Unterschiede zwischen externen und internen Abhängigkeiten besteht darin, dass es für externe Abhängigkeiten Versionen gibt und diese Versionen unabhängig vom Quellcode des Projekts vorhanden sind.

Automatisches und manuelles Abhängigkeitsmanagement

Mit Build-Systemen können die Versionen externer Abhängigkeiten entweder manuell oder automatisch verwaltet werden. Bei manueller Verwaltung listet die Build-Datei explizit die Version auf, die aus dem Artefakt-Repository heruntergeladen werden soll, häufig mit einem semantischen Versionsstring wie z. B. 1.1.4. Bei der automatischen Verwaltung gibt die Quelldatei eine Reihe von zulässigen Versionen an und das Build-System lädt immer die neueste Version herunter. Mit Gradle kann beispielsweise eine Abhängigkeitsversion als "1.+" deklariert werden, um anzugeben, dass jede Neben- oder Patchversion einer Abhängigkeit akzeptabel ist, solange die Hauptversion 1 ist.

Automatisch verwaltete Abhängigkeiten können für kleine Projekte praktisch sein, sind jedoch in der Regel ein Schema für die Katastrophe auf Projekten kleiner Größe oder die, an denen mehr als ein Entwickler arbeitet. Das Problem bei automatisch verwalteten Abhängigkeiten besteht darin, dass Sie keine Kontrolle darüber haben, wann die Version aktualisiert wird. Es gibt keine Möglichkeit, dafür zu sorgen, dass externe Parteien keine funktionsgefährdenden Updates vornehmen, selbst wenn sie behaupten, eine semantische Versionierung zu verwenden. Daher kann ein Build, der an einem Tag funktioniert hat, am nächsten Tag gebrochen werden, ohne dass man leicht erkennen kann, was passiert oder in einen funktionsfähigen Zustand versetzt werden. Selbst wenn der Build nicht beeinträchtigt wird, können leichte Verhaltensweisen oder Leistungsänderungen auftreten, die nicht ermittelt werden können.

Da jedoch manuell verwaltete Abhängigkeiten eine Änderung der Versionsverwaltung erfordern, können sie problemlos erkannt und zurückgesetzt werden. Es ist auch möglich, eine ältere Version des Repositorys aufzurufen und mit älteren Abhängigkeiten zu erstellen. Bei Bazel müssen Versionen aller Abhängigkeiten manuell angegeben werden. Bei gleichmäßiger Skalierung lohnt sich der Aufwand für die manuelle Versionsverwaltung aufgrund der Stabilität.

Die Versionsregel

Verschiedene Versionen einer Bibliothek werden normalerweise durch verschiedene Artefakte dargestellt. Daher gibt es theoretisch keinen Grund, dass verschiedene Versionen derselben externen Abhängigkeit nicht beide im Build-System unter verschiedenen Namen deklariert werden können. Auf diese Weise kann jedes Ziel auswählen, welche Version der Abhängigkeit verwendet werden soll. Dies führt in der Praxis zu zahlreichen Problemen, weshalb Google für alle Abhängigkeiten von Drittanbietern in der Codebasis eine strenge Einversionsregel erzwingt.

Das größte Problem bei der Freigabe mehrerer Versionen ist das Problem mit der Rauteabhängigkeit. Angenommen, Ziel A hängt von Ziel B und von v1 einer externen Bibliothek ab. Wenn Ziel B später refaktoriert wird, um eine Abhängigkeit von v2 derselben externen Bibliothek hinzuzufügen, funktioniert Ziel A nicht, da es jetzt implizit von zwei verschiedenen Versionen derselben Bibliothek abhängig ist. Tatsächlich ist es nie sicher, eine neue Abhängigkeit von einem Ziel zu einer Drittanbieterbibliothek mit mehreren Versionen hinzuzufügen, da jeder Nutzer dieses Ziels bereits von einer anderen Version abhängig sein könnte. Die Einhaltung der Ein-Version-Regel macht diesen Konflikt unmöglich. Wenn ein Ziel eine Abhängigkeit von einer Drittanbieterbibliothek hinzufügt, sind vorhandene Abhängigkeiten bereits in derselben Version vorhanden, sodass sie problemlos koexistieren können.

Transitive externe Abhängigkeiten

Der Umgang mit den transitiven Abhängigkeiten einer externen Abhängigkeit kann besonders schwierig sein. In vielen Artefakt-Repositories wie Maven Central können Artefakte Abhängigkeiten von bestimmten Versionen anderer Artefakte im Repository angeben. Build-Tools wie Maven oder Gradle laden häufig rekursiv jede Standardeinstellung herunter. Dies bedeutet, dass das Hinzufügen einer einzigen Abhängigkeit in Ihrem Projekt dazu führen kann, dass insgesamt Dutzende von Artefakten heruntergeladen werden.

Dies ist sehr praktisch: Wenn Sie eine Abhängigkeit von einer neuen Bibliothek hinzufügen, wäre es sehr mühsam, jede transitive Abhängigkeit dieser Bibliothek aufzuspüren und alle manuell hinzuzufügen. Es gibt jedoch auch einen großen Nachteil: Da verschiedene Bibliotheken von verschiedenen Versionen derselben Drittanbieterbibliothek abhängen können, verstößt diese Strategie notwendigerweise gegen die Ein-Version-Regel und führt zu einem Diamantenabhängigkeitsproblem. Wenn Ihr Ziel von zwei externen Bibliotheken abhängt, die unterschiedliche Versionen derselben Abhängigkeit verwenden, gibt es keine Auskunft darüber, welche Sie erhalten. Dies bedeutet auch, dass das Aktualisieren einer externen Abhängigkeit scheinbar unzusammenhängende Fehler in der gesamten Codebasis verursachen kann, wenn die neue Version in Konflikt stehende Versionen einiger Abhängigkeiten abruft.

Aus diesem Grund werden von Bazel nicht automatisch transitive Abhängigkeiten heruntergeladen. Leider gibt es keine Aufzählungspunkte: Die Alternative von Bazel ist die Verwendung einer globalen Datei, die alle externen Abhängigkeiten des Repositorys und eine explizite Version, die für diese Abhängigkeit im gesamten Repository verwendet wird, auflistet. Glücklicherweise stellt Bazel Tools bereit, mit denen eine solche Datei automatisch erstellt werden kann. Diese Datei enthält die transitiven Abhängigkeiten einer Reihe von Maven-Artefakten. Dieses Tool kann einmal ausgeführt werden, um die erste WORKSPACE-Datei für ein Projekt zu generieren. Diese Datei kann dann manuell aktualisiert werden, um die Versionen der einzelnen Abhängigkeiten anzupassen.

Wie bereits erwähnt, liegt die Entscheidung hier in der Nutzerfreundlichkeit und Skalierbarkeit. Kleine Projekte brauchen sich möglicherweise keine Gedanken um die Verwaltung von transitiven Abhängigkeiten zu machen und können möglicherweise die Verwendung von automatischen transitiven Abhängigkeiten vermeiden. Diese Strategie wird immer weniger ansprechend, wenn die Organisation und die Codebasis wachsen und immer mehr Konflikte und unerwartete Ergebnisse auftreten. In großem Maßstab sind die Kosten für die manuelle Verwaltung von Abhängigkeiten viel niedriger als die Kosten für die Fehlerbehebung bei der automatischen Abhängigkeitsverwaltung.

Build-Ergebnisse im Cache mit externen Abhängigkeiten im Cache speichern

Externe Abhängigkeiten werden meistens von Drittanbietern bereitgestellt, die stabile Versionen von Bibliotheken freigeben, möglicherweise ohne Quellcode. Einige Organisationen stellen möglicherweise auch eigenen Code als Artefakte bereit. Dadurch sind andere Codeteile als Drittanbieter und nicht von internen Abhängigkeiten abhängig. Dies kann theoretisch Builds beschleunigen, wenn Artefakte langsam erstellt, aber schnell heruntergeladen werden können.

Dies führt jedoch auch zu einem hohen Overhead und Komplexität: Ein Mitarbeiter muss für jedes dieser Artefakte verantwortlich sein und sie in das Artefakt-Repository hochladen. Außerdem müssen die Clients immer darauf achten, dass sie in der { 101}die neueste Version. Außerdem wird die Fehlerbehebung wesentlich schwieriger, da aus verschiedenen Teilen des Repositorys verschiedene Teile des Systems erstellt wurden und die Quellstruktur nicht mehr konsistent angezeigt wird.

Eine bessere Methode zur Lösung des Problems mit Artefakten, die viel Zeit zum Erstellen benötigen, ist die Verwendung eines Build-Systems, das Remote-Caching unterstützt, wie zuvor beschrieben. Ein solches Build-System speichert die resultierenden Artefakte von jedem Build an einem Standort, der von Entwicklern gemeinsam genutzt wird. Wenn ein Entwickler also auf ein Artefakt angewiesen ist, das kürzlich von einer anderen Person erstellt wurde, lädt das Build-System automatisch statt sie zu erstellen. Dies bietet alle Leistungsvorteile, die durch eine direkte Abhängigkeit von Artefakten entstehen, und gleichzeitig wird gewährleistet, dass die Builds so konsistent sind, als ob sie immer aus derselben Quelle erstellt worden wären. Diese Strategie wird intern von Google verwendet und Bazel kann für die Verwendung eines Remote-Caches konfiguriert werden.

Sicherheit und Zuverlässigkeit externer Abhängigkeiten

Die Verwendung von Artefakten aus Quellen von Drittanbietern ist mit Risiken verbunden. Es besteht ein Verfügbarkeitsrisiko, wenn die Drittanbieterquelle (z. B. ein Artefakt-Repository) ausfällt. Dies liegt daran, dass Ihr gesamter Build möglicherweise gestoppt wird, wenn es keine externe Abhängigkeit herunterladen kann. Es besteht auch ein Sicherheitsrisiko: Wenn das Drittanbietersystem von einem Angreifer manipuliert wird, könnte der Angreifer das referenzierte Artefakt durch ein eigenes Design ersetzen, sodass er beliebigen Code in Ihren Build einfügen kann. aus. Beide Probleme lassen sich beheben, indem Sie alle Artefakte, die Sie benötigen, auf Server spiegeln, die Sie steuern, und den Zugriff Ihres Build-Systems auf Artefakt-Repositories von Drittanbietern wie Maven Central blockieren. Der Kompromiss besteht darin, dass diese Spiegel Aufwand und Ressourcen erfordern. Die Entscheidung, ob sie verwendet werden, hängt also häufig vom Umfang des Projekts ab. Das Sicherheitsproblem kann auch mit geringem Overhead vollständig verhindert werden, da der Hash jedes Drittanbieterartefakts im Quell-Repository angegeben werden muss. Dies führt dazu, dass der Build fehlschlägt, wenn das Artefakt manipuliert wurde. Eine weitere Alternative, die das Problem vollständig umgeht, besteht darin, die Abhängigkeiten Ihres Projekts bereitzustellen. Wenn ein Projekt die Abhängigkeiten von Drittanbietern bereitstellt, werden diese zusammen mit dem Quellcode des Projekts in die Versionsverwaltung eingecheckt, entweder als Quelle oder als Binärdateien. Das bedeutet, dass alle externen Abhängigkeiten des Projekts in interne Abhängigkeiten umgewandelt werden. Google verwendet diesen Ansatz intern. Dabei wird jede Drittanbieterbibliothek, die in Google referenziert wird, in einem third_party-Verzeichnis im Stammverzeichnis der Google-Quellstruktur geprüft. Dies funktioniert jedoch nur bei Google, da das Versionsverwaltungssystem von Google speziell für ein extrem großes Monopo entwickelt wurde. Daher ist die Vermittlung möglicherweise nicht für alle Organisationen eine Option.