מצב טיפוסי בעת הטמעת CI/CD ב-Kubernetes: האפליקציה חייבת להיות מסוגלת לא לקבל בקשות לקוח חדשות לפני הפסקה מוחלטת, והכי חשוב, להשלים בהצלחה בקשות קיימות.

עמידה בתנאי זה מאפשרת לך להשיג אפס זמן השבתה במהלך הפריסה. עם זאת, גם בעת שימוש בחבילות פופולריות מאוד (כמו NGINX ו-PHP-FPM), אתה יכול להיתקל בקשיים שיובילו לגל של שגיאות בכל פריסה...
תֵאוֹרִיָה. איך התרמיל חי
כבר פרסמנו בפירוט על מחזור החיים של תרמיל . בהקשר של הנושא הנדון, אנו מעוניינים בדברים הבאים: ברגע שהפוד נכנס למדינה סיום, בקשות חדשות מפסיקות להישלח אליו (pod מרשימת נקודות הקצה של השירות). לפיכך, כדי למנוע השבתה במהלך הפריסה, מספיק לנו לפתור את בעיית עצירת האפליקציה בצורה נכונה.
כדאי גם לזכור שתקופת החסד המוגדרת כברירת מחדל היא : לאחר מכן, הפוד ייסגר והאפליקציה חייבת להספיק לעבד את כל הבקשות לפני תקופה זו. שים לב: למרות שכל בקשה שלוקחת יותר מ-5-10 שניות היא כבר בעייתית, וכיבוי חינני כבר לא יעזור לה...
כדי להבין טוב יותר מה קורה כאשר תרמיל מסתיים, פשוט התבונן בתרשים הבא:

A1, B1 - קבלת שינויים לגבי מצב האח
A2 - יציאה SIGTERM
B2 - הסרת תרמיל מנקודות הקצה
B3 - קבלת שינויים (רשימת נקודות הקצה השתנתה)
B4 - עדכון כללי iptables
שימו לב: מחיקת הפוד של נקודת הקצה ושליחת SIGTERM לא מתרחשת ברצף, אלא במקביל. ובשל העובדה ש-Ingress לא מקבלת מיד את הרשימה המעודכנת של נקודות הקצה, בקשות חדשות מלקוחות יישלחו לפוד, מה שיגרום לשגיאת 500 במהלך סיום הפוד (לחומר מפורט יותר בנושא זה, אנו ). יש לפתור בעיה זו בדרכים הבאות:
- שלח חיבור: סגור את כותרות התגובה (אם זה נוגע לאפליקציית HTTP).
- אם לא ניתן לבצע שינויים בקוד, אזי המאמר הבא מתאר פתרון שיאפשר לכם לטפל בבקשות עד תום תקופת החסד.
תֵאוֹרִיָה. כיצד NGINX ו-PHP-FPM מפסיקים את התהליכים שלהם
nginx
נתחיל עם NGINX, מכיוון שהכל פחות או יותר ברור איתו. כשצוללים לתוך התיאוריה, אנו למדים של-NGINX יש תהליך מאסטר אחד וכמה "עובדים" - אלו הם תהליכים ילדים המעבדים בקשות של לקוחות. ניתנת אפשרות נוחה: שימוש בפקודה nginx -s <SIGNAL> לסיים תהליכים במצב כיבוי מהיר או כיבוי חינני. ברור שהאופציה האחרונה היא שמעניינת אותנו.
ואז הכל פשוט: אתה צריך להוסיף פקודה שתשלח אות כיבוי חינני. ניתן לעשות זאת בפריסה, בבלוק המכולה:
lifecycle:
preStop:
exec:
command:
- /usr/sbin/nginx
- -s
- quitכעת, כשהפוד יכבה, נראה את הדברים הבאים ביומני המיכל של NGINX:
2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down וזה אומר מה שאנחנו צריכים: NGINX מחכה לבקשות להסתיים, ואז הורג את התהליך. עם זאת, להלן נשקול גם בעיה נפוצה שבגללה, אפילו עם הפקודה nginx -s quit התהליך מסתיים בצורה שגויה.
ובשלב הזה סיימנו עם NGINX: לפחות מהלוגים אפשר להבין שהכל עובד כמו שצריך.
מה הקטע עם PHP-FPM? איך הוא מתמודד עם כיבוי חינני? בוא נבין את זה.
PHP-FPM
במקרה של PHP-FPM, יש קצת פחות מידע. אם אתה מתמקד ב לפי PHP-FPM, זה יגיד שהאותות POSIX הבאים מתקבלים:
-
SIGINT,SIGTERM- כיבוי מהיר; -
SIGQUIT- כיבוי חינני (מה שאנחנו צריכים).
האותות הנותרים אינם נדרשים במשימה זו, ולכן נשמיט את הניתוח שלהם. כדי לסיים את התהליך בצורה נכונה, תצטרך לכתוב את ה-preStop Hook הבא:
lifecycle:
preStop:
exec:
command:
- /bin/kill
- -SIGQUIT
- "1"במבט ראשון, זה כל מה שנדרש לביצוע כיבוי חינני בשני המכולות. עם זאת, המשימה קשה יותר ממה שהיא נראית. להלן שני מקרים שבהם כיבוי חינני לא עבד וגרם לאי זמינות לטווח קצר של הפרויקט במהלך הפריסה.
תרגול. בעיות אפשריות עם כיבוי חינני
nginx
קודם כל, כדאי לזכור: בנוסף לביצוע הפקודה nginx -s quit יש עוד שלב אחד ששווה לשים לב אליו. נתקלנו בבעיה שבה NGINX עדיין ישלח את SIGTERM במקום את האות SIGQUIT, מה שגורם לבקשות לא להשלים כהלכה. ניתן למצוא מקרים דומים, למשל, . לרוע המזל, לא הצלחנו לקבוע את הסיבה הספציפית להתנהגות זו: היה חשד לגבי גרסת NGINX, אך הוא לא אושר. הסימפטום היה שהודעות נצפו ביומני המכולה של NGINX "פתח שקע מס' 10 שנותר בחיבור 5", לאחר מכן התרמיל נעצר.
אנו יכולים לראות בעיה כזו, למשל, מהתגובות ב-Ingress שאנו צריכים:

אינדיקטורים של קודי מצב בזמן הפריסה
במקרה זה, אנו מקבלים רק קוד שגיאה 503 מ-Ingress עצמה: היא לא יכולה לגשת לקונטיינר NGINX, מכיוון שהוא כבר לא נגיש. אם אתה מסתכל על יומני המכולה עם NGINX, הם מכילים את הדברים הבאים:
[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13לאחר שינוי אות העצירה, המכולה מתחילה לעצור כהלכה: הדבר מאושר על ידי העובדה ששגיאת 503 אינה נצפית עוד.
אם אתה נתקל בבעיה דומה, הגיוני להבין באיזה אות עצירה נעשה שימוש במיכל ואיך בדיוק נראה וו ה-preStop. בהחלט ייתכן שהסיבה נעוצה דווקא בזה.
PHP-FPM... ועוד
הבעיה ב-PHP-FPM מתוארת בצורה טריוויאלית: היא לא ממתינה להשלמה של תהליכי צאצא, היא מפסיקה אותם, וזו הסיבה שמתרחשות שגיאות 502 במהלך הפריסה ופעולות אחרות. ישנם מספר דיווחי באגים ב-bugs.php.net מאז 2005 (למשל и ), המתאר בעיה זו. אבל סביר להניח שלא תראה שום דבר ביומנים: PHP-FPM תכריז על השלמת התהליך שלו ללא שגיאות או הודעות צד שלישי.
כדאי להבהיר שהבעיה עצמה עשויה להיות תלויה במידה פחותה או יותר באפליקציה עצמה ואולי לא תתבטא, למשל, בניטור. אם אתה נתקל בזה, תחילה עולה בראש פתרון פשוט: הוסף וו PreStop עם sleep(30). זה יאפשר לך להשלים את כל הבקשות שהיו קודם לכן (ואנחנו לא מקבלים חדשות, מאז pod כבר מסוגל סיום), ואחרי 30 שניות הפוד עצמו יסתיים באות SIGTERM.
מתברר כי lifecycle עבור המיכל ייראה כך:
lifecycle:
preStop:
exec:
command:
- /bin/sleep
- "30" עם זאת, בשל 30 השניות sleep אנחנו בתוקף אנו נגביר את זמן הפריסה, מכיוון שכל פוד יסתיים מינימום 30 שניות, וזה רע. מה ניתן לעשות בנידון?
נפנה אל הגורם האחראי לביצוע ישיר של הבקשה. במקרה שלנו זה כן PHP-FPMאשר כברירת מחדל אינו מפקח על ביצוע תהליכי הצאצא שלו: תהליך המאסטר מסתיים באופן מיידי. אתה יכול לשנות התנהגות זו באמצעות ההנחיה process_control_timeout, המציין את מגבלות הזמן לתהליכי צאצא להמתין לאותות מהמאסטר. אם תגדיר את הערך ל-20 שניות, זה יכסה את רוב השאילתות הפועלות במיכל ויעצור את תהליך המאסטר לאחר השלמתן.
עם הידע הזה, בואו נחזור לבעיה האחרונה שלנו. כאמור, Kubernetes אינה פלטפורמה מונוליטית: תקשורת בין מרכיביה השונים לוקחת זמן מה. זה נכון במיוחד כאשר אנו בוחנים את הפעולה של Ingresses ורכיבים קשורים אחרים, שכן עקב עיכוב כזה בזמן הפריסה קל לקבל גל של 500 שגיאות. לדוגמה, שגיאה עלולה להתרחש בשלב שליחת הבקשה ל-upstream, אבל "הפרש הזמן" של אינטראקציה בין רכיבים הוא די קצר - פחות משנייה.
לכן, בסך הכל עם ההנחיה שכבר הוזכרה process_control_timeout אתה יכול להשתמש בבנייה הבאה עבור lifecycle:
lifecycle:
preStop:
exec:
command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"] במקרה זה, נפצה על העיכוב בפקודה sleep ואל תגדילו מאוד את זמן הפריסה: האם יש הבדל ניכר בין 30 שניות לאחת?.. למעשה, זה process_control_timeoutו - lifecycle משמש רק כ"רשת ביטחון" במקרה של פיגור.
באופן כללי ההתנהגות המתוארת והפתרון המתאים לא חלים רק על PHP-FPM. מצב דומה עלול להיווצר בדרך זו או אחרת בעת שימוש בשפות/מסגרות אחרות. אם אינך יכול לתקן כיבוי חינני בדרכים אחרות - למשל, על ידי כתיבה מחדש של הקוד כך שהאפליקציה תעבד נכון את אותות סיום - תוכל להשתמש בשיטה המתוארת. זה אולי לא הכי יפה, אבל זה עובד.
תרגול. בדיקת עומס לבדיקת פעולת הפוד
בדיקת עומסים היא אחת הדרכים לבדוק איך המיכל עובד, שכן הליך זה מקרב אותו לתנאי לחימה אמיתיים כאשר משתמשים מבקרים באתר. כדי לבדוק את ההמלצות לעיל, אתה יכול להשתמש : זה מכסה את כל הצרכים שלנו בצורה מושלמת. להלן טיפים והמלצות לביצוע בדיקות עם דוגמה ברורה מניסיוננו הודות לגרפים של Grafana ו-Yandex.Tank עצמה.
הדבר הכי חשוב כאן הוא לבדוק שינויים צעד אחר צעד. לאחר הוספת תיקון חדש, הפעל את הבדיקה ובדוק אם התוצאות השתנו בהשוואה לריצה האחרונה. אחרת, יהיה קשה לזהות פתרונות לא יעילים, ובטווח הארוך זה יכול רק להזיק (למשל, להגדיל את זמן הפריסה).
ניואנס נוסף הוא להסתכל על יומני המכולה במהלך סיומו. האם נרשם שם מידע על כיבוי חינני? האם יש שגיאות ביומנים בעת גישה למשאבים אחרים (לדוגמה, למיכל PHP-FPM סמוך)? שגיאות באפליקציה עצמה (כמו במקרה של NGINX שתואר לעיל)? אני מקווה שהמידע המבוא ממאמר זה יעזור לך להבין טוב יותר מה קורה למיכל במהלך סיומו.
אז, ריצת המבחן הראשונה התקיימה ללא lifecycle וללא הנחיות נוספות עבור שרת היישומים (process_control_timeout ב-PHP-FPM). מטרת בדיקה זו הייתה לזהות את המספר המשוער של שגיאות (והאם קיימות). כמו כן, ממידע נוסף, כדאי לדעת שזמן הפריסה הממוצע של כל פוד היה כ-5-10 שניות עד שהוא היה מוכן לחלוטין. התוצאות הן:

לוח המידע של Yandex.Tank מציג עלייה של 502 שגיאות, שהתרחשו בזמן הפריסה ונמשכו בממוצע עד 5 שניות. ככל הנראה זה היה בגלל שבקשות קיימות לפוד הישן בוטלו כאשר הוא הופסק. לאחר מכן, הופיעו 503 שגיאות, שהייתה תוצאה של קונטיינר NGINX שהופסק, שגם הפיל חיבורים בגלל ה-backend (מה שמנע מ-Ingress להתחבר אליו).
בוא נראה איך process_control_timeout ב-PHP-FPM יעזור לנו לחכות להשלמת תהליכי צאצא, כלומר. לתקן שגיאות כאלה. פריס מחדש באמצעות הנחיה זו:

אין עוד שגיאות במהלך הפריסה ה-500! הפריסה מוצלחת, כיבוי חינני עובד.
עם זאת, כדאי לזכור את הבעיה בקונטיינרים של Ingress, אחוז קטן של שגיאות בהן אנו עשויים לקבל עקב פיגור זמן. כדי להימנע מהם, כל מה שנותר הוא להוסיף מבנה איתו sleep וחזור על הפריסה. עם זאת, במקרה הספציפי שלנו, לא נראו שינויים (שוב, ללא שגיאות).
מסקנה
כדי לסיים את התהליך בחן, אנו מצפים להתנהגות הבאה מהאפליקציה:
- המתן מספר שניות ואז הפסק לקבל חיבורים חדשים.
- המתן עד שכל הבקשות ישלימו וסגרו את כל חיבורי Keepalive שאינם מבצעים בקשות.
- סיים את התהליך שלך.
עם זאת, לא כל היישומים יכולים לעבוד בצורה זו. פתרון אחד לבעיה במציאות של Kubernetes הוא:
- הוספת וו לפני עצירה שיחכה מספר שניות;
- לומד את קובץ התצורה של backend שלנו עבור הפרמטרים המתאימים.
הדוגמה עם NGINX מבהירה שאפילו אפליקציה שבהתחלה צריכה לעבד אותות סיום בצורה נכונה עשויה שלא לעשות זאת, ולכן חיוני לבדוק 500 שגיאות במהלך פריסת האפליקציה. זה גם מאפשר להסתכל על הבעיה בצורה רחבה יותר ולא להתמקד בפוד או קונטיינר בודד, אלא להסתכל על כל התשתית כמכלול.
ככלי בדיקה, אתה יכול להשתמש ב-Yandex.Tank בשילוב עם כל מערכת ניטור (במקרה שלנו, הנתונים נלקחו מ-Grafana עם Prometheus backend עבור הבדיקה). בעיות עם כיבוי חינני נראות בבירור תחת עומסים כבדים שהמדד יכול ליצור, והניטור עוזר לנתח את המצב ביתר פירוט במהלך הבדיקה או אחריה.
בתגובה למשוב על המאמר: ראוי להזכיר שהבעיות והפתרונות מתוארים כאן ביחס ל-NGINX Ingress. למקרים אחרים, ישנם פתרונות נוספים, אותם אנו עשויים לשקול בחומרים הבאים של הסדרה.
נ.ב.
אחר מסדרת הטיפים והטריקים של K8s:
- «";
- «";
- «";
- «".
מקור: www.habr.com
