כיצד בודקים מיקרו-שירותים? בעזרת יהלום הבדיקות, כמובן! (פוסט מאויר)

שאלו אותי פעם איך מבצעים בדיקות למיקרו-שירותים. "כמו כל קוד אחד" – עניתי בטבעיות. למה שמשהו השתנה?

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

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

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

לא גלידה, לא פירמידה – אלא יהלום

המודל הקלאסי של בדיקות אוטומציה הוא מודל "פירמידת הבדיקות":

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

המודל המועדף עלי הוא דווקא מודל היהלום:

לאחר שנים שהייתי חסיד של מודל הפירמידה, וריבוי בכתיבות Unit Tests – השתכנעתי שמודל היהלום הוא יעיל יותר.
ההבחנה של ההבדלים בין בדיקות Integration, Component, ו E2E – היא משנית. העיקר הוא:

  • יש לנו מעט בדיקות ידניות: על מנת לא לשחוק, לא לעשות עבודה רוטינית שוב-ושוב-ושוב – אלא להשתמש במוח האנושי שאין לו תחליף, לזהות דברים לא טובים במערכת. (ויש אנשים שעושים זאת טוב יותר מאחרים).
  • יש לנו כמות בינונית של בדיקות יחידה:
    • בדיקות יחידה הן מעולות (!!) לבדיקת Pure Business Logic, קרי parsers, business rules, אלגוריתמים וכו' – כל מה שיש לו מחזור: קלט – הרבה עבודה לוגית – פלט. ניתן לזהות ביתר קלות אזורי קוד כאלה בעזרת צפיפות של משפטי if ו for (בשפות המתאימות).
    • בדיקות יחידה הן פחות יעילות לקוד אינטגרציה ("עשה א, ואז עשה ב, ואז עשה ג" – כמו שליפת נתונים מבסיס נתונים).
    • בדיקות יחידה הן דיי לא יעילות ל UI.
    • בקיצור: נשתמש בהן ב sweet spot שלהן בלבד: Pure business logic.
  • הדגש של המודל הוא על בדיקות Component (לעתים נקראות אינטגרציה, או API) – הבודקות התנהגות של כל שירות בפני עצמו. בבדיקה פשוטה אפשר לכסות הרבה מאוד קוד, בסביבה יחסית מציאותית, מה שמייצר מעין  sweet spot של עלות-תועלת: בין כמות ההשקעה בבדיקה – והערך שהיא מחזירה.
העקרונות של הפירמידה עדיין נשמרים ביהלום:
  • כמה שעולים למעלה הבדיקות הן: איטיות יותר להרצה, דורשות יותר תחזוקה –> יקרות יותר, ולכן ממעיטים בהן.
  • כמה שיורדים למטה הבדיקות הן ספציפיות יותר, רצות מהר יותר, ותלויות פחות בסביבה / אמינות יותר.

מה עם סוגי הבדיקות השונים?

ישנה חוסר סטנדרטיזציה ברורה ובעייתית בשמות בהן משתמשים לתיאור בדיקות שונות: מהו API Test ומהו Integration Test? – רבים לא יסכימו על ההגדרה. אני אצמד למונחים שמקובלים ב Gett, לא כי הם "בהכרח הטובים ביותר", אלא כי אני רגיל אליהם כרגע.

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

לצורך הדיון, כך נראה שירות:

  • תקשורת בינו ובין שירותים אחרים מתבצעת על גבי HTTP (סינכרונית) או על גבי RabbitMQ (אסינכרונית)
  • ה flow המרכזי של השירות מופעל ע"י איזה Invoker מסתורי –  שעדיף לא לחשוב עליו לפרטים, כשכותבים את הבדיקות. הוא עלול לעשות כל מה שה API מאפשר. לרוב זה יהיה שירות אחר.
הנה כמה דוגמאות לבדיקות יחידה:

  • המחלקה A היא מחלקה "חברה" של מחלקה X – הבודקת אותה. זוהי בדיקת pure unit tests, האידאל של בדיקות יחידה.
  • המחלקה B היא מחלקה "חברה" של מחלקה Y – הבודקת אותה. מכיוון ש Y תלויה ב X, ואנו רוצים בדיקה "טהורה", קרי: נקודתית ומבודדת – אנו יוצרים Mock של X וכך מריצים את הבדיקה B על המחלקה Y בלבד.
  • מחלקה C בודקת את המחלקה Z, אבל גם את החברה האחרת שלה – מחלקה X. לכן היא נקראת sociable unit tests. היא חברותית.
  • כמה שבסיס הקוד שנבדק ע"י בדיקת יחידה הוא גדול יותר (יותר branching של ה flow – בעיקר), בדיקת היחידה היא יעילה פחות: יהיה קשה יותר לבדוק מקרי קצה, וכשלון של בדיקות יצביע בצורה פחות מדוייקת על מקור התקלה.
  • ככלל, אנו מעדיפים בדיקות pure unit tests על פני ב sociable unit tests – אבל זו הבחנה שלא תמיד מדייקת. למשל: אם עשינו refactoring למחלקה גדולה Z, והוצאנו ממנה קוד למחלקה חדשה X – אזי הבדיקה C הפכה ל sociable unit tests. למרות זאת, היא טובה בדיוק באותה המידה כפי שהייתה לפני ה Refactoring.
  • הערה: בתרשים נראה שאני בודק את כל המחלקות שיש לי, בפועל כנראה שאבדוק בעזרת בדיקות יחידה רק 10-30% מהמחלקות ב Service (תלוי כמובן בשירות)

המאסה העיקרית של האוטומציה מתבצעת ע"י בדיקות Component – הבודקות רכיב בודד במערכת.

  • במקרה של MSA, הרכיב הוא שירות, עם ה Database שלו – אך ללא תלויות בשירותים חיצוניים.
  • הבדיקות מתבצעות רק דרך ה APIs של השירות, אם כי פעמים רבות מייצרים נתונים ב Database לפני הבדיקה ישירות דרך ה Models של השירות עצמו (כך שקל לייצר נתונים עקביים, בפורמט המעודכן ביותר).
  • המטאפורה של השם Component היא כמו של רכיב אלקטרוני. המערכת עשויה להיות מחשב ויש לה ערך עסקי רק כשהיא שלמה, אבל כל רכיב (Component) נבדק ביסודיות בפני עצמו: זיכרון, דיסק, מעבד, וכו'.
    יצרני הזיכרון, למשל, בודקים את הרכיב שלהם בקנאות – בודקים שהוא עובד, גם בכל מצבי הקצה, כפי שמצופה ממנו. כאשר הממשקים ("contracts") ברורים, וכל רכיב בדוק כראוי – ניתן כבר להשיג רמת אמינות מרשימה של המערכת.
כמובן שאי אפשר להסתמך רק על בדיקת הרכיבים העצמאיים. אנו עושים בדיקות של כלל המערכת ("מפעילים את המחשב").
בעולם הפיסי מתחילים אולי בהדלקת המחשב, לראות שלא יוצא ממנו עשן (להלן "Smoke Tests") – אך ממשיכים בבדיקות יותר מקיפות הבודקות את כלל המערכת (למשל: לגלוש ב 10 טאבים במקביל בדפדפן, תוך כדי שתוכנה להמרת וידאו רצה ברקע).
במונחים שלנו מדובר על End-To-End Tests, ובקיצור E2E Tests (מה שלעתים קרוי גם System Test):
  • אמנם אין בתרשים הרבה שירותים – אנא הניחו שמדובר במערכת מלאה (עשרות Services ויותר), עם תצורה קרובה ככל האפשר ל Production, עם האפליקציה / UI, ושירותים חיצונים בהם משתמשים (למשל: שירות לשליחת הודעות SMS).
  • המטרה: לפני שמשחררים שינוי משמעותי לפרודקשיין, נרצה להשקיע בבדיקה ולאתר תקלות לפני ההגעה לפרודקשיין, בסביבה אמיתית ומלאה ככל האפשר.
ואכן בדיקות E2E הן יקרות, וכדאי למעט בהן.
כאן מגיע התפקיד של בדיקות אינטגרציה: לאזן קצת בין עלות לתועלת:
  • בבדיקות אינטגרציה בודקים כמה שירותים כשהם עובדים ביחד. בדרך כלל זו קבוצה קבועה של שירותים הנמצאים בקשר עמוק זה עם זה, מה שניתן גם לכנות גם: "sub-system".
  • לפני הרצת בדיקת E2E – מריצים את בדיקת האינטגרציה המתאימה לשירות / תסריט (אם יש כזו). הקמת הסביבה היא מהירה וזולה יותר (נניח 2 עד 7 שירותים – במקום עשרות רבות של שירותים). הבדיקות רצות מהר יותר והן ממוקדות יותר (כלומר: סבירות גבוהה יותר לאתר תקלות).
  • עבור שינוי קטן או בינוני באחד השירותים, ניתן להסתפק בהרצה של ה sub-system הרלוונטי. עניין של בחירה וניהול סיכונים.
  • ב Sub-System שבתרשים – שירות A הוא "המוביל" ותמיד דרכו ייעשו הבדיקות, גם כאשר השינוי שנבדק הוא בשירות אחר.
    • לא נשלח באמת SMSים, אלא נבצע Mock לשירות החיצוני.
    • ייתכן ואחד השירותים שלנו ב Sub-system הוא מורכב מדי להפעלה / קונפיגורציה – ולכן גם אותו נחליף ב Mock. כדאי להימנע מגישה זו במידת האפשר.

סיכום

זהו, זו הייתה סקירה מהירה משהו.
אני מקווה שהיא מובנת, ויכולה לחדש לקוראים רבים ככל האפשר.
שיהיה בהצלחה!
—–

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

First Class Tests – מאמר דעה של הדוד בוב בנושא, הוא הרי חסיד של בדיקות יחידה קפדניות ו TDD.

מצגת על מיקרו-שירותים ב DevConTLV בפורים

בפורים, ממש לפני כמה שבועות הופעתי בכנס DevConTLV שהיה בסינמטק.

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

האמת שהייתי במהלך וירוס שהכה בי, ומי שהיה בהרצאה ראה כנראה שהייתי חלש וכמה פעמים אף התקשתי לסיים משפטים 🙁

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

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

מקווה שתמצאו את החומר מעניין ושימושי!

ליאור

הכנס השני לארכיטקטורת תוכנה (30 לנובמבר – 1 בדצמבר)

בסוף החודש הקרוב, 30 בנובמבר – 1 בדצמבר, יתקיים בהרצליה הכנס השני לארכיטקטורת תוכנה.

הכנס מאורגן ע\"י IASA ואילתם.

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

ILTAM, היא קהילה מקצועית, רחבה יותר, המורכבת בעיקר מחברות הייטק גדולות ומבוססות (לא רק חברות תוכנה נטו: אלתא, 3M, וצה\"ל למשל – הם חברים) במטרה לקדם ולשתף ידע.

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

היום ראשון מורכב מהרצאות. הנה התכנית:

ביום שני מתקיים Tutorial בהדרכתה של Rebecca Wirfs-Brock (שהגיעה לארץ במיוחד, אני מניח), בנושאי ארכיטקטורה באג\'ייל ו Quality Attributes בפרט (הנה פוסט שפירסמתי בנושא, אם אתם רוצים לקבל מושג במה מדובר).

האם כדאי לבוא?

תקשיבו, זו שאלה דיי אינדיבדואלית, ואני בד\"כ זהיר במתן המלצות. בכל זאת, כשאני מסתכל התכנים – נראה לי שהצליחו לרכז באמת שורה של נושאים ומרצים מעניינים ביום הראשון – שרלוונטיים לאנשי-תוכנה כמעט מכל הסוגים.
היום השני הוא באמת ממוקד יותר לארכיטקטים, או מי שרוצה שהתעמק בטכניקה תאורטית שהתמורה שלה להשקעה היא ארוכת-טווח. אני לא יודע להמליץ ספיצית על ה Tutorial שנבחר, אבל אם הוא נבחר באותה רוח של בניית האג\'נדה ליום הראשון – ייתכן בהחלט וזה יהיה Tutorial מוצלח!
  • אני נותן הרצאה על מיקרו שירותים (עדיין לא החלטתי בדיוק איך להעביר את הנושא…). אם אתם מגיעים – קפצו לומר שלום!
  • קצת אחרי ייתן הרצאה יונתן ממן, שכתב כאן פוסט אורח בנושא – ממש לאחרונה.
  • הנה אתר הכנס: http://conference70.wix.com/sw-architecture
  • בטופס ההרשמה שבאתר, ניתן לקבל הנחה אם מזינים את המילה \"Presenter\", בסעיף של קבוצת שיוך (אופס, אני מקווה שבאמת היה מותר לי לספר את זה…).
שיהיה בהצלחה!
ליאור
 

כל מה שרצית לדעת על ארכיטקטורת מיקרו-אפליקציות (Micro-Apps Architecture) – ולא העזת לשאול

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


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

Fox News, ה Rolling Stone, "הארץ", "בלוג ארכיטקטורת תוכנה" (בדיחה, בדיחה), ועוד. אאוטבריין מייצרת כ-+150 מיליארד המלצות תוכן בחודש – כמות Traffic יוצאת דופן! בזירת ההייטק הישראלית היא נחשבת מובילת-דרך בכל הנוגע ל Scale, Operations, וכו'. החברה גם פעילה למדי בקידום אירועים ושיתוף ידע בתעשייה (IL TechTalks, רברסים וכו').

 

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

  • בסיס קוד קטן, פשוט וקריא. כל service מבצע מספר קטן של משימות, ויש לו אחריות מאוד מוגדרת.
  • ממשק וגבולות ברורים לכל service, ולכן יש גם בעלות ברורה שעוזרת לשיפור האיכות.
  • כל service נכתב בטכנולוגיה שמתאימה לו ולצוות שאחראי עליו.
  • ה service עולה יורד באופן מהיר יותר, ולכן מתאים מאוד לפריסה מתמשכת (Continuous Deployment).
  • משפר את יציבות מערכת – אם service אחד נפל, service-ים אחרים ממשיכים לתפקד. אם service אחד עמוס אפשר בקלות להתקין שרתים נוספים שיריצו את אותו השירות.
ה tutorials שתמצאו, כנראה ידברו על שירותי רשת (web services) – ללא UI ובעיקר ללא ניהול session. אבל מה עם אפליקציות רשת (web applications), שם ה UI, וניהול ה session של המשתמש הם החלקים המרכזים ? האם גם אותם ניתן לפצל ל ״מיקרו-אפליקציות״ ? נניח שלקחת אפליקציה אחת ובדרך קסם פיצלת אותה לשניים, איך תתמודד עם:

  • שמירה על חווית כניסה (login) אחת?
  • מעבר חלק, ושיתוף session בין האפליקציות?
  • מניעת שכפול קוד ?
בוא ניקח לדוגמה אפליקציית רשת נפוצה: אתר של חברת סטארטאפ.
כמו שכל בית צריך מרפסת, כל סטארטאפ  צריך אתר. מה יש באתר? ובכן, זה לא רק אתר שיווקי. זהי אפליקציית רשת שמאפשרת ללקוחות להשתמש במוצר ולתפעל אותו בשירות עצמי. למשל: המשתמשים יכולים לקבוע פרמטרים, לשנות הגדרות, להוריד דוחות וגרפים. יש גם כמובן מנגנוני רישום והתחברות, הגדרת פרופיל משתמש, שחזור והחלפת ססמה, CAPTCHA, וכו'.
האתר לרוב יהיה מהמערכות הוותיקות בחברה – ולכן גם עם חוב טכנולוגי גבוה / Technology Stack ישן.
לכאורה מועמד מצוין לשדרוג ולמעבר לארכיטקטורת מיקרו services, אבל אליה וקוץ בה: דווקא אפליקציית רשת קשה מאוד לפצל ויש מעט מאוד מידע ברשת כיצד לעשות זאת.
פוסט זה יתאר ארכיטקטורת מיקרו-אפליקציות (Micro-Apps Architecture) שמטרתה (בדומה ל Micro-Services Architecture) לסייע בפיצול אפליקציית רשת למיקרו אפליקציות ובכך ליהנות מהיתרונות המדוברים.
אז איך עושים את זה ?
בשלב הראשון ננתח את המבנה הלוגי של האפליקציה. סביר להניח שאפליקציה שמשרתת כמה וכמה תרחישים, תורכב מכמה אזורים עיקריים.

כל אזור (אדום, צהוב או כתום) הוא מועמד להיות מיקרו אפליקציה עצמאית.
בשלב השני נוציא כל אזור מתוך האפליקציה ונהפוך אותו למיקרו אפליקציה. מהי מיקרו אפליקציה? מיקרו אפליקציה היא אפליקציה  שאחראית על פונקציונליות ברורה מוגדרת ומצומצמת. לדוגמה משרתת סוג לקוחות מסוים, או חושפת יכולות של מוצר מסוים משלל המוצרים של החברה.
כשבאים לקבוע את הגבולות בין האפליקציות ניתן להשתמש באותם שיקולים בהם משתמשים בחלוקת service , למיקרו .services.
המיקרו אפליקציה בנויה מטכנולוגיה המתאימה לה ולצוות שלה ומותקנת על שרת ייעודי. האפליקציה יכולה להגיש גם את צד הלקוח (client side) וגם את צד השרת (server side). האפליקציה מותקנת בתוך ה firewall ולא מקבלת תעבורת רשת באופן ישיר.
micro-colored.png
נייצר service מארח (hosting service)  דרכו עוברת כל התעבורה למיקרו אפליקציות. ברוב המקרים ניתן להשתמש באפליקציה המקורית (המונוליטית) כ service מארח, כיון שאחרי שהוצאנו מתוכה את רוב הפונקציונליות כנראה שנותר שם רק התשתית של ניהול הsession, אימות המשתמש, אבטחה וכו'.
ה service המארח משמש כמעין נתב. ״נתב על סטרואידים״, אם תרצו.

למה נתב? כי ה service המארח רק מקבל את הבקשה דרך התשתיות הבסיסיות ומשמש כצינור בכדי להעביר את הבקשה למיקרו אפליקציה הרלוונטית.

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

מה עושה הפרוקסי?

  • הפרוקסי הוא יחידה לוגית בתוך ה service המארח. תפקידו להוות צינור ולהעביר בקשות מהלקוח למיקרו אפליקציה עליה הוא אחראי. התקשורת בין הפרוקסי למיקרו אפליקציה באמצעות פרוטוקול HTTP. לכל מיקרו אפליקציה קיים פרוקסי משרת אותה.
  • כל פרוקסי נרשם על מסלול אחר של ה URL. לדוגמה כל בקשה למשאב תחת הכתובת http://www.yoursite.com/app1 תופנה לפרוקסי האדום.
  • כשהבקשה מגיעה לפרוקסי כבר ידוע מיהו המשתמש, ולכן הוא יכול להעביר את שם המשתמש (או כל מידע אחר עליו) למיקרו אפליקציה באמצעות HTTP Header.
  • הפרוקסי מנהל את כמות החיבורים הפתוחים לאפליקציה שלו, ומדווח על כמות החיבורים הפתוחים למערכת הניטור.
  • הפרוקסי מקבל בחזרה את התשובה מהמיקרו אפליקציה ומעביר אותה חזרה למשתמש.

יתרונות – או מה יצא לנו מכל זה?

  • הפרדה ברורה בין אפליקציות: זליגת זכרון במיקרו אפליקציה אחת לא תביא לנפילה של מיקרו אפליקציה אחרת.
  • ה service המארח הופך מהר מאוד לתשתית שבה אין הרבה פיצ'רים חדשים. הוא הופך לפשוט וקל יותר לתחזוקה.
  • כל מיקרו service נכתב בטכנולוגיה שמתאימה לדרישותיו ולצוות שמפתח אותו.
  • ניתן לעשות את המעבר מאפליקציית רשת מונוליטית למיקרו אפליקציות בשלבי, ובכך להקטין את הסיכון. לדוגמה:
    • לייצר מיקרו אפליקציה שתגיש רק את קוד הלקוח שנכתב בטכנולוגיה חדשה, נניח ב AngualrJS, ולהשאיר את קוד השרת ב service המארח.
    • לייצר מיקרו אפליקציה חדש שיגיש קוד שרת בלבד. ניתן להחליט שלא מעבירים קוד ישן אלא רק קוד חדש של פיצ'רים חדשים. וכך לבצע החלפה אטית אך שיטתית ומדורגת מתשתית שרת ישנה לתשתית שרת מודרנית יותר.
  • ניטור — כל פרוקסי מדווח על כמות הכישלונות, הצלחות, מספר החיבורים התפוסים וכו'.
  • רישום בקבצים – ה service המארח אחראי על כתיבת קבצי הגישה access log עבור כל המיקרו אפליקציות.
  • אבטחה — ה service המארח מבצע את כל מנגנוני האימות של כניסת משתמשים ו CAPTCHA.
  • אפליקציות חדשות — נניח שאתה רוצה לפתח אפליקציית-רשת חדשה. תוכל לממש אותה כמיקרו אפליקציה בתוך ה service המארח. כך תחסוך בפיתוח מנגנוני אבטחה, רישום וכניסה, כתיבת קבצי גישה וכו', כל מה שתדרש זה להוסיף עוד חוק ניתוב service המארח. בנוסף לא תצטרך את עזרת אנשי ה OPS. גישה זו היא מיושרת עם רעיון ה DEVOPS.
כמובן שאין ארוחות חינם. אז מה אנחנו מפסידים?

  • עוד תחנה במסלול — טיפול בבקשה של משתמש לוקחת זמן ארוך יותר.
  • שיתוף קוד לקוח — כבר לא תוכל לשנות בקלות את ה JS/CSS שמשותפים עבור כל המיקרו אפליקציות. בכדי לשתף קוד לקוח (למשל ניווט בתוך האפליקציה), תצטרך להתייחס אל הקוד כאל ספרית צד שלישי. תוכל להשתמש ב bower בכדי לנהל את הגרסאות ולהעלות את הקוד ל repository. דרך נוספת תהיה להעלות קבצים משותפים ל CDN.
  • הגדל הקושי בתחזוקה ובמציאת תקלות – כל תוספת של חלק נע במערכת מקשה על מציאת באגים. עוד קבצי לוג שצריך לבדוק כמשנסים לאתר תקלה. נניח שמשתמש מנסה לגשת למיקרו אפליקציה ומקבל HTTP CODE 403. יהיה עליכם לבדוק האם ה service המארח חסם את הגישה, או אולי האבטחה של המיקרו אפליקציה חסמה את הבקשה.
  • הפרוקסים הם פשוטים ולא משוכללים כמו HAProxy.
הניסיון שלנו באאוטברין:

  • פתחנו את הארכיטקטורה הזו בתחילת 2015, כאשר הבנו שאנחנו הולכים לחשוף הרבה יכולות של המוצר שלנו למשתמשי הקצה באמצעות שירות עצמי דרך אפליקציית רשת. אפליקציית הרשת שלנו הייתה מבוססת על תשתיות ישנות והבנו שמבחינה טכנולוגית אנחנו צריכים לבצע שדרג משמעותי.
  • המטרה הייתה לפתח יישומים חדשים בטכנולוגיות מודרניות באופן בלתי תלוי ע"י צוותים שונים עם סט בדיקות שונה תוך כדי שיתוף session בין האפליקציות, כל זאת בלי לכתוב מחדש את כל התשתית של אפליקציית הרשת הקיימת.
  • התחלנו עם מיקרו אפליקציה אחת, ועכשיו יש לנו 4 מיקרו אפליקציות שמגישות גם צד שרת וגם צד לקוח. הנה דוגמה ל Stack הטכנולוגי של 3 מארבעת מיקרו אפליקציות בתשתית של אאוטברין:
  • לכל מיקרו אפליקציה יש מערכת בדיקות עצמאית וההחלטה האם אפשר לעשות Deploy ל production נמצאת בידי הצוות שמפתח את האפליקציה.
  • לכל מיקרו אפליקציה יש ניטור משלה וההתראות מגיעות לצוות הרלוונטי.
  • הוספנו ניטור ל service המארח בכדי לוודא שכמות החיבורים לכל מיקרו אפליקציה לא עוברת ערך מקסימלי וכמות השגיאות וההצלחות.
  • קובץ הגישה ב service המארח חושף את כל הפעילות של המשתמש בכל המיקרו אפליקציות השונות ונותן מבט אחד על פעולות המשתמש.
  • הוספת מיקרו אפליקציה נוספת היא מאמץ קטן של מספר שעות פיתוח.
  • כמות השינויים שבוצעו במערכת הישנה היה קטן יחסית, וזמן הפיתוח היה קצר – ולכן הסיכון שבמאמץ הפיתוחי היה נמוך.

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