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

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

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

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

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

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

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

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

  • מתזמר פלטפורמה או בקר בהתאמה אישית: שירות ברמת הבקרה או בקר Kubernetes שמנהל את מחזורי החיים של הסשנים. הוא עוקב אחרי אירועים של SandboxClaim, פותר את המטא-נתונים של נפח האחסון של הדייר, קורא ל-API של דמון צומת האחסון כדי לקשור או לבטל את הקשר של האחסון, ומנהל את פעולות הניקוי הסופיות של המחיקה.
  • Storage node daemon: דמון עם הרשאות DaemonSet שפועל בכל צומת gVisor ומציג API של הרכבה.
  • SandboxTemplate עם force-shared: תבנית של gVisor sandbox pod שמאפשרת להעביר את הנפחים של המארח באופן דינמי לקונטיינר של gVisor sandbox.
  • SandboxWarmPool: מאגר של תרמילי ארגז חול שמוכנים להפעלה ומחכים לקבל בקשות להרכבה באופן מיידי עם קבלת ההצהרה.

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

יוצרים מניפסט בשם storage-node-daemon.yaml שמכיל את ההרשאות המיוחדות DaemonSet:

החלת המניפסט:

kubectl apply -f storage-node-daemon.yaml

פריסה של SandboxTemplate ושל SandboxWarmPool

יוצרים מניפסט בשם 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. הקצאה דינמית של נפח אחסון באמצעות מניפסט PVC בשם 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
    

    החלת ה-PVC:

    kubectl apply -f agent-volume-pvc.yaml
    
  3. מאחזרים את פרטי הייצוא של ה-PV, הצומת וה-UID של הפוד שהוקצו:

    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 ברקע. בגלל ה-finalizer, ‏ 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. כדי להשלים את ההפסקות, מסירים את ה-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":[]}}'
    

כדי להמשיך את הסשן מאוחר יותר, צריך לתבוע פוד חדש שחומם מראש ולשלוח את בקשת הקישור באמצעות ה-PVC הקיים (session-1-pvc). הפוד החדש של ארגז החול מקבל מיד גישה למצב של סביבת העבודה שנשמר.

שיקולים לגבי הפקה ועיצוב של בקרים בהתאמה אישית

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

כשמתכננים את בקר הייצור ואת דמון הצומת, צריך להטמיע את דפוסי הארכיטקטורה הבאים:

אוטומציה של מחזור החיים של ההתאמה והסופיות

הדמון של צומת האחסון מבצע הרכבות ברמת המארח מחוץ לניהול מחזור החיים של ממשק האחסון של מאגרי Kubernetes (CSI), ולכן Kubelet לא מודע להרכבות פעילות בתוך emptyDir של ה-pod. אם פוד נמחק בזמן שההרכבה פעילה, Kubelet לא מצליח להסיר את הספרייה emptyDir ומופיעה שגיאת Device or resource busy, והפוד נתקע במצב Terminating.

הבקר המותאם אישית שלכם צריך להפוך אוטומטית מכונת מצבים קפדנית לשימוש ב-finalizers (כמו agent.sandbox/storage-cleanup):

  1. יצירת טענה וקישור שלה:
    • לצרף לכל SandboxClaim סטטיק פיינלייזר (static finalizer) בזמן היצירה.
    • צפייה בעדכוני סטטוס SandboxClaim של Kubernetes API. כשבקשה נקשרת לתא של מאגר חם, צריך לחלץ את המאפיינים pod_uid, nodeName וכרך הגיבוי של Filestore שהוקצו.
    • שליחת בקשת bind מאומתת לדמון של צומת האחסון שפועל בצומת היעד.
    • מבצעים תיקון מיידי לאובייקט Pod הפועל כדי להוסיף את ה-finalizer הדינמי. מפרטי SandboxTemplate לא תומכים ב-finalizers של פודים סטטיים, ולכן צריך להחיל תיקון על הפוד באופן דינמי כדי להגן עליו במהלך ניקוז הצמתים או אירועי תזמון מחדש שבהם הפוד מפונה אבל SandboxClaim נשאר פעיל.
  2. טיפול בסיום מבוקר ובפינוי:
    • צפייה בdeletionTimestamp במשאבי SandboxClaim וPod.
    • כשמזוהה מחיקה או פינוי, מתבצעת קריאה לנקודת הקצה unbind של הדמון של הצומת כדי לבטל את הטעינה של ספריית המארח בצורה נקייה (umount -l).
    • מוודאים שההסרה הצליחה ושהכתיבה בהמתנה הושלמה לפני שמבצעים תיקון של Pod ו-SandboxClaim כדי להסיר את ה-finalizers שלהם. כך תוכלו לבצע פירוק נקי במקרים של מחיקות תקינות של הצהרות, שדרוגים של צמתים ב-GKE, הפסקות של VM במודל Spot וסגירה של תהליכים בגלל אין זיכרון פנוי (OOM).

אבטחה וחיזוק של דמון צומת האחסון

  • מחליפים את kubectl exec בממשקי API מאומתים: בסביבת הייצור, אל תשתמשו ב-kubectl exec או תקשרו את הדמון ל-localhost. מגדירים את דמון צומת האחסון לחשיפת נקודת קצה ייעודית של gRPC או HTTPS ברשת האשכול, שמאובטחת באמצעות TLS הדדי (mTLS) או אימות אסימון Kubernetes ServiceAccount.
  • בידוד של מרחבי שמות של שדים וגישה לרשת: פריסת ההרשאות המיוחדות storage-node-daemon DaemonSet במרחב שמות אדמיניסטרטיבי מוגבל (לדוגמה, sandbox-storage-system) ולא במרחבי השמות default או הדייר. החלת כללי NetworkPolicy של Kubernetes שמאפשרים כניסה לממשק ה-API של הדמון רק מתרמילי הבקרה המותאמים אישית וחסימת כל התנועה מתרמילי הסוכן של ארגז החול.
  • שימוש בקובצי אימג' של קונטיינרים מוכנים מראש: מומלץ להימנע מהתקנת חבילות כמו nfs-common בזמן הריצה ב-initContainer. כדי למנוע עיכובים בהפעלת הצומת ותלות במאגר חיצוני, כדאי להשתמש בקובץ אימג' של קונטיינר שנוצר מראש ואי אפשר לשנות אותו, עם כל כלי הטעינה הנדרשים שכבר מותקנים מראש.

אכיפת מכסות אחסון ובידוד של דיירים מרובים

  • מעקב אחר השימוש בנפח אחסון לכל סוכן: כשמבצעים bind-mount דינמי של ספריית משנה מנפח ReadWriteMany (RWX) Filestore משותף אל emptyDir, אי אפשר לאכוף מכסות אחסון לכל סוכן בנתיב ה-NFS המצורף באמצעות הגדרות emptyDir.sizeLimit רגילות של Kubernetes. כדי למנוע מצב שבו סוכן אחד שפועל ללא הפסקה ינצל את כל נפח האחסון המשותף ויגרום למניעת שירות (DoS), צריך להטמיע מעקב אחר מכסת ספרייה בכלי לארגון תהליכי עבודה או להקצות נפחי אחסון ייעודיים באמצעות מאגרי נפח אחסון.
  • התאמת מטען ייעודי (payload) של נקודות חיבור למצבי גישה שונים של סביבת עבודה: הבקר יכול לתמוך בטופולוגיות שונות של אחסון סוכנים על ידי שינוי הפרמטרים שנשלחים לדמון של הצומת:
    • סביבות עבודה פרטיות ומבודדות: אפשר לקשר ספריית משנה ייחודית של דייר או PVC ייעודי לפוד יחיד של ארגז חול עם הרשאות קריאה וכתיבה.
    • סביבות עבודה שיתופיות: אפשר לקשור בו-זמנית את אותה ספריית משנה משותפת עם הרשאות קריאה, כתיבה והרצה (RWX) בכמה פודים של סוכנים מתואמים לשיתוף קבצים בזמן אמת.
    • סביבות עבודה של הסתעפות ניתוח: אפשר להגדיר ספריית תבניות בסיסית כקריאה בלבד (ro) כדי שהסוכנים יוכלו לקרוא נכסים משותפים בלי לשנות את עותק הזהב, ובו-זמנית להפנות כתיבות חדשות לנתיב זמני נפרד שניתן לכתיבה או לספרייה של העתקה בשחזור.

תיאום של תמונות מצב בנקודת זמן וניקוי

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

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

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