Kapitel 4. Speicherung von Containern
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Während Kubernetes seine Anfänge in der Welt der zustandslosen Workloads hatte, werden zustandsabhängige Dienste immer häufiger eingesetzt. Sogar komplexe zustandsabhängige Arbeitslasten wie Datenbanken und Warteschlangen finden ihren Weg in Kubernetes-Cluster. Um diese Workloads zu unterstützen, muss Kubernetes Speicherfähigkeiten bieten, die über ephemere Optionen hinausgehen. Und zwar Systeme, die bei verschiedenen Ereignissen wie dem Absturz einer Anwendung oder der Verlagerung eines Workloads auf einen anderen Host eine höhere Ausfallsicherheit und Verfügbarkeit bieten.
In diesem Kapitel werden wir untersuchen, wie unsere Plattform den Anwendungen Speicherdienste anbieten kann. Wir beginnen mit den wichtigsten Aspekten der Anwendungspersistenz und den Erwartungen an das Speichersystem, bevor wir uns den in Kubernetes verfügbaren Speicherprimitiven zuwenden. Wenn wir uns mit fortgeschrittenen Anforderungen an die Speicherung befassen, werden wir uns mit dem Container Storage Interface (CSI) beschäftigen, das die Integration mit verschiedenen Speicheranbietern ermöglicht. Schließlich werden wir uns mit der Verwendung eines CSI-Plug-ins beschäftigen, um unseren Anwendungen eine Self-Service Speicherung zu ermöglichen.
Hinweis
Speicherung ist an sich schon ein großes Thema. Unsere Absicht ist es, dir gerade genug Details zu geben, um fundierte Entscheidungen über die Speicherung zu treffen, die du deinen Workloads anbieten kannst. Wenn du dich mit Speicherung nicht auskennst, empfehlen wir dir, diese Konzepte mit deinem Infrastruktur-/Speicherteam durchzugehen. Kubernetes macht das Fachwissen über Speicherung in deinem Unternehmen nicht überflüssig!
Überlegungen zur Speicherung
Bevor wir uns mit den Speichermustern und -optionen von Kubernetes beschäftigen, sollten wir einen Schritt zurücktreten und einige wichtige Überlegungen zum potenziellen Speicherbedarf anstellen. Auf der Ebene der Infrastruktur und der Anwendung ist es wichtig, die folgenden Anforderungen zu bedenken.
-
Zugangsmodi
-
Volumenerweiterung
-
Dynamische Provisionierung
-
Sicherung und Wiederherstellung
-
Block-, Datei- und Objektspeicherung
-
Flüchtige Daten
-
Einen Anbieter auswählen
Zugriffsmodi
Es gibt drei Zugangsmodi, die für Anwendungen unterstützt werden können:
- ReadWriteOnce (RWO)
-
Ein einzelner Pod kann auf dem Volume lesen und schreiben.
- ReadOnlyMany (ROX)
-
Mehrere Pods können den Band lesen.
- ReadWriteMany (RWX)
-
Mehrere Pods können auf dem Volume lesen und schreiben.
Für Cloud-native Anwendungen ist RWO das mit Abstand häufigste Muster. Wenn du gängige Anbieter wie Amazon Elastic Block Storage (EBS) oder Azure Disk Storage nutzt, bist du auf RWO beschränkt, weil die Festplatte nur an einen Knoten angeschlossen werden kann. Diese Einschränkung mag zwar problematisch erscheinen, aber die meisten nativen Cloud-Anwendungen funktionieren am besten mit dieser Art von Speicherung, bei der das Volume ausschließlich ihnen gehört und eine hohe Lese-/Schreibleistung bietet.
Oft finden wir Legacy-Anwendungen, die RWX benötigen. Oft sind sie so aufgebaut, dass sie auf ein Netzwerkdateisystem (NFS) zugreifen. Wenn Dienste ihren Status gemeinsam nutzen müssen, gibt es oft elegantere Lösungen als die gemeinsame Nutzung von Daten über NFS, z. B. die Verwendung von Nachrichtenwarteschlangen oder Datenbanken. Wenn eine Anwendung Daten austauschen möchte, ist es in der Regel am besten, diese über eine API offenzulegen, anstatt Zugriff auf das Dateisystem zu gewähren. Das macht viele Anwendungsfälle für RWX manchmal fragwürdig. Wenn NFS nicht die richtige Wahl ist, stehen die Plattformteams vor der schwierigen Entscheidung, RWX-kompatible Speicherung anzubieten oder von ihren Entwicklern zu verlangen, dass sie ihre Anwendungen umgestalten. Sollte die Entscheidung fallen, dass die Unterstützung von ROX oder RWX erforderlich ist, gibt es mehrere Anbieter, die integriert werden können, z. B. Amazon Elastic File System (EFS) und Azure File Share.
Volumen Expansion
Im Laufe der Zeit kann eine Anwendung beginnen, ihr Volume zu füllen. Das kann eine Herausforderung sein, denn wenn das Volume durch ein größeres ersetzt wird, müssen die Daten migriert werden. Eine Lösung für dieses Problem ist die Unterstützung der Volume-Erweiterung. Aus der Perspektive eines Container-Orchestrators wie Kubernetes umfasst dies einige Schritte:
-
Fordere zusätzliche Speicherung beim Orchestrator an (z. B. über einen PersistentVolumeClaim).
-
Erweitere die Größe des Volumens über den Anbieter der Speicherung.
-
Erweitere das Dateisystem, um das größere Volumen zu nutzen.
Sobald dies geschehen ist, hat der Pod Zugriff auf den zusätzlichen Speicherplatz. Diese Funktion hängt von der Wahl unseres Speicher-Backends ab und davon, ob die Integration in Kubernetes die vorangegangenen Schritte erleichtern kann. Wir werden später in diesem Kapitel ein Beispiel für die Volumenerweiterung untersuchen.
Volumenbereitstellung
Es stehen dir zwei Bereitstellungsmodelle zur Verfügung: dynamische und statische Bereitstellung. Bei der statischen Bereitstellung wird davon ausgegangen, dass auf den Knoten Volumes erstellt werden, die von Kubernetes genutzt werden können. Bei der dynamischen Bereitstellung läuft ein Treiber innerhalb des Clusters und kann Speicheranforderungen von Workloads erfüllen, indem er mit einem Speicheranbieter kommuniziert. Von diesen beiden Modellen ist die dynamische Bereitstellung, wenn möglich, vorzuziehen. Oft hängt die Entscheidung zwischen den beiden Modellen davon ab, ob dein zugrunde liegendes Speichersystem einen kompatiblen Treiber für Kubernetes hat. Auf diese Treiber gehen wir später in diesem Kapitel ein.
Sicherung und Wiederherstellung
Die Datensicherung ist einer der komplexesten Aspekte der Speicherung, vor allem wenn eine automatische Wiederherstellung erforderlich ist. Im Allgemeinen ist ein Backup eine Kopie der Daten, die für den Fall eines Datenverlustes gespeichert wird. Normalerweise stimmen wir Backup-Strategien mit den Verfügbarkeitsgarantien unserer Speicherung ab. So sind Backups zwar immer wichtig, aber weniger wichtig, wenn unser Speichersystem eine Replikationsgarantie hat, bei der ein Hardwareverlust nicht zu einem Datenverlust führt. Eine weitere Überlegung ist, dass Anwendungen unterschiedliche Verfahren für die Sicherung und Wiederherstellung erfordern können. Die Vorstellung, dass wir ein Backup eines ganzen Clusters machen und es jederzeit wiederherstellen können, ist in der Regel eine naive Vorstellung oder zumindest eine, die viel technischen Aufwand erfordert, umsie zu erreichen.
Die Entscheidung, wer für die Sicherung und Wiederherstellung von Anwendungen verantwortlich sein soll, kann eine der schwierigsten Debatten in einem Unternehmen sein. Wiederherstellungsfunktionen als Plattformdienst anzubieten, kann ein "nice to have" sein. Sie kann jedoch aus den Nähten platzen, wenn es um anwendungsspezifische Komplexität geht - zum Beispiel, wenn eine Anwendung nicht neu gestartet werden kann und Aktionen ausgeführt werden müssen, die nur den Entwicklern bekannt sind.
Eine der beliebtesten Backup-Lösungen für den Kubernetes-Status und den Anwendungsstatus ist Project Velero. Velero kann Kubernetes-Objekte sichern, wenn du sie zwischen Clustern migrieren oder wiederherstellen möchtest. Außerdem unterstützt Velero die Planung von Volume-Snapshots. Wenn wir in diesem Kapitel tiefer in die Erstellung von Volume-Snapshots eintauchen, werden wir feststellen, dass die Möglichkeit, Snapshots zu planen und zu verwalten, nicht für uns erledigt ist. Vielmehr werden uns oft die Snapshot-Primitive zur Verfügung gestellt, aber wir müssen einen Orchestrierungsablauf um sie herum definieren. Und schließlich unterstützt Velero Backup- und Restore-Hooks. Diese ermöglichen es uns, Befehle im Container auszuführen, bevor wir ein Backup oder eine Wiederherstellung durchführen. So kann es zum Beispiel erforderlich sein, den Datenverkehr anzuhalten oder einen Flush auszulösen, bevor ein Backup erstellt wird. Dies ist mit den Hooks in Velero möglich.
Blockgeräte sowie Datei- und Objektspeicher
Die Arten der Speicherung, die unsere Anwendungen erwarten, sind der Schlüssel zur Auswahl der geeigneten zugrunde liegenden Speicherung und der Kubernetes-Integration. Die häufigste Art der Speicherung, die von Anwendungen genutzt wird, ist die Dateispeicherung. Bei der Speicherung von Dateien handelt es sich um ein Blockgerät mit einem Dateisystem darauf. So können Anwendungen in Dateien schreiben, wie wir es von jedem Betriebssystem her kennen.
Einem Dateisystem liegt ein Blockgerät zugrunde. Anstatt ein Dateisystem darauf aufzubauen, können wir das Gerät so anbieten, dass Anwendungen direkt mit dem Rohblock kommunizieren können. Dateisysteme verursachen beim Schreiben von Daten einen zusätzlichen Aufwand. In der modernen Softwareentwicklung ist der Overhead eines Dateisystems eher selten ein Problem. Wenn dein Anwendungsfall jedoch eine direkte Interaktion mit Raw-Block-Geräten rechtfertigt, können bestimmte Speichersysteme dies unterstützen.
Die letzte Speicherart ist die Objektspeicherung. Die Speicherung von Objekten unterscheidet sich von Dateien dadurch, dass es keine herkömmliche Hierarchie gibt. Die Speicherung von Objekten ermöglicht es Entwicklern, unstrukturierte Daten zu nehmen, ihnen eine eindeutige Kennung zu geben, sie mit Metadaten zu versehen und sie zu speichern. Cloud-Provider-Objektspeicher wie Amazon S3 sind zu beliebten Orten für Unternehmen geworden, um Bilder, Binärdateien und vieles mehr zu speichern. Diese Beliebtheit wurde durch die voll ausgestattete Web-API und die Zugriffskontrolle noch verstärkt. Die Interaktion mit Objektspeichern erfolgt in der Regel über die Anwendung selbst, wobei die Anwendung eine Bibliothek zur Authentifizierung und Interaktion mit dem Anbieter verwendet. Da es weniger standardisierte Schnittstellen für die Interaktion mit Objektspeichern gibt,sind sieseltener als Plattformdienste integriert, mit denen Anwendungen transparent interagieren können.
Flüchtige Daten
Auch wenn die Speicherung ein Maß an Persistenz voraussetzt, das über den Lebenszyklus eines Pods hinausgeht, gibt es durchaus Anwendungsfälle, in denen die Nutzung ephemerer Daten unterstützt wird. Container, die in ihr eigenes Dateisystem schreiben, verwenden standardmäßig eine ephemere Speicherung. Wenn der Container neu gestartet wird, geht diese Speicherung verloren. Der Volume-Typ emptyDir steht für eine ephemere Speicherung zur Verfügung, die resistent gegen Neustarts ist. Er ist nicht nur resistent gegen Container-Neustarts, sondern kann auch für die gemeinsame Nutzung von Dateien zwischen Containern im selben Pod verwendet werden.
Das größte Risiko bei ephemeren Daten besteht darin, dass deine Pods nicht zu viel von der Speicherkapazität des Hosts beanspruchen. Zahlen wie 4Gi pro Pod mögen nicht viel erscheinen, aber bedenke, dass ein Knoten Hunderte, in manchen Fällen sogar Tausende von Pods betreiben kann. Kubernetes unterstützt die Möglichkeit, die kumulative Menge an ephemerer Speicherung für Pods in einem Namensraum zu begrenzen. Die Konfiguration dieser Belange wird in Kapitel 12 behandelt.
Auswahl eines Anbieters für die Speicherung
Es gibt eine Vielzahl von Anbietern von Speicherlösungen, die du nutzen kannst. Die Optionen reichen von Speicherlösungen, die du selbst verwalten kannst, wie Ceph, bis hin zu vollständig verwalteten Systemen wie Google Persistent Disk oder Amazon Elastic Block Store. Die Vielfalt der Optionen würde den Rahmen dieses Buches sprengen. Wir empfehlen jedoch, die Fähigkeiten der Speichersysteme zu verstehen und zu wissen, welche dieser Fähigkeiten sich leicht in Kubernetes integrieren lassen. So kannst du herausfinden, wie gut eine Lösung im Vergleich zu einer anderen die Anforderungen deiner Anwendung erfüllen kann. Falls du dein eigenes Speichersystem verwaltest, solltest du nach Möglichkeit ein System verwenden, mit dem du bereits Erfahrung hast. Wenn du Kubernetes zusammen mit einem neuen Speichersystem einführst, bedeutet das für dein Unternehmen eine Menge neuer betrieblicher Komplexität.
Kubernetes Speicher Primitive
Kubernetes bietet von Haus aus mehrere Primitive zur Unterstützung der Speicherung von Workloads. Diese Primitive sind die Bausteine, die wir verwenden werden, um ausgefeilte Lösungen für die Speicherung anzubieten. In diesem Abschnitt werden wir PersistentVolumes, PersistentVolumeClaims und StorageClasses anhand eines Beispiels für die Zuweisung von schnellem, vorab bereitgestelltem Speicher für Container vorstellen.
Persistente Volumina und Claims
Volumes und Claims bilden die Grundlage der Speicherung in Kubernetes. Sie werden über die APIs PersistentVolume und PersistentVolumeClaim zugänglich gemacht. Die Ressource PersistentVolume stellt ein Kubernetes bekanntes Speichervolumen dar. Nehmen wir an, ein Administrator hat einen Knoten so vorbereitet, dass er 30 Gigabyte schnelle Speicherung auf dem Host bietet. Nehmen wir außerdem an, dass der Administrator diese Speicherung unter /mnt/fast-disk/pod-0 bereitgestellt hat. Um dieses Volume in Kuberneteszu repräsentieren, kann der Administrator ein PersistentVolume-Objekt erstellen:
apiVersion
:
v1
kind
:
PersistentVolume
metadata
:
name
:
pv0
spec
:
capacity
:
storage
:
30Gi
volumeMode
:
Filesystem
accessModes
:
-
ReadWriteOnce
storageClassName
:
local-storage
local
:
path
:
/mnt/fast-disk/pod-0
nodeAffinity
:
required
:
nodeSelectorTerms
:
-
matchExpressions
:
-
key
:
kubernetes.io/hostname
operator
:
In
values
:
-
test-w
Die Menge an Speicherung, die in diesem Band verfügbar ist. Wird verwendet, um festzustellen, ob ein Anspruch an dieses Volumen gebunden werden kann.
Gibt an, ob das Volume ein Blockgerät oder einDateisystem ist.
Gibt den Zugriffsmodus des Volumes an. Enthält
ReadWriteOnce
,ReadMany
undReadWriteMany
.Verknüpft dieses Volume mit einer Speicherklasse. Wird verwendet, um einen eventuellen Anspruch mit diesem Volume zu verbinden.
Gibt an, mit welchem Knoten dieses Volume verbunden werden soll.
Wie du sehen kannst, enthält das PersistentVolume Details zur Implementierung des Volumes. Um eine weitere Abstraktionsebene zu schaffen, wird ein PersistentVolumeClaim eingeführt, der sich je nach Anforderung an ein entsprechendes Volume bindet. In der Regel wird dies vom Anwendungsteam definiert, zu seinem Namespace hinzugefügt und von seinem Pod referenziert:
apiVersion
:
v1
kind
:
PersistentVolumeClaim
metadata
:
name
:
pvc0
spec
:
storageClassName
:
local-storage
accessModes
:
-
ReadWriteOnce
resources
:
requests
:
storage
:
30Gi
---
apiVersion
:
v1
kind
:
Pod
metadata
:
name
:
task-pv-pod
spec
:
volumes
:
-
name
:
fast-disk
persistentVolumeClaim
:
claimName
:
pvc0
containers
:
-
name
:
ml-processer
image
:
ml-processer-image
volumeMounts
:
-
mountPath
:
"
/var/lib/db
"
name
:
fast-disk
Prüft, ob ein Volume der Klasse
local-storage
mit dem ZugriffsmodusReadWriteOnce
vorliegt.Bindet an ein Volume mit einer Speicherung von >=
30Gi
.Erklärt diesen Pod zu einem Verbraucher des PersistentVolumeClaims.
Basierend auf den Einstellungen des PersistentVolumes nodeAffinity
wird der Pod automatisch auf dem Host eingeplant, auf dem dieses Volume verfügbar ist. Es ist keine zusätzliche Affinitätskonfiguration seitens des Entwicklers erforderlich.
Dieser Prozess hat einen sehr manuellen Ablauf gezeigt, wie Administratoren diese Speicherung den Entwicklern zur Verfügung stellen können. Wir bezeichnen dies als statische Provisionierung. Mit der richtigen Automatisierung könnte dies ein gangbarer Weg sein, um schnelle Festplatten auf Hosts für Pods verfügbar zu machen. Der Local Persistence Volume Static Provisioner kann zum Beispiel imCluster eingesetzt werden, um bereits zugewiesene Speicherung zu erkennen und sie automatisch als PersistentVolumes bereitzustellen. Er bietet auch einige Funktionen zur Verwaltung des Lebenszyklus, wie z. B. das Löschen von Daten bei der Zerstörung des PersistentVolumeClaims.
Warnung
Es gibt mehrere Möglichkeiten, eine lokale Speicherung zu erreichen, die dich zu einer schlechten Praxis verleiten können. So kann es zum Beispiel verlockend erscheinen, Entwicklern die Möglichkeit zu geben, hostPath zu verwenden, anstatt eine lokale Speicherung im Voraus bereitstellen zu müssen. hostPath
ermöglicht es, einen Pfad auf dem Host anzugeben, an den gebunden werden soll, anstatt ein PersistentVolume und PersistentVolumeClaim zu verwenden. Dies kann ein großes Sicherheitsrisiko darstellen, da es Entwicklern ermöglicht, sich an Verzeichnisse auf dem Host zu binden, was negative Auswirkungen auf den Host und andere Pods haben kann. Wenn du Entwicklern eine flüchtige Speicherung zur Verfügung stellen willst, die einen Neustart des Pods übersteht, aber nicht, wenn der Pod gelöscht oder auf einen anderen Knoten verschoben wird, kannst du EmptyDir verwenden. Dadurch wird die Speicherung in dem von Kube verwalteten Dateisystem zugewiesen und ist für den Pod transparent.
Klassen der Speicherung
In vielen Umgebungen ist es unrealistisch zu erwarten, dass die Knoten bereits im Voraus mit Festplatten und Volumes ausgestattet sind. In solchen Fällen ist oft eine dynamische Bereitstellung erforderlich, bei der die Volumes je nach den Bedürfnissen unserer Ansprüche zur Verfügung gestellt werden können. Um dieses Modell zu erleichtern, können wir unseren Entwicklern Speicherklassen zur Verfügung stellen. Diese werden mit der StorageClass API definiert. Angenommen, dein Cluster läuft in AWS und du möchtest Pods dynamisch EBS-Volumes zur Verfügung stellen, kann die folgende StorageClass hinzugefügt werden:
apiVersion
:
storage.k8s.io/v1
kind
:
StorageClass
metadata
:
name
:
ebs-standard
annotations
:
storageclass.kubernetes.io/is-default-class
:
true
provisioner
:
kubernetes.io/aws-ebs
parameters
:
type
:
io2
iopsPerGB
:
"
17
"
fsType
:
ext4
Der Name der StorageClass, auf die in Ansprüchen verwiesen werden kann.
Legt diese StorageClass als Standard fest. Wenn in einem Antrag keine Klasse angegeben ist, wird diese verwendet.
Verwendet den
aws-ebs
provisioner, um die Volumes auf der Grundlage von Ansprüchen zu erstellen.Provider-spezifische Konfiguration, wie Volumes bereitgestellt werden sollen.
Du kannst den Entwicklern eine Vielzahl von Speicheroptionen anbieten, indem du mehrere StorageClasses zur Verfügung stellst. Dazu gehört auch die Unterstützung von mehr als einem Anbieter in einem einzigen Cluster - zum Beispiel Ceph neben VMware vSAN. Du kannst auch verschiedene Stufen der Speicherung über denselben Anbieter anbieten. So könntest du zum Beispiel günstigere Speicherung neben teureren Optionen anbieten. Leider gibt es in Kubernetes keine granularen Kontrollen, um zu begrenzen, welche Klassen Entwickler/innen anfordern können. Die Kontrolle kann als validierende Zulassungskontrolle implementiert werden, die in Kapitel 8 behandelt wird.
Kubernetes bietet eine Vielzahl von Anbietern wie AWS EBS, Glusterfs, GCE PD, Ceph RBD und viele mehr. In der Vergangenheit wurden diese Anbieter baumintern implementiert. Das bedeutet, dass die Anbieter von Speicherungen ihre Logik in das Kubernetes-Kernprojekt implementieren mussten. Dieser Code wurde dann in die entsprechenden Kubernetes-Kontrollplankomponenten ausgeliefert.
Dieses Modell hatte mehrere Nachteile. Zum einen konnte der Anbieter der Speicherung nicht out of band verwaltet werden. Alle Änderungen am Provider mussten an ein Kubernetes-Release gebunden sein. Außerdem wurde jeder Kubernetes-Einsatz mit unnötigem Code ausgeliefert. Cluster, auf denen AWS läuft, hatten zum Beispiel immer noch den Provider-Code für die Interaktion mit GCE PDs. Es wurde schnell klar, dass es von großem Wert war, diese Provider-Integrationen auszulagern und die bauminternen Funktionen zu veralten. Die FlexVolume-Treiber waren eine Out-of-Tree-Implementierungsspezifikation, die dieses Problem ursprünglich lösen sollte. FlexVolumes wurde jedoch zugunsten unseres nächsten Themas, dem Container Storage Interface (CSI), in den Wartungsmodus versetzt.
Das Container Storage Interface (CSI)
Das Container Storage Interface ist die Antwort auf die Frage, wie wir Block- und Dateispeicher für unsere Workloads bereitstellen. Die Implementierungen von CSI werden als Treiber bezeichnet, die über das operative Wissen verfügen, um mit den Anbietern von Speicherungen zu kommunizieren. Diese Anbieter reichen von Cloud-Systemen wie Google Persistent Disks bis hin zu Speichersystemen (wie Ceph), die von dir eingesetzt und verwaltet werden. Die Treiber werden von den Anbietern der Speicherung in Projekten implementiert, die außerhalb des Baumes leben. Sie können vollständig außerhalb des Clusters verwaltet werden, in dem sie eingesetzt werden.
CSI-Implementierungen bestehen im Wesentlichen aus einem Controller-Plug-in und einem Node-Plug-in. Die Entwickler von CSI-Treibern haben viel Flexibilität bei der Implementierung dieser Komponenten. In der Regel werden die Controller- und Node-Plug-ins in derselben Binärdatei gebündelt und beide Modi über eine Umgebungsvariable wie X_CSI_MODE
aktiviert. Die einzigen Erwartungen sind, dass sich der Treiber beim Kubelet registriert und die Endpunkte der CSI-Spezifikation implementiert werden.
Der Controller-Dienst ist für die Verwaltung des Erstellens und Löschens von Volumes im Speicheranbieter zuständig. Diese Funktionalität umfasst auch (optionale) Funktionen wie die Erstellung von Volume-Snapshots und die Erweiterung von Volumes. Der Node-Dienst ist für die Vorbereitung der Volumes zuständig, die von den Pods auf dem Node genutzt werden sollen. Das bedeutet oft, dass er die Mounts einrichtet und Informationen über die Volumes auf dem Knoten meldet. Sowohl der Node- als auch der Controller-Dienst implementieren außerdem Identitätsdienste, die Informationen über die Plug-ins, deren Fähigkeiten und den Zustand des Plug-ins melden. Abbildung 4-1 zeigt eine Cluster-Architektur, in der diese Komponenten zum Einsatz kommen.
Schauen wir uns diese beiden Komponenten, den Controller und den Knoten, genauer an.
CSI-Controller
Der CSI-Controller-Dienst bietet APIs für die Verwaltung von Volumes in einer persistenten Speicherung. Die Kubernetes-Kontrollebene interagiert nicht direkt mit dem CSI-Controller-Dienst. Stattdessen reagieren die von der Kubernetes-Speicher-Community verwalteten Controller auf Kubernetes-Ereignisse und übersetzen sie in CSI-Anweisungen, wie z. B. CreateVolumeRequest, wenn ein neuer PersistentVolumeClaim erstellt wird. Da der CSI-Controller-Dienst seine APIs über UNIX-Sockets zur Verfügung stellt, werden die Controller normalerweise als Sidecars neben dem CSI-Controller-Dienst eingesetzt. Es gibt mehrere externe Controller, die sich alle unterschiedlich verhalten:
- external-provisioner
-
Wenn PersistentVolumeClaims erstellt werden, fordert dies die Erstellung eines Volumes beim CSI-Treiber an. Sobald das Volume in der Speicherung erstellt ist, erstellt dieser Provisioner ein PersistentVolume-Objekt in Kubernetes.
- extern-attacher
-
Überwacht die VolumeAttachment-Objekte, die angeben, dass ein Volume an einen Knoten angehängt oder von ihm abgetrennt werden soll. Sendet die Attachment- oder Detach-Anfrage an den CSI-Treiber.
- external-resizer
-
Erkennt Änderungen der Speichergröße in PersistentVolumeClaims. Sendet Anfragen zur Erweiterung an den CSI-Treiber.
- external-snapshotter
-
Wenn VolumeSnapshotContent-Objekte erstellt werden, werden Snapshot-Anfragen an den Treiber gesendet.
Hinweis
Bei der Implementierung von CSI-Plug-ins sind die Entwickler nicht verpflichtet, die oben genannten Controller zu verwenden. Ihre Verwendung wird jedoch empfohlen, um zu verhindern, dass die Logik in jedem CSI-Plug-in dupliziert wird.
CSI-Knoten
Das Node-Plug-in führt normalerweise denselben Treibercode aus wie das Controller-Plug-in. Im "Node-Modus" konzentriert es sich jedoch auf Aufgaben wie das Einbinden von angeschlossenen Volumes, die Einrichtung ihres Dateisystems und das Einbinden von Volumes in Pods. Diese Aufgaben werden über das Kubelet abgefragt. Neben dem Treiber sind häufig auch die folgenden Sidecars im Pod enthalten:
- node-driver-registrar
-
Sendet eine Registrierungsanfrage an das Kubelet, um es auf den CSI-Treiber aufmerksam zu machen.
- liveness-probe
Implementierung von Speicherung als Service
Wir haben nun die wichtigsten Überlegungen zur Speicherung von Anwendungen, die in Kubernetes verfügbaren Speicherprimitive und die Treiberintegration mithilfe der CSI behandelt. Jetzt ist es an der Zeit, diese Ideen zusammenzuführen und eine Implementierung zu entwickeln, die Entwicklern Speicherung als Service bietet. Wir wollen eine deklarative Möglichkeit bieten, Speicherung anzufordern und sie für Workloads verfügbar zu machen. Außerdem möchten wir dies dynamisch tun, d. h. wir möchten nicht, dass ein Administrator im Voraus Speicherplatz bereitstellen und zuweisen muss. Vielmehr möchten wir dies nach Bedarf und entsprechend den Anforderungen der Arbeitslasten tun.
Um mit dieser Implementierung zu beginnen, werden wir Amazon Web Services (AWS) nutzen. Dieses Beispiel ist mit der elastischen Block Speicherung von AWS integriert. Wenn du einen anderen Anbieter wählst, ist der Großteil dieses Inhalts immer noch relevant! Wir verwenden diesen Anbieter lediglich als konkretes Beispiel dafür, wie alle Teile zusammenpassen.
Als Nächstes befassen wir uns mit der Installation des Integrationstreibers, der Bereitstellung von Speicheroptionen für Entwickler, der Nutzung des Speichers mit Workloads, der Größenänderung von Volumes und der Erstellung von Volume-Snapshots.
Installation
Die Installation ist ein ziemlich einfacher Prozess, der aus zwei wichtigen Schritten besteht:
-
Konfiguriere den Zugang zum Anbieter.
-
Setze die Treiberkomponenten im Cluster ein.
Der Anbieter, in diesem Fall AWS, wird verlangen, dass sich der Fahrer identifiziert, um sicherzustellen, dass er den richtigen Zugang hat. In diesem Fall stehen uns drei Möglichkeiten zur Verfügung. Eine ist, das Instanzprofil der Kubernetes-Knoten zu aktualisieren. Dadurch müssen wir uns keine Gedanken über die Anmeldeinformationen auf Kubernetes-Ebene machen, erhalten aber universelle Berechtigungen für Arbeitslasten, die die AWS-API erreichen können. Die zweite und wahrscheinlich sicherste Option ist die Einführung eines Identitätsdienstes, der IAM-Berechtigungen für bestimmte Arbeitslasten bereitstellen kann. Ein Projekt, das ein Beispiel dafür ist, ist kiam. Dieser Ansatz wird in Kapitel 10 behandelt. Schließlich kannst du Anmeldedaten in einem Geheimnis hinzufügen, das in den CSI-Treiber eingebunden wird. In diesem Modell würde das Geheimnis wie folgt aussehen:
apiVersion
:
v1
kind
:
Secret
metadata
:
name
:
aws-secret
namespace
:
kube-system
stringData
:
key_id
:
"AKIAWJQHICPELCJVKYNU"
access_key
:
"jqWi1ut4KyrAHADIOrhH2Pd/vXpgqA9OZ3bCZ"
Warnung
Dieses Konto hat Zugriff auf die Manipulation einer zugrunde liegenden Speicherung. Der Zugriff auf dieses Geheimnis sollte sorgfältig verwaltet werden. In Kapitel 7 findest du weitere Informationen.
Mit dieser Konfiguration können die CSI-Komponenten installiert werden. Zunächst wird der Controller als Deployment installiert. Wenn er mehrere Replikate betreibt, bestimmt er per Leader-Election, welche Instanz aktiv sein soll. Dann wird das Node-Plug-in in Form eines DaemonSets installiert, das auf jedem Knoten einen Pod ausführt. Nach der Initialisierung registrieren sich die Instanzen des Node-Plug-ins bei ihren Kubelets. Das Kubelet meldet dann den CSI-fähigen Knoten, indem es ein CSINode-Objekt für jeden Kubernetes-Knoten erstellt. Die Ausgabe eines Clusters mit drei Knoten sieht folgendermaßen aus:
$
kubectl get csinode NAME DRIVERS AGE ip-10-0-0-205.us-west-2.compute.internal1
97m ip-10-0-0-224.us-west-2.compute.internal1
79m ip-10-0-0-236.us-west-2.compute.internal1
98m
Wie wir sehen können, sind drei Knoten aufgelistet und auf jedem Knoten ist ein Treiber registriert. Wenn wir die YAML eines CSINode untersuchen, sehen wir Folgendes:
apiVersion
:
storage.k8s.io/v1
kind
:
CSINode
metadata
:
name
:
ip-10-0-0-205.us-west-2.compute.internal
spec
:
drivers
:
-
allocatable
:
count
:
25
name
:
ebs.csi.aws.com
nodeID
:
i-0284ac0df4da1d584
topologyKeys
:
-
topology.ebs.csi.aws.com/zone
Die maximale Anzahl von Volumes, die auf diesem Knoten erlaubt sind.
Wenn ein Knoten für einen Workload ausgewählt wird, wird dieser Wert in der CreateVolumeRequest übergeben, damit der Treiber weiß, wo er das Volume erstellen muss. Dies ist wichtig für Speichersysteme, bei denen die Knoten im Cluster nicht auf dieselbe Speicherung zugreifen können. Wenn zum Beispiel in AWS ein Pod in einer Verfügbarkeitszone geplant wird, muss das Volume in derselben Zone erstellt werden.
Außerdem ist der Treiber offiziell beim Cluster registriert. Die Details sind unter im CSIDriver-Objekt zu finden:
apiVersion
:
storage.k8s.io/v1
kind
:
CSIDriver
metadata
:
name
:
aws-ebs-csi-driver
labels
:
app.kubernetes.io/name
:
aws-ebs-csi-driver
spec
:
attachRequired
:
true
podInfoOnMount
:
false
volumeLifecycleModes
:
-
Persistent
Der Name des Anbieters, der diesen Treiber repräsentiert. Dieser Name ist an die Klasse(n) der Speicherung gebunden, die wir den Nutzern der Plattform anbieten.
Legt fest, dass ein Anhängevorgang abgeschlossen sein muss, bevor Volumeseingehängt werden.
Die Pod-Metadaten müssen beim Einrichten eines Mounts nicht als Kontext übergeben werden.
Das Standardmodell für die Bereitstellung von persistenten Volumes. Die Inline-Unterstützung kann aktiviert werden, indem diese Option auf
Ephemeral
gesetzt wird. Im ephemeren Modus wird erwartet, dass die Speicherung nur so lange dauert wie der Pod.
Die Einstellungen und Objekte, die wir bisher untersucht haben, sind Artefakte unseres Bootstrapping-Prozesses. Das CSIDriver-Objekt erleichtert die Erkennung von Treiberdetails und wurde in das Deployment-Bundle des Treibers aufgenommen. Die CSINode-Objekte werden von dem Kubelet verwaltet. Ein generisches Registrar-Sidecar ist im Pod des Node-Plug-ins enthalten, das die Daten des CSI-Treibers abruft und den Treiber beim Kubelet registriert. Das Kubelet meldet dann die Anzahl der verfügbaren CSI-Treiber auf jedem Host. Abbildung 4-2 veranschaulicht diesen Bootstrapping-Prozess.
Speicheroptionen aufdecken
Um Entwicklern Speicheroptionen zur Verfügung stellen zu können, müssen wir StorageClasses erstellen. Für dieses Szenario nehmen wir an, dass es zwei Arten von Speicherung gibt, die wir anbieten möchten.Die erste Option ist eine billige Festplatte, die für die Persistenz von Workloads verwendet werden kann. In vielen Fällen brauchen Anwendungen keine SSD, da sie nur einige Dateien speichern, die nicht schnell gelesen und geschrieben werden müssen. In diesem Fall wird die billige Festplatte (HDD) die Standardoption sein. Dann möchten wir eine schnellere SSD mit einer individuell konfigurierten IOPS pro Gigabyte anbieten. Tabelle 4-1 zeigt unsere Angebote; die Preise spiegeln die AWS-Kosten zum Zeitpunkt der Erstellung dieses Artikels wider.
Name des Anbieters | Art der Speicherung | Maximaler Durchsatz pro Volumen | AWS-Kosten |
---|---|---|---|
Standard-Block |
HDD (optimiert) |
40-90 MB/s |
0,045 $ pro GB pro Monat |
Leistungs-Block |
SSD (io1) |
~1000 MB/s |
0,125 $ pro GB pro Monat + 0,065 $ pro bereitgestelltem IOPS pro Monat |
Um diese Angebote zu erstellen, legen wir für jedes eine Speicherklasse an. In jeder Speicherklasse gibt es ein Feld parameters
. Hier können wir Einstellungen vornehmen, die den Merkmalen in Tabelle 4-1 entsprechen.
kind
:
StorageClass
apiVersion
:
storage.k8s.io/v1
metadata
:
name
:
default-block
annotations
:
storageclass.kubernetes.io/is-default-class
:
"
true
"
provisioner
:
ebs.csi.aws.com
allowVolumeExpansion
:
true
volumeBindingMode
:
WaitForFirstConsumer
parameters
:
type
:
st1
---
kind
:
StorageClass
apiVersion
:
storage.k8s.io/v1
metadata
:
name
:
performance-block
provisioner
:
ebs.csi.aws.com
parameters
:
type
:
io1
iopsPerGB
:
"
20
"
Dies ist der Name der Speicherung, die wir den Nutzern der Plattform anbieten. Es wird von PeristentVolumeClaims referenziert.
Damit wird das Angebot als Standard festgelegt. Wenn ein PersistentVolumeClaim ohne Angabe einer StorageClass erstellt wird, wird
default-block
verwendet.Mapping, zu dem der CSI-Treiber ausgeführt werden soll.
Erlaube die Erweiterung der Volumengröße durch Änderungen an einem PersistentVolumeClaim.
Stelle das Volume erst bereit, wenn ein Pod den PersistentVolumeClaim verbraucht hat. So wird sichergestellt, dass das Volume in der entsprechenden Verfügbarkeitszone des geplanten Pods erstellt wird. Außerdem wird so verhindert, dass verwaiste PVCs Volumes in AWS erstellen, die dir dann in Rechnung gestellt werden.
Gibt an, welche Art von Speicherung der Treiber erwerben soll, um Forderungen zu erfüllen.
Speicherung verbrauchen
Damit sind wir bereit für die Nutzung dieser verschiedenen Klassen von Speicherung durch die Nutzer. Zunächst schauen wir uns an, wie der Entwickler die Speicherung anfordert. Dann gehen wir die internen Abläufe durch, wie sie erfüllt werden. Schauen wir uns zunächst an, was ein Entwickler erhält, wenn er die verfügbaren StorageClasses auflistet:
$
kubectl get storageclasses.storage.k8s.io NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE default-block(
default)
ebs.csi.aws.com Delete Immediate performance-block ebs.csi.aws.com Delete WaitForFirstConsumer ALLOWVOLUMEEXPANSIONtrue
true
Warnung
Indem wir es Entwicklern ermöglichen, PVCs zu erstellen, erlauben wir ihnen, auf jede beliebige StorageClass zu verweisen. Wenn das problematisch ist, solltest du die Validating Admission Control einführen, um zu prüfen, ob die Anfragen angemessen sind. Dieses Thema wird inKapitel 8 behandelt.
Nehmen wir an, der Entwickler möchte eine günstigere HDD und eine leistungsfähigere SSD für eine Anwendung zur Verfügung stellen. In diesem Fall werden zwei PersistentVolumeClaims erstellt. Wir nennen sie pvc0
und pvc1
:
apiVersion
:
v1
kind
:
PersistentVolumeClaim
metadata
:
name
:
pvc0
spec
:
resources
:
requests
:
storage
:
11Gi
---
apiVersion
:
v1
kind
:
PersistentVolumeClaim
metadata
:
name
:
pvc1
spec
:
resources
:
requests
:
storage
:
14Gi
storageClassName
:
performance-block
Dabei wird die Standard-Speicherklasse (
default-block
) verwendet und andere Standardeinstellungen wie RWO und die Art der Speicherung im Dateisystem übernommen.Stelle sicher, dass
performance-block
an den Treiber angefordert wird und nichtdefault-block
.
Basierend auf den StorageClass-Einstellungen zeigen diese beiden ein unterschiedliches Bereitstellungsverhalten. Die performante Speicherung (von pvc1
) wird als unverbundenes Volume in AWS erstellt. Dieses Volume kann schnell angeschlossen werden und ist sofort einsatzbereit. Die standardmäßige Speicherung (von pv0
) befindet sich in einem Pending
Zustand, in dem der Cluster wartet, bis ein Pod das PVC verbraucht, um Speicher in AWS bereitzustellen. Dies erfordert zwar mehr Arbeit bei der Bereitstellung, wenn ein Pod den Claim schließlich verbraucht, aber die ungenutzte Speicherung wird dir nicht in Rechnung gestellt! Die Beziehung zwischen dem Claim in Kubernetes und dem Volumen in AWS ist in Abbildung 4-3 zu sehen.
Nehmen wir nun an, der Entwickler erstellt zwei Pods. Ein Pod verweist auf pv0
und der andere auf pv1
. Sobald jeder Pod auf einem Node geplant ist, wird das Volume an diesen Node angehängt, um verbraucht zu werden. Für pv0
wird das Volume vorher in AWS erstellt. Wenn die Pods geplant und die Volumes angehängt sind, wird ein Dateisystem eingerichtet und die Speicherung in den Container eingehängt. Da es sich um persistente Volumes handelt, haben wir jetzt ein Modell eingeführt, bei dem das Volume mitgenommen werden kann, selbst wenn der Pod auf einen anderen Knoten verschoben wird. Abbildung 4-4 zeigt, wie wir die Self-Service-Anfrage für die Speicherung erfüllt haben.
Hinweis
Ereignisse sind besonders hilfreich bei der Fehlersuche im Zusammenspiel von Speicherung und CSI. Da Provisioning, Attachment und Mounting allesamt stattfinden, um eine PVC zu erfüllen, solltest du dir die Ereignisse zu diesen Objekten ansehen, da die verschiedenen Komponenten berichten, was sie getan haben. kubectl describe -n $NAMESPACE pvc $PVC_NAME
ist eine einfache Möglichkeit, diese Ereignisse zu sehen.
Größenänderung
Die Größenänderung ist eine unterstützte Funktion im aws-ebs-csi-driver
. In den meisten CSI-Implementierungen wird der external-resizer
Controller verwendet, um Änderungen in PersistentVolumeClaim-Objekten zu erkennen. Wenn eine Größenänderung erkannt wird, wird sie an den Treiber weitergeleitet, der das Volume dann vergrößert. In diesem Fall erleichtert der Treiber, der im Controller-Plug-in läuft, die Erweiterung mit der AWS EBS API.
Sobald das Volume in EBS erweitert wurde, ist der neue Speicherplatz nicht sofort für den Container nutzbar. Das liegt daran, dass das Dateisystem immer noch nur den ursprünglichen Speicherplatz belegt. Damit das Dateisystem erweitert werden kann, müssen wir darauf warten, dass die Treiberinstanz des Node Plug-ins das Dateisystem erweitert. Dies kann geschehen , ohne den Pod zu beenden. Die Erweiterung des Dateisystems ist in den folgenden Protokollen des CSI-Treibers des Node-Plug-ins zu sehen:
mount_linux.go: Attempting to determine if disk "/dev/nvme1n1" is formatted using blkid with args: ([-p -s TYPE -s PTTYPE -o export /dev/nvme1n1]) mount_linux.go: Output: "DEVNAME=/dev/nvme1n1\nTYPE=ext4\n", err: <nil> resizefs_linux.go: ResizeFS.Resize - Expanding mounted volume /dev/nvme1n1 resizefs_linux.go: Device /dev/nvme1n1 resized successfully
Warnung
Kubernetes unterstützt die Verkleinerung des Größenfelds eines PVCs nicht. Wenn der CSI-Treiber keine Umgehung für dieses Problem bereitstellt, kannst du die Größe nicht verringern, ohne ein Volume neu zu erstellen. Behalte dies im Hinterkopf, wenn du Volumes vergrößerst.
Schnappschüsse
Um regelmäßige Backups der von Containern genutzten Volume-Daten zu ermöglichen, gibt es die Snapshot-Funktion. Diese Funktion ist oft in zwei Controller unterteilt, die für zwei verschiedene CRDs zuständig sind. Zu den CRDs gehören VolumeSnapshot und VolumeContentSnapshot. Auf einer höheren Ebene ist VolumeSnapshot für den Lebenszyklus von Volumes zuständig. Basierend auf diesen Objekten werden VolumeContentSnapshots vom external-snapshotter Controller verwaltet. Dieser Controller wird in der Regel als Sidecar im Controller-Plugin der CSI ausgeführt und leitet die Anfragen an den Treiber weiter.
Hinweis
Zum Zeitpunkt der Erstellung dieses Dokuments sind diese Objekte als CRDs implementiert und nicht als Kernobjekte der Kubernetes-API. Dies erfordert, dass der CSI-Treiber oder die Kubernetes-Distribution die CRD-Definitionen im Voraus bereitstellt.
Ähnlich wie bei der Speicherung über StorageClasses wird auch die Erstellung von Snapshots durch die Einführung einer Snapshot-Klasse ermöglicht. Die folgende YAML stellt diese Klasse dar:
apiVersion
:
snapshot.storage.k8s.io/v1beta1
kind
:
VolumeSnapshotClass
metadata
:
name
:
default-snapshots
driver
:
ebs.csi.aws.com
deletionPolicy
:
Delete
An welchen Treiber die Snapshot-Anfrage delegiert werden soll.
Ob der VolumeSnapshotContent gelöscht werden soll, wenn der VolumeSnapshot gelöscht wird. In der Tat könnte das eigentliche Volume gelöscht werden (je nach Unterstützung durch den Anbieter).
Im Namespace der Anwendung und PersistentVolumeClaim kann ein VolumeSnapshot erstellt werden. Ein Beispiel lautet wie folgt:
apiVersion
:
snapshot.storage.k8s.io/v1beta1
kind
:
VolumeSnapshot
metadata
:
name
:
snap1
spec
:
volumeSnapshotClassName
:
default-snapshots
source
:
persistentVolumeClaimName
:
pvc0
Gibt die Klasse an, die der Treiber verwenden soll.
Gibt den Volumenanspruch an, der das Volumen für den Snapshot angibt.
Die Existenz dieses Objekts informiert über die Notwendigkeit, ein VolumeSnapshotContent-Objekt zu erstellen. Dieses Objekt hat einen clusterweiten Geltungsbereich. Die Erkennung eines VolumeSnapshotContent-Objekts löst eine Anfrage zur Erstellung eines Snapshots aus, die der Treiber durch Kommunikation mit AWS EBS erfüllt. Sobald dies geschehen ist, meldet der VolumeSnapshot ReadyToUse. Abbildung 4-5 veranschaulicht die Beziehung zwischen den verschiedenen Objekten.
Mit einem Snapshot können wir ein Szenario für einen Datenverlust untersuchen. Unabhängig davon, ob das ursprüngliche Volume versehentlich gelöscht wurde, einen Defekt hatte oder durch das versehentliche Löschen eines PersistentVolumeClaims entfernt wurde, können wir die Daten wiederherstellen. Dazu wird ein neuer PersistentVolumeClaim mit der Angabe spec.dataSource
erstellt. dataSource
unterstützt den Verweis auf einen VolumeSnapshot, mit dem die Daten in den neuen Claim eingefügt werden können. Das folgende Manifest stellt die Daten aus dem zuvor erstellten Snapshot wieder her:
apiVersion
:
v1
kind
:
PersistentVolumeClaim
metadata
:
name
:
pvc-reclaim
spec
:
accessModes
:
-
ReadWriteOnce
storageClassName
:
default-block
resources
:
requests
:
storage
:
600Gi
dataSource
:
name
:
snap1
kind
:
VolumeSnapshot
apiGroup
:
snapshot.storage.k8s.io
Sobald der Pod neu erstellt wird, um diesen neuen Anspruch zu referenzieren, kehrt der letzte Snapshot-Status in den Container zurück! Jetzt haben wir Zugang zu allen Grundlagen, um eine robuste Backup- und Recovery-Lösung zu erstellen. Die Lösungen reichen von der Planung von Snapshots über einen CronJob, über das Schreiben eines eigenen Controllers bis hin zur Verwendung von Tools wie Velero, um Kubernetes-Objekte zusammen mit Datenvolumina nach einem Zeitplan zu sichern.
Zusammenfassung
In diesem Kapitel haben wir uns mit einer Reihe von Themen rund um die Speicherung von Containern beschäftigt. Zunächst müssen wir die Anforderungen der Anwendung genau kennen, um unsere technischen Entscheidungen zu treffen. Dann müssen wir sicherstellen, dass unser Anbieter für die Speicherung diese Anforderungen erfüllen kann und dass wir (bei Bedarf) über das nötige Fachwissen für den Betrieb verfügen. Schließlich sollten wir eine Integration zwischen dem Orchestrator und dem Speichersystem herstellen, um sicherzustellen, dass die Entwickler die benötigte Speicherung erhalten, ohne sich mit dem zugrunde liegenden Speichersystem auskennen zu müssen.
Get Produktion Kubernetes now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.