将 Filestore 代理卷与 GKE Agent Sandbox 动态存储空间后期绑定搭配使用

借助动态存储后期绑定,您可以在声明时将 Filestore 智能体存储卷直接注入正在运行且已预热的 Google Kubernetes Engine (GKE) Agent Sandbox Pod 中。通过绕过标准 Kubernetes 卷挂接生命周期,动态延迟绑定可实现低于 100 毫秒的存储挂接延迟时间,而无需重启 Pod。

此架构可让高密度、低延迟的代理平台执行以下任务:

  • 消除 Pod 冷启动和容器初始化延迟。
  • 根据需要动态挂接和分离永久性工作区。
  • 暂停或休眠空闲代理会话,并在任何可用的预热沙盒 Pod 上恢复这些会话,同时保留文件系统状态。

准备工作

  1. 完成为 Filestore 智能体存储卷设置 GKE 环境中的初始设置。
  2. 验证您的 GKE 集群是否运行 1.36.0-gke.3302001 版或更高版本。
  3. 验证您的 volume-pool-sc StorageClass 是否指定了 volumeBindingMode: Immediate 和 reclaimPolicy: Delete。

架构概览

后期绑定架构由四个组件组成:

  • 平台编排器或自定义控制器:管理会话生命周期的控制平面服务或 Kubernetes 控制器。它会监视 SandboxClaim 事件、解析租户卷元数据、调用存储节点守护程序 API 来绑定或解除绑定存储,以及管理删除终结器。
  • 存储节点守护进程:在每个 gVisor 节点上运行的特权 DaemonSet,用于公开装载 API,以将 Filestore NFS 导出绑定到主机 Pod 目录。
  • SandboxTemplate 与 force-shared:gVisor 沙盒 Pod 模板,可让主机装载动态传播到 gVisor 沙盒容器中。
  • SandboxWarmPool:一个由预热的运行中沙盒 Pod 组成的池,可在声明后立即接收装载请求。

动态后期绑定的运作方式

动态延迟绑定不使用在 Pod 创建时附加 PersistentVolumeClaim (PVC) 的方式,而是使用以下生命周期:

  1. 预热沙盒:SandboxWarmPool 维护运行中的 gVisor Pod,这些 Pod 配置了 emptyDir 卷、HostToContainer 装载传播和 dev.gvisor.empty-dir.VOLUME_NAME.force-shared: "true" 注解,其中 <var>VOLUME_NAME</var> 是 emptyDir 卷的名称。
  2. 认领和预配:智能体对话开始后,编排器会创建一个 SandboxClaim(带有删除终结器)来认领一个温启动 Pod,并预配或选择一个后备 Filestore PVC。
  3. 在主机上绑定存储:编排器会解析声明的 Pod 的 UID、节点和 Filestore NFS 导出详细信息,向 Pod 添加删除终结器,并指示该节点上的存储节点守护程序将 NFS 卷装载到主机上 Pod 的后备 emptyDir 路径中。启用装载传播后,装载的工作区会立即显示在正在运行的 gVisor 容器中。
  4. 在拆解前解除绑定:当会话暂停或结束时,您的编排器会指示节点守护程序从主机目录中卸载 NFS 卷,然后再从 Pod 和 SandboxClaim 中移除终结器。

如需查看使用自定义控制器和经过身份验证的节点守护程序自动执行这些步骤的生产参考实现,请参阅 GitHub 上的 GKE Sandbox 延迟绑定存储示例。

部署存储节点守护程序

如需测试后期绑定工作流,请在 gVisor 节点上部署特权 DaemonSet,以便在主机上安装 NFS 客户端实用程序,并为主机装载命令提供执行环境。

  1. 创建名为 storage-node-daemon.yaml 的清单:

    apiVersion: apps/v1
    kind: DaemonSet
    metadata:
      name: storage-node-daemon
      namespace: default
    spec:
      selector:
        matchLabels:
          app: storage-node-daemon
      template:
        metadata:
          labels:
            app: storage-node-daemon
        spec:
          hostPID: true
          nodeSelector:
            sandbox.gke.io/runtime: gvisor
          tolerations:
            - key: "sandbox.gke.io/runtime"
              operator: "Equal"
              value: "gvisor"
              effect: "NoSchedule"
          containers:
            - name: daemon
              image: debian:stable-slim
              securityContext:
                privileged: true
              command: ["/bin/sh", "-c"]
              args:
                - |
                  nsenter -t 1 -m -u -i -n -- apt-get update && \
                  nsenter -t 1 -m -u -i -n -- apt-get install -y nfs-common || true
                  sleep infinity
    
  2. 应用清单:

    kubectl apply -f storage-node-daemon.yaml
    

配置 SandboxTemplate 和 SandboxWarmPool

如需使动态主机装载传播到正在运行的 gVisor 沙盒中,您的 SandboxTemplate 必须包含 dev.gvisor.empty-dir.workspace-volume.force-shared: "true" 注解,并在 emptyDir 卷装载上设置 mountPropagation: HostToContainer。

  1. 创建一个名为 sandbox-latebind.yaml 的清单,其中包含模板和温池:

    apiVersion: extensions.agents.x-k8s.io/v1alpha1
    kind: SandboxTemplate
    metadata:
      name: late-bind-template
      namespace: default
    spec:
      networkPolicyManagement: Unmanaged
      podTemplate:
        metadata:
          annotations:
            dev.gvisor.empty-dir.workspace-volume.force-shared: "true"
        spec:
          runtimeClassName: gvisor
          automountServiceAccountToken: false
          nodeSelector:
            sandbox.gke.io/runtime: gvisor
          tolerations:
            - key: "sandbox.gke.io/runtime"
              value: "gvisor"
              effect: "NoSchedule"
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            runAsGroup: 1000
            fsGroup: 1000
          containers:
            - name: agent
              image: busybox:latest
              securityContext:
                capabilities:
                  drop: ["ALL"]
                allowPrivilegeEscalation: false
              volumeMounts:
                - name: workspace-volume
                  mountPath: /workspace
                  mountPropagation: HostToContainer
              env:
                - name: MOUNT_PATH
                  value: "/workspace"
                - name: SUB_DIR
                  value: "user_data"
              command: ["/bin/sh", "-c"]
              args:
                - |
                  echo "Waiting for late-bind signal..."
                  while [ ! -f $MOUNT_PATH/.ready ]; do sleep 0.05; done
                  echo "Filestore volume mounted successfully!"
                  ls -la $MOUNT_PATH/$SUB_DIR
                  date >> $MOUNT_PATH/$SUB_DIR/session.txt
                  sleep infinity
          volumes:
            - name: workspace-volume
              emptyDir: {}
    ---
    apiVersion: extensions.agents.x-k8s.io/v1alpha1
    kind: SandboxWarmPool
    metadata:
      name: late-bind-warmpool
      namespace: default
    spec:
      replicas: 2
      sandboxTemplateRef:
        name: late-bind-template
    
  2. 应用清单:

    kubectl apply -f sandbox-latebind.yaml
    

申领沙盒并动态绑定存储空间

  1. 创建一个名为 late-bind-claim.yaml 的清单,该清单声明一个具有删除终结器 (agent.sandbox/storage-cleanup) 的温沙盒,并预配一个 Filestore 智能体存储卷 PVC:

    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
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: session-1-pvc
      namespace: default
    spec:
      accessModes: [ReadWriteMany]
      storageClassName: volume-pool-sc
      resources:
        requests:
          storage: 1Gi
    
  2. 应用清单:

    kubectl apply -f late-bind-claim.yaml
    
  3. 检索分配的 Pod 详细信息和后备 Filestore NFS 导出属性:

    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. 向声明的 Pod 添加删除终结器,以防止 Kubelet 在主机装载处于活跃状态时删除 Pod:

    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'
    
  5. 将 Filestore NFS 卷装载到 Pod 的主机 emptyDir 目录中,并发出就绪信号:

    DAEMON_POD=$(kubectl get pods -l app=storage-node-daemon \
      --field-selector spec.nodeName="${NODE_NAME}" \
      -o jsonpath='{.items[0].metadata.name}')
    HOST_DIR="/var/lib/kubelet/pods/${POD_UID}/volumes/kubernetes.io~empty-dir/workspace-volume"
    kubectl exec "${DAEMON_POD}" -- nsenter -t 1 -m -- sh -c "
      mkdir -p ${HOST_DIR}/user_data &&
      mount -t nfs ${NFS_IP}:${NFS_PATH} ${HOST_DIR}/user_data &&
      chown -R 1000:1000 ${HOST_DIR}/user_data &&
      echo ready > ${HOST_DIR}/.ready
    "
    
  6. 验证卷是否已装载到正在运行的沙盒 Pod 中:

    kubectl logs "${POD_NAME}" -c agent
    

    输出会确认卷已装载:

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

解除存储空间绑定并释放沙盒

当代理会话暂停或结束时,请先卸载主机存储空间,然后再允许 Kubernetes 删除 Pod。

  1. 在后台启动 SandboxClaim 的删除。由于终结器的存在,Kubernetes 会将声明标记为待删除,但会暂停 Pod 终止:

    kubectl delete sandboxclaim late-bind-session-1 --wait=false
    
  2. 在主机节点上卸载 Filestore NFS 卷:

    kubectl exec "${DAEMON_POD}" -- nsenter -t 1 -m -- \
      umount -l "${HOST_DIR}/user_data"
    
  3. 从 SandboxClaim 和 Pod 中移除终结器,以完成终止:

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

如需稍后恢复会话,请创建一个新的 SandboxClaim,并将现有 PVC (session-1-pvc) 装载到新声明的热 Pod 中。

后续步骤