Usa volúmenes de agentes de Filestore con la vinculación tardía del almacenamiento dinámico de GKE Agent Sandbox

La vinculación tardía del almacenamiento dinámico te permite insertar volúmenes del agente de Filestore directamente en los Pods de Google Kubernetes Engine (GKE) Agent Sandbox en ejecución y precalentados cuando se reclaman. Al omitir el ciclo de vida estándar de la conexión de volúmenes de Kubernetes, la vinculación tardía dinámica logra una latencia de conexión de almacenamiento inferior a 100 ms sin necesidad de reiniciar los pods.

Esta arquitectura permite que las plataformas de agentes de alta densidad y baja latencia hagan lo siguiente:

  • Elimina los retrasos en el inicio en frío del Pod y la inicialización del contenedor.
  • Conectar y desconectar dinámicamente espacios de trabajo persistentes según demanda
  • Pausar o hibernar las sesiones de agentes inactivas y reanudarlas en cualquier pod de zona de pruebas precalentado disponible, a la vez que se conserva el estado del sistema de archivos

Antes de comenzar

  1. Completa la configuración inicial en Configura el entorno de GKE para los volúmenes del agente de Filestore.
  2. Verifica que tu clúster de GKE ejecute la versión 1.36.0-gke.3302001 o posterior. Esta versión admite la anotación force-shared necesaria para la propagación del montaje de emptyDir de gVisor.
  3. Verifica que tu volume-pool-sc StorageClass especifique volumeBindingMode: Immediate y reclaimPolicy: Delete.

Descripción general de la arquitectura

La arquitectura de vinculación tardía consta de cuatro componentes:

  • Organizador de la plataforma o controlador personalizado: Es un servicio del plano de control o un controlador de Kubernetes que administra los ciclos de vida de las sesiones. Supervisa los eventos de SandboxClaim, resuelve los metadatos de volumen del inquilino, llama a la API del daemon del nodo de almacenamiento para vincular o desvincular el almacenamiento y administra los finalizadores de eliminación.
  • Daemon del nodo de almacenamiento: Un DaemonSet con privilegios que se ejecuta en cada nodo de gVisor y expone una API de montaje.
  • SandboxTemplate con force-shared: Es una plantilla de pod de zona de pruebas de gVisor que permite que las vinculaciones del host se propaguen de forma dinámica en el contenedor de zona de pruebas de gVisor.
  • SandboxWarmPool: Es un grupo de pods de zona de pruebas en ejecución preparados previamente y listos para recibir solicitudes de vinculación de inmediato tras la reclamación.

Implementa el daemon del nodo de almacenamiento

Crea un manifiesto llamado storage-node-daemon.yaml que contenga el DaemonSet con privilegios:

Aplica el manifiesto

kubectl apply -f storage-node-daemon.yaml

Implementa SandboxTemplate y SandboxWarmPool

Crea un manifiesto llamado sandbox-latebind.yaml que contenga la plantilla y el grupo de instancias en espera:

Aplica el manifiesto

kubectl apply -f sandbox-latebind.yaml

Cómo reclamar una zona de pruebas y vincular almacenamiento de forma dinámica

  1. Crea un manifiesto de reclamo llamado late-bind-claim.yaml que incluya un finalizador de eliminación (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
    

    Aplica el reclamo:

    kubectl apply -f late-bind-claim.yaml
    
  2. Aprovisiona un volumen de forma dinámica con un manifiesto de PVC llamado 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
    

    Aplica el PVC:

    kubectl apply -f agent-volume-pvc.yaml
    
  3. Recupera el UID del Pod, el nodo y los detalles de exportación del PV de respaldo asignados:

    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. Envía el indicador de activación al daemon del nodo en el 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. Aplica un finalizador de eliminación al Pod reclamado para evitar la limpieza prematura mientras la conexión del host esté activa:

    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'
    
  6. Verifica la activación del volumen dentro del pod en ejecución:

    kubectl logs "${POD_NAME}" -c agent
    

    El resultado confirma que el volumen se activó correctamente:

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

Cómo pausar y reanudar una sesión

Cuando una sesión del agente finaliza o entra en hibernación, tu orquestador debe desmontar el almacenamiento del host antes de permitir que Kubernetes finalice o recicle el pod.

  1. Inicia la eliminación de SandboxClaim en segundo plano. Debido al finalizador, Kubernetes marca el reclamo para su eliminación, pero detiene la finalización del Pod:

    kubectl delete sandboxclaim late-bind-session-1 --wait=false
    
  2. Desactiva el recurso compartido de NFS en el 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. Quita los finalizadores del SandboxClaim y del pod para completar la finalización:

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

Para reanudar la sesión más tarde, solicita un nuevo pod preparado previamente y envía la solicitud de vinculación con el PVC existente (session-1-pvc). El nuevo pod de zona de pruebas obtiene acceso de inmediato al estado conservado del espacio de trabajo.

Consideraciones de producción y diseño de controles personalizados

Los comandos manuales de este documento demuestran los mecanismos de bajo nivel de la vinculación tardía dinámica. Para ejecutar esta arquitectura de forma confiable en producción, debes desarrollar un controlador de Kubernetes personalizado o un organizador de plataformas adaptado al ciclo de vida de la sesión de tu aplicación.

Cuando diseñes el controlador de producción y el daemon de nodos, implementa los siguientes patrones arquitectónicos:

Automatiza el ciclo de vida de la conciliación y el finalizador

Debido a que el daemon del nodo de almacenamiento realiza activaciones a nivel del host fuera de la administración del ciclo de vida estándar de la interfaz de almacenamiento de contenedores (CSI) de Kubernetes, Kubelet no conoce las activaciones activas dentro del emptyDir del pod. Si se borra un pod mientras el montaje está activo, Kubelet no podrá quitar el directorio emptyDir y generará un error Device or resource busy, lo que dejará el pod atascado en un estado Terminating.

Tu controlador personalizado debe automatizar una máquina de estados estricta con finalizadores (como agent.sandbox/storage-cleanup):

  1. Creación y vinculación de reclamos:
    • Adjunta un finalizador estático a cada SandboxClaim en el momento de la creación.
    • Observa la API de Kubernetes para ver las actualizaciones de estado de SandboxClaim. Cuando un reclamo se vincula a un pod de grupo de recursos activo, extrae los atributos asignados de pod_uid, nodeName y el volumen de Filestore de respaldo.
    • Envía una solicitud bind autenticada al daemon del nodo de almacenamiento que se ejecuta en el nodo de destino.
    • Aplica un parche de inmediato al objeto Pod en ejecución para agregar el finalizador dinámico. Dado que las especificaciones de SandboxTemplate no admiten finalizadores de Pod estáticos, se requiere aplicar parches al Pod de forma dinámica para protegerlo durante los eventos de drenaje o reprogramación de nodos en los que se expulsa el Pod, pero SandboxClaim permanece activo.
  2. Control de desalojo y finalización correcta:
    • Observa deletionTimestamp en los recursos SandboxClaim y Pod.
    • Cuando se detecta una eliminación o desalojo, llama al extremo unbind del daemon del nodo para desmontar correctamente el directorio del host (umount -l).
    • Verifica que el desmontaje se haya realizado correctamente y que todas las escrituras pendientes se hayan vaciado antes de aplicar parches a Pod y SandboxClaim para quitar sus finalizadores. Esto puede ayudarte a lograr una limpieza en la eliminación de reclamos correctos, las actualizaciones de nodos de GKE, las interrupciones de VM Spot y las finalizaciones por falta de memoria (OOM).

Protege y endurece el daemon del nodo de almacenamiento

  • Reemplaza kubectl exec por APIs autenticadas: En producción, no uses kubectl exec ni vincules el daemon a localhost. Configura el daemon del nodo de almacenamiento para exponer un extremo dedicado de gRPC o HTTPS a través de la red del clúster protegida con TLS mutua (mTLS) o autenticación de token de ServiceAccount de Kubernetes.
  • Aísla los espacios de nombres de los daemons y el acceso a la red: Implementa el storage-node-daemon DaemonSet con privilegios en un espacio de nombres administrativo restringido (por ejemplo, sandbox-storage-system) en lugar de los espacios de nombres default o de inquilino. Aplica reglas de NetworkPolicy de Kubernetes que permitan el ingreso a la API del daemon exclusivamente desde los Pods del controlador personalizado y bloqueen todo el tráfico de los Pods del agente en zona de pruebas.
  • Usa imágenes de contenedor precompiladas: Evita instalar paquetes como nfs-common en el tiempo de ejecución en un initContainer. Usa una imagen de contenedor inmutable compilada previamente con todas las utilidades de montaje necesarias preinstaladas para eliminar las demoras en el inicio de los nodos y las dependencias de repositorios externos.

Aplica cuotas de almacenamiento y aislamiento multiusuario

  • Supervisa el uso de almacenamiento por agente: Cuando vinculas de forma dinámica un subdirectorio desde un volumen compartido ReadWriteMany (RWX) de Filestore a un emptyDir, la configuración estándar de emptyDir.sizeLimit de Kubernetes no puede aplicar cuotas de almacenamiento por agente en la ruta de acceso NFS montada. Para evitar que un solo agente descontrolado agote el volumen compartido y provoque una denegación de servicio (DoS), implementa la supervisión de cuotas de directorio en tu orquestador o aprovisiona volúmenes dedicados con grupos de volúmenes.
  • Adapta las cargas útiles de la vinculación para los diferentes modos de acceso al espacio de trabajo: Tu controlador puede admitir varias topologías de almacenamiento de agentes variando los parámetros que se envían al daemon del nodo:
    • Espacios de trabajo privados aislados: Vincula un subdirectorio único del usuario o un PVC dedicado a un solo pod de zona de pruebas con permisos de lectura y escritura.
    • Espacios de trabajo colaborativos: Vincula de forma simultánea el mismo subdirectorio RWX compartido en varios pods de agentes coordinadores para compartir archivos en tiempo real.
    • Espacios de trabajo de ramificación de exploración: Monta un directorio de plantillas base como de solo lectura (ro) para que los agentes puedan leer los recursos compartidos sin modificar la copia dorada, mientras enruta las escrituras nuevas a una ruta de acceso de borrador grabable independiente o a un directorio de copia en restauración.

Coordina las instantáneas de un momento determinado y la limpieza

  • Logra la quiescencia de escritura antes de las instantáneas: Para capturar instantáneas coherentes del espacio de trabajo en un momento determinado sin corrupción de datos, tu orquestador debe pausar las escrituras activas iniciando el flujo de trabajo de desvinculación (o vaciando los búferes del sistema de archivos) antes de archivar el directorio del espacio de trabajo o activar una instantánea de Filestore.
  • Automatiza la anulación del aprovisionamiento del arrendatario: Cuando una sesión de usuario o un espacio de trabajo vencen de forma permanente, asegúrate de que tu controlador primero desvincule cualquier montaje activo en todos los nodos antes de ejecutar tareas asíncronas en segundo plano para borrar los directorios persistentes del arrendatario del volumen de respaldo.

Para ver una implementación de referencia completa que muestre la administración dinámica de finalizadores, el aislamiento de múltiples arrendatarios y los flujos de trabajo de restauración de instantáneas, consulta el ejemplo de almacenamiento de vinculación tardía de GKE Sandbox en GitHub.

¿Qué sigue?