הגדרת Kubernetes StatefulSets ליישומים עם מצב

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

היכולות שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • עיצוב לוגו לחברת B2B Advance
    עיצוב לוגו לחברת B2B Advance
    776
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח לוגו חברת AIDER
    פיתוח לוגו חברת AIDER
    1022
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113

StatefulSet Kubernetes: מה נשבר בלי זהויות יציבות

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

זהויות יציבות של פודים — זה לא קישוט, אלא חוזה. mysql-0, mysql-1, mysql-2 — שמות קבועים, כל אחד מקושר ל-PersistentVolumeClaim משלו, ובהפעלה מחדש הפוד חוזר בדיוק לכרך שלו.

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

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

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

מה נותנת עבודה מנוהלת עם מצב: אפקטים מדידים

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

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

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

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

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

מצבי פריסה של StatefulSet Kubernetes: טבלת השוואה

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

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

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

מצב ניהול המצב מוגדר בנפרד מהמדיום: podManagementPolicy קובע אם הרפליקות עולות בתור או במקביל, updateStrategy — איך עובר העדכון, ו-headless-שירות נותן שמות צפויים לתקשורת בין הרפליקות.

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

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

סדר העבודות: אבחון, סכמה, מיגרציה, בדיקה, מסירה

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

  1. מיפוי ואבחון האשכול. אוספים רשימת אפליקציות עם מצב, בקרים נוכחיים, בקשות לכרכים, מחלקות אחסון זמינות, מכסות ופילוח לפי אזורי זמינות.
  2. סכמת פריסה. מתארים StatefulSet יעד: headless Service לשמות DNS צפויים, volumeClaimTemplates עם מחלקת האחסון הנדרשת, סדר הפעלת pod וכללי פריסה לפי צמתים ואזורים.
  3. העברת נתונים והפעלה ראשונה. בודקים תכונה מרכזית: ביצירה מחדש של pod הכרך ושם הרשת שלו נשמרים, האפליקציה עולה על אותם נתונים בלי פעולות ידניות.
  4. הגדרת עמידות בכשלים. בדיקות מוכנות וחיים, תקציב הפרעות, מגבלות משאבים, עדכון pod אחד בכל פעם — כדי שהקוורום לא יאבד באמצע ההפצה.
  5. בדיקת עמידות בכשלים. מכבים צמתים ואזורים באופן מתוכנן, מוציאים צומת מהקוורום, מודדים זמן בחירה מחדש של המנהיג והשחזור מגיבוי.
  6. מסירה לתפעול. מוסרים סכמה, נהלים לפעולות שגרה וחירום, התראות לפי מצב כרכים ורפליקציה, וגבולות סקיילינג.

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

מה הלקוח מקבל לידיים: סכמות, מניפסטים, נהלים

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

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

מקרה בוחן: העברת תורים ומאגרים לזהויות יציבות

לפני: הזהות אבדה בכל הפעלה מחדש

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

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

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

מה שינו

העברנו תורים ומאגרים ל-StatefulSet עם שירות headless: הפוד מקבל שם צפוי ורשומת DNS, והכרך נוצר מתבנית ונשאר מקושר לרפליקה שלו. סדר ההפעלה והעצירה הפך קפדני — קודם עולה המנהיג, אחר כך העוקבים, והפינוי לא מתחיל עד שהרפליקה אישרה מוכנות. הוספנו תקציב הפרעות ובדיקת מוכנות שמסתכלת על זמינות האחסון לכתיבה, לא על חיוניות התהליך.

אחרי: מדדים

  • שחזור רפליקה — מ-40 דקות ל-4, בלי צעדים ידניים.
  • כשלים בגלל כרכים שאבדו — אפס בחצי שנת תצפית.
  • תחזוקת צמתים מתבצעת בשעות העבודה: אין קריאות לילה לתורן.

הסיבה נעלמה, לא הסימפטום: החלפת זהות כבר לא משוחזרת, ולכן כשלים שחזרו כל חודש לא דורשים טיפול מהיר.

מה לעשות אם כרכים קבועים כבר נפרסו ידנית?

אפשר להעביר מערכת עובדת ל-StatefulSet בלי לעצור את השירות — בתנאי שהכרכים לא נוצרים מחדש, אלא מתקבלים כפי שהם. ב-Kubernetes StatefulSet מתאים כרך לפוד לפי שם: שם התבנית בתוספת מספר סידורי.

אם PVC קיימים נקראים לפי הכלל הזה ומכריזים על האובייקט החדש כבעלים, הבקר יאסוף את הנתונים שכבר קיימים, ולא ישלח בקשה ליצירת כרך ריק מ-StorageClass.

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

רק לאחר מכן מורידים את הפודים הישנים, ו-StatefulSet מעלה פוד עם אותו כרך מוצהר — הנתונים נשארים אותם קבצים על אותו מדיום.

עבור בסיסי נתונים עם רפליקציה החלפה ישירה של פוד לא מתאימה: צומת שמסתכל לכרך קיים צריך להישאר חבר בקבוצה, והמעבר מתבצע בהחלפת מנהיג. קודם פוד חדש על הכרך המתקבל מדביק את היומן, אחר כך מועברת אליו תפקיד המנהיג, ורק потом מכבים את הישן — האשכול עונה לבקשות כל הזמן.

ביטוח מפני תוצאה בלתי הפיכה — מדיניות שמירה: מכבים איסוף בקשות אוטומטי, שומרים מניפסטים ישנים ומעגל גיבוי עד להשוואת checksums. אם נמצאה אי-התאמה, חזרה לאחור היא החזרת הבקר הקודם והבקשות לבעלים הישן, ולא שחזור מעותק.

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

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

צומת עם בסיס נתונים נפל — הנתונים אבדו?

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

איך להעביר מצב לאשכול אחר?

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

גרסה שברה את המצב — איך חוזרים לאחור?

האימג'ים מוצמדים לפי digest, ולכן חזרה היא מעבר למניפסט הקודם. את סכמת הנתונים משנים בצעדים תואמים לאחור: קודם מוסיפים שדות, אחר כך מעבירים קריאה, ורק потом מסירים את הישן — כך חזרה לאחור של האפליקציה לא גוררת חזרה לאחור של בסיס הנתונים.

גיבויים — לפי לוח זמנים או לפי דרישה?

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

נדון במשימה: נבנה סכמה ונחזור עם הערכה

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

לאחר מכן הולכים במסלול ברור:

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

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