L'association tardive du stockage dynamique vous permet d'injecter des volumes d'agent Filestore directement dans les pods Google Kubernetes Engine (GKE) Agent Sandbox préchauffés et en cours d'exécution lors de la revendication. En contournant le cycle de vie standard de l'association de volumes Kubernetes, l'association tardive dynamique permet d'obtenir une latence d'association de stockage inférieure à 100 ms sans nécessiter de redémarrage des pods.
Cette architecture permet aux plates-formes d'agents à haute densité et à faible latence de :
- Éliminez les délais de démarrage à froid des pods et d'initialisation des conteneurs.
- Associez et dissociez dynamiquement des espaces de travail persistants à la demande.
- Mettez en veille ou en hibernation les sessions d'agent inactives, puis reprenez-les sur n'importe quel pod de bac à sable préchauffé disponible tout en conservant l'état du système de fichiers.
Avant de commencer
- Effectuez la configuration initiale dans Configurer l'environnement GKE pour les volumes d'agent Filestore.
- Vérifiez que votre cluster GKE exécute la version
1.36.0-gke.3302001ou ultérieure. Cette version est compatible avec l'annotationforce-sharedrequise pour la propagation du montageemptyDirgVisor. - Vérifiez que votre
volume-pool-scStorageClassspécifievolumeBindingMode: ImmediateetreclaimPolicy: Delete.
Présentation de l'architecture
L'architecture à liaison tardive se compose de quatre composants :
- Orchestrateur de plate-forme ou contrôleur personnalisé : service de plan de contrôle ou contrôleur Kubernetes qui gère les cycles de vie des sessions. Il surveille les événements
SandboxClaim, résout les métadonnées de volume du locataire, appelle l'API du démon du nœud de stockage pour associer ou dissocier le stockage, et gère les finaliseurs de suppression. - Daemon de nœud de stockage :
DaemonSetprivilégié s'exécutant sur chaque nœud gVisor et exposant une API de montage. SandboxTemplateavecforce-shared: modèle de pod de bac à sable gVisor qui permet aux montages hôtes de se propager de manière dynamique dans le conteneur de bac à sable gVisor.SandboxWarmPool: pool de pods bac à sable préchauffés et en cours d'exécution, prêts à recevoir des demandes de montage instantanément lors de la revendication.
Déployer le daemon du nœud de stockage
Créez un fichier manifeste nommé storage-node-daemon.yaml contenant le DaemonSet privilégié :
Appliquez le fichier manifeste :
kubectl apply -f storage-node-daemon.yaml
Déployer SandboxTemplate et SandboxWarmPool
Créez un fichier manifeste nommé sandbox-latebind.yaml contenant le modèle et le pool de préchauffage :
Appliquez le fichier manifeste :
kubectl apply -f sandbox-latebind.yaml
Revendiquer un bac à sable et associer dynamiquement le stockage
Créez un fichier manifeste de revendication nommé
late-bind-claim.yamlqui inclut un finaliseur de suppression (agent.sandbox/storage-cleanup) :apiVersion: extensions.agents.x-k8s.io/v1alpha1 kind: SandboxClaim metadata: name: late-bind-session-1 namespace: default finalizers: - agent.sandbox/storage-cleanup spec: sandboxTemplateRef: name: late-bind-templateAppliquez la revendication :
kubectl apply -f late-bind-claim.yaml
Provisionnez dynamiquement un volume à l'aide d'un fichier manifeste PVC nommé
agent-volume-pvc.yaml:apiVersion: v1 kind: PersistentVolumeClaim metadata: name: session-1-pvc namespace: default spec: accessModes: [ReadWriteMany] storageClassName: volume-pool-sc resources: requests: storage: 1GiAppliquez la PVC :
kubectl apply -f agent-volume-pvc.yaml
Récupérez l'UID du pod attribué, le nœud et les détails d'exportation du PV de sauvegarde :
POD_NAME=$(kubectl get pods \ -l extensions.agents.x-k8s.io/claimed-by=late-bind-session-1 \ -o jsonpath='{.items[0].metadata.name}') POD_UID=$(kubectl get pod "${POD_NAME}" -o jsonpath='{.metadata.uid}') NODE_NAME=$(kubectl get pod "${POD_NAME}" -o jsonpath='{.spec.nodeName}') PV_NAME=$(kubectl get pvc session-1-pvc -o jsonpath='{.spec.volumeName}') NFS_IP=$(kubectl get pv "${PV_NAME}" \ -o jsonpath='{.spec.csi.volumeAttributes.ip}') NFS_PATH="/$(kubectl get pv "${PV_NAME}" \ -o jsonpath='{.spec.csi.volumeAttributes.volume}')"Envoyez le signal de montage au daemon de nœud sur l'hôte du pod :
DAEMON_POD=$(kubectl get pods -l app=storage-node-daemon \ --field-selector spec.nodeName="${NODE_NAME}" \ -o jsonpath='{.items[0].metadata.name}') kubectl exec "${DAEMON_POD}" -c daemon -- python3 -c " import urllib.request, json payload = json.dumps({ 'action': 'bind_nfs', 'pod_uid': '${POD_UID}', 'volume_name': 'workspace-volume', 'sub_dir': 'user_data', 'nfs_server': '${NFS_IP}', 'nfs_path': '${NFS_PATH}' }).encode() req = urllib.request.Request( 'http://localhost:9090', data=payload, headers={'Content-Type': 'application/json'}) print(urllib.request.urlopen(req).read().decode()) "Appliquez un finaliseur de suppression au pod revendiqué pour éviter un nettoyage prématuré tant que le montage de l'hôte est actif :
kubectl patch pod "${POD_NAME}" --type=merge \ -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'Vérifiez le montage du volume dans le pod en cours d'exécution :
kubectl logs "${POD_NAME}" -c agentLe résultat confirme que le volume a bien été installé :
Waiting for late-bind signal... Filestore volume mounted successfully! drwxr-xr-x 2 1000 1000 4096 ... user_data
Mettre en pause et reprendre une session
Lorsqu'une session d'agent se termine ou entre en hibernation, votre orchestrateur doit démonter le stockage hôte avant d'autoriser Kubernetes à arrêter ou recycler le pod.
Lancez la suppression de
SandboxClaimen arrière-plan. En raison du finaliseur, Kubernetes marque la revendication pour suppression, mais suspend l'arrêt du pod :kubectl delete sandboxclaim late-bind-session-1 --wait=false
Démontez le partage NFS sur le nœud hôte :
kubectl exec "${DAEMON_POD}" -c daemon -- python3 -c " import urllib.request, json payload = json.dumps({ 'action': 'unbind', 'pod_uid': '${POD_UID}', 'volume_name': 'workspace-volume', 'sub_dir': 'user_data' }).encode() req = urllib.request.Request( 'http://localhost:9090', data=payload, headers={'Content-Type': 'application/json'}) print(urllib.request.urlopen(req).read().decode()) "Supprimez les finaliseurs de
SandboxClaimet du pod pour terminer l'arrêt :kubectl patch sandboxclaim late-bind-session-1 --type=merge \ -p '{"metadata":{"finalizers":[]}}' kubectl patch pod "${POD_NAME}" --type=merge \ -p '{"metadata":{"finalizers":[]}}'
Pour reprendre la session ultérieurement, revendiquez un nouveau pod préprovisionné et envoyez la requête d'association à l'aide de la PVC existante (session-1-pvc). Le nouveau pod de bac à sable accède immédiatement à l'état de l'espace de travail conservé.
Considérations relatives à la production et conception de contrôleurs personnalisés
Les commandes manuelles de ce document illustrent les mécanismes de bas niveau de la liaison tardive dynamique. Pour exécuter cette architecture de manière fiable en production, vous devez développer un contrôleur Kubernetes personnalisé ou un orchestrateur de plate-forme adapté au cycle de vie des sessions de votre application.
Lorsque vous concevez votre contrôleur de production et votre daemon de nœud, implémentez les modèles architecturaux suivants :
Automatiser le cycle de vie du rapprochement et du finaliseur
Étant donné que le daemon du nœud de stockage effectue des installations au niveau de l'hôte en dehors de la gestion standard du cycle de vie de l'interface CSI (Container Storage Interface) de Kubernetes, Kubelet n'a pas connaissance des installations actives dans le emptyDir du pod. Si un pod est supprimé alors que le montage est actif, Kubelet ne parvient pas à supprimer le répertoire emptyDir et génère une erreur Device or resource busy, ce qui laisse le pod bloqué dans un état Terminating.
Votre contrôleur personnalisé doit automatiser une machine à états stricte à l'aide de finaliseurs (tels que agent.sandbox/storage-cleanup) :
- Création et liaison des revendications :
- Associez un finaliseur statique à chaque
SandboxClaimlors de sa création. - Surveillez l'API Kubernetes pour obtenir des informations sur l'état de
SandboxClaim. Lorsqu'une revendication est associée à un pod de pool chaud, extrayez les attributspod_uid,nodeNameet de volume Filestore de sauvegarde qui lui sont attribués. - Envoyez une requête
bindauthentifiée au daemon du nœud de stockage s'exécutant sur le nœud cible. - Corrigez immédiatement l'objet
Poden cours d'exécution pour ajouter le finaliseur dynamique. Étant donné que les spécificationsSandboxTemplatene sont pas compatibles avec les finalizers de pods statiques, il est nécessaire de corriger le pod de manière dynamique pour le protéger lors des événements de vidange ou de reprogrammation de nœuds où le pod est expulsé, mais oùSandboxClaimreste actif.
- Associez un finaliseur statique à chaque
- Gestion de l'arrêt progressif et de l'expulsion :
- Surveillez
deletionTimestampsur les ressourcesSandboxClaimetPod. - Lorsqu'une suppression ou une expulsion est détectée, appelez le point de terminaison
unbinddu daemon de nœud pour démonter proprement le répertoire hôte (umount -l). - Vérifiez que le démontage a réussi et que toutes les écritures en attente ont été vidées avant de corriger
PodetSandboxClaimpour supprimer leurs finaliseurs. Cela peut vous aider à effectuer un arrêt propre lors de la suppression de revendications normales, des mises à niveau de nœuds GKE, des préemptions de VM Spot et des arrêts pour mémoire insuffisante (OOM).
- Surveillez
Sécuriser et renforcer le démon du nœud de stockage
- Remplacez
kubectl execpar des API authentifiées : en production, n'utilisez paskubectl execni liez le démon àlocalhost. Configurez le daemon du nœud de stockage pour exposer un point de terminaison gRPC ou HTTPS dédié sur le réseau du cluster sécurisé avec l'authentification par jetonServiceAccountKubernetes ou TLS mutuel (mTLS). - Isoler les espaces de noms de démon et l'accès au réseau : déployez le
storage-node-daemonDaemonSetprivilégié dans un espace de noms administratif restreint (par exemple,sandbox-storage-system) plutôt que dans les espaces de nomsdefaultou de locataire. Appliquez des règles KubernetesNetworkPolicyqui autorisent l'entrée vers l'API du démon exclusivement à partir de vos pods de contrôleur personnalisés et bloquent tout le trafic provenant des pods d'agent en bac à sable. - Utilisez des images de conteneur préconçues : évitez d'installer des packages comme
nfs-commonlors de l'exécution dans uninitContainer. Utilisez une image de conteneur immuable et préconfigurée avec tous les utilitaires de montage requis préinstallés pour éliminer les retards de démarrage des nœuds et les dépendances de dépôt externes.
Appliquer des quotas de stockage et l'isolation multilocataire
- Surveiller l'utilisation de l'espace de stockage par agent : lorsque vous effectuez un montage par liaison dynamique d'un sous-répertoire à partir d'un volume Filestore
ReadWriteMany(RWX) partagé dans unemptyDir, les paramètresemptyDir.sizeLimitKubernetes standards ne peuvent pas appliquer de quotas de stockage par agent sur le chemin NFS monté. Pour éviter qu'un agent unique et incontrôlable n'épuise le volume partagé et ne provoque un déni de service (DoS), implémentez la surveillance des quotas de répertoire dans votre orchestrateur ou provisionnez des volumes dédiés à l'aide de pools de volumes. - Adapter les charges utiles de montage pour différents modes d'accès à l'espace de travail : votre contrôleur peut prendre en charge plusieurs topologies de stockage d'agent en faisant varier les paramètres envoyés au daemon de nœud :
- Espaces de travail privés et isolés : associez un sous-répertoire de locataire unique ou un PVC dédié à un seul pod de bac à sable avec des autorisations de lecture et d'écriture.
- Espaces de travail collaboratifs : liez simultanément le même sous-répertoire RWX partagé à plusieurs pods d'agents coordonnés pour le partage de fichiers en temps réel.
- Espaces de travail de branching d'exploration : montez un répertoire de modèle de base en lecture seule (
ro) afin que les agents puissent lire les composants partagés sans modifier la copie de référence, tout en acheminant les nouvelles écritures vers un chemin d'accès temporaire inscriptible distinct ou un répertoire de copie lors de la restauration.
Coordonner les instantanés à un moment précis et le nettoyage
- Atteindre l'état de quiescence en écriture avant les instantanés : pour capturer des instantanés d'espace de travail cohérents à un moment précis sans corruption de données, votre orchestrateur doit suspendre les écritures actives en lançant le workflow de dissociation (ou en vidant les tampons du système de fichiers) avant d'archiver le répertoire de l'espace de travail ou de déclencher un instantané Filestore.
- Automatisez le deprovisioning des locataires : lorsqu'une session utilisateur ou un espace de travail expirent définitivement, assurez-vous que votre contrôleur supprime d'abord tous les montages actifs sur tous les nœuds avant d'exécuter des tâches en arrière-plan asynchrones pour supprimer les répertoires persistants du locataire du volume de stockage.
Pour obtenir une implémentation de référence complète montrant la gestion dynamique des finaliseurs, l'isolation multitenant et les workflows de restauration des instantanés, consultez l'exemple de stockage à liaison tardive GKE Sandbox sur GitHub.
Étapes suivantes
- Découvrez l'intégration de l'environnement de bac à sable de l'agent statique.
- Déployez des charges de travail GKE autogérées.
- Découvrez comment créer et gérer des pools de volumes.