2018-02-24

Bulkheads - דפוס עיצוב של חוסן (Resiliency Design Pattern)


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


[ב]


ירידה לעומקו של עניין


רעיון ה bulkhead[א] הוא רעיון עקרוני ליציבות של מערכות, שאותו ניתן לראות בשימוש גם בעולם התוכנה.

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

בניגוד ל redundancy (רעיון דומה), ב bulkhead כל החלקים הם בשימוש. כלומר: אין חלקים יתירים. 
ב redundancy התסריט הנפוץ הוא פגיעה ללא הפרעה לשירות, אך ב bulkhead - ישנה פגיעה חלקית במקום מלאה (תמורת חסכון עלויות ה redundancy).

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


הנה שתי דוגמאות מוכרות ליישום של bulkhead שאנו מכירים מהיום-יום:
  • availability zones ב AWS (או המקבילה בעננים אחרים) - כשל של AZ יחיד יפגע בשירות (בטווח הקצר) - אך יאפשר לנו להמשיך את השירות כרגיל ב AZs האחרים. 
    • לצורך כך מושקעים ב Amazon מאמצים רבים על מנת לוודא ש AZ אינם תלויים זה בזה, ושכשל באחד ה AZ (הצפה, נפילת מתח, בעיית תוכנה, וכו') - לא יגרור כשל של ה AZ האחרים.
    • כמובן שבתכנון מערכת המשתמשת ב AWS עלינו ליצור יתירות של שירותים חיוניים (למשל: NAT gateway או בסיס-נתונים) על מנת שנוכל להמשיך ולרוץ בזמן ש AZ אחד כשל.
  • תהליכים במערכת ההפעלה - מערכת ההפעלה יוצרת הפרדה בין תהליכים (processes) שונים כך שכשל בתהליך אחד לא ישפיע על תהליכים אחרים: תהליך אחד קורס - והשאר יכולים להמשיך לרוץ ללא הפרעה.
    • למען הדיוק הטכני שווה לציין שההפרדה הזו אינה bullet proof כאשר מדובר בגישה למשאבים משותפים.
      למשל: תהליך שגוזל 100% CPU עלול להיות מתוזמן (לחלופין) על כל ה cores של המכונה ולשתק בפועל את כולה. עלינו להצמיד את התהליך (בעזרת CPU binding / affinity) ל core מסוים - בכדי לקבל הגנה טובה בפני תסריט ה "CPU 100%". עניין דומה קיים לגבי זיכרון, גישה ל I/O, או כל משאב משותף אחר. 
היישום שאני רוצה להתמקד בו הוא יישום אפליקטיבי של מערכת (ווב).

יישום בסיסי של bulkheads: להפריד את השרתים שלנו לשני clusters שונים (ומבודדים זה-מזה) ולנתב בקשות שונות ל cluster שונה. החלוקה יכולה להיות עבור microservice בודד, קבוצה של microservices, או אולי אפילו כל המערכת.



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

החכמה ביישום bulkhead מוצלח היא חלוקה סלקטיבית ע"פ שני קריטריונים:
  • מאפייני כשל (failure conditions) - כך שתעבורה מסוג I עלולה לכשול בעוד תעבורה מסוג II עשויה לעבוד כרגיל.
  • יתרון עסקי (financial benefit) - כאשר יש חשיבות עסקית מאחורי סוגי התעבורה השונים שעשויה להצדיק מצב בו תעבורה סוג I שורדת בעוד תעבורה סוג II כושלת.
Bulkhead מוצלח עשוי להיות על בסיס שני הקריטריונים, או רק אחד מהם.


הנה כמה דוגמאות ליישום של Bulkhead ברמה האפליקטיבית:

הדוגמה הקלאסית היא כנראה הפרדה בין לקוחות משלמים ללקוחות לא-משלמים. 
נניח: אתר שנותן שירות מוגבל בחינם, אך שירות משופר בתשלום (freemium).
שימו לב שהחלוקה היא עסקית.
וריאציה מקובלת: שני clusters:
  • Cluster A - ללקוחות משלמים
  • Cluster B - ללקוחות שאינם משלמים.
אם יש בעיה בפיצ'ר של לקוחות לא-משלמים שגורם לבעיה - לקוחות משלמים יכולים (ובצדק!) להמשיך ליהנות משירות תקין.
אפשר לשים יותר חומרה ומשאבים, קונפיגרציות יותר אמינות (גם אם עולות יותר) - ב cluster של הלקוחות המשלמים.

החולשה של המודל היא במאפייני הכשל: דווקא הלקוחות המשלמים מקבלים כנראה יותר יכולות, ולכן יש סבירות גבוהה יותר שדווקא הטראפיק שלהם ייתקל בבאג כלשהו - שלא יקרה ללקוחות ה"חינמיים".
קצת פדיחה אם Cluster A נפל - בעוד cluster B עובד כרגיל...

תת וריאציה היא ש Cluster B יקבל תעבורה של שני סוגי הלקוחות: משלמים ולא-משלמים.
במקרה של תקלה - אפשר לדחות לקוחות לא-משלמים כליל מהמערכת. אם יש משהו שיציל את התעבורה של לקוחות משלמים (נניח: עוד חומרה) - אדרבא!
אם יש כשל שנובע מ"פיצ'ר חינמי" (נניח: פרסומות) - יש הגיון עסקי רב לבודד את הכשל מלקוחות משלמים.
הוריאציה הזו הגיונית ככל ש Cluster B גדול מ Cluster A (נניח: פי כמה מונים).



דוגמה: הפרדה בין לקוחות ישירים ללקוחות דרך שותפים עסקיים -- בהנחה שה flow של שותפים עסקיים הוא שונה (למשל: חישוב מחיר שונה, מעקב שונה, ועדכון השותף העסקי בזמן אמת - וכו').

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

בדוגמה הזו יש ל bulkheads פוטנציאל גדול יותר להשיג שיפור ממשי מהדוגמה הקודמת.





דוגמה: הפרדה לפי שווקים

למשל:
  • Cluster ללקוחות ארה"ב
  • Cluster ללקוחות מערב אירופה
  • Cluster ללקוחות מזרח-אירופה
  • Cluster ללקוחות אנגליה
בהנחה שעבור כל מדינה יש חלקי קוד ייחודים המתאימים לרגולציה ו/או settings מעט שונים שהם מקובלים יותר (בגלל הבדלים בין השירות במדינות) - העלולים לגרום לתנאי כשל שונים.

ייתכן ויש בעצם 20 מדינות בהן עובדים, כאשר לכל מדינה יש תצורת עבודה מעט שונה. אבל - מאוד יקר לנהל 20 clusters, וגם אחוז המשאבים המבוזבז (כי לא משתפים אותם) - יגדל ויתעצם.

ניתוח של תנאי הכשל (אלו מדינות משתמשות בפיצ'רים שונים --> חשיפה לתנאי כשל פוטנציאלים שונים) והמשמעות העסקית מובילה אותנו לחלוקה ל-4 clusters.

במידה וכל השווקים (לאחר ה clustering) הם בעלי חשיבות עסקית דומה, הפוטנציאל של bulkheads המתואר זה תלוי בעצם בתנאי-כשל שונים משמעותית בין ה clusters. ככל שתנאי הכשל שונים בין ה clusters - כך ההצדקה להצבת bulkheads הולכת ועולה.

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



דוגמה אחרונה: מנגנון חדש מול מנגנון ישן ("canary release")

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

ה bulkheads יאפשרו שכשל מתגלגל במנגנון החדש, לא יפגע במאסה של הביזנס -- שפועל עדיין על המנגנון הישן.



אמנם כל הדוגמאות שנתתי הן ברמת ה cluster האפליקטיבי, אבל הרעיון של Bulkhead הוא כללי ויכול להיות מיושם ברמות שונות. למשל: ברמת ה thread pool או רמת הסכמה בבסיס הנתונים.






אזהרת Patterns!!! (גנרית)


Bulkheads הוא סוג של דפוס עיצוב (Design Pattern) - ודפוסי עיצוב הם דבר "מדליק" המושכים אותנו ליישם אותם.
כני אדם, אנחנו נוטים לנגן שוב בראשנו את הסיפור כיצד השימוש ב Pattern "הציל את המצב" ומלבישים את הסיפור ההוא (שקרה במקום אחר, ואנחנו לא באמת מודעים לפרטים עד הסוף) עלינו, על המערכת שלנו, ועל הארגון שלנו.

הסיפור יכול להישמע טוב - ועדיין להיות לא-בר-קיימא למערכת / לארגון שלכם.

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

אני רק מקווה שפוסט זה יצליח לייצר יותר תועלת (פתרון בעיות אמיתיות) מנזק (over-engineering).
זו דילמה שיש לי לפני כל פרסום פוסט שעוסק ב"דפוס-עיצוב".


שיהיה בהצלחה!


---

[א] בעולם הספנות bulkheads נקראים גם partitions. המונח "partitions" בעולם התוכנה הוא מאוד מוכר ומתייחס בעולם לרעיון מעט אחר, ולכן בהקשר לתוכנה משתמשים רק במונח bulkheads על מנת לתאר ... bulkheads.

[ב] התרשים הזה הוא חלק מהסבר כיצד למדו בעולם הספנות לבנות bulkheads נכונים יותר: על bulkheads שבקצוות האוניה (בעיקר בחרטום) עלול להיות מופעל לחץ גדול יותר ברגע שהוא דולף, ואז הספינה נוטה ומתחילה לשקוע - ועל כן חשוב לבנות אותם חזקים יותר, משאר ה bulkheads בספינה.
בהשלכה אלינו: bulkheads יש לבנות בחכמה: הם לא פתרון קסם לכל מצב.



3 תגובות:

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

    השבמחק
    תשובות
    1. היי, תודה!

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

      ארכיטקטורת מיקרו-שירותים מאפשרת לבצע bulkheads ברמת השירות הבודד (או מספר שירותים) ולא כלל המערכת - כך שהשניים עובדים טוב ביחד. נתקלתי בכמה יישומים שכאלו במהלך הקריירה.

      מחק
  2. כרגיל תודה על המאמר.
    דוגמא יפה שאנחנו משתמשים בה בצורה די משמעותית היא בספריית ריאקט, כאשר הוסיפו את האירוע של componentDidCatch שהוא מעין try catch דקלרטיבי, המאפשר למתכנת לקבוע בצורה פשוטה למדי איפה למקם את המחיצות הללו באוניה.
    יותר מזה, בתוכנות המותקנות אצל הלקוח אשר עדכונן תלוי לעיתים בלקוח, לדוגמא אפליקציות מובייל, המחיצות הללו מאפשרות לפתח פיצ׳רים חדשים אשר לא ישברו את האפליקציה של מי שטרם עידכן.

    השבמחק