Utilizzare i volumi dell'agente Filestore con il binding tardivo dell'archiviazione dinamica di GKE Agent Sandbox

Il binding tardivo dell'archiviazione dinamica consente di inserire volumi dell'agente Filestore direttamente nei pod della sandbox dell'agente Google Kubernetes Engine (GKE) pre-riscaldati al momento della richiesta. Evitando il ciclo di vita standard di collegamento dei volumi di Kubernetes, il collegamento dinamico tardivo raggiunge una latenza di collegamento dello spazio di archiviazione inferiore a 100 ms senza richiedere riavvii dei pod.

Questa architettura consente alle piattaforme di agenti a bassa latenza e ad alta densità di:

  • Elimina i ritardi di avvio a freddo del pod e di inizializzazione del container.
  • Collega e scollega dinamicamente gli spazi di lavoro persistenti on demand.
  • Metti in pausa o iberna le sessioni degli agenti inattivi e riprendile in qualsiasi pod sandbox pre-riscaldato disponibile, mantenendo lo stato del file system.

Prima di iniziare

  1. Completa la configurazione iniziale in Configura l'ambiente GKE per i volumi dell'agente Filestore.
  2. Verifica che il cluster GKE esegua la versione 1.36.0-gke.3302001 o successive. Questa versione supporta l'annotazione force-shared richiesta per la propagazione del montaggio di gVisor emptyDir.
  3. Verifica che volume-pool-sc StorageClass specifichi volumeBindingMode: Immediate e reclaimPolicy: Delete.

Panoramica dell'architettura

L'architettura late-binding è composta da quattro componenti:

  • Orchestratore della piattaforma o controller personalizzato:un servizio del control plane o un controller Kubernetes che gestisce i cicli di vita delle sessioni. Monitora gli eventi SandboxClaim, risolve i metadati del volume del tenant, chiama l'API del daemon del nodo di archiviazione per associare o dissociare l'archiviazione e gestisce i finalizzatori di eliminazione.
  • Daemon del nodo di archiviazione:un DaemonSet con privilegi in esecuzione su ogni nodo gVisor che espone un'API di montaggio.
  • SandboxTemplate con force-shared: un modello di pod sandbox gVisor che consente di propagare dinamicamente i montaggi dell'host nel container sandbox gVisor.
  • SandboxWarmPool: un pool di pod sandbox preinizializzati in esecuzione pronti a ricevere richieste di montaggio immediatamente dopo la rivendicazione.

Esegui il deployment del daemon del nodo di archiviazione

Crea un manifest denominato storage-node-daemon.yaml contenente il privilegio DaemonSet:

Applica il manifest:

kubectl apply -f storage-node-daemon.yaml

Esegui il deployment di SandboxTemplate e SandboxWarmPool

Crea un manifest denominato sandbox-latebind.yaml contenente il modello e il pool caldo:

Applica il manifest:

kubectl apply -f sandbox-latebind.yaml

Richiedere una sandbox e associare dinamicamente lo spazio di archiviazione

  1. Crea un manifest di rivendicazione denominato late-bind-claim.yaml che includa un finalizzatore di eliminazione (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-template
    

    Applica la rivendicazione:

    kubectl apply -f late-bind-claim.yaml
    
  2. Esegui il provisioning dinamico di un volume utilizzando un manifest PVC denominato 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: 1Gi
    

    Applica il PVC:

    kubectl apply -f agent-volume-pvc.yaml
    
  3. Recupera i dettagli di esportazione di UID, nodo e PV di backup del pod assegnato:

    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}')"
    
  4. Invia il segnale di montaggio al daemon del nodo sull'host del 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())
    "
    
  5. Applica un finalizzatore di eliminazione al pod rivendicato per impedire la pulizia prematura mentre il montaggio dell'host è attivo:

    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'
    
  6. Verifica il montaggio del volume all'interno del pod in esecuzione:

    kubectl logs "${POD_NAME}" -c agent
    

    L'output conferma che il volume è stato montato correttamente:

    Waiting for late-bind signal...
    Filestore volume mounted successfully!
    drwxr-xr-x    2 1000     1000          4096 ... user_data
    

Mettere in pausa e riprendere una sessione

Quando una sessione dell'agente termina o entra in ibernazione, l'orchestratore deve smontare lo spazio di archiviazione host prima di consentire a Kubernetes di terminare o riciclare il pod.

  1. Avvia l'eliminazione di SandboxClaim in background. A causa del finalizzatore, Kubernetes contrassegna la richiesta di eliminazione, ma mette in pausa la terminazione del pod:

    kubectl delete sandboxclaim late-bind-session-1 --wait=false
    
  2. Smonta la condivisione NFS sul nodo host:

    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())
    "
    
  3. Rimuovi i finalizer sia da SandboxClaim sia dal pod per completare la terminazione:

    kubectl patch sandboxclaim late-bind-session-1 --type=merge \
      -p '{"metadata":{"finalizers":[]}}'
    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":[]}}'
    

Per riprendere la sessione in un secondo momento, richiedi un nuovo pod pre-riscaldato e invia la richiesta di binding utilizzando la PVC esistente (session-1-pvc). Il nuovo pod sandbox ottiene immediatamente l'accesso allo stato dello spazio di lavoro conservato.

Considerazioni sulla produzione e progettazione di controller personalizzati

I comandi manuali in questo documento mostrano i meccanismi di basso livello del dynamic late-binding. Per eseguire questa architettura in modo affidabile in produzione, devi sviluppare un controller Kubernetes personalizzato o un orchestratore di piattaforme personalizzato in base al ciclo di vita della sessione della tua applicazione.

Quando progetti il controller di produzione e il daemon del nodo, implementa i seguenti pattern architetturali:

Automatizzare il ciclo di vita della riconciliazione e del finalizzatore

Poiché il daemon del nodo di archiviazione esegue i montaggi a livello di host al di fuori della gestione del ciclo di vita standard di Container Storage Interface (CSI) di Kubernetes, Kubelet non è a conoscenza dei montaggi attivi all'interno di emptyDir del pod. Se un pod viene eliminato mentre il montaggio è attivo, Kubelet non riesce a rimuovere la directory emptyDir e genera un errore Device or resource busy, lasciando il pod bloccato nello stato Terminating.

Il controller personalizzato deve automatizzare una macchina a stati rigorosa utilizzando i finalizzatori (ad esempio agent.sandbox/storage-cleanup):

  1. Creazione e associazione di rivendicazioni:
    • Al momento della creazione, collega un finalizzatore statico a ogni SandboxClaim.
    • Monitora l'API Kubernetes per gli aggiornamenti di stato di SandboxClaim. Quando una richiesta si lega a un pod del pool caldo, estrai gli attributi pod_uid, nodeName e del volume Filestore di backup assegnati.
    • Invia una richiesta bind autenticata al daemon del nodo di archiviazione in esecuzione sul nodo di destinazione.
    • Applica immediatamente la patch all'oggetto Pod in esecuzione per aggiungere il finalizzatore dinamico. Poiché le specifiche di SandboxTemplate non supportano i finalizzatori statici dei pod, è necessario applicare patch al pod in modo dinamico per proteggerlo durante gli svuotamenti dei nodi o gli eventi di riprogrammazione in cui il pod viene rimosso, ma SandboxClaim rimane attivo.
  2. Gestione dell'arresto controllato e dell'espulsione:
    • Cerca deletionTimestamp nelle risorse SandboxClaim e Pod.
    • Quando viene rilevata un'eliminazione o un'espulsione, chiama l'endpoint unbind del daemon del nodo per smontare correttamente la directory host (umount -l).
    • Verifica che l'unmount sia riuscito e che tutte le scritture in attesa siano state scaricate prima di applicare la patch a Pod e SandboxClaim per rimuovere i relativi finalizzatori. Questo può aiutarti a ottenere una rimozione pulita in caso di eliminazioni di rivendicazioni controllate, upgrade dei nodi GKE, interruzioni delle VM spot e terminazioni per esaurimento della memoria (OOM).

Proteggere e rafforzare il daemon del nodo di archiviazione

  • Sostituisci kubectl exec con API autenticate: in produzione, non utilizzare kubectl exec o associa il daemon a localhost. Configura il daemon del nodo di archiviazione in modo che esponga un endpoint gRPC o HTTPS dedicato sulla rete del cluster protetta con autenticazione mutual TLS (mTLS) o token Kubernetes ServiceAccount.
  • Isola gli spazi dei nomi dei daemon e l'accesso alla rete:esegui il deployment di storage-node-daemon DaemonSet con privilegi in uno spazio dei nomi amministrativo con limitazioni (ad esempio sandbox-storage-system) anziché negli spazi dei nomi default o tenant. Applica regole NetworkPolicy di Kubernetes che consentono l'accesso in entrata all'API del daemon esclusivamente dai pod del controller personalizzato e blocca tutto il traffico dai pod dell'agente in sandbox.
  • Utilizza immagini container precompilate:evita di installare pacchetti come nfs-common in fase di runtime in un initContainer. Utilizza un'immagine container predefinita e immutabile con tutte le utilità di montaggio richieste preinstallate per eliminare i ritardi di avvio dei nodi e le dipendenze del repository esterno.

Applica quote di archiviazione e isolamento multi-tenant

  • Monitora l'utilizzo dello spazio di archiviazione per agente:quando esegui il bind-mount dinamico di una sottodirectory da un volume Filestore ReadWriteMany (RWX) condiviso in un emptyDir, le impostazioni emptyDir.sizeLimit standard di Kubernetes non possono applicare quote di spazio di archiviazione per agente al percorso NFS montato. Per impedire a un singolo agente non controllato di esaurire il volume condiviso e causare un attacco Denial of Service (DoS), implementa il monitoraggio della quota di directory nell'orchestratore o esegui il provisioning di volumi dedicati utilizzando i pool di volumi.
  • Adatta i payload di montaggio per diverse modalità di accesso allo spazio di lavoro: il controller può supportare più topologie di archiviazione degli agenti variando i parametri inviati al daemon del nodo:
    • Workspace isolati privati:associa una sottodirectory tenant univoca o un PVC dedicato a un singolo pod sandbox con autorizzazioni di lettura/scrittura.
    • Spazi di lavoro collaborativi: associa contemporaneamente la stessa sottodirectory RWX condivisa a più pod di agenti coordinati per la condivisione di file in tempo reale.
    • Workspace di ramificazione dell'esplorazione: monta una directory di modelli di base come di sola lettura (ro) in modo che gli agenti possano leggere le risorse condivise senza modificare la copia di riferimento, mentre indirizzano le nuove scritture a un percorso scratch scrivibile separato o a una directory di copia al ripristino.

Coordinare gli snapshot point-in-time e la pulizia

  • Raggiungi la quiescenza di scrittura prima degli snapshot:per acquisire snapshot dello spazio di lavoro coerenti in un determinato momento senza danneggiare i dati, l'orchestratore deve mettere in pausa le scritture attive avviando il flusso di lavoro di annullamento del binding (o svuotando i buffer del file system) prima di archiviare la directory dello spazio di lavoro o attivare uno snapshot Filestore.
  • Automatizza il deprovisioning del tenant:quando una sessione utente o uno spazio di lavoro scade in modo permanente, assicurati che il controller annulli prima l'associazione di tutti i montaggi attivi su tutti i nodi prima di eseguire attività asincrone in background per eliminare le directory permanenti del tenant dal volume di archiviazione.

Per un'implementazione di riferimento completa che mostri la gestione dinamica dei finalizzatori, l'isolamento multi-tenant e i workflow di ripristino degli snapshot, consulta l'esempio di archiviazione con binding tardivo di GKE Sandbox su GitHub.

Passaggi successivi