הקצאה מאוחרת של נפח אחסון דינמי מאפשרת להחדיר נפחי אחסון של סוכני Filestore ישירות לפודים של ארגז החול לסוכנים ב-Google Kubernetes Engine (GKE) שפועלים ומחוממים מראש, עם יצירת התביעה. הקצאה דינמית מאוחרת מאפשרת לעקוף את מחזור החיים הרגיל של צירוף נפח אחסון ב-Kubernetes, וכך להשיג זמן אחזור של פחות מ-100 אלפיות השנייה לצירוף נפח אחסון בלי להצטרך להפעיל מחדש את ה-Pod.
הארכיטקטורה הזו מאפשרת לפלטפורמות של סוכנים בצפיפות גבוהה עם זמן אחזור נמוך:
- מניעת עיכובים בהפעלת פודים ובאתחול קונטיינרים.
- צירוף והסרה דינמיים של סביבות עבודה קבועות לפי דרישה.
- להשהות או להעביר למצב תנומה סשנים של סוכנים בלי פעילות ולחדש אותם בכל Pod זמין של ארגז חול שהוכן מראש, תוך שמירה על מצב מערכת הקבצים.
לפני שמתחילים
- משלימים את ההגדרה הראשונית במאמר הגדרת סביבת GKE לנפחי סוכן של Filestore.
- מוודאים שגרסת אשכול GKE שלכם היא
1.36.0-gke.3302001ואילך. הגרסה הזו תומכת בהערהforce-sharedשנדרשת להפצת הטמעה של gVisoremptyDir. - מוודאים שבשדה
volume-pool-scStorageClassמצוינים הערכים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
תביעת בעלות על ארגז חול וקישור דינמי של אחסון
יוצרים קובץ מניפסט של תלונה בשם
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
הקצאה דינמית של נפח אחסון באמצעות מניפסט 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
מאחזרים את פרטי הייצוא של ה-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}')"שולחים את אות ההרכבה לדמון של הצומת במארח של ה-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()) "כדי למנוע ניקוי מוקדם בזמן שהטעינה של המארח פעילה, צריך להחיל על ה-Pod שתבעתם סופית מחיקה:
kubectl patch pod "${POD_NAME}" --type=merge \ -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'בודקים את נקודת העיגון של עוצמת הקול בתוך ה-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.
מתחילים את המחיקה של
SandboxClaimברקע. בגלל ה-finalizer, Kubernetes מסמן את התלונה למחיקה אבל משהה את סיום הפעולה של ה-Pod:kubectl delete sandboxclaim late-bind-session-1 --wait=false
מבטלים את הטעינה של שיתוף ה-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()) "כדי להשלים את ההפסקות, מסירים את ה-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):
- יצירת טענה וקישור שלה:
- לצרף לכל
SandboxClaimסטטיק פיינלייזר (static finalizer) בזמן היצירה. - צפייה בעדכוני סטטוס
SandboxClaimשל Kubernetes API. כשבקשה נקשרת לתא של מאגר חם, צריך לחלץ את המאפייניםpod_uid,nodeNameוכרך הגיבוי של Filestore שהוקצו. - שליחת בקשת
bindמאומתת לדמון של צומת האחסון שפועל בצומת היעד. - מבצעים תיקון מיידי לאובייקט
Podהפועל כדי להוסיף את ה-finalizer הדינמי. מפרטיSandboxTemplateלא תומכים ב-finalizers של פודים סטטיים, ולכן צריך להחיל תיקון על הפוד באופן דינמי כדי להגן עליו במהלך ניקוז הצמתים או אירועי תזמון מחדש שבהם הפוד מפונה אבלSandboxClaimנשאר פעיל.
- לצרף לכל
- טיפול בסיום מבוקר ובפינוי:
- צפייה ב
deletionTimestampבמשאביSandboxClaimוPod. - כשמזוהה מחיקה או פינוי, מתבצעת קריאה לנקודת הקצה
unbindשל הדמון של הצומת כדי לבטל את הטעינה של ספריית המארח בצורה נקייה (umount -l). - מוודאים שההסרה הצליחה ושהכתיבה בהמתנה הושלמה לפני שמבצעים תיקון של
Podו-SandboxClaimכדי להסיר את ה-finalizers שלהם. כך תוכלו לבצע פירוק נקי במקרים של מחיקות תקינות של הצהרות, שדרוגים של צמתים ב-GKE, הפסקות של VM במודל Spot וסגירה של תהליכים בגלל אין זיכרון פנוי (OOM).
- צפייה ב
אבטחה וחיזוק של דמון צומת האחסון
- מחליפים את
kubectl execבממשקי API מאומתים: בסביבת הייצור, אל תשתמשו ב-kubectl execאו תקשרו את הדמון ל-localhost. מגדירים את דמון צומת האחסון לחשיפת נקודת קצה ייעודית של gRPC או HTTPS ברשת האשכול, שמאובטחת באמצעות TLS הדדי (mTLS) או אימות אסימון KubernetesServiceAccount. - בידוד של מרחבי שמות של שדים וגישה לרשת: פריסת ההרשאות המיוחדות
storage-node-daemonDaemonSetבמרחב שמות אדמיניסטרטיבי מוגבל (לדוגמה,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.
המאמרים הבאים
- כאן אפשר לראות שילוב של ארגז חול של סוכן.
- פריסת עומסי עבודה ב-GKE בניהול עצמי.
- איך יוצרים ומנהלים מאגרי נפח