-->

יום שבת, 19 בספטמבר 2015

AWS: "השירותים הפשוטים" של אמזון - חלק ב', וארכיטקטורה מונחית-אירועים


בפוסט הקודם כיסינו שני "שירותים אפליקטיביים" בסיסיים של AWS: שירות ההודעות SNS, ושירות התורים SQS.
בפוסט הזה נמשיך את הסקירה ונכסה את שירות ה Workflows שנקרא SWF - שירות שימושי, אך קצת פחות מוכר.

Simple WorkFlow Service (בקיצור SWF)


SWF בא לפתור לנו כמה בעיות שלא נפתרות ב SQS או SNS. למשל:
  • נניח שיש לי סדרה של הודעות בין שירותים הקריטיות לביזנס, שעלינו לוודא שכולן טופלו בהצלחה + לדעת מתי כולן הסתיימו.
    כיצד מתמודדים עם משהו שכזה בעזרת SQS? מחזיקים הודעות אימות ואז סורקים אותן כל כמה זמן?! - לא יעיל.
  • נניח שיש לי מקביליות-חלקית ברצף של הודעות שבהן ארצה לטפל: חלק מההודעות עלי לשלוח בזו אחר זו, אבל חלק ניתן לשלוח במקביל - ולקצר את התהליך.
    כאשר אני שולח הודעות ב SNS או SQS  אין לי שליטה מה קורה איתן, ובטח שאינני יכול לתזמן את הטיפול בין הודעות שונות....

הפתרון הפשוט לבעיה זו הוא לנהל רצפי-הודעות שכאלה בצורה ריכוזית של תקשורת "הודעה-תשובה" בקוד של שירות כלשהו. למשל שירות 'A':
  • שירות A שולח את הודעה 1 לטיפול ומחכה שתסתיים.
  • שירות A שולח את הודעות 2, 3, ו 4 לטיפול במקביל (משיקולי ביצועים). הוא מממש Pattern של Monitor - שנותן אות ברגע שהטיפול בשלושת ההודעות הסתיים.
  • שירות A שולח את הודעה 4 לטיפול - והנה כאן ה"טיפול המיוחד" מסתיים.
הקוד שעלי לכתוב הוא לא מסובך, אבל גם לא סופר-פשוט (תלוי מאוד בשפת התכנות וספריות-העזר שיש בידי).
קשה יותר לכתוב אותו שיהיה יעיל ו Scalable (שוב: תלוי בשפה / סביבת ריצה), וקשה אפילו יותר לכתוב אותו שיהיה גם Highly Available.

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



רגע של תיאוריה

ישנו סגנון ארכיטקטוני שנקרא Event-Driven Architecture (ארכיטקטורה מונחית-אירועים, להלן EDA).

לכאורה, כל פתרון בו יש הודעות א-סינכרוניות - יכול להיחשב "סוג של EDA": כל אפליקציית UI או קוד ג'אווהסקריפט
בפועל, החכמה ב EDA היא לא עצם השימוש בהודעות / תקשורת א-סינכרונית, אלא בארגון שלהן.

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

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

למשל, אפיון לדוגמה של אינטראקציה של הודעות יכול להיות:
  • Broker - מתווך ביניים שיוצר decoupling בין שולח ההודעה, ובין המקבל שלה, אך עושה זאת בתצורה של "הודעה-תשובה".
  • Message Broker - שהוא כמו Broker, אבל עובד בתצורה של "שגר ושכח"
  • Publisher-Subscriber, תצורה של Message Broker עם Fan Out, כלומר: שליחת הודעה אחת וקבלה שלה ע"י כמה נמענים (נוסח SNS של אמזון).
  • Message Queue - נוסח SQS של אמזון.
  • ועוד ועוד...

מכיוון שיש הרבה סוגי אינטראקציה אפשריים, ועל כל אחד ניתן לעשות כמה וריאציות, ההמלצה היא קודם כל לחשוב ב "טופולוגיות" ורק אז לבחור את סוגי האינטרציה. קיימות 2 טופלוגיות מרכזיות:
  • Broker Topology - שליחה וטיפול בהודעות בודדות ובלתי-תלויות.
    כל הודעה עומדת בפני עצמה, והמערכת שלנו היא כמו "כוורת דבורים" של שליחת הודעות וטיפול בהודעות - ש"זורמות מעצמן", ללא ניהול מרכזי.
  • Mediator Topology - ההודעות, או לפחות חלק מהן, אינן זורמות באופן עצמאי - אלא מנוהלות במסגרת מקומית מסוימת.
    למשל: יש לנו בכוורת כמה "דבורים סמליות (sergeant)" שמנהלות בצורה אקטיבית כמה תהליכים שהוגדרו כמורכבים או קריטיים במיוחד. ה Mediator הוא בעצם סוג של Workflow Engine. הנה דוגמה:
טופולוגיה של Broker. מקור - Mark Richards

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

ה Mediator מנהל עשרות תורים קטנים (בשם Channels, למעלה), כאשר כל תור מתאר instance של Workflow בהרצה. ה Mediator מחזיק State Machine המאתר את ה Workflow. בכל שלב הוא שולח הודעות א-סינכרוניות (בד"כ) ל Processors הנכונים (בתרשים למטה לכל processor יש תור), וברגע שטיפול בהודעה הסתיים, הוא עובר שלב ב State machine, וכך יודע מה ההודעה הבאה שעליו לשלוח - עד סיום ה Workflow.

---


את ה Mediator (= דפוס עיצוב) ניתן לממש לבד בקוד. שירות SWF הוא מעין "תשתית" שמאפשרת ליישם Mediator בצורה קלה ואמינה יותר.

הערה: SWF הוא לא חדשני. יש הרבה Frameworks לניהול Workflows (שמות נרדפים הם Process Integration, EAI, וכו'), ותיקים ומשוכללים בהרבה. כמו ששמו מעיד עליו: הוא שירות פשוט ובסיסי למדי, אבל שיכול לספק אמינות גבוהה ואינטגרציה טובה לסביבת אמזון.

ניתן לתאר את עיקר התכונות של SWF באופן הבא:
  • שירות מבוזר, אמין, highly scalable, המנוהל כשירות.
  • השירות מספק בעיקר כלי ניהול והרצה של ה workflow - קרי Mediator.
    את ה activities - יהיה עליכם לממש לבד (בג'אווה, רובי, וכו') ולעשות deploy של הקוד על מכונות EC2 שבבעלותכם או אפילו מכונה מרוחקת (לא על AWS). מה ש SWF עושה הוא לקרוא לקוד שלכם ברגעים הנכונים, אבל הלוגיקה - היא בבעלותכם.
    • ניתן לשלב בצעדי ה Workflow קריאה לשירותים שלא רצות בענן (למשל: אפליקציות on-premises) או פעולות אנושיות (למשל: אישור או דחיה של בקשה).
    • ה workflow יכול לחיות עד כשנה (ניתן להגדיר retention period משלכם לכל workflow), לאחר מכן - אמזון תמחק אותו.
    • את ה workflow מנהלים בתוך domains, שלרוב נקצה אחד כזה לכל אפליקציה (לצורך ארגון והפרדה).
      • כל domain יכול להכיל מספר workflows.
      • workflow מדומיין A מנוע מלתקשר עם workflow מדומיין B.
    • ה state של ה Workflow execution, נשמר במקום אמין במיוחד (S3?) שמבטיח שלא סביר [א] שהוא יאבד.

    השחקנים העקריים ב Workflow של SWF הם:
    • Activity Worker - הוא מי שמבצע את ה Activity. לפעמים זה יהיה חישוב פשוט, ולעתים - delegation של העבודה לחלק אחר של תוכנה, או לאדם שייקח החלטה.
    • Decider - לוקח החלטה מה לעשות ברגע ש Activity יחיד ב Workflow הסתיים - כלומר, מה השלב הבא ב workflow. המימוש הוא לרוב סט חוקים פשוט או State Machine.
    הקוד שמפעיל את ה workflow נקרא Workflow Starter.

    דוגמה ל workflow פשוט של עיבוד תמונה: מתאימים לה את הגודל, מוסיפים כיתוב (watermark) ושולחים הודעה שהתמונה מוכנה.

    הערה קטנה של מינוחים:

    start_to_close_timeout - הוא פרמטר של workflow או activity, שלאוזן ישראלית ממוצעת עלול להישמע כמו "מתי מתחילים לסגור את העניינים" :)
    בפועל מדברים על timeout נוקשה, שיעצור את ה activity/worfklow אם מרגע התחלת הפעולה (start) עד סופה (close) - היא לא הסתיימה.



    באופן דומה יש timeout שנקרא schedule_to_start, שמגביל את הזמן בו workflow יכול להמתין לפני שהוא מתחיל להיות בכלל מטופל (כלומר: להמתין ב Queue לתחילת טיפול).


    בתיעוד של אמזון, ניתן למצוא תיעוד ברור על קונספטים מתקדמים יותר של שירות ה SWF, כגון: markers, signals, tags ו child workflows.

    בקיצור רב (רק בכדי לקבל מושג):
    • Signals מאפשרים לעדכן את ה workflow בשינוי state מעניין שהתרחש - מאז שה workflow החל לפעול.
    • tags - מאפשרים לתייג, לצורך חיפוש עתידי ב workflow history , אחר מצבים מיוחדים שה workflow הגיע אליהם.
    • Markers - מידע (או state) נוסף שה Deciders יכולים לשמור על ה workflow - לצורך ביצוע החלטות עתידיות.
    • Child Workflow - צעד בודד ב workflow, פותח workflow משל עצמו. הצעד ב workflow מסתיים כאשר ה child workflow הסתיים. הגיוני.


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



    ----

    לינקים רלוונטיים

    Webinar: Introduction to Amazon SWF

    AWS in Plain English - הצעה לשמות טובים יותר לשירותים של AWS + הסבר קצר על כל שירות. אהבתי (!) אם כי דווקא ההסבר על SWF הוא לא כ"כ מוצלח לטעמי (הייתי פשוט קורא לו "Workflow Service").

    -

    [א] "לא סביר" הוא עדיין אפשרי, כמובן.
          "חוק מרפי-בענן" גורס: "כל מה שעלול להיכשל - אכן ייכשל: בסוף השבוע, בחצות, ובשיא העומס".



    יום ראשון, 6 בספטמבר 2015

    Fault-Tolerance בארכיטקטורת Microservices, ובכלל

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

    למה בלתי צפויה?

    ובכן, אתחיל במעט רקע: לאחרונה פירקנו מתוך המערכת הראשית שלנו ("המונוליט") כ 7 מיקרו-שירותים חדשים, חלקם קריטיים לפעולה תקינה של המערכת. כל שירות הותקן בסביבה של High Availability עם שניים או שלושה שרתים ב Availability Zones שונים באמזון. חיברנו Monitoring לשירותים - פעולות סטנדרטיות. בהמשך תכננו לפתח את היציבות אפילו יותר, ולהוסיף Circuit Breakers (דפוס עיצוב עליו כתבתי בפוסט קודם) - בכדי להתמודד בצורה יפה יותר עם כשלים חלקיים במערכת.

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

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

    - "נראה כמו" ?

    כן. אנחנו עדיין בודקים את זה עם אמזון. לא הצלחנו לשים אצבע בדיוק על מקור התופעה. אחת לכמה זמן, שירותים מסוימים במערכת חווים תנאי-רשת דיי קיצוניים:
    • Latency בתוך ה Data Center (בין AZs) שקופץ מ 2 מילי-שניות בממוצע לכ 80 מילי-שניות בממוצע. 80 מילי-שניות הוא הממוצע, אבל גם לא נדיר להיתקל ב latency של 500 מילי-שניות - כאילו השרת נמצא ביבשת אפריקה (ולא ממש ב Data Center צמוד, עם קווי תקשורת dedicated).
    • גלים של אי-יציבות, בהם אנו חווים אחוז גבוה מאוד של timeouts (קריאות שלא נענות בזמן סביר), במקרים הקיצוניים: יותר מ 1% מניסיונות ה tcp connections שאנו מבצעים - נכשלים (בצורת timeout - לאחר זמן מה).
    אנחנו רגילים לתקשורת-לא מושלמת בסביבה של אמזון - אבל זה הרבה יותר ממה שהיינו רגילים אליו עד עתה. בניגוד לעבר - אנו מתנסים לראשונה בכמות גדולה של קריאות סינכרוניות שהן גם קריטיות למערכת (כמה אלפי קריאות בדקה - סה"כ).


    זה נקרא "ענן" - אבל ההתנהגות שאנו חווינו דומה הרבה יותר ללב-ים: לעתים שקט ונוח - אבל כשיש סערה, הטלטלה היא גדולה.
    מקור: http://wallpoper.com/


    סימני הסערה


    החוויה החריגה הראשונה שנתקלנו בה - היא starvation: בקשות בשירותים השונים הממתינות בתור לקבל CPU (ליתר דיוק: להיות מתוזמנות ל process של ה Application Server - אולי אספר עוד בפוסט נפרד), וממתינות זמן רב - שניות. אותן קריאות שבד"כ מטופלות בעשרות מילי-שניות.

    כאשר מוסיפים nodes ל cluster, למשל 50%, 100% או אפילו 200% יותר חומרה - המצב לא משתפר: זמני ההמתנה הארוכים (שניות ארוכות) נותרים, ויש אחוז גבוה של כישלונות.

    בדיקה מעמיקה יותר גילתה את הסיבה ל starvation: שירות A מבקש משהו משירות B, אך כ 3% מהקריאות לשירות B לא נענות תוך 10 שניות (להזכיר: זמן תגובה ממוצע הוא עשרות בודדות של מילי-שניות).
    תוך זמן קצר, כמעט כל התהליכים עסוקים ב"המתנות ארוכות" לשירות B - והם אינם פנויים לטיפול בעוד בקשות.
    כאשר כל טרנזקציה בשירות B מבצעת כ 3 קריאות לשירות A ("מה זה משנה? - הן כ"כ מהירות"), הסבירות ל"תקיעת" הטרנזקציה על timeout עולה מ 1% לכ 3% - כאשר יש כ 1% התנתקויות של connections.

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

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


    הפעולה המידית הייתה:
    • הפחתת ה timeouts ל-2 שניות, במקום 10 שניות. הקשר: השירות הכי אטי שלנו עונה ב 95% מהקריאות, גם בשעות עומס - בפחות מ 500 מילי-שניות.
    • צמצום מספר הקריאות בין השירותים: בעזרת caches קצרים מאוד (כ 15 שניות) או בעזרת קריאות bulk. למשל: היה לנו שירות D שבכל טרנזקציה ביצע 12 קריאות לשירות E, למה? - חוסר תשומת לב. שינוי הקוד לקריאת bulk לא הפך את הקוד למסורבל.
      בעצם ריבוי הקריאות, השירות בעצם הכפיל את הסיכוי שלו בפי-12, להיתקע על timeout כלשהו. קריאה אחת שמטפלת ב 12 הבקשות היא יעילה יותר באופן כללי, אך גם מצמצת את סבירות ההתקלויות ב timeouts.
      בכל מקרה: קריאה שלא נענתה בזמן סביר - היא כבר לא רלוונטית.


    כששטים במים עמוקים - חשוב לבנות את כלי-השיט כך שיהיה יציב.
    מקור: http://www.tutorvista.com/


    המשך הפתרון


    הסיפור הוא עוד ארוך ומעניין, אך אתמקד בעיקרי הדברים:

    • באופן פרדוקסלי, השימוש ב timeouts (כישלון מהיר) - מעלה את רמת היציבות של המערכת.
    • השימוש ב timeouts + צמצום מספר הקריאות המרוחקות, כמו שהיה במקרה שלנו, בעצם מקריב כ 1% מהבקשות (קיצרנו את ההמתנה ל-2 שניות, אך לא סיפקנו תשובה מלבד הודעת שגיאה) - על מנת להציל את 99% הבקשות האחרות, בזמני סערה (אין ל timeout השפעה כאשר הכל מתנהג כרגיל).

    הקרבה של כ 1% מהבקשות הוא עדיין קשה מנשוא, ולכן אפרט את המנגנון שהתאמנו לבעיה.
    זו דוגמה נפלאה כיצד דפוס העיצוב המקובל (למשל: Circuit Breaker) הוא כמעט חסר חשיבות - למקרה ספציפי שלנו (הכישלונות הם לא של השרת המרוחק - אלא בדרך הגישה אליו). אם היינו מחברים circuit breakers בכל נקודה במערכת - לא היינו פותרים את הבעיה, על אף השימוש ב "דפוס עיצוב מקובל ל Fault-Tolerance".


    המנגנון שהרכבנו, בנוי מכמה שכבות:
    1. Timeouts - על מנת להגן על המערכת בפני starvation (ומשם: cascading failures).
    2. Retries - ביצוע ניסיון תקשורת נוסף לשירות מרוחק, במידה וה connection התנתק.
    3. Fallback (פונקציה נקודתית לכל endpoint מרוחק) - המספקת התנהגות ברירת מחדל במידה ולא הצלחנו, גם ב retry - לקבל תשובה מהשירות המרוחק.
    4. Logging and Monitoring - שיעזרו לנו לעשות fine-tune לכל הפרמטרים של הפתרון. ה fine-tuning מוכיח את עצמו כחשוב ביותר.
    5. Circuit Breakers - המנגנון שיעזור לנו להגיע ל Fallbacks מהר יותר, במידה ושרת מרוחק כשל כליל (לא המצב שכרגע מפריע לנו).

    אני מבהיר זאת שוב: Timeouts ללא Fallback או Retries זו בעצם הקרבה של ה traffic. היא יותר טובה מכלום, במצבים מסוימים - אך זו איננה התנהגות רצויה כאשר מדובר בקריאות שחשובות למערכת.


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

    ===================================================
    1. בצע קריאה מרוחקת (עם timeout של עד 2000 מילי-שניות, בד"כ פחות).
      1. אם הקריאה הצליחה - שמור אותה ב fallback cache (הסבר - מיד).
    2. אם היה timeout - ספק תשובה מתוך ה fallback cache (פשרה).
    ===================================================

    חשוב להבהיר: ה Fallback cache הוא Cache נפרד מ Cache רגיל (אם יש כזה). הוא ארוך טווח (כרגע: אנו מנסים 24 שעות), ולעתים הוא שומר מידע פחות ספציפי - כדי שיישאר נהיל ולא יהיה גדול מדי.

    ע"פ המדידות שלנו, בזמנים טובים רק כ 1 מ 20,000 או 25,000 קריאות תגיע ל fallback - כלומר משתמש אחד מקבל תשובה שהיא פשרה (degraded service).
    בזמני סערה, אחוז הקריאות שמגיעת מתוך ה fallback מגיע ל 1 מ 500 עד 1 ל 80 (די גרוע!) - ואז הפשרה היא התנהגות משמעותית.

    באחוזים שכאלו - הסיכוי שבקשה לא תהיה ב fallback cache היא סבירה (מאוד תלוי בשירות, אבל 10% הוא לא מספר מופרך). למקרים כאלו יש לנו גם התנהגות fallback סינתטית - שאינה תלויה ב cache.

    מצב ברור שבו אין cache - כאשר אנו מעלים מכונה חדשה. ה caches שלנו, כיום, הם per-מכונה - ולא per-cluster ע"מ לא להסתמך על קריאות רשת בזמן סערה. למשל: יש לנו שירות אחד שניסינו cache מבוזר של רדיס. זה עובד מצוין (גם ביצועים, ו hit ratio מוצלח יותר) - עד שפעם אחת זה לא עבד מצוין.... (והמבין יבין).


    כלומר, המנגנון בפועל נראה דומה יותר לכך:

    ===================================================
    1. בצע קריאה מרוחקת (עם timeout של עד 2000 מילי-שניות, בד"כ פחות).
      1. אם הקריאה הצליחה - שמור אותה ב fallback cache (הסבר מייד).
    2. אם היה timeout - ספק תשובה מתוך ה fallback cache (פשרה), או שתספק fallback סינתטי.
    ===================================================

    איך מגדירים fallback סינתטי? זה לא-פשוט, ותלוי מאוד בתסריט הספציפי. כקו-מנחה יש לחשוב על:
    • ערכי ברירת-מחדל טובים.
    • התנהגות שתמנע מפיצ'רים פחות חשובים לעבוד (למשל: לא להציג pop-up פרסומי למשתמש - אם אין מספיק נתונים להציג אותו יפה)
    • להניח שהמצב שאנו לא יודעים לגביו - אכן מתרחש (כן... המונית עדיין בדרך).
    • להניח שהמצב שאנו לא יודעים לגביו - התרחש בצורה הרעה ביותר (למשל: לא הצלחנו לחייב על הנסיעה)
    • אפשרות: הציגו הודעת שגיאה נעימה למשתמש ובקשו ממנו לנסות שוב. המשתמש לרוב מגיב בטווח של כמה שניות - זמן ארוך כ"כ (בזמני-מחשב), שמספר דברים עשויים להשתנות לטובה במצב ה caches / הידע שלנו בפרק זמן שכזה.


    תמונה מלאה יותר


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

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

    עוד דבר שגילינו, הוא שלמרות שהצלחות בפתיחת connection מתרחשות ב 99.85% בפחות מ-3 מילי-שניות, הניסיון לעשות retry לפתיחת connection מחדש לאחר כ 5 מילי-שניות - כמעט ולא שיפר דבר. לעומת-זאת, ניסיון retry לפתיחת connection מחדש לאחר כ 30 מילי-שניות עשה פלאים - וברוב הגדול של הפעמים הסתיים ב connection "בריא".

    האם מדובר בסערה שאיננה אקראית לחלוטין, או שיש לכך איזה הסבר מושכל שתלוי בסביבת הריצה (AWS, וירטואליזציה, וכו')? קשה לומר - לא חקרנו את העניין לשורש. נסתפק בכך כ retry לאחר 30 מילי-שניות - משפר את מצבנו בצורה משמעותית.

    עניין חשוב לשים לב אליו הוא שעושים קריאות חוזרות (retry) רק ל APIs שהם Idempotent, כלומר: לא יהיה שום נזק אם נקרא להם פעמיים. (שליפת נתונים - כן, חיוב כרטיס אשראי - לא).


    שימוש ב Cache קצר-טווח הוא עדיין ואלידי ונכון
    כל עוד לא מערבבים אותו עם ה fallback cache.

    מכאן התוצאה היא:

    ===================================================
    1. בצע קריאה מרוחקת
      1. נסה קודם לקחת מה cache הרגיל
      2. אם אין - בצע קריאה מרוחקת (עם timeout של עד 2000 מילי-שניות לקריאה, בד"כ פחות + timeout של 30ms ליצירת connection).
        1. אם היה timeout + במידת האפשר - בצע קריאה שנייה = retry.
        2. אם הקריאה הצליחה - שמור אותה ב fallback cache.
    2. אם לא הצלחנו לספק תשובה - ספק תשובה מתוך ה fallback cache (פשרה), או שתספק fallback סינטתי.
      1. ספק למשתמש חווית-שימוש הטובה ביותר למצב הנתון. חוויה זו ספציפית לכל מקרה.
    ===================================================

    תהליך זה מתרחש לכל Endpoint משמעותי, כאשר יש לעשות fine-tune ל endpoints השונים.

    שימו לב להדרגה שנדרשת ב timeouts: אם שירות A קורא לשירות B שקורא לשירות C - ולכל הקריאות יש timeout של 2000 מילי-שניות, כל timeout בין B ל C --> יגרור בהכרח timeout בין A ו B, אפילו אם ל B היה fallback מוצלח לחוסר התשובה של C.

    בגלל שכל קריאה ברשת מוסיפה כ 1-3 מילי-שניות, ה timeouts צריכים ללכת ולהתקצר ככל שאנו מתקדמים בקריאות.
    ה retry - מסבך שוב את העניין.

    כרגע אנחנו משחקים עם קונפיגורציה ידנית שהיא הדרגתית (cascading), קרי: ה timeout בקריאה C <-- B תמיד יהיה קצר מה timeout בקריאה בין B <-- A.

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

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


    כמובן שאת כל המנגנון יעטוף גם Circuit Breaker - זה קצת פחות דחוף עכשיו....


    כך נראתה השמדה-עצמית בשנות השמונים.
    מקור: http://tvtropes.org/


    סיכום


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

    מה שחשוב הוא להבין מה הבעיה - ולפתור אותה. לא לפתור את הבעיות שהיו ל Amazon, נטפליקס או SoundCloud.

    באופן דומה, אני ממליץ לכם לא לרוץ ולממש את המנגנון שתיארתי - אלא אם אתם נתקלים בהתנהגות דומה.

    אם למישהו מכם, יש ניסיון בהתמודדות דומה - אשמח לשוחח על כך. אנא כתבו לי הודעה או שלחו לי מייל (liorb [at] gett.com) ואשמח להחליף תובנות.

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


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


    ---


    לינקים רלוונטיים:

    AWS Noiseness