Cloud & Infrastruktur

Wie Kubernetes CRI exec, attach und Port-Forwarding verarbeitet

Ein detaillierter Artikel aus dem Jahr 2024 erklärt die einzigartige URL-basierte Streaming-Architektur hinter den Befehlen der Kubernetes Container Runtime Interface (CRI).

Diagramm, das die getrennten Steuerungs- und Datenpfade im Kubernetes CRI Streaming veranschaulicht
Bild: Kubernetes Blog, lizenziert unter CC BY 4.0

Automatisch aus dem englischen Original übersetzt.

In einem Beitrag im Kubernetes Blog vom Mai 2024 erläuterte Sascha Grunert die internen Mechanismen von drei spezifischen Remote Procedure Calls (RPCs) des Container Runtime Interface (CRI). Der Artikel erklärt, wie sich Exec, Attach und PortForward von standardmäßigen gRPC-Interaktionen unterscheiden, indem sie ein eigenständiges URL-basiertes Streaming-Modell nutzen, das seit seinem Design im Jahr 2016 konsistent geblieben ist.

Was passiert ist

Das Kubernetes CRI dient als primäre Brücke zwischen dem kubelet und den Container-Runtimes. Es erfordert, dass Runtimes einen gRPC-Server bereitstellen, der einer definierten Protocol Buffer-Schnittstelle entspricht. Während die meisten CRI-Operationen auf einfachen Unary-Calls oder Server-seitigem Streaming basieren, hob Grunert hervor, dass Exec, Attach und PortForward anders funktionieren. Diese drei Funktionen sind für Entwickler entscheidend, die Befehle in Containern ausführen, Live-Ausgaben anzeigen oder Netzwerkports zum Debuggen weiterleiten müssen.

Grunert verfolgte die Geschichte dieser Funktionen bis zu einem Designdokument aus dem Jahr 2016 zurück, das vor den modernen Kubernetes Enhancement Proposals entstand. Vor der CRI-Initiative waren diese Fähigkeiten eng an spezifische Runtimes wie Docker oder rkt gebunden. Die Community erwog die Implementierung von nativem RPC-Streaming, lehnte dies jedoch ab, da es Netzwerkknoten im kubelet erzeugen und die Flexibilität der Runtimes einschränken würde. Stattdessen adoptierte man ein Modell, bei dem die Runtime einen Streaming-Server bereitstellt, sodass jede Implementierung Verbindungen unabhängig verwalten kann.

Diese architektonische Entscheidung bedeutet, dass zwar die anfängliche Anfrage über die Standard-gRPC-Schnittstelle läuft, der tatsächliche Datentransfer jedoch über eine separate HTTP-Verbindung erfolgt. Diese Trennung ermöglicht es Runtimes, ihre Streaming-Implementierungen weiterzuentwickeln, ohne die Kern-CRI-Definition zu ändern. Obwohl im Laufe der Jahre kleinere Verbesserungen eingeflossen sind, hat sich das Grundmuster – eine URL anfordern und sich dann direkt damit verbinden – nicht geändert.

Wie es funktioniert

Der Prozess beginnt, wenn ein Client, wie kubectl oder crictl, eine gRPC-Anfrage an die Runtime für eine Exec-, Attach- oder PortForward-Sitzung sendet. Im Gegensatz zu typischen API-Aufrufen, die Daten direkt zurückgeben, validiert die Runtime die Anfrage und speichert sie in einem Connection-Tracking-Cache. Sie gibt dann eine Antwort zurück, die nur eine vollständig qualifizierte URL enthält. Der Client muss sich anschließend mit dieser URL verbinden und die Verbindung auf das SPDY-Protokoll oder zunehmend WebSockets upgraden, um mit dem Streaming der Daten zu beginnen.

Figure from the original article: Wie Kubernetes CRI exec, attach und Port-Forwarding verarbeitet
Abbildung aus dem Originalartikel · Kubernetes Blog · CC BY 4.0

Für Exec und Attach definiert Kubernetes ein spezifisches Protokoll mit fünf Versionen, aktuell bis v5.channel.k8s.io. Dieses Protokoll nutzt das erste Byte jedes Pakets zur Identifizierung des Stream-Typs, wie Standard-Eingabe, Standard-Ausgabe, Standard-Fehler oder Steuersignale wie Terminal-Resize und Close. Der Quellcode des kubelets stellt eine wiederverwendbare Bibliothek bereit, die diese Protokollinterpretation handhabt, sodass Runtimes nur die Logik für die Ausführung von Befehlen oder das Anhängen an Prozesse implementieren müssen. PortForward funktioniert anders, da es keine strenge Protokolldefinition besitzt. Stattdessen betritt die Runtime den Netzwerk-Namespace des Containers und streamt rohe SPDY-Frames, wobei sie auf Bibliotheken wie moby/spdystream vertraut, um den Datenfluss zu verwalten.

Wichtige Details

  • Die CRI-RPCs Exec, Attach und PortForward geben in ihrer Antwort nur einen URL-String zurück, nicht den tatsächlichen Datenstrom.
  • Clients müssen die HTTP-Verbindung nach Erhalt der URL auf SPDY oder WebSockets upgraden, um die Streaming-Sitzung zu etablieren.
  • Die Exec- und Attach-Protokolle nutzen das erste Byte jedes Pakets, um zwischen stdin, stdout, stderr, Fehlern, Resize-Ereignissen und Close-Signalen zu unterscheiden.
  • Es existieren fünf Versionen des Remote-Befehl-Protokolls, wobei v5 Unterstützung für ein CLOSE-Signal für WebSockets hinzufügt.
  • Das kubelet stellt eine wiederverwendbare Bibliothek mit einer Runtime-Schnittstelle bereit, die Runtimes implementieren müssen, um die zugrunde liegende Befehlsausführung und das Betreten des Netzwerk-Namespaces zu handhaben.
  • Zukünftige Bemühungen konzentrieren sich darauf, SPDY durch WebSockets zu ersetzen; Tools wie crictl v1.30 unterstützen bereits einen --transport-Flag zur Auswahl zwischen ihnen.

Warum es wichtig ist

Für Software-Ingenieure, die Container-Runtimes entwickeln oder warten, ist das Verständnis dieser Architektur für eine korrekte Implementierung unerlässlich. Die Entkopplung der Control Plane (gRPC) von der Data Plane (HTTP-Streaming) bedeutet, dass Runtime-Entwickler diese Aufrufe nicht einfach als Standard-API-Anfragen behandeln können. Sie müssen gleichzeitige Streaming-Sitzungen verwalten, Protokoll-Upgrades handhaben und die Kompatibilität mit mehreren Protokollversionen sicherstellen. Eine fehlerhafte Interpretation des ersten Bytes eines Datenpakets oder das Nichtunterstützen von Terminal-Resize-Ereignissen kann zu beeinträchtigten Benutzererlebnissen in gängigen Entwicklungsworkflows führen.

Dieses Design beeinflusst auch, wie Debugging-Tools mit Clustern interagieren. Da die Daten nach der initialen URL-Abrufung am kubelet vorbeileiten, müssen Netzwerkrichtlinien und Proxies die direkte Kommunikation zwischen dem Client und dem Node, der den Container hostet, zulassen. Während das Ökosystem sich in Richtung WebSockets bewegt, müssen Ingenieure sicherstellen, dass ihre Tooling die neue Transportebene unterstützt. Die laufenden Arbeiten in Projekten wie CRI-O, die die Streaming-Logik zu conmon-rs verschieben, zeigen, wie diese Flexibilität es Runtimes ermöglicht, Sitzungen auch dann am Leben zu erhalten, wenn der Hauptprozess der Runtime neu gestartet wird, was die Zuverlässigkeit für langlaufende Debugging-Sitzungen verbessert.

Was Sie tun können

  • Stellen Sie sicher, dass Ihre Container-Runtime das neueste v5.channel.k8s.io-Protokoll unterstützt, um eine ordnungsgemäße Handhabung von Stream-Closure-Signalen zu gewährleisten.
  • Aktualisieren Sie Client-Tools wie crictl auf Version 1.30 oder höher, um die Unterstützung des WebSocket-Transports unter Verwendung des --transport-Flags zu testen.
  • Überprüfen Sie Ihre Netzwerkrichtlinien, um sicherzustellen, dass Clients die Node-IPs und Ports erreichen können, die für Streaming-Verbindungen verwendet werden, nicht nur den API-Server.
  • Wenn Sie eine benutzerdefinierte Runtime entwickeln, nutzen Sie die wiederverwendbare Streaming-Bibliothek des kubelets zur Handhabung der Protokollanalyse, anstatt sie von Grund auf neu zu implementieren.
  • Beobachten Sie die Adoption von WebSockets in Ihrer Umgebung, da SPDY zugunsten modernerer Web-Standards schrittweise eingestellt wird.
  • Prüfen Sie, ob Ihre Runtime Streaming-Sitzungen an externe Monitore wie conmon-rs auslagern kann, um die Resilienz während Runtime-Updates zu verbessern.

Tools aus dem Bytechap-Shop

Weiterlesen

Alle Artikel