Lernen von Go Projektarchitektur im Großmaßstab von Kubernetes
Ethan Miller
Product Engineer · Leapcell

Bevor wir versuchen, ein groß angelegtes Projekt mit hoher Skalierbarkeit, Zuverlässigkeit und Wartbarkeit mit Go zu erstellen, wollen wir uns zunächst die Projektstruktur von Kubernetes ansehen, um zu sehen, wie es eine Reihe von Funktionsmodulen für die Container-Orchestrierung organisiert.
Kubernetes Code Layout
Im Folgenden finden Sie eine Liste der wichtigsten Verzeichnisse der obersten Ebene von Kubernetes und ihrer Hauptfunktionen. Als Nächstes werden wir den Zweck jedes Verzeichnisses einzeln erläutern.
- api: Speichert Schnittstellenprotokolle
- build: Code im Zusammenhang mit dem Erstellen von Anwendungen
- cmd:
main
-Einstiegspunkte für jede Anwendung - pkg: Hauptimplementierung jeder Komponente
- staging: Speichert vorübergehend Code, der unter Komponenten voneinander abhängig ist
api
Hier werden OpenAPI- und Swagger-Dateien gespeichert, einschließlich Definitionen für JSON und Protocol.
build
Dies enthält Skripte zum Erstellen des Kubernetes-Projekts, einschließlich des Erstellens jeder K8s-Komponente sowie der erforderlichen Images, wie z. B. des Pause-Programms.
cmd
Das Verzeichnis cmd
speichert die Quelldateien des Hauptpakets zum Erstellen ausführbarer Dateien. Wenn mehrere ausführbare Dateien erstellt werden müssen, kann jede ausführbare Datei in einem eigenen Unterverzeichnis platziert werden. Sehen wir uns die spezifischen Unterverzeichnisse unter dem Kubernetes cmd
-Verzeichnis an:
- cmd: Die `main`-Methoden jeder Anwendung
- kube-proxy: Verantwortlich für netzwerkbezogene Regeln
- kube-apiserver: Stellt K8s-APIs bereit und verarbeitet Anfragen, wodurch CURD-Operationen für verschiedene Ressourcen (Pod, ReplicaSet, Service) bereitgestellt werden
- kube-controller-manager
- kube-scheduler: Überwacht neu erstellte Pods und wählt Knoten für deren Ausführung aus
- kubectl: Befehlszeilentool für den Zugriff auf den Cluster
Wie wir sehen können, finden sich hier vertraute Komponenten in K8s wie kube-proxy und kube-apiserver.
pkg
Das pkg
-Verzeichnis enthält sowohl Abhängigkeiten, die das Projekt selbst benötigt, als auch exportierte Pakete.
- pkg: Hauptimplementierungen jeder Komponente
- proxy: Netzwerk-Proxy-Implementierung
- kubelet: Verwaltet Pods auf dem Knoten
- cm: Containerverwaltung, wie z. B. Cgroups
- stats: Ressourcenauslastung, implementiert durch `cAdvisor`
- scheduler: Implementierung der Pod-Planung
- framework
- controlplane: Steuerungsebene
- apiserver
staging
Pakete im Staging-Verzeichnis werden über symbolische Links in k8s.io verlinkt. Erstens hilft dies, da das Kubernetes-Projekt riesig ist, Entwicklungshemmnisse zu vermeiden, die durch fragmentierte Repositories verursacht werden, sodass der gesamte Code in einem Pull Request eingereicht und überprüft werden kann. Auf diese Weise wird die Modularität sichergestellt und gleichzeitig die Vollständigkeit des Hauptcode-Repositorys gewahrt.
Gleichzeitig müssen Sie durch die Verwendung der replace
-Direktive in go mod nicht jedes Mal jede Abhängigkeit taggen, was die Versionsverwaltung und Release-Prozesse vereinfacht.
Wenn wir dies nicht so gemacht hätten und stattdessen den Monorepo-Ansatz verwendet hätten – indem wir den gesamten Code unter Staging in unabhängige Repositories aufgeteilt hätten – dann müssten wir, wann immer sich der Code dieser Sub-Repositories geändert hätte, zuerst im Sub-Repository einreichen, ein neues Tag veröffentlichen und dann das alte Tag in go mod ersetzen, bevor wir mit der weiteren Entwicklung fortfahren könnten. Dies würde zweifellos die gesamten Entwicklungskosten erhöhen.
Daher vereinfacht das Verlinken der Pakete im Staging-Verzeichnis mit dem Haupt-Repository über symbolische Links die Versionsverwaltung und den Release-Prozess effektiv.
Vergleich mit dem Standard-Go-Projektlayout
Das Verzeichnis internal
wird für Pakete verwendet, die nicht für den externen Gebrauch exportiert werden sollen. In Go ist das Prinzip hinter internal
, dass es innerhalb des Projekts selbst normal verwendet werden kann, während gleichzeitig sichergestellt wird, dass es für externe Projekte nicht sichtbar ist.
Es gibt jedoch kein internal
-Verzeichnis in Kubernetes. Dies liegt daran, dass das Kubernetes-Projekt um 2014 gestartet wurde, während das Konzept des internal
-Verzeichnisses erst in Go 1.4 (veröffentlicht Ende 2014) eingeführt wurde. Während der frühen Entwicklung des Kubernetes-Projekts war die Konvention, internal
zu verwenden, noch nicht weit verbreitet, und es gab später keine großen Refaktorierungen, um es einzuführen.
Gleichzeitig ist eines der Designziele von Kubernetes Modularität und Entkopplung. Es erreicht die Kapselung durch explizite Paketorganisation und Codestruktur, ohne internal
-Pakete verwenden zu müssen, um den Paketzugriff einzuschränken.
An dieser Stelle verstehen wir bereits die Standardverzeichnisstruktur der obersten Ebene für das Erstellen eines Projekts.
Go hat kein Standard-Directory-Framework wie Java. Wenn man verschiedene Projekte startet, muss man sich daher immer an die jeweilige Codestruktur des Projekts gewöhnen. Selbst innerhalb desselben Teams können unterschiedliche Strukturen vorhanden sein, was ein erhebliches Hindernis für Neulinge darstellen kann, die versuchen, das Projekt zu verstehen.
Aufgrund dieser Hindernisse kann die Zusammenarbeit schwierig sein. Eine einheitliche Verzeichnisstruktur der obersten Ebene ermöglicht es uns, schnell Code zu finden und einen Standardeinstiegspunkt zu haben, wenn wir ein Projekt übernehmen, was die Entwicklungseffizienz verbessert und Verwirrung über Codespeicherorte während der Zusammenarbeit reduziert.
Aber macht eine einheitliche Codeverzeichnisstruktur allein ein perfektes Großprojekt aus? Die Antwort ist natürlich nein.
Sich ausschließlich auf eine einheitliche Verzeichnisstruktur zu verlassen, kann das Problem, dass Code allmählich verfällt und chaotisch wird, nicht ein für alle Mal lösen. Nur fundierte Designprinzipien können den Designkontext klar halten, während das Projekt weiter wächst.
Deklarative Designphilosophie
Die deklarative API zieht sich durch das gesamte Code-Design von Kubernetes und verhindert, dass es in prozedurale Programmierung verfällt.
Wenn Sie beispielsweise den Zustand einer Ressource ändern, sollten Sie K8s den gewünschten Zustand mitteilen, anstatt K8s mitzuteilen, welche Schritte unternommen werden sollen. Dies ist auch der Grund, warum das rollierende Update von kubelet auslief, da sein Design den gesamten Prozess der Aktualisierung eines Pod mikromanagte.
Indem Sie Kubernetes über den gewünschten Zustand informieren, kann kubelet entsprechend diesem Zustand geeignete Maßnahmen ergreifen, und es ist keine übermäßige Intervention von außen erforderlich.
An dieser Stelle fragen Sie sich vielleicht: Warum hilft eine deklarative API dabei, Module klar zu halten, wenn das Projekt wächst? Ist das nicht etwas, das Benutzer bei der Verwendung von Kubernetes wahrnehmen? Wie hängt das mit dem internen Design zusammen?
Wenn wir Schnittstellen entwerfen, wenn wir den gesamten operativen Prozess für Benutzer freigeben und sie Schritt für Schritt in die Aktualisierung unseres Pod eingreifen lassen, dann werden die von uns entworfenen Module zwangsläufig prozedural sein. Auf diese Weise werden unsere Code-Module schwer übersichtlich zu halten, weil sie mit vielen Benutzeroperationen gekoppelt sind.
Durch die Verwendung einer deklarativen API kann der Cluster nach der Mitteilung des gewünschten Zustands an K8s mehrere interne Komponenten koordinieren, um letztendlich den gewünschten Zustand zu erreichen. Benutzer müssen nicht wissen, wie die Dinge intern aktualisiert werden. Darüber hinaus können neue Module direkt hinzugefügt werden, wenn zusätzliche Kollaborations-Plugins benötigt werden, ohne weitere APIs für Benutzeroperationen freizugeben.
cAdvisor überwacht von K8s bereitgestellte Ressourcen und sammelt Container-Ressourcenmetriken. Es arbeitet unabhängig und ist nicht auf externe Komponenten angewiesen. Der Controller vergleicht diese Metriken dann mit benutzerdeklarierten Zielen, um festzustellen, ob die Bedingungen für das Hoch- oder Runterskalieren erfüllt sind.
Da die Module unabhängig sind, muss sich cAdvisor nur auf das Sammeln und Zurückgeben von Überwachungsmetriken konzentrieren, ohne sich darum zu kümmern, wie diese Metriken verwendet werden – ob zur Beobachtung oder als Grundlage für die automatische Skalierung.
Dies ist auch ein Schlüsselprinzip beim Entwerfen verschiedener Aufgabenkomponenten: Definieren Sie klar die Anforderungen, die erfüllt werden müssen. Konzentrieren Sie sich beim Übertragen von Informationen nur auf Ein- und Ausgabe. Was die interne Implementierung betrifft, kann sie gekapselt werden, ohne sie extern freizugeben, wodurch sie so einfach wie möglich für die externe geschäftliche Nutzung ist.
Vermeidung von Over-Engineering
Exzessives Engineering-Design ist oft schlimmer als unzureichendes Design.
Die früheste Version von Kubernetes war 0.4. Für die Vernetzung bestand die offizielle Implementierung darin, dass GCE Salt-Skripte ausführt, um Brücken zu erstellen, während die empfohlenen Lösungen für andere Umgebungen Flannel und OVS waren.
Als sich Kubernetes entwickelte, reichte Flannel in einigen Situationen nicht mehr aus. Um 2015 tauchten Calico und Weave in der Community auf, die das Netzwerkproblem grundsätzlich lösten. Kubernetes musste sich daher nicht mehr selbst darum kümmern, also führte es CNI ein, um Netzwerk-Plugins zu standardisieren.
Es ist klar, dass Kubernetes nicht von Anfang an perfekt designt war. Stattdessen wurden im Laufe der Zeit, als neue Probleme auftraten, neue Designs eingeführt, um sich an Veränderungen in verschiedenen Umgebungen anzupassen.
Beim Starten eines Projekts sind die Abhängigkeiten relativ klar. Daher treten zu Beginn des Engineering-Designs keine zirkulären Abhängigkeiten auf. Mit dem Wachstum des Projekts treten diese Probleme jedoch allmählich auf. Funktionale Anforderungen im Produkt führen zu Querverweisen im Code-Design.
Selbst wenn wir unser Bestes geben, um alle geschäftlichen Hintergründe und zu lösenden Probleme vor dem Start zu verstehen, werden zwangsläufig neue Probleme auftreten, wenn sich Produktfunktionen ändern und Programme iterieren. Was wir tun können, ist, auf das Moduldesign und das Abhängigkeitsmanagement zu achten, Funktionen so kohärent wie möglich zu halten und bei späteren Ergänzungen von Abstraktionen zu vermeiden, dass wir den gesamten vorherigen Code auf „Refactoring“-Art und Weise überarbeiten müssen.
Ein System für „Skalierbarkeit“ überzudesignen, nur um des Designs willen zu designen, kann zu einem Stolperstein für zukünftige Änderungen werden.
Lassen Sie uns die Designentwicklung anhand eines E-Commerce-Geschäftsszenarios veranschaulichen.
Anfangs hat das System zwei Module:
- Bestellmodul: Verantwortlich für die Bearbeitung von Bestellungen, Zahlungen, Statusaktualisierungen usw. Es hängt vom Benutzermodul für Benutzerinformationen ab (z. B. Lieferadresse, Kontaktdaten usw.).
- Benutzermodul: Verantwortlich für die Verwaltung von Benutzerinformationen, Registrierung, Anmeldung und Speicherung von Benutzerdaten. Es hängt nicht vom Bestellmodul ab.
In diesem anfänglichen Design ist die Abhängigkeit einseitig: Das Bestellmodul hängt vom Benutzermodul ab.
In dieser Phase ist es nicht erforderlich, im Code übermäßig zu abstrahieren. Viele Projekte können nicht vorhersehen, ob sie erfolgreich sein werden oder scheitern, daher ist es aus Sicht der Produktfreigabe nicht sinnvoll, zu viel Aufwand in das Design zu stecken, und wenn sich das Produktkonzept drastisch ändert, kann ein Überdesign ein Hindernis für zukünftige Änderungen darstellen.
Mit der Weiterentwicklung der Anforderungen entsteht ein neues Bedürfnis: Die Plattform muss den Benutzern personalisierte Produkte basierend auf ihrer Kaufhistorie (Bestellhistorie) empfehlen.
Um personalisierte Empfehlungen zu erreichen, muss das Benutzermodul nun die API des Bestellmoduls aufrufen, um die Bestellhistorie eines Benutzers abzurufen.
Jetzt sehen die Abhängigkeiten wie folgt aus:
- Das Bestellmodul hängt vom Benutzermodul für Benutzerinformationen ab.
- Das Benutzermodul hängt vom Bestellmodul für die Bestellhistorie ab.
Diese Änderung erzeugt eine zirkuläre Abhängigkeit: Das Bestellmodul hängt vom Benutzermodul ab, und das Benutzermodul hängt auch vom Bestellmodul ab.
Um die zirkuläre Abhängigkeit zu lösen, können mehrere Lösungen in Betracht gezogen werden:
Entkopplung der Modulzuständigkeiten: Einführung eines neuen Moduls, z. B. eines Empfehlungsmoduls, das sich der personalisierten Empfehlungslogik widmet. Das Empfehlungsmodul kann Daten separat vom Benutzer- und Bestellmodul abrufen, wodurch direkte Abhängigkeiten zwischen ihnen vermieden werden.
Durch die Extraktion von Modulen lösen wir die Kopplung zwischen dem Benutzer- und dem Bestellmodul.
Es entsteht jedoch eine neue Anforderung: Während Werbeaktionen kaufen Benutzer aktionsspezifische Produkte. Der Produktmanager möchte, dass das Empfehlungsmodul solche Bestellungen sofort erkennen und Empfehlungen für verwandte Aktionsprodukte geben kann. Wenn ein Benutzer beispielsweise eine reduzierte Sportuhr kauft und wir auch reduzierte Bluetooth-Sportkopfhörer empfehlen, könnte die Wiederkaufrate des Benutzers höher sein.
In diesem Szenario ist es eindeutig unerwünscht, dass das Bestellmodul das Empfehlungsmodul direkt aufruft, um Daten zu übergeben, da das Empfehlungsmodul bereits vom Bestellmodul für Benutzerkaufdaten abhängt, wodurch eine einseitige Abhängigkeit entsteht. Wenn wir das Bestellmodul das Empfehlungsmodul aufrufen lassen, entsteht wieder eine zirkuläre Abhängigkeit.
Wie kann das Empfehlungsmodul also schnell Änderungen in Bestellungen erkennen? Dies erfordert eine ereignisgesteuerte Architektur.
Durch die Verwendung eines ereignisgesteuerten Ansatzes löst das Bestellmodul ein Ereignis aus, wenn ein Benutzer eine Bestellung aufgibt, und das Empfehlungsmodul abonniert Ereignisse im Zusammenhang mit Benutzerbestellungen. Auf diese Weise müssen die beiden Module nicht die APIs des jeweils anderen direkt aufrufen, sondern Daten werden über Ereignisse übergeben.
Nach Erhalt der Daten kann das Empfehlungsmodul sofort ein neues Empfehlungsmodell neu trainieren und dem Benutzer verwandte Produkte empfehlen.
Aus dem obigen Beispiel können wir eine große Herausforderung in Unternehmensanwendungen erkennen: die Modellierung von Geschäftsdomänen.
Bei der Modellierung handelt es sich eher um einen Prozess der Optimierung des Designs, wenn sich die Anforderungen kontinuierlich weiterentwickeln.
Die oben beschriebenen Benutzer-, Bestell- und Empfehlungsmodule sind auch gängige Szenarien in der Entwicklung der meisten To-C-Produkte (Consumer-Facing).
Wie wir unser Moduldesign und unsere Codestruktur im Zuge der Weiterentwicklung kontinuierlich optimieren und die Iterationsgeschwindigkeit verbessern können, müssen wir erforschen und darüber nachdenken.
Zusammenfassung
Fassen wir den Inhalt dieses Artikels zusammen:
- Beim Erstellen großer Projekte kann eine einheitliche Verzeichnisstruktur die Zusammenarbeitseffizienz verbessern, aber fundierte Designprinzipien sind der Schlüssel, um Klarheit und Erweiterbarkeit zu erhalten, während das Projekt wächst.
- Die deklarative API von Kubernetes hilft Modulen, unabhängig zu bleiben, und vermeidet die Fallstricke der prozeduralen Programmierung.
- Das Projektdesign sollte sich Schritt für Schritt an die tatsächlichen Bedürfnisse anpassen und Over-Engineering vermeiden.
- Konzentrieren Sie sich auf die richtige Trennung von Modulzuständigkeiten und Abhängigkeiten und verwenden Sie ereignisgesteuerte Ansätze, um die Kopplung zwischen Modulen zu lösen.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Go-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Language-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützt 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Auto-Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf das Erstellen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ