חוסן בתוכנה מתייחס ליכולת של אפליקציה להמשיך לתפקד בצורה חלקה ומהימנה, גם מול בעיות או כשלים בלתי צפויים. בפרויקטי פינטק לחוסן יש חשיבות גבוהה במיוחד בגלל מספר סיבות. ראשית, חברות מחויבות לעמוד בדרישות הרגולטוריות והרגולטורים הפיננסיים שמים דגש על חוסן תפעולי כדי לשמור על יציבות במערכת. יתרה מכך, ריבוי הכלים הדיגיטליים וההסתמכות על ספקי שירותים של צד שלישי חושפים את עסקי הפינטק לאיומי אבטחה מוגברים. חוסן גם עוזר להפחית את הסיכונים של הפסקות הנגרמות על ידי גורמים שונים כגון איומי סייבר, מגיפות או אירועים גיאופוליטיים, שמירה על פעולות הליבה העסקיות ונכסים קריטיים.
לפי דפוסי חוסן, אנו מבינים קבוצה של שיטות עבודה ואסטרטגיות מומלצות שנועדו להבטיח שתוכנה תוכל לעמוד בהפרעות ולשמור על פעילותה. דפוסים אלה פועלים כמו רשתות ביטחון, ומספקים מנגנונים לטיפול בשגיאות, ניהול עומס והתאוששות מתקלות, ובכך מבטיחים שהיישומים יישארו חזקים ומהימנים בתנאים שליליים.
אסטרטגיות החוסן הנפוצות ביותר כוללות מחיצה, מטמון, סתירה, ניסיון חוזר ומפסק זרם. במאמר זה, אדון בהם ביתר פירוט, עם דוגמאות לבעיות שהם יכולים לעזור לפתור.
הבה נסתכל על ההגדרה לעיל. יש לנו אפליקציה מאוד רגילה עם כמה קצה אחורי מאחורינו שאפשר לקבל מהם נתונים. ישנם מספר לקוחות HTTP המחוברים ל-backends אלה. מסתבר שכולם חולקים את אותו מאגר חיבורים! וגם משאבים אחרים כמו מעבד ו-RAM.
מה יקרה, אם אחד מהחלקים האחוריים יתקל בבעיות מסוימות שיובילו להשהיית בקשה גבוהה? בשל זמן התגובה הגבוה, כל מאגר החיבורים יתפוס במלואו בבקשות הממתינות לתגובות מ-backend1. כתוצאה מכך, בקשות המיועדות ל-backend2 ול-backend3 בריאים לא יוכלו להמשיך כי המאגר נגמר. משמעות הדבר היא שכשל באחד מהממשקים האחוריים שלנו עלול לגרום לכשל בכל האפליקציה. באופן אידיאלי, אנו רוצים שרק הפונקציונליות הקשורה ל-backend הכושל תחווה השפלה, בעוד ששאר האפליקציה תמשיך לפעול כרגיל.
מהי תבנית המחיצה?
המונח, Bulkhead pattern, נובע מבניית ספינות, הוא כרוך ביצירת מספר תאים מבודדים בתוך ספינה. אם מתרחשת דליפה בתא אחד, היא מתמלאת במים, אך התאים האחרים לא מושפעים. בידוד זה מונע מהכלי לשקוע בשל פרצה אחת.
ניתן להשתמש בתבנית Bulkhead כדי לבודד סוגים שונים של משאבים בתוך יישום, ולמנוע כשל בחלק אחד להשפיע על המערכת כולה. כך נוכל ליישם את זה על הבעיה שלנו:
נניח שלמערכות האחוריות שלנו יש סבירות נמוכה להיתקל בשגיאות בנפרד. עם זאת, כאשר פעולה כרוכה בשאילתה של כל הקצה האחורי הללו במקביל, כל אחד מהם יכול להחזיר שגיאה באופן עצמאי. מכיוון ששגיאות אלו מתרחשות באופן עצמאי, ההסתברות הכוללת לטעות באפליקציה שלנו גבוהה מהסתברות השגיאה של כל קצה אחורי בודד. ניתן לחשב את הסתברות השגיאה המצטברת באמצעות הנוסחה P_total=1−(1−p)^n, כאשר n הוא מספר המערכות האחוריות.
לדוגמה, אם יש לנו עשרה חלקים אחוריים, שלכל אחד מהם הסתברות שגיאה של p=0.001 (המקבילה ל-SLA של 99.9%), הסתברות השגיאה המתקבלת היא:
P_total=1−(1−0.001)^10=0.009955
המשמעות היא שה-SLA המשולב שלנו יורד לכ-99%, מה שממחיש כיצד האמינות הכוללת פוחתת כאשר מבצעים שאילתות של מספר קצה אחורי במקביל. כדי להקל על בעיה זו, אנו יכולים ליישם מטמון בזיכרון.
מטמון בזיכרון משמש כמאגר נתונים במהירות גבוהה, מאחסן נתונים שנגישים אליהם לעתים קרובות ומבטל את הצורך להביא אותם ממקורות שעלולים להיות איטיים בכל פעם. מכיוון שלמטמונים המאוחסנים בזיכרון יש סיכוי של 0% לשגיאה בהשוואה לאחזור נתונים דרך הרשת, הם מגדילים משמעותית את האמינות של האפליקציה שלנו. יתר על כן, שמירה במטמון מפחיתה את תעבורת הרשת, ומקטינה עוד יותר את הסיכוי לשגיאות. כתוצאה מכך, על ידי שימוש במטמון בזיכרון, אנו יכולים להשיג שיעור שגיאות נמוך עוד יותר באפליקציה שלנו בהשוואה למערכות האחוריות שלנו. בנוסף, מטמונים בזיכרון מציעים אחזור נתונים מהיר יותר מאשר אחזור מבוסס רשת, ובכך מפחיתים את זמן האחזור של האפליקציה - יתרון בולט.
עבור נתונים מותאמים אישית, כגון פרופילי משתמשים או המלצות, שימוש במטמונים בזיכרון יכול להיות יעיל מאוד. אבל אנחנו צריכים להבטיח שכל הבקשות ממשתמש יעברו באופן עקבי לאותו מופע אפליקציה כדי להשתמש בנתונים המאוחסנים עבורו, מה שדורש הפעלות דביקות. יישום הפעלות דביקות יכול להיות מאתגר, אך עבור תרחיש זה, איננו זקוקים למנגנונים מורכבים. איזון מחדש של תעבורה קטן מקובל, אז אלגוריתם איזון עומסים יציב כמו גיבוב עקבי יספיק.
יתרה מכך, במקרה של כשל בצומת, גיבוב עקבי מבטיח שרק המשתמשים הקשורים לצומת הכושל יעברו איזון מחדש, תוך מזעור הפרעות במערכת. גישה זו מפשטת את הניהול של מטמונים מותאמים אישית ומשפרת את היציבות והביצועים הכוללים של האפליקציה שלנו.
אם הנתונים שאנו מתכוונים לשמור במטמון הם קריטיים ונעשה בהם שימוש בכל בקשה שהמערכת שלנו מטפלת בהם, כגון מדיניות גישה, תוכניות מנוי או ישויות חיוניות אחרות בדומיין שלנו - מקור הנתונים הללו עלול להוות נקודת כשל משמעותית במערכת שלנו. כדי להתמודד עם אתגר זה, גישה אחת היא לשכפל את הנתונים הללו באופן מלא ישירות לזיכרון של האפליקציה שלנו.
בתרחיש זה, אם נפח הנתונים במקור ניתן לניהול, נוכל להתחיל את התהליך על ידי הורדת תמונת מצב של נתונים אלה בתחילת היישום שלנו. לאחר מכן, אנו יכולים לקבל אירועי עדכונים כדי להבטיח שהנתונים המאוחסנים במטמון יישארו מסונכרנים עם המקור. על ידי אימוץ שיטה זו, אנו משפרים את המהימנות של הגישה לנתונים החיוניים הללו, שכן כל אחזור מתרחש ישירות מהזיכרון עם הסתברות של 0% לשגיאה. בנוסף, אחזור נתונים מהזיכרון מהיר במיוחד, ובכך מייעל את הביצועים של האפליקציה שלנו. אסטרטגיה זו מפחיתה למעשה את הסיכון הכרוך בהסתמכות על מקור נתונים חיצוני, ומבטיחה גישה עקבית ואמינה למידע קריטי עבור פעולת האפליקציה שלנו.
עם זאת, הצורך להוריד נתונים על הפעלת האפליקציה, ובכך לעכב את תהליך האתחול, מפר את אחד העקרונות של 'יישום 12 הגורמים' הדוגל בהפעלה מהירה של יישומים. אבל, אנחנו לא רוצים לוותר על היתרונות של שימוש במטמון. כדי להתמודד עם הדילמה הזו, נבדוק פתרונות אפשריים.
הפעלה מהירה היא חיונית, במיוחד עבור פלטפורמות כמו Kubernetes, המסתמכות על הגירה מהירה של יישומים לצמתים פיזיים שונים. למרבה המזל, Kubernetes יכול לנהל יישומים עם הפעלה איטית באמצעות תכונות כמו בדיקות אתחול.
אתגר נוסף שעשוי לעמוד בפנינו הוא עדכון תצורות בזמן שהאפליקציה פועלת. לעתים קרובות, יש צורך בהתאמת זמני מטמון או פסקי זמן של בקשה כדי לפתור בעיות ייצור. גם אם נוכל לפרוס במהירות קבצי תצורה מעודכנים לאפליקציה שלנו, יישום השינויים הללו דורש בדרך כלל הפעלה מחדש. עם זמן האתחול הממושך של כל אפליקציה, הפעלה מחדש מתגלגלת יכולה לעכב משמעותית את פריסת התיקונים למשתמשים שלנו.
כדי להתמודד עם זה, פתרון אחד הוא לאחסן תצורות במשתנה במקביל ולעדכן אותו מדי פעם שרשור רקע. עם זאת, פרמטרים מסוימים, כגון פסק זמן של בקשת HTTP, עשויים לדרוש אתחול מחדש של HTTP או לקוחות מסד נתונים כאשר התצורה המתאימה משתנה, מה שמציב אתגר פוטנציאלי. עם זאת, חלק מהלקוחות, כמו מנהל ההתקן Cassandra עבור Java, תומכים בטעינה מחדש אוטומטית של תצורות, מה שמפשט את התהליך הזה.
הטמעת תצורות הניתנות לטעינה מחדש יכולה להפחית את ההשפעה השלילית של זמני הפעלה ארוכים של יישומים ולהציע יתרונות נוספים, כגון הקלה על הטמעת דגל תכונות. גישה זו מאפשרת לנו לשמור על אמינות יישומים ותגובתיות תוך ניהול יעיל של עדכוני תצורה.
כעת נסתכל על בעיה נוספת: במערכת שלנו, כאשר בקשת משתמש מתקבלת ומעובדת על ידי שליחת שאילתה ל-backend או למסד נתונים, מדי פעם מתקבלת תגובת שגיאה במקום הנתונים הצפויים. לאחר מכן, המערכת שלנו מגיבה למשתמש ב'שגיאה'.
עם זאת, בתרחישים רבים, ייתכן שעדיף להציג נתונים מעט מיושנים יחד עם הודעה המציינת שיש עיכוב לרענון נתונים, במקום להשאיר את המשתמש עם הודעת שגיאה אדומה גדולה.
כדי לטפל בבעיה זו ולשפר את התנהגות המערכת שלנו, נוכל ליישם את דפוס ה- Fallback. הרעיון מאחורי דפוס זה כולל מקור נתונים משני, שעשוי להכיל נתונים באיכות נמוכה יותר או טריות בהשוואה למקור הראשי. אם מקור הנתונים הראשי אינו זמין או מחזיר שגיאה, המערכת יכולה לחזור לאחזר נתונים ממקור משני זה, ולהבטיח שצורה כלשהי של מידע תוצג למשתמש במקום הצגת הודעת שגיאה.
אם תסתכל על התמונה למעלה, תבחין בדמיון בין הבעיה שאנו מתמודדים איתה כעת לבין זו שנתקלנו בה עם דוגמה למטמון.
כדי לפתור אותה, נוכל לשקול ליישם דפוס המכונה ניסיון חוזר. במקום להסתמך על מטמונים, ניתן לתכנן את המערכת כך שתשלח מחדש את הבקשה באופן אוטומטי במקרה של שגיאה. דפוס ניסיון חוזר זה מציע חלופה פשוטה יותר ויכול להפחית ביעילות את הסבירות לטעויות באפליקציה שלנו. שלא כמו שמירה במטמון, שלעיתים קרובות דורש מנגנוני אי תוקף מורכבים של מטמון כדי לטפל בשינויי נתונים, ניסיון חוזר של בקשות שנכשלו הוא פשוט יחסית ליישום. מכיוון שפסילת מטמון נחשבת כאחת המשימות המאתגרות ביותר בהנדסת תוכנה, אימוץ אסטרטגיית ניסיון חוזר יכולה לייעל את הטיפול בשגיאות ולשפר את עמידות המערכת.
עם זאת, אימוץ אסטרטגיית ניסיון חוזר מבלי לקחת בחשבון השלכות אפשריות עלול להוביל לסיבוכים נוספים.
בואו נדמיין שאחד מהחלקים האחוריים שלנו חווה כישלון. בתרחיש כזה, התחלת ניסיונות חוזרים ל-backend הכושל עלולה לגרום לעלייה משמעותית בנפח התעבורה. הזינוק הפתאומי הזה בתנועה עלול להציף את הקצה האחורי, להחמיר את הכשל ולגרום לאפקט מפל על פני המערכת.
כדי להתמודד עם האתגר הזה, חשוב להשלים את דפוס הניסיון החוזר עם דפוס המפסק. מפסק החשמל משמש כמנגנון הגנה המנטר את שיעור השגיאות של שירותים במורד הזרם. כאשר שיעור השגיאה עולה על סף מוגדר מראש, המפסק קוטע בקשות לשירות המושפע למשך זמן מוגדר. במהלך תקופה זו, המערכת נמנעת מלשלוח בקשות נוספות כדי לאפשר לשירות הכושל להתאושש. לאחר המרווח המיועד, המפסק מאפשר בזהירות למספר מוגבל של בקשות לעבור, ומוודא אם השירות התייצב. אם השירות התאושש, התעבורה הרגילה משוחזרת בהדרגה; אחרת, המעגל נשאר פתוח, ממשיך לחסום בקשות עד שהשירות יחזור לפעולה רגילה. על ידי שילוב דפוס מפסק המעגל לצד לוגיקה של ניסיון חוזר, נוכל לנהל ביעילות מצבי שגיאה ולמנוע עומס יתר של המערכת במהלך כשלים בקצה האחורי.
לסיכום, על ידי יישום דפוסי חוסן אלו, נוכל לחזק את האפליקציות שלנו מפני מקרי חירום, לשמור על זמינות גבוהה ולספק חוויה חלקה למשתמשים. בנוסף, ברצוני להדגיש שטלמטריה היא עוד כלי שאין להתעלם ממנו בעת מתן חוסן לפרויקט. יומנים ומדדים טובים יכולים לשפר באופן משמעותי את איכות השירותים ולספק תובנות חשובות לגבי הביצועים שלהם, ולסייע בקבלת החלטות מושכלות כדי לשפר אותם עוד יותר.