שימוש בכרכים של סוכן Filestore עם אחסון דינמי של ארגז החול לסוכנים ב-GKE

קישור מאוחר של אחסון דינמי מאפשר להוסיף נפחי אחסון של סוכני Filestore ישירות לפודים פעילים של Agent Sandbox ב-Google Kubernetes Engine ‏(GKE) שחוממו מראש, ברגע שהם נדרשים. כשעוקפים את מחזור החיים הרגיל של צירוף אמצעי אחסון ב-Kubernetes, הקישור הדינמי המאוחר מאפשר להשיג זמן אחזור של פחות מ-100 אלפיות השנייה בצירוף אמצעי האחסון, בלי להפעיל מחדש את ה-pod.

הארכיטקטורה הזו מאפשרת לפלטפורמות של נציגים עם צפיפות גבוהה וזמן אחזור קצר לבצע את המשימות הבאות:

  • ביטול ההשהיות בהפעלה קרה של Pod ובאתחול של קונטיינר.
  • לצרף ולנתק באופן דינמי סביבות עבודה קבועות לפי דרישה.
  • להשהות או להעביר למצב שינה סשנים של סוכנים לא פעילים, ולהמשיך אותם בכל פוד זמין של ארגז חול שחומם מראש, תוך שמירה על המצב של מערכת הקבצים.

לפני שמתחילים

  1. משלימים את ההגדרה הראשונית בהגדרת סביבת GKE לנפחי אחסון לסוכנים ב-Filestore.
  2. מוודאים שאשכול ה-GKE פועל בגרסה 1.36.0-gke.3302001 ואילך.
  3. מוודאים שב-volume-pool-sc StorageClass מצוינים volumeBindingMode: Immediate ו-reclaimPolicy: Delete.

סקירה כללית של הארכיטקטורה

ארכיטקטורת הקישור המאוחר מורכבת מארבעה רכיבים:

  • מנהל תזמור של פלטפורמה או בקר בהתאמה אישית: שירות במישור הבקרה או בקר Kubernetes שמנהל את מחזורי החיים של הסשנים. הוא עוקב אחרי אירועי SandboxClaim, פותר מטא-נתונים של נפח אחסון של דיירים, קורא ל-API של הדימון של צומת האחסון כדי לקשר או לבטל קישור של אחסון, ומנהל את ה-finalizers של המחיקה.
  • דמון של צומת אחסון: DaemonSet עם הרשאות שפועל בכל צומת gVisor וחושף API לחיבור, כדי לקשר ייצוא של NFS ב-Filestore לספריות של פודים מארחים.
  • ‫SandboxTemplate עם force-shared: תבנית של Pod של ארגז חול ב-gVisor שמאפשרת להפיץ באופן דינמי נקודות טעינה של מארח לקונטיינר של ארגז החול ב-gVisor.
  • ‫SandboxWarmPool: מאגר של פודים של ארגזי חול שפועלים ומחוממים מראש, שמוכנים לקבל בקשות טעינה באופן מיידי אחרי שהם נתבעים.

איך פועל קישור דינמי מאוחר

במקום לצרף PersistentVolumeClaim (PVC) בזמן יצירת ה-Pod, הקישור המאוחר הדינמי משתמש במחזור החיים הבא:

  1. ארגזי חול שחוממו מראש: SandboxWarmPool מנהל פודים פועלים של gVisor שמוגדרים עם אמצעי אחסון emptyDir, הפצת טעינה HostToContainer וההערה dev.gvisor.empty-dir.VOLUME_NAME.force-shared: "true", כאשר <var>VOLUME_NAME</var> הוא השם של אמצעי האחסון emptyDir.
  2. תביעה והקצאה: כשסשן של נציג מתחיל, ה-orchestrator יוצר SandboxClaim (עם פיינלייזר מחיקה) כדי לתבוע pod חם, ומקצה או בוחר PVC של Filestore לגיבוי.
  3. קישור האחסון במארח: כלי הניהול פותר את פרטי ה-UID, הצומת וייצוא ה-NFS של Filestore של ה-pod שנתבע, מוסיף פיינלייזר מחיקה ל-pod ומורה לדמון של צומת האחסון בצומת הזה לטעון את נפח ה-NFS לנתיב הגיבוי emptyDir של ה-pod במארח. כשהתכונה "הפצת טעינה" מופעלת, סביבת העבודה שנטענה מופיעה מיד בתוך הקונטיינר הפועל של gVisor.
  4. ביטול הקישור לפני הפירוק: כשהסשן מושהה או מסתיים, ה-orchestrator מורה לדמון של הצומת לבטל את הטעינה של אמצעי האחסון של NFS מספריית המארח לפני הסרת ה-finalizers מ-Pod ומ-SandboxClaim.

ב-GitHub תוכלו למצוא דוגמה להטמעה של אחסון עם קישור מאוחר ב-GKE Sandbox, שבה התהליך הזה מתבצע באופן אוטומטי באמצעות בקר מותאם אישית ודמון מאומת של צומת.

פריסת הדימון של צומת האחסון

כדי לבדוק את תהליך העבודה של הקישור המאוחר, פורסים DaemonSet עם הרשאות בצומתי gVisor שמתקין את כלי הלקוח של 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" ולהגדיר את mountPropagation: HostToContainer בטעינת הנפח של emptyDir.

  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) ומקצה PVC לנפח אחסון לסוכן ב-Filestore:

    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 שהוקצה ואת מאפייני הייצוא של ה-NFS של Filestore:

    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 בזמן שה-host mount פעיל:

    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'
    
  5. מגדירים את נפח ה-NFS של Filestore בספריית המארח emptyDir של ה-Pod ומסמנים שההגדרה מוכנה:

    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 ברקע. בגלל ה-finalizer, ‏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. כדי להשלים את הסיום, מסירים את ה-finalizers גם מ-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) לפוד החם החדש שהוקצה.

המאמרים הבאים