כל מה שרצית לדעת על ארכיטקטורת מיקרו-אפליקציות (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 המארח חושף את כל הפעילות של המשתמש בכל המיקרו אפליקציות השונות ונותן מבט אחד על פעולות המשתמש.
  • הוספת מיקרו אפליקציה נוספת היא מאמץ קטן של מספר שעות פיתוח.
  • כמות השינויים שבוצעו במערכת הישנה היה קטן יחסית, וזמן הפיתוח היה קצר – ולכן הסיכון שבמאמץ הפיתוחי היה נמוך.

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").

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

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.

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

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

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

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

AWS Noiseness

ארכיטקטורה של Hyper-Scaling

נושא ה Scalability הוא פופולרי-במיוחד בכמה השנים האחרונות.לאחרונה עברתי על עשרות קורות חיים של מועמדים – ובכמעט כולם צוינו מושגים כמו "High Scalability", או "Big Data", "NoSQL", "Hadoop" – וכו'. כנראה שכל מי שעבד במערכת עם הרבה transcriptions per seconds או נפחים גדולים של נתונים – סיפר על כך בהרחבה, ומי שלא – התאמץ להראות איזו זיקה. זה "המשחק" היום, ונראה לי שהייתי עושה בעצמי את אותו הדבר בדיוק!

בפוסט הזה אני רוצה לספר על תהליך של Hyper-Scaling שאנו עוברים בחברת Gett – וכיצד הוא משפיע על עבודת הארכיטקטורה.

No Scale

אני רוצה להזכיר שהמונח "Scalability", מתייחס בהנדסת תוכנה לשני סוגים של אתגרים:

  • Software Scalability – התמודדות עם יותר משתמשים, יותר פעילות, יותר נתונים.
  • Development Scalability – היכולת להתנהל עם צוות פיתוח גדול יותר.
ב Gett יש לנו Software Scale מסוים, שהוא לא קטן – אבל גם לא ענק. ככה וככה נתונים, ככה וככה פעולות בשנייה.
ההתמודדות העכשווית שלנו היא דווקא יותר עם Development Scalability, שכמו שאנסה להראות במהלך הפוסט – יש לה דמיון לא-קטן ל Software Scalability.
לפני כחצי שנה, כשהגעתי ל Gett היו בצוות צד-השרת כשישה מתכנתים. הגעתי מעט לאחר גיוס ענק של 150M$ שהחברה ביצעה. עם הגיוס, החברה החליטה להגדיל משמעותית את קבוצת ה R&D – בכדי לקבל משמעותית יותר תפוקה. בעת כתיבת הפוסט יש לנו כבר עשרים וחמישה (!!!) מתכנתי צד-השרת – ואנחנו עוד מגייסים.את הכלל של "לא להגדיל גוף פיתוח ביותר מ 50% בשנה" – שברנו כבר מזמן… מה עושים עכשיו? ואיך מתמודדים עם זה מצד הארכיטקטורה?

Scale

ההקבלה בין Scale של תוכנה ו Scale של קבוצות-פיתוח

נוסחה מקובלת בעולם ה Software Scale היא זו:
אנו מגיעים ל Scale כאשר יש לנו כמות משאבים (למשל: שרתים) מסוימת, וכל שרת מבצע עבודה בקצב מסוים.
גדילה ב Scale, כלומר: Scaling – מתבצעת ע"י הוספת שרתים או לחלופין ע"י שיפור הביצועים של כל מחשב בודד במערכת.
ככל שהמערכת גדלה – סביר שנחווה מצב בו כל מחשב נוסף שאנו מוסיפים הוא פחות יעיל מקודמו. מדוע? מכיוון ש:
  • פעולות על כמות גדולה יותר של נתונים – אורכות יותר זמן. למשל: אינדקסים בבסיס נתונים רלציוני, הפחתת הרציפות בדיסק, caches פחות יעילים או סתם פעולות joins גדולות יותר (merge על work set גדול יותר).
  • יותר תקשורת שנדרשת בין המחשבים השונים במערכת. יש הבדל בין הודעות עדכון שנשלחות ל 6 מחשבים – וכאלו שנשלחות ל 25 מחשבים. פעם נתקלתי במערכת שהוספה של מחשבים למערכת, מעל 16 מחשבים, כבר לא הגדילה את ה scale – בגלל ריבוי של הודעות כאלו.
  • כמות הקוד שלא ניתן למקבל (parallelism) באופן טבעי תגדל, ולא תקטן – אלא אם הייתה השקעה משמעותית בצמצום קוד שכזה.
  • חוסרי יעילות שונים – שצצים במערכת באקראיות טבעית.

באופן דומה, גם בגדילה של גוף פיתוח – המפתח האחרון שנוסף נוטה להיות (בממוצע!) פחות יעיל מקודמו:

  • כל עבודה רוחבית במערכת (למשל: Refracting גדול), הופכים להיות קשים וארוכים פי כמה – כאשר כמות הקוד גדולה יותר.
  • יותר תקשורת וסנכרון נדרשת בין המפתחים בקבוצה. אם פעם היה מספיק להרים את הראש מבהייה במסך – בכדי ליצור קשר עם מתכנת שמכיר היטב היבט מסוים של המערכת, היום כבר צריך לקום מהמקום, לחפש – ולעתים לגלות שצריך לדבר עם כמה אנשים בכדי לקבל תמונה מלאה.
  • תמונות-העולם של המפתחים בארגון מתבזרות במהירות: בניגוד לצוות שהיה לו זמן להתגבש במשך תקופה ארוכה – כעת יש זרימה של אנשים חדשים, שכל אחד רגיל לשיטות שונות וגישות שונות.
    הגישות הללו, עבור כל אחד, "הוכיחו את עצמן מעל ספק סביר – בעבר". אמת. אבל מה עושים כאשר הגישות הפוכות זו לזו? האם ORM הוא טוב להכל, טוב רק לקונפיגורציה, או "רעה-חולה שיש להסיר מהמערכת בהקדם האפשרי!"?
  • יותר ידיים עובדות => יותר קוד => מערכת מורכבת יותר. כל שינוי במערכת מורכבת יותר – אורך יותר זמן בעצמו (מגמה לחוסר יעילות מובנה).
  • נוצרים יותר צווארי בקבוק ("רק משה מכיר את הקוד הזה") – שהולכים ומקשים יותר ויותר על התקדמות הפיתוח.
  • יותר ישיבות, יותר המוניות, יותר אנשים שיש להכיר ולהתרגל לעבוד איתם – חוסרי יעילות שונים, שצצים במערכת באקראיות כל-כך טבעית.
ההשקעה ב Development Scale בפיתוח אמנם עוסקת במידה רבה בגיוס עובדים ("Capacity"), אבל לא פחות מכך – בשיפור היעילות של כל עובד ("Performance"). תהליכי ה Continuous Integration (ליתר דיוק: on-going integration) – מוגדרים מחדש, אנו משקיעים יותר בשיתוף הידע – ופישוט שלו, וכאן יש לצוות הארכיטקטים תפקיד חשוב.
אנו חותרים ל Continuous Delivery (הפעם: באמת) – בכדי לשפר את יכולת התגובה לתקלות במערכת, וכדי לעשות אותה יציבה יותר. באופן פרדוקסלי משהו, הניסיון בתעשייה מראה שדווקא העלאת תכיפות ה deployments מגדיל את יציבות המערכת – בטווח הבינוני-ארוך. יותר deploys = יותר "שבירות", אבל אז גם יותר לקחים, יותר מנגנוני-התאוששות ובקרה, ויותר אוטומציה. כל עוד אין מנגנון ארגוני שמותיר "לעצור את הקצב, ולצמצם את קצב ה deploys" – האנרגיות ינותבו לשיפור המערכת, ומנגנוני הייצוב שלה.
ב Software Scale, יש את השאיפה התמידית ל Linear Scalability: האידאל שלפיו הוספה של כל מכונה למערכת, תתרום את החלק היחסי שלה. למשל: הכפלת כמות השרתים – תכפיל את הספק העבודה (למשל: כמות בקשות בשנייה).
לא ממש Linear Scaling: ככל שמספר הבקשות עולה – יש להוסיף חלק יחסי גדול יותר של שרתים בכדי לענות על הביקוש.
יש במערכת הזו צווארי בקבוק מסוימים ל scalability.
בקבוצת ה R&D כולנו מבינים שככל שהמערכת גדלה – היעילות של המתכנים הולכת וקטנה. אין לנו שאיפות ל Linear Development Scalability. אנחנו גם מכירים במגבלות המקבילות האנושית ("תשע נשים לא יכולות ללדת ילד בחודש אחד").
בשונה מאיתנו ל Business דווקא יש ציפיות ל Linear Scalability – מפורשות יותר או פחות.
"פי-2 אנשי support עונים לפי-1.9 קריאות במוקד?" – הם מספרים, "כן… אנחנו מבינים שהנדסה זה קצת יותר מורכב. הגדלנו את הפיתוח פי 4, ואי-אפשר לקבל פי-4 פיצ'רים – אבל גם אי אפשר כבר לצפות לפחות לפי-3 יותר פיצ'רים, או לקבל אותם לפחות פי-3 יותר מהר?"הלחץ מצד הביזנס הוא אולי משני, אבל הוא משפיע – וגורם לנו להתייעל יותר בכל הנוגע ל Development Scalability של הפיתוח. בעיקר ע"י צמצום חוסר-היעילות שהמערכת יוצרת למפתח הבודד.

ב Software Scale, יש "קסם" שיכול לסייע למערכת לצמוח ב Scale שהוא יותר מלינארי: מצב בו פי-2 שרתים, משרתים יותר מפי-2 משתמשים. כיצד זה קורה? יש כמה דרכים, אבל ה"קסם" הנפוץ ביותר הוא Cache (או memoization – בגרסה התאורטית שלו).
כאשר אנו יכולים לבצע חישוב מורכב רק פעם ב 5 דקות, ואז להפיץ את התוצאות לעוד ועוד משתמשים – כמות גדולה אפילו יותר של משתמשים תגדיל את הלחץ רק על ערוץ ההפצה (CDN?) – ולא על יצירת התוכן (החישוב).

ככל שנתכנן את המערכת שלנו בצורה בה ניתן יהיה להשתמש יותר ויותר ב Caches שכאלו – נשפר את ה Scalability של המערכת. תכנון שכזה כולל, הרבה פעמים – משא ומתן עם אנשי הביזנס ("תקבלו את זה מעודכן פעם בשעה – לא כל הזמן").

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

Scaling שהוא טוב מ Linear-Scaling: הוספת שרת למערכת – מוסיפה יכולת לספק קצת יותר משתמשים מחלקו במערכת.

ב Development Scaling יש גם כמה "קסמים" שכאלו. הבולט בהם – הוא code re-usability: היכולת להשתמש בקוד שנכתב בעבר – עבור פיצ'ר חדש.

פתרון שונה-דומה הוא Generalization: כתיבת קוד כללי יותר – שיכול לשרת מטרות דומות, אך שונות.

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

מהנדסים צעירים, ואולי אף מהנדסים בכלל – נוטים לבצע הערכת יתר (גסה?) ליכולת שלהם לייצר קוד יעיל לשימוש חוזר / קוד כללי יעיל. משם נוצר הכלל You Ain't Gonna Need It (בקיצור: YAGNI) המציע פשוט לא לנסות לחזות מקרים אלו מראש, אלא לעשות Refactoring לקוד כללי רק ברגע שהוכח הצורך – מעל ספק סביר.

בכל מקרה: שימוש חוזר בקוד והכללה, גם אם נעשים בדיעבד – הם כלים חשובים מאוד לשיפור ה Development Scalability.

אז מה עם ארכיטקטורה ל Hyper-Scaling?!

אולי אתם מאוכזבים מעט מהפוסט: יש בו הרבה דיבורים כללים, ואין בו Hadoop, HPC או Big Data משום צורה!

אני אנסה לתמצת:
הפיתוח של Gett עובר כרגע תהליך של Development Hyper-Scaling. יש גם בעיות של Software-Scaling – אבל הן (עדיין) פחות מאתגרות – אולי אזכיר לקחים משם בפוסט אחר.

הארכיטקטורה, או תוכנית-העל שלנו להתמודד עם בעיות ה Development Hyper Scaling הן כאלו:

  • בראש ובראשונה – מעבר ל Micro-Services: הפיכת מערכת אחת מורכבת – לכמה מערכות קטנות יותר, ומורכבות פחות. המעבר הוא אינטנסיבי, אבל הוא מאפשר להבין ביתר קלות את כלל המערכת – ולדעת להיכן לצלול בעת הצורך. כמו כן – הוא מצמצם במידה רבה את הצורך בתקשורת מרובה, לטובת תקשורת בסיסית יותר וממוקדת יותר, למשל: סך האחריויות של כל שירות, וה APIs שלו – שמוגדרים היטב (אנחנו משתמשים ב Swagger לתיעוד – כלי שמשרת אותנו היטב).
    את השימוש ב MSA להתמודדות עם Development Hyper-Scaling לא המצאנו בעצמנו: למדנו מ case-studies על חברות שעמדו באתגר דומה (למשל: אמזון).

    • שימוש-חוזר בקוד, הוא רעיון שקשה לממש (מעבר לפונקציה פה ושם). דווקא Micro-services, בכך שאנו מגדירים שירותים עם שימוש עסקי ברור, ו APIs מוגדרים היטב – מסייעים לנו ליצור יחידות גדולות של קוד שמתאימות לשימוש-חוזר. כבר בחצי-שנה האחרונה, היו לנו כמה הצלחות יפות.
  • אנו עוסקים בצורה פרואקטיבית בארגון בשיתוף ידע על חלקי המערכת השונים, האחריויות שלהם, וה flows העיקריים במערכת. לא עובר כמעט שבוע שאני לא עושה session שכזה, לצוות כלשהו בפיתוח – ואני לא היחידי. עוד פעם ועוד פעם – עד שלכולם כבר יימאס (אנחנו עוד רחוקים משם…).
    שמות פשוטים, מטפורות טובות, וסיפורים קליטים – הם מרכיב עקרי בבניית והפצת הידע.
  • צוות הארכיטקטים לוקח תפקיד קצת יותר ריכוזי מהרגיל (אולי: יותר מהאידאל האישי שלי?!) בהגדרת superflows חדשים במערכת. כן! אנחנו רוצים לעבוד יותר agile ולתת לאנשים יותר ויותר אחריות והשפעה, אבל בנקודת הזמן הזו – תוצאות טובות יותר מושגות כאשר לפחות את ה flows העיקרים – מוגדרים מרכזית ע"י הארכיטקטים.
    כאשר מפתחים עושים שינויים ושיפורים ב flows – זו סיבה לשמחה (אלא אם בכך הם סותרים עקרונות או flows אחרים במערכת).
  • אנו מנסים לקדם בקוד כמה עקרונות:
    • קידום תרבות של הצגת פתרונות – ולא רק בעיות (בכל ה R&D).
    • קוד פשוט להבנה – עדיף יותר על פני קוד קצר או מתוחכם. אם אתם קוראים של הבלוג זמן רב, אתם אולי יודעים שזו הנטייה הטבעית שלי – אבל זה לא הסטנדרט הברור של ריילס (שם עקרונות של קוד קצר ו DRY – מושרשים עמוק בקהילה).
    • יותר כלי monitoring ו supportability עבור הפיתוח. בכתיבה של כל פיצ'ר – לחשוב איזו השקעה תהיה משתלמת כאשר לפיצ'ר הזה תהיה בעיה ב production. כלי supportability יכולים להציג חווית שימוש עלובה למדי – כל עוד הם עוזרים.
    • הכנסה של אוטומציה / בדיקות יחידה / בדיקות-API / בדיקות-אינטגרציה. בכל ארגון שראיתי בעשור האחרון זו הייתה המגמה – אבל אנחנו עכשיו צריכים את זה יותר.

אני מודע לכך שעדיין אין פה Design Patterns הנדסיים משמעותיים (אולי מלבד MSA) – אבל זה מה שעובד, ואנו עושים את מה שעובד – ולא רק מה שמתאים לציפיה מסוימת (ארכיטקטורה = "תרשימים של ריבועים"). סורי! 🙂

זהו… מקווה שנהניתם, ואולי אף השכלתם. כרגיל – אשמח לתגובות.

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

מערכות מבוזרות – מבט עדכני

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

קיטרתי – גם אני לא עשיתי זאת בעצמי (כמעט).

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

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

  • מוצרים: אפליקציית לקוח של טורנט, Tor (רשת פרוקסי לאנונימיות), או משחק Massively Multi-player Online.
  • Frameworks: המאפשרים פתרון בעיות מבוזרות ב"רמת הפשטה גבוהה".
יש משהו מאוד קוסם בסקירה כיצד מוצרים מבוזרים עובדים – זה יכול להיות ממש מעניין!
האמת היא שיש לי (ובכלל בקהילה) – ידע מוגבל על הנושא. (הנה פוסט נחמד על Tor)
 
בסקירה של קטגורית Frameworks יש משהו יותר מעשי: סביר יותר שתבחרו Framework לעבוד בו – מאשר לכתוב מערת מבוזרת מ scratch, תוך כדי שאתם מושפעים ממימוש של מערכות אחרות.
כמו כן – הידע עליהן זמין ונגיש. לאחרונה התחלנו לעבוד עם Hadoop – מה שעוזרת לי לשלב את תוכן הפוסט עם העבודה השוטפת.
 
בסופו של דבר, גם ה Frameworks וגם המוצרים המבוזרים שמתוארים בפוסט עושים שימוש נרחב במנגנונים ("פרימיטיביים") של מערכות מבוזרות שתיארתי בפוסט הקודם: Multi-cast ורפליקציה, בחירת מנהיג, השגת קונצנזוס ועוד. חלק מכובד מהמערכות שאתאר בפוסט זה משתמש ב Zookeeper, מוצר שהזכרתי בפוסט הקודם – בכדי לבצע את הפרימיטיביים המבוזרים הללו.
 
 
תוך כדי כתיבת הפוסט – נוכחתי שאני מתמקד בקטגוריה מאוד ספציפית של מוצרים מבוזרים: מוצרי Big Data.
 
זה בסדר.
 
מוצרי Big Data לרוב מחשבים… הרבה נתונים. כ"כ הרבה נתונים שלא ניתן לאחסן במחשב אחד. לרוב גם לא בעשרה.
כדי לטפל בכמויות הנתונים (Volume) או במהירות שלהם (Velocity) – זקוקים למערכת מבוזרת. אותם מוצרים (או Frameworks) הם גם במובן אחד "מערכת מבוזרת", וגם "מוצר Big Data". בפוסט – נבחן בעיקר את הפן המבוזר שלהן.
 
בכל מקרה: כנראה שחלק מכובד מאוד מאנשי התוכנה שמתמודדים עם מערכות מבוזרות – עושים זאת דרך עבודה עם מוצרי Big Data, הפופולריים כ"כ היום.
 
 
בקיצור: זהו איננו פוסט על Big Data. יום אחד אולי אקדיש לנושא פוסט ראוי, ואולי גם לא 🙂
 
 

חישוב מבוזר

Scatter – Gather הוא השם המקובל לדפוס חישוב מבוזר, המבוסס על 2 רעיונות:

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

נ.ב. "זמן סביר" הוא יחסי. לא נדיר להיתקל בשאילתות שרצות על מערכות Hadoop במשך שבועות – עד לסיום החישוב.

Map Reduce – הוא היישום הידוע של פרדיגמת Scatter-Gather, שנחשב היום אמנם כבוגר – ומובן היטב.

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

  • Map – עיבוד נתונים נקודתי על המכונה הלוקאלית (חיפוש / פילטור / עיבוד נתונים וכו')
  • Reduce – ביצוע סיכום של החישוב וחיבור התשובות של המחשבים הבודדים לתשובה אחת גדולה.
החלוצים בתחום של Map Reduce מבוזר על פני מספר רב של מחשבים הם חברת גוגל, ופרויקט Apache Hadoop – שהושפע רבות ממאמרים שגוגל פרסמה על מימוש ה Mapreduce שלה. בעשור מאז הושק הפך פרויקט Hadoop לסטנדט בתעשייה, עם קהילה עשירה ורחבה. כאשר הרווחים מתמיכה ב Hadoop הגיעה למאות מיליוני דולרים בשנה – נכנסו לשם גם שחקני ה Enterprise הגדולים (כלומר: EMC, IBM, Oracle, וכו')
.
Google Map-reduce ו Hadoop מבוססים על מערכת קבצים מבוזרת (GFS ו HDFS בהתאמה), ותשתית לביצוע חישוב מבוזר על גבי ה nodes שמחזיקים את הנתונים הנ"ל (ב Hadoop נקראת YARN, בגוגל כנראה פשוט "Mapreduce").
 
בפועל יש ב Map reduce שלושה שלבים עיקריים: Reduce, Map ו Shuffle (העברת סיכומי הביניים בין ה Mappers ל Reducers – פעולה אותו מספק ה Framework).
 
דוגמה לבעיית מיון מבוזרת (הניחו שמדובר במיליארדי רשומות) הנפתרת בעזרת MapReduce. המפתחים כותבים את פונקציות ה Map וה Reduce, ה framework מספק להם את ה Shuffle (העברת הנתונים בין ה mappers ל reducers).

ניתן לכתוב פונקציות "Map" ו "Reduce" בקוד, אך סביר יותר להניח שכאשר אתם מגדירים חישובי Map-reduce רבים – תעדיפו להשתמש ברמת הפשטה גבוהה יותר כגון אלו שמספקים Pig (מן "שפת תכנות" מיוחדת ל Map-Reduce) או Hive (המבוסס על SQL).

כדרך אגב: Pig קיבל את שמו מכיוון שחזירים אוכלים גם צמחים וגם בשר – ו Pig יודע לטפל גם ב structured data וגם ב unstructured data.

השנים עברו, וכיום המודל של חלוקת החישוב המבוזר לשלבי Map ו Reduce  – נחשב מעט מוגבל.
 
כמו כן, ההסתמכות על מערכת קבצים כבסיס לחישוב היא טובה ל DataSets ענק שלא ניתן להכיל בזיכרון (יש clusters של Hadoop שמנתחים עשרות, ואולי מאות PetaBytes של נתונים), אבל בעשור האחרון הזיכרון הפך לזול (מכונה עם 1TB זכרון היא בהישג יד) – והיכולת לבצע את החישוב בזיכרון יכולה להאיץ את מהירות החישוב בסדר גודל או שניים, ואולי אף יותר (תלוי בחישוב). עובדה זו מקדמת את הפופולריות של הפתרונות לחישוב מבוזר מבוסס-הזיכרון (כמו Storm, או Spark), ומשאירה ל Hadoop את היתרון בעיקר עבור מאגרי הנתונים הבאמת-גדולים (נאמר: 100TB ויותר של נתונים, מספר שבוודאי ישתנה עם הזמן).
 
המוצרים העיקריים ב Eco-System של Hadoop
 

דוגמה נוספת לדפוס של Scatter – Gather הוא הדפוס בו עושים שימוש ב Spark. אני לא בטוח שזהו שם רשמי – אך אני מכיר את המינוח "Transform – Action" (כאשר "map" הוא פעולת transform אפשרית, ו "reduce" הוא action אפשרי).

Spark הוא הכוכב העולה של עולם ה Big Data. בניגוד ל Hadoop הוא מאפשר (כחלק מהמוצר) מספר רב יותר של פונקציות מאשר "map" ו "reduce".
במקום פעולת ה map ניתן להשתמש בסמנטיקות כגון map, filter, sample, union, join, partitionby ועוד…
במקום פעולת ה reduce ניתן להשתמש בסמנטיקות כגון reduce, collect, count, first, foreach, saveAsXxx ועוד.

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

המוצרים ב Eco-System ה מתהווה של Spark (חלקם מקבילים לכלים קיימים של Hadoop). מקור: databricks

ל Spark יש עוד שתי תכונות חשובות:

  • הוא מחזיק (סוג של Cache) את הנתונים בזיכרון – אליו הגישה מהירה בסדרי גודל מאשר בדיסק.
  • הוא מרכז את רוב שרשרת החישוב באותו node ומצמצם את הצורך להעביר נתונים בין nodes ב cluster (גם ל Hadoop יש כלים שעוזרים לצמצם העברות של נתונים בין nodes, אך Map-reduce פחות מוצלח בכך באופן טבעי)
פרטים נוספים:
  • ה "agent" של Spark רץ במקביל למערכת ה storage המבוזר, תהיה זו Hadoop או Cassandra (או סתם קבצים בפורמט נתמך) – ושואב ממנו את הנתונים הגולמיים לחישוב.
  • ה Agent (נקרא node – אני נמנע משם זה בכדי למנוע בלבול) מקבל Job (לצורך העניין: קטע קוד) לבצע חישוב מסוים על הנתונים. יש כבר ב Agent קוד של פונקציות שימושיות (filter, sort, join, וכו' – לפעולות "map" או count, reduce, collect – לפעולות "reduce").
  • ה Agent מחלק את המשימה ל stages ובונה גרף חישוב (על בסיס הידע כיצד מאורגנים הנתונים ב cluster) – ושולח tasks חישוביים ל Agents ב cluster.
ל Spark יש לא מעט נקודות אינטגרציה עם Hadoop – כך שהתחרות באמת היא לא בין Hadoop ל Spark (לפחות – עדיין), אלא יותר תחרות בין Spark ל YARN (מנוע ה Map-reduce).

 

Event Processing

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

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

Storm

  • מערכת Horizontal scalable ו fault-tolerant – כמובן!
  • מערכת המספקת תשובות ב low-latency (שלא לומר real-time – כי זה פשוט לא יהיה נכון), תוך כדי שימוש בכוח חישוב מבוזר.
  • At-lest-once Processing מה שאומר שהנחת היסוד במערכת המבוזרת היא שחלקים שלה כושלים מדי פעם, ולכן לעתים נעשה אותו חישוב יותר מפעם אחת (לרוב: פעמיים) – בכדי להמשיך לקבל תוצאות גם במקרה של כשל מקומי.

Storm נולדה מתוך פרויקט בטוויטר לחישוב trending analysis ב low-latency.

המבנה של Storm Computation Cluster הוא סוג של Pipes & Filters, כאשר יש:

  • Stream – רצף של tuples של נתונים.
  • Spout – נקודת אינטגרציה של המערכת, המזרימה נתונים ל streams.
  • Bolt – נקודת חישוב ("Filter") בגרף החישוב ("הטופולוגיה").
    על מערכת Storm מריצים טופולוגיות חישוב רבות במקביל – כל אחת מבצעת חישוב אחר.
  • Supervisor – הוא node של Storm, שכולל כמה Workers שיכולים להריץ את קוד ה bolts או ה spouts.
  • Stream Grouping – האסטרטגיה כיצד לחלק עבודה בין bolts שונים.
בעוד פעולות אצווה סורקות את הנתונים מקצה אחד לקצה שני – וחוזרות עם תשובה ("42"), ב Event Processing כל הזמן נכנסים נתונים, והערך אותו אנו מחשבים (אחד או יותר) – יתעדכן באופן תדיר ("43, לא רגע… עכשיו 47… ועכשיו 40…").
 
טופולוגיה לדוגמה של Storm. מקור: michael-noll.com
 

אם העבודה היא חישובית בלבד – כל אחד מה supervisors יוכל להריץ את כל הטופולוגיה (ואז לא צריך להעביר נתונים בין מחשבים במערכת – שזה עדיף). נוכל לבחור, למשל, ב Local Stream Grouping – שמורה ל Storm לשלוח את הנתונים ל Worker על אותה המכונה.

במידה ויש נתונים מסוימים שיש להשתמש בהם לצורך החישוב, והם נמצאים על supervisors מסוימים – יש להגדיר את Stream Grouping כך שיידע לשלוח את הנתונים ל supervisor הנכון. במקרה כזה אולי נרצה לבחור ב Fields Stream Grouping – המורה ל Storm לשלוח את הנתונים למחשב ע"פ שדה מסוים בנתונים (למשל: "ארץ" – כי כל supervisor מחזיק נתונים של ארץ אחרת).
 
הערכה גסה איזה כלי יכול להתאים לכם – בהינתן בזמני ההמתנה לתשובה שאתם מוכנים לסבול. * Tez הוא מנוע שכולל שיפורים ל Map-Reduce, שמרביתם מזכירים את הארכיטקטורה של Spark – מלבד הגמישות לתאר תהליכים כיותר מ-2 שלבים עיקריים.

 

Mahout

ארצה להזכיר בקצרה את Apache Mahout (שהוא חלק מ Hadoop), פירוש השם Mahout הוא "נהג הפיל" – ואכן Mahout רוכבת על הפיל (Hadoop).
זהו כלי שמספק סט של אלגוריתמים שימושיים הממומשים בצורה מבוזרת, חלקם על גבי Map-Reduce וחלקם על גבי Spark – ומאפשר לעבוד ברמת הפשטה גבוהה אפילו יותר.

סוגי הבעיות ש Mahout מכסה (כל אחת – בעזרת כמה אלגוריתמים שונים) הן:

  • סינון שיתופי – "אנשים שאהבו את x אהבו גם את y"
  • Classification – אפיון "מרחב" הנתונים מסוג מסוים – גם אלו שאין לנו. סוג של חיזוי.
  • רגרסיות – הערכת הקשרים בין משתנים שונים.
  • Clustering – חלוקה של פריטי מידע לקבוצות – ע"פ פרמטרים מסוימים.
  • ועוד
מימושים של האלגוריתמים  הנ"ל תוכלו למצוא בקלות למכונה יחידה, אבל אם יש לכם סט גדול של נתונים, או שאתם רוצים לחשב את האלגוריתמים בצורה תכופה – Mahout יחסוך לכם עבודה רבה!
 
 
 

.

Lambda Architecture

הכלים שהצגנו למעלה – מאפשרים לנו בחירה בין:

  • תשובה מדויקת – אטית (למשל: Map-reduce)
  • תשובה "לא מדויקת" – מהירה (למשל: Storm או Spark)
    בהנחה שאנו מקריבים דיוק עבור המהירות. למשל: משתמשים בחישוב בנתונים בני חמש דקות – ולא העדכניים ביותר, או חישוב על סמך דגימה חלקית – ולא על סמך כלל הנתונים.

בארגון בעל צרכי מידע – למה לבחור בין האופציות? האם אי אפשר לקבל את שניהם?

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

  • מייצרים במערכת התפעולית שטף של אירועים – כולם immutable (בתרשים למעלה: "new data stream").
  • את האירועים אוספים ומפצלים ל-2 ערוצים:
    • ערוץ של שמירה לטווח ארוך – נוסח HDFS או AWS S3, ואז ביצוע שאילתות ארוכות באצווה (Batch).
    • ערוץ של עיבוד מיידי והסקת תובנות – נוסח Storm, שבו ההודעות ייזרקו מיד לאחר שיעובדו (הן שמורות לנו מהערוץ הראשון) – הערוץ המהיר.
  • את התובנות קצרות הטווח של הערוץ המהיר, ואת התובנות ארוכות הטווח – של ערוץ האצווה מרכזים ל storage שלישי (Service Layer) – לרוב HBase, Impala או RedShift, ממנו נבצע שליפות חזרה למערכת התפעולית.

כמו ארכיטקטורה, "ארכיטקטורת למבדה" היא רק תבנית מקובלת – ונכון יותר יהיה להתאים אותה לצרכים הייחודיים שלכם, ולא להיצמד בכוח לאיזו הגדרה מקובלת..

Apache Kafka

לסיום אני רוצה להזכיר עוד מערכת מבוזרת שהופכת פופולרית – Distributed Message Queue בשם קפקא.

בדומה למערכות הקודמות, קפקא היא מערכת Horizontally scalable ו Fault-tolerant.
בנוסף יש לה מימד של durability – הודעות נשמרות לדיסק ומשוכפלות בין מחשבים שונים.

בקפקא נלקחו בה כמה החלטות תכנוניות בכדי לתמוך בשטף אדיר של אירועים. קפקא מטפלת יפה בקצבים גבוהים מאוד של נתונים, במחיר של latency מסוים בכתיבת ובשליפת הודעות. שמעתי על חבר'ה שחווים latency אף של ל 2-3 שניות.

כלל-אצבע ששמעתי אומר כך: "אם יש לך עד 100,000 הודעות בשנייה – השתמש ב RabbitMQ, אם יש יותר – כדאי לשקול מעבר לקפקא".

 

כמה הנחות בעולם של קפקא הן:

  • הודעות ב Queue הן immutable – בלתי ניתנות לשינוי. ניתן להוסיף הודעות ל queue – ולקרוא אותן, אך לא לעדכן או למחוק אותן. ההודעות יימחקו כחלק מתהליך של ה Queue (למשל: לאחר 24 שעות) – והצרכן צריך לרשום לעצמו ולעקוב אלו הודעות כבר נקראו – כדי לא לקרוא אותן שוב. הן פשוט יושבות שם – כמו על דיסק.
    קפקא מנהלת את ההודעות כקובץ רציף בדיסק – מה שמשפר מאוד את יעילות ה I/O שלה. כל הפעולות נעשות ב Bulks.
  • ה Queue נקרא "Topic" ולא מובטח בו סדר מדויק של ההודעות (פרטים בהמשך).
  • Broker הוא שרת (או node) במערכת. אם לברוקר אין מספיק דיסק בכדי לאכסן את תוכן ה Topic, או שאין לו מספיק I/O בכדי לכתוב לדיסק את ההודעות בקצב מהיר מספיק – יש לחלק (באחריות ה Admin האנושי) את ה topic על גבי כמה partitions. ההודעות שמתקבלות ישמרו על partitions שונים, וקריאת ההודעות – גם היא תעשה מה partitions השונים.
קפקא עצמה מאוד מרשימה מבחינת יציבות וביצועים – אך זה נעשה במחיר אי-החבאת מורכבויות של מימוש מהמשתמשים שלו:
 
ה producer צריך לדעת על מספר ה partitions שבשימוש, מכיוון שקפקא מסתמך על כך שהוא עושה בעצמו מן "Client Side Load Balancing" (דומה לרעיון של Client-Side Directory שהסברתי בפוסט הקודם) – ומפזר את ההודעות בין ה partitions השונים (או שכל producer עושה זאת בעצמו – או שהם עושים זאת – כקבוצה).
הפיזור כמובן לא צריך להיות אקראי אלא יכול להיות ע"פ שדה מסוים בהודעה, שדה כמו "ארץ" – שיש לו משמעות עסקית.
 
ה consumer לא יכול לקרוא את ההודעות ב topic ע"פ הסדר – כי הן מפוזרות בין ה partitions השונים (קריאה נעשית רק מ partition מסוים). אין שום מנגנון בקפקא שמסייע להבין באיזה partition נמצאת ההודעה הבאה. בפועל מה שעושים הוא שמציבים consumer לכל partition – והם מעבדים את ההודעות במקביל, מבלי להתייחס להודעות שב partitions האחרים.
כמו כן – מישהו צריך לעקוב מהי ההודעה האחרונה שנקראה מכל partition: קפקא לא עושה זאת. הדרך המקובלת לעשות זאת היא להיעזר ב Zookeeper לניהול ה state המבוזר הזה (שיהיה יציב, זמין, וכו').
ב AWS Kinesis (וריאציה של Kafka – של אמזון) – מנהלים את ה state המבוזר ב DynamoDB (בסיס נתונים K/V – גם הוא מבוזר).
 
כל partition ממספר את ההודעות של ה topic באופן בלתי-תלוי. מקור: kafka.apache.org
 
לא מתאים לכם? – אתם יכולים לסדר את המידע בחזרה בעזרת מערכת נוספת שתעשה את העבודה (למשל: Storm?).
 
לכל Broker יש רפליקה (אחת או יותר), במבנה של Active-Passive. ה Active משרת את ה consumer – ואם הוא כושל, אחד ה Passives נבחר להיות ה leader – והופך להיות "ה Active החדש".
 
ה producer הוא האחראי על מדיניות ה consistency ויכול לבחור להחשיב כתיבה ל queue כמוצלחת כאשר ה Leader קיבל אותה, או רק כאשר ה Leader סיים לעשות רפליקציה של הנתונים ל broker נוסף (אחד או יותר – תלוי ברמת הפרנויה שלכם). התוצאה כמובן היא trade-off בין latency ל durability – שניתן לשחק בו בצורה דינאמית.
 
אפשר להשוות את קפקא למשאית: היא מאוד לא נעימה לצורך יציאה לדייט עם בחורה, או בכדי לקנות משהו בסופר.
אבל אם אתם רוצים להעביר אלפי טונות של מוצר כלשהו – בוודאי שתרצו כמה משאיות שיעשו את העבודה. צי של רכבים קטנים – פשוט לא יעשה עבודה טובה באותה המידה…

 

סיכום

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

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

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

—-

לינקים רלוונטיים:
השוואה בין Spark ל YARN

מצגת טובה אודות Spark