יום שני, 6 במרץ 2017

Workshop - Resilient software design in theory & practice

ביום שלפני תחילת הכנס עצמו היה יום סדנאות. אני הצטרפתי לסדנא בהדרכת Uwe Friedrichsen שעסקה בנושא Resilient software design in theory & practice. סדנא מאד מעניינת. אני מביא פה כמובן רק תקציר של הנקודות החשובות, אפשר לשמוע תקציר של הסדנא ע"י Uwe עצמו בהרצאה שהוא נתן למחרת בכנס (שקפים), ומי שיצליח להשתלב בסדנא שלו בהמשך - מומלץ!

אנחנו במהלך הסדנא
אנחנו במהלך הסדנא

חלק 1 - What is that "resilience" thing?

אנחנו כותבים תוכנה כדי להביא ערך עסקי. אבל היא מביאה ערך אמיתי רק עם התוכנה רצה ב-production ועובדת. לכן לעובדה שתוכנה רצה ועובדת ב-production יש ערך עסקי אמיתי, שהוא לא תמיד גדול יותר או קטן יותר מדברים אחרים, ובאחריות ה-product owner להעריך אותו ולשלב משימות resilience ב-backlog שלו.

מכיוון שבלי שתוכנה רצה ועובדת היא לא מביאה ערך עסקי, הנושא של זמינות (Availablilty) הוא נושא חשוב מאד בתוכנה. אבל מהי בכלל זמינות? איך מודדים אותה?

יש נוסחא פשוטה:

Availibilty= MTTF
MTTF+MTTR

(MTTF=Mean Time To Failure, MTTR=Mean Time To Recovery)

כלומר הזמן שהמערכת רצה בצורה תקינה חלקי כמות הזמן הכוללת.

כדי להגדיל את הזמינות (לגרום לנוסחא לשאוף ל-1) יש לנו שתי אפשרויות: להגדיל את ה-MTTF (למנוע כשלים) או להקטין את ה-MTTR (לשפר את היכולת שלנו להתמודד עם כשלים). קלאסית, תמיד עסקנו בהגדלת ה-MTTF ע"י שיפורי תהליכים והשקעה בתשתיות. אבל העולם מתקדם למערכות מבוזרות ולמערכות התלויות במידע מבחוץ:

A distributed system is on in which a failure of a computer you didn't even know existed can render your own computer unusable.  - Leslie Lamport

במערכות כאלה מניעת כשלים היא הרבה יותר קשה, כיוון שכשלים ברמת התשתית תמיד קורים. מחקר של מייקרוסופט שבדק את כשלי החומרה ב-Data Center שלהם מדבר על חציון של 40.8 כשלי network link ליום, כאשר חציון הזמן לתיקון הכשל עומד על 5 דקות. החציון המוערך של כמות הפקטות שעובדות בכל כשל כזה הוא כ-59,000. מחקר אחר מראה שגם בין אתרים של אמזון (שמצטיינים ב-latency נמוך) יש לפעמים latency חציוני של למעלה מ-350 מילישניות.

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

Do not try to avoid failures. Embrace them.


כשאנחנו מדברים על מצבים בלתי צפויים בתוכנה, אנחנו מדברים על 3 סוגים (או 3 שלבים) של בעיות:

  1. Fault (תקלה) - קיימת איזושהי בעייה במערכת
  2. Error (שגיאה) - הבעייה "הופעלה", כלומר היא הגיעה למצב שבו היא משפיעה על המערכת
  3. Failure (כשל) - הבעייה גרמה לפגיע בזמינות המערכת. חשוב להגיד שאנחנו לאו דווקא מדברים על נפילת מערכת - זה אירוע שקל יחסית להתמודד איתו. כשל יכול לבוא לידי ביטוי גם כתגובה לא יציבה (חלק מהבקשות נכשלות), בעייה של איטיות (ובמערכות מבוזרות זה יותר קשה), בעיות של תגובות שגויות (למשל בעיות קונסיסטנטיות), והכי גרוע - לפעמים שגיאה ככה ולפעמים ככה (מכונה Byzantine Failure ומחוץ לתחום של הסדנא הזאת). 

כלומר, מה שאנחנו מנסים לעשות הוא למנוע מ-Fault להפוך ל-Error ובעיקר למנוע מ-Error להפוך ל-Failure.

עוד אזהרה אחת שחשוב להגיד: צריך להימנע מ-"מלכודת ה-100% זמינות". זה מאד קל ל-product owner להגיד: "זמינות זה עניין טכני באחריות המפתחים. אתם צריכים לעשות מה שצריך כדי שהמערכת תהיה תמיד זמינה". אבל זו גישה לא נכונה - האם עכשיו עדיף שלא נפתח שום יכולת במערכת כי נשקיע רק בזמינות? או לחילופין, שניתן מערכת עם המון יכולות אבל שלא עובדת חצי מהזמן?

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

חלק 2 - Designing for resilience

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

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

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

  1. לקבל החלטות ליבה לגבי מבנה המערכת
  2. לזהות שגיאות (error). אחרי שזיהינו אותן ניתן להתמודד איתם באחת משתי דרכים:
    1. להתאושש מהן
    2. להתמודד עם קיומן
  3. בהתאם להתמודדות שבחרנו, נבחר בפתרון שיעזור לנו למנוע מהשגיאה להפוך לכשל.
  4. נמצא פתרונות שיעזרו לנו למנוע שגיאות אחרות.

החלטות ליבה

ישנן שתי החלטות ליבה שעלינו לקבל כאשר אנחנו מתכננים מערכת: בידוד רכיבים (isolation) ובחירת שיטת תקשורת.

בידוד רכיבים (isolation)

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

כאשר מתכננים את החלוקה לרכיבים של מערכת יוצרים משהו שנקרא bulkheads: בחירה של המחיצות ואופן החלוקה של המערכת. אפשר לעשות זאת ע"י חלוקה ל-Microservices, או על ידי חלוקה ל-Actors למשל.

אבל לעשות חלוקה שכזאת זה קשה! למה? כי אם שירות A תלוי בשירות B, שירות B עדיין יכול למנוע משירות A לעבוד (כן, אפילו אם שירות A משתמש ב-circute breaker). ואם גם שירות B תלוי בשירותים אחרים, והם תלויים בשירותים נוספים, אז גם אם כל אחד זמין ב-99.99999% מהזמן הזמינות של A יכולה לרדת משמעותית, אפילו ל-75%. להכניס את כל השירותים לאותו bulkhead רק יחזיר אותנו לבעיות המונוליט. גם שימוש בכלים אחרים שאנחנו מכירים מהנדסת קוד, כגון DRY, פירוק לפונקציונאליות, שכבות וכד' לא יעזרו לנו.

מה כן יעזרו לנו? העקרונות הבסיסיים של חלוקה לרכיבים: High Cohesion, Low Coupling יחד עם speration of concerns הם קריטיים כשחוצים את גבולות ה-process. זהו נושא שאנחנו עדיין לומדים אותו בתעשייה ולא התקדמנו הרבה מאד בתחום (למשל מאז המאמר הזה מ-1972).

איך מתחילים לעשות חלוקה שכזאת?

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

אחרי זה צריך למצוא את הדרך הקצרה ביותר לענות לבקשות - יותר מדי hops בדרך מקטינים את הזמינות ומגדילים את ה-delay, ולכן אנחנו נחפש תמיד את הדרך הקצרה ביותר להחזיר את התשובה ונשאף להקטין את כמות הקריאות (RMI) שאנחנו עושים בתהליך. את העיקרון הזה חשוב לאזן מול המשיכה לשבור את ה-seperation of concerns ולחזור למונוליט כדי להקטין את כמות הקריאות - צריך לאזן בין שני הדברים האלה. Cache-ים יכולים לעזור לנו לפתור חלק מהבעיות האלה, אבל חשוב לזכור ש-cache לא יתקן עיצוב לא טוב והוא מביא איתו מורכבויות משלו.

עוד משהו שחשוב בשביל לעשות isolation טוב: לוותר על Reusability מחוץ לגבולות ה-process. גם אם אפשר להשתמש בשירות מסויים בכל מיני מקומות, זה לא אומר שכדאי לעשות את זה. השימוש החוזר בשירות (לעומת האלטרנטיבה של, לדוגמא, לשתף jar) גורם לעוד hop בתהליך ולכן פוגע בזמינות. מלבד זאת, הוא גם מעודד עיצוב לא טוב.

Uwe סיפר שיש סטטיסטיקות של שימוש חוזר ברכיבים, ובסוף הממוצע הוא שימוש של 1.1 פעמים בשירות, וזה באמת לא משתלם וזו אחת מהסיבות לקריסת הרעיון של SOA. אומרים שלעשות משהו כתשתית זה עולה פי 9: פי 3 מאמץ למימוש יציב בכל המקרים, ושוב פי 3 כדי ליצור API טוב, תחזוקתי ומוגן. עדיף לוותר על העקרון של reusability, ובמקומו לאמץ רעיון אחר: replacability - אפשר להחליף את המימוש בצורה נקייה בכל נקודה. באופן לא מפתיע, replacability טוב מגיע איפה שיש seperation of concerns טוב.

שיטת תקשורת

החלטת הליבה השנייה היא לבחור את צורת התקשורת במערכת: request-replay (סינכורני) \ messaging (כמו ב-AKKA למשל) \ events וכו'. זאת החלטה חשובה שמשפיעה מאד על העיצוב, ולעיתים קרובות לא מייחסים לה חשיבות מספקת:

 

זיהוי בעיות

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

Timeouts

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

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

Circut breaker

הדפוס הזה הוצג לראשונה בספר "!Release It" של M. Nygard, וכנראה הוא הדפוס שמוזכר לעיתים הקרובות ביותר בהקשר ל-resilience.

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

מדובר בדפוס פשוט, ניתן לממש אותו לבד או להשתמש בתשתית כדי לחסוך את בעיות ה-thread safety (כמו לדוגמא Hystrix של Netflix שמוצעת כחלק מה-OSS שלהם. היא באה גם עם Dashboard מובנה שמציג את סטטוס ה-breakers אך לא נמצא בפיתוח יותר. Uwe גם ממליץ מאד לקרוא את הקוד של Hystrix, יש הרבה מה ללמוד ממנו על בנייה של קוד cuncurrent בצורה טובה).

Acknowledgment

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

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

בדיקה שגרתית

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

ניטור

להריץ מערכת בלי ניטור זה אמיץ. להריץ מערכת מבוזרת בלי ניטור זה טיפשי.

גם בהקשרי ניטור, כדי להקטין את ה-MTTR אנחנו צריכים לתת מקסימום מענה אוטומטי למקרים ורק ליידע אנשים שקרה אירוע, אבל בלי שתומך צריך להתערב (למשל - restart לשרתים).

זה אומר שרמות הלוג שנשתמש בהם:

  • Debug - מידע למפתח
  • Info - הייתה בעייה אבל המערכת התמודדה
  • Error - יש צורך בהתערבות תומך

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

טיפול בשגיאות

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

התאוששות

איך מתאוששים משגיאה שקרתה? הנה כמה אפשרויות:

  • נסיון חוזר - מנגנון ההתאוששות הכי בסיסי: לנסות שוב. צריך להגביל את מספר הנסיונות שמנסים, ומומלץ להגביל לפעם אחת נוספת. כדאי לשים לב גם שלעיתים קרובות זה דורש idempotency מהרכיבים ברשת.
  • חזרה לאחור - חזרה לאחור למצב יציב קודם (בתוך השירות). יוצרים פעם בכמה זמן checkpoint או safepoint (שההבדל ביניהן מעבר לתחום ההרצאה) שאליהם אפשר לחזור אחרי שגיאה ולהתחיל ממקום שאנחנו יודעים שהוא בסדר, ולנסות להריץ את הפעולה מחדש.
    גם כאן חשוב להגביל את הכמות.
  • "בחזרה לעתיד" - לדלג על הפעולה (Roll-Forward) - להתקדם לנקודה בריצה שהיא מעבר לפעולה שנכשלה ולהמשיך טיפול. זאת החלטה עסקית האם זה רעיון טוב או לא לעשות דבר כזה - לדוגמא אם משתמש עשה הזמנה באתר ולא הצלחנו לאשר תשלום, עדיף להכשיל את הפעולה; אבל אם לא הצלחנו להודיע למחסן להוציא משלוח ייתכן שעדיף להמשיך את התהליך ולתקן את זה יותר מאוחר. ניתן להשתמש בטכניקה הזאת גם רק על חלק מהפעולה. גם כאן ניתן להשתמש ב-checkpoints ו-safepoints.
  • איתחול - כשכל השאר נכשל, אפשר לאתחל את השירות מחדש. אפשר גם לאתחל מידע במקרה הצורך. לפעמים גם התחברות מחדש למקור מידע חיצוני זה מספיק.
  • Failover - העברת אחריות - אם אני לא מצליח לבצע פעולה מסויימת, אולי מישהו אחר יצליח. הדוגמא הקלאסית היא שימוש ב-Load Balancer כדי להעביר את הבקשה ל-instance אחר. זה בדרך כלל דורש יתירות בשירותים, מה שאומר שאנחנו משקיעים משאבים כדי להשיג זמינות (שוב, החלטה עסקית).

התמודדות

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

  • Fallback - אם יש שגיאה בפעולה מסויימת, אני יכול להחליט לבצע במקומה תרחיש אלטרנטיבי אחר. זה הבסיס לרוב שיטות ההתמודדות. דוגמאות: ברירת מחדל לתשובות אם השירות שאני תלוי בו לא זמין, להתעלם ולהמשיך הלאה (פגיעה מבוקרת בתהליך).
  • תור משאבים - (בלתי מוגבל) מיועד בעיקר לשימוש במקרה של בעיית ביצועים ומשאבים זמניים. הבעייה עם תור משאבים זה שהוא יכול לתפוח ואז ליצור latency ארוך מדי. לכן כדאי לשלב איתו את השניים הבאים.
  • Backpressure - כאשר השירות מקבל בקשות רבות מכדי שהוא יכול למלא, הוא מודיע לאלה שקוראים לו להנמיך את הקצב. הדוגמא הכי בסיסית היא החזרה של סטטוס 503 לכל בקשה נוספת. באסטרטגיה הזאת צריך לשים לב שנדרש ערוץ חוזר מהשירות אל הקוראים לו, מה שלא תמיד נכון במודל אסינכרוני.
  • תור חסום - דומה לתור משאבים אבל בעל גודל מוגבל. כאשר התור מלא הודעות חדשות מקבלות Backpressure או נזרקות, או אפשרות אחרת (לדוגמא זורקים את הישנה ביותר).
  • חלוקת עומסים - יוצרים עוד מופע של השרת או המשאב כדי להתמודד עם העומס. חשוב להקטין ככל האפשר את הסנכרון הנדרש בין המופעים של המשאב.

מניעת שגיאות

חוץ מאשר לזהות שגיאות ואז להתמודד איתן, אפשר גם לעשות פעילות אקטיבית למניעת תקלות.

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

הדרך הכי טובה היא טיפול שגרתי - טיפול 10,000 לרכב לא מונע תקלות, אבל מקטין מאד את ההסתברות שתקלה תקרה. באותה צורה, במערכת אנחנו צריכים להשקיע זמן במניעת תקלות שאפשר למנוע, בגילוי תקלות ושגיאות שקורות ב-production ותיקונן, ומדי פעם לחזור ולאזן כמה משאבים אנחנו משקיעים בעמידות המערכת למול שאר הצרכים. כדי לעשות את זה אנחנו חייבים ליצור לעצמנו יכולת לדעת מה קורה ב-production, מתי שגיאות קורות וכו'.

עוד נושאים

עוד כמה נושאים ששווה לחשוב עליהם בהקשר לבניית מערכת עמידה:

  • יתירות - זה הבסיס להרבה מדרכי ההתמודדות עם שגיאות. אבל בד"כ אנחנו מתמקדים רק ביתירות למקרה של נפילה - ניתן להחיל את השיטה הזאת גם על מקרים אחרים של שגיאות.
  • idempotency - קריאה לאותה מתודה מספר פעמים ברצף תביא לאותה תוצאה, כלומר שליחה כפולה של בקשה לא תשפיע. מאד קשה לייצר מערכות מבוזרות בלי זה - זה עוזר להצמיד צימודים בין הרכיבים ולהתמודד עם שגיאות - אפשר פשוט להפעיל את הקריאה שוב בלי חשש.
    דרך אחת לעשות את זה היא לתת unique request token לבקשות שדרכו אני מוודא שלא טיפלתי כבר בבקשה (זה לא תמיד קל - בעיות consisteny, ניקוי עם GC וכד').
  • Stateless - קשה לתחזק state במערכת מבוזרת (CAP), לכן עדיף לוותר עליו. אבל מאחר שלרוב צריך הקשר לפנייה, אז הפתרון הוא להעביר אותו 'למעלה' או 'למטה' - אם הוא קטן, להעביר אותו ללקוח ולבקש ממנו תמיד לשלוח אותו (לדוגמא עם cookie), או להעביר אותו לתוך ה-data store.
  • אסקלציה - במערכת מבוזרת ללקוח לא אכפת איזה חלק במערכת לא עובד. עדיף לשים שירות עליון שיקבל כל את כל התקלה ויחזיר משהו ללקוח. לעיתים קרובות זה נעשה בהיררכיה - כמה רמות שמרכזות שגיאות מהרמות מתחתיהן. עדיף להפריד את קוד הטיפול בתקלות מהקוד שעושה את העיבוד במערכת - האסקלציה יכולה לעשות כלפי שירות ייעודי ולאו דוקא דרך השירותים שדרכם הגענו לקריאה הנוכחית.

איך משתמשים בכל התבניות האלה?!

איך מחברים יחד את כל הדברים האלה

קודם כל, מה לא - אנחנו לא רוצים להשתמש בכל התבניות האלה. התבניות האלה הן אפשרויות, לא חובה. עלינו לבחור באלו שמתאימות לנו, וגם לא לבחור ביותר מדי - כל תבנית מוסיפה מורכבות, וכל תבנית עולה לנו כסף בסוף. יש דוגמא לְמה ש-Netflix בחרו לממש ולְ-מה ש-Erlang בחרו לממש. בדוגמא הזו למשל, Erlang הצליחו להגיע ל-5 תשיעיות של זמינות.

חלק 3 - תרגיל עיצוב

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

תרשים ניתוח מערכת

מסקנות עיקריות מהעבודה על התרגיל:

  •  בפתרון מבוסס event-ים:
    • היה שימוש ב-health check כדי לדעת אם המאזינים לאירועים תקינים
    • ניטור שבודק את האירועים שעוברים במערכת
    • ה-UI (או הרכיב שמתקשר איתו) גם כן שומע אירועים ויכול לדווח ללקוח מה הסטטוס, או בהיעדרם משהו בינתיים.
    • כדאי לעשות את השירותים idempotent.
  • בפתרון מבוסס תקשורת אסינכורנית (messaging):
    • לדברים שיכולים לקרות יותר מאוחר (למשל - הזמנה מהמחסן, עדכון השימוש בקופון) כדאי להשתמש בתורים לא מוגבלים.
    • לחליפין, במקרה של כשלון בעדכון דברים שיכולים לקרות אח"כ אפשר ליצור שירות רקע שעובר על הנתונים ומתקן אנומליות (למשל עובר על רשימת התשלומים ומחפש הזמנות שחסרות בשירות המחסן).
    • אפשר לחלק את התגובה שלקוח מחכה לה ל-2: תגובה עם timeout קצר שבמקרה שעבר הזמן מחזיר ללקוח דף שמאפשר לו לבדוק את הסטטוס של התהליך, וטיפול עם timout ארוך שאם נכשל מחזיר כישלון כללי.
    • ל-AKKA יש יכולות להתמודד עם וידוא של תהליכים אסינכרונים (נקרא supervision).

חלק 4 - לסיום - מהתיאוריה לפרקטיקה

ישנן עוד המון תבניות.

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

תהליך העיצוב

  1. מקבלים את החלטות הליבה.
  2. חלוקה לרכיבים (bulkheads). זה המקום להשקיע! זה 50% מהעבודה ומאד קשה. חשוב לשים לב שאנחנו מחפשים loose coupling ברמה העסקית ולא ברמה הטכנית.
  3. לבחור טכניקות לזיהוי, התאוששות והתמודדות עם שגיאות.
  4. לממש.
  5. לעלות ל-production.
  6. למדוד את התנהגות המערכת! "אל תנחש, תדע".
  7. ללמוד מהמדידות.
  8. להעריך באילו תבניות כדאי לבחור כדי לפתור בעיות: עלות מול תועלת. יותר זה לא תמיד יותר טוב...
  9. להתחיל את התהליך מהתחלה ולעשות review להחלטות שעשינו.

Adopting Resilient Software

איך גורמים לפיתוח כזה להפוך לחלק מהארגון שלנו? Uwe הציע שלושה שלבים:

  • ליצור מודעות
    • צריך לוודא שהאנשים העסקיים מבינים את החשיבות - למה להשקיע בזה
    • ברמת המפתחים:
      • ליצור feedback loop שמאפשר הבנה של המערכת
      • ליצור devops
      • ליצור Site Reliability Engineering - מערכת של גוגל שעוזרת לוודא מה יחס ההשקעה בין היכולות החדשות לבין שיפור הקיים
      • ... או ליצור שיטה משלנו - העיקר שיש לנו feedback loop כדי שנתבסס על נתונים אמיתיים
  • לבנות יכולת - ידע (הכשרה, hackathon-ים שבהם אנשים מתנסים), כלים ונסיון (לעשות!)
  • לבנות עמידות רצופה - לתרגל את עמידות המערכת כדי שלא נופתע בפעם הראשונה ששגיאה באמת קוראת
    • להכניס שגיאות (כמו ה-Simian Army של Netflix) ולראות איך המערכת מתנהגת
    • ליצור תרגילים מתוכננים שדורשים התערבות תומך. זה מתרגל את התומכים והם ידעו להתמודד עם מקרים. עד לרגע התרגיל רק מנהל התרגיל יודע את התסריט.

רשימת קריאה

נקודות מעניינות מה-Q&A בסוף הסדנא

  • ככל שהמערכת קטנה יותר, יותר קל לעשות סביבת בדיקות מלאה. ככל שהיא גדולה יותר זה קשה יותר. Netflix, למשל, לא יקבלו הרבה מסביבת בדיקות כי אין להם יכולת להרים כמות גדולה כל-כך של משאבים לבדיקות ולדמות כל כך הרבה תעבורה, ככה שהם יעדיפו ככל האפשר טכניקות Test In Production.
  • צריך להעדיף משאבים שמאפשרים להבין את סטטוס המערכת ב-production. זה חשוב כדי לאפשר את כל הטכניקות האחרות.
  • שאלה: איזה דברים נתקלת בהם שהיו בעליל שגויים? התשובה: להתחיל ממודל המידע. הרבה אנשים מתחילים בכך שהם עושים ERD, וזאת טעיות כי יוצרים תלויות בין רכיבים שביניהם חילוקנו את הישויות. צריך תמיד להתחיל מהפונקציונאליות (התנהגות).
  • ב-CQRS מפרידים בין מודל המידע בכתיבה למודל המידע בקריאה.
  • מה לגבי serverless? אנחנו עדיין יותר מדי בתחילת הדרך.
  • "אני לא מחנך מפתחים. אני נותן להם רעיונות לדברים שצריך לעשות"
  • אם יש Product Owner שלא אכפת לו מ-"דברים טכניים", הוא לא מתאים לתפקיד.

אין תגובות :

פרסום תגובה