将 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 或更高版本。此版本支持 gVisor emptyDir 挂载传播所需的 force-shared 注解。
  3. 验证您的 volume-pool-sc StorageClass 是否指定了 volumeBindingMode: ImmediatereclaimPolicy: Delete

架构概览

后期绑定架构包含以下四个组件:

  • 平台编排工具或自定义控制器:管理会话生命周期的控制平面服务或 Kubernetes 控制器。它会监控 SandboxClaim 事件,解析租户卷元数据,调用存储节点守护程序 API 来绑定或取消绑定存储,并管理删除终结器。
  • 存储节点守护程序:在每个 gVisor 节点上运行的特权 DaemonSet,用于公开装载 API。
  • SandboxTemplate(含 force-shared:一种 gVisor 沙盒 pod 模板,可让主机挂载动态传播到 gVisor 沙盒容器中。
  • SandboxWarmPool:预热的正在运行的沙盒 Pod 池,在声明时可立即接收装载请求。

部署存储节点守护程序

创建一个名为 storage-node-daemon.yaml 的清单,其中包含特权 DaemonSet

应用清单:

kubectl apply -f storage-node-daemon.yaml

部署 SandboxTemplateSandboxWarmPool

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

应用清单:

kubectl apply -f sandbox-latebind.yaml

声明沙盒并动态绑定存储空间

  1. 创建一个名为 late-bind-claim.yaml 的声明清单,其中包含删除终结器 (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
    

    应用声明:

    kubectl apply -f late-bind-claim.yaml
    
  2. 使用名为 agent-volume-pvc.yaml 的 PVC 清单动态预配卷:

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: session-1-pvc
      namespace: default
    spec:
      accessModes: [ReadWriteMany]
      storageClassName: volume-pool-sc
      resources:
        requests:
          storage: 1Gi
    

    应用 PVC:

    kubectl apply -f agent-volume-pvc.yaml
    
  3. 检索分配的 pod UID、节点和后备 PV 导出详细信息:

    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 主机上的节点守护程序:

    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. 向已声明的 pod 应用删除终结器,以防止在宿主装载处于活动状态时过早清理:

    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'
    
  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. 在宿主节点上卸载 NFS 共享:

    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. SandboxClaim 和 pod 中移除终结器,以完成终止:

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

如需稍后恢复会话,请声明一个新的预热 Pod,并使用现有 PVC (session-1-pvc) 发送绑定请求。新的沙盒 Pod 会立即获得对已保留的工作区状态的访问权限。

生产环境注意事项和自定义控制器设计

本文档中的手动命令演示了动态后期绑定的底层机制。为了在生产环境中可靠地运行此架构,您必须开发一个自定义 Kubernetes 控制器或平台编排器,以适应应用的会话生命周期。

在设计生产控制器和节点守护程序时,请实现以下架构模式:

自动执行协调和 Finalizer 生命周期

由于存储节点守护程序在标准 Kubernetes 容器存储接口 (CSI) 生命周期管理之外执行主机级装载,因此 Kubelet 不知道 pod 的 emptyDir 内是否存在有效装载。如果 Pod 在挂载处于活跃状态时被删除,Kubelet 将无法移除 emptyDir 目录并引发 Device or resource busy 错误,导致 Pod 停留在 Terminating 状态。

您的自定义控制器必须使用终结器(例如 agent.sandbox/storage-cleanup)自动执行严格的状态机:

  1. 声明创建和绑定
    • 在创建时,将静态 finalizer 附加到每个 SandboxClaim
    • 监控 Kubernetes API,以获取 SandboxClaim 状态更新。当声明绑定到暖池 pod 时,提取分配的 pod_uidnodeName 和后备 Filestore 卷属性。
    • 向目标节点上运行的存储节点守护程序发送经过身份验证的 bind 请求。
    • 立即修补正在运行的 Pod 对象,以添加动态 finalizer。 由于 SandboxTemplate 规范不支持静态 pod 终结器,因此需要动态修补 pod,以便在节点排空或重新调度事件(其中 pod 被逐出但 SandboxClaim 保持活跃状态)期间保护 pod。
  2. 正常终止和逐出处理
    • SandboxClaimPod 资源中查找 deletionTimestamp
    • 检测到删除或逐出操作时,调用节点守护程序的 unbind 端点以干净地卸载宿主目录 (umount -l)。
    • 在修补 PodSandboxClaim 以移除其终结器之前,请验证卸载是否成功,以及所有待处理的写入操作是否已刷新。这有助于您在正常声明删除、GKE 节点升级、Spot 虚拟机抢占和内存不足 (OOM) 终止时实现干净的拆解。

保护和强化存储节点守护程序

  • kubectl exec 替换为经过身份验证的 API:在生产环境中,请勿使用 kubectl exec 或将守护程序绑定到 localhost。配置存储节点守护程序,以通过集群网络公开专用的 gRPC 或 HTTPS 端点,该端点通过双向 TLS (mTLS) 或 Kubernetes ServiceAccount 令牌身份验证进行保护。
  • 隔离守护程序命名空间和网络访问权限:在受限的管理命名空间(例如 sandbox-storage-system)中部署特权 storage-node-daemon DaemonSet,而不是在 default 或租户命名空间中部署。应用 Kubernetes NetworkPolicy 规则,以仅允许从自定义控制器 Pod 向守护程序 API 入站,并阻止来自沙盒代理 Pod 的所有流量。
  • 使用预先构建的容器映像:避免在 initContainer 中运行时安装 nfs-common 等软件包。使用预构建的不可变容器映像,其中预安装了所有必需的装载实用程序,以消除节点启动延迟和外部代码库依赖项。

强制执行存储空间配额和多租户隔离

  • 监控每个代理的存储空间用量:当您将共享 ReadWriteMany (RWX) Filestore 卷中的子目录动态绑定装载到 emptyDir 中时,标准 Kubernetes emptyDir.sizeLimit 设置无法对装载的 NFS 路径强制执行每个代理的存储空间配额。为防止单个失控代理耗尽共享卷并导致拒绝服务 (DoS),请在编排器中实现目录配额监控,或使用卷池配置专用卷。
  • 针对不同的工作区访问模式调整装载载荷:您的控制器可以通过改变发送到节点守护程序的参数来支持多种代理存储拓扑:
    • 私有隔离工作区:将唯一的租户子目录或专用 PVC 绑定到具有读写权限的单个沙盒 pod。
    • 协作式工作区:在多个协调的代理 Pod 中同时绑定同一共享 RWX 子目录,以实现实时文件共享。
    • 探索分支工作区:以只读 (ro) 方式装载基本模板目录,以便代理可以读取共享资源,而无需修改黄金副本,同时将新的写入操作路由到单独的可写暂存路径或恢复时复制目录。

协调时间点快照和清理

  • 在创建快照之前实现写入静止:为了捕获一致的时间点工作区快照而不损坏数据,您的编排器应在归档工作区目录或触发 Filestore 快照之前,通过启动取消绑定工作流(或刷新文件系统缓冲区)来暂停活跃的写入操作。
  • 自动取消配置租户:当用户会话或工作区永久过期时,请确保控制器先取消绑定所有节点上的任何有效装载,然后再执行异步后台任务以从后备卷中删除租户的持久性目录。

如需查看完整的参考实现,了解动态终结器管理、多租户隔离和快照恢复工作流,请参阅 GitHub 上的 GKE Sandbox 延迟绑定存储示例

后续步骤