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

בעיות של מערכות מבו…זרות

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

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

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

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

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

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

A Collection of independent computers that appear to its users as one computer — Andrew Tannenbaum

כאשר:

  • המחשבים במערכת פועלים בו-זמנית (concurrently).
  • המחשבים במערכת כושלים באופן בלתי-תלוי אחד מהשני.
  • השעון של המחשבים במערכת לא מכוונים לאותה השעה.
התנאי השלישי היא המעניין: למה שלא יהיו מכוונים לאותה השעה? ובכן:
  • אנו, בני-אדם, מחשיבים שעונים כמכוונים לאותה השעה גם כאשר יש ביניהם הפרש של כמה שניות [א]. עבור מחשבים, הפרש של כמה מליוניות שניה – יכול לעשות את ההבדל.
  • יש להניח שהתקשורת בין המחשבים במערכת עוברת ברשת – רשת בה יש אקראיות. אפילו אם המחשבים קרובים זה לזה פיסית, הרשת תגרום לזה שהודעה שנשלחה בזמן t לשני מחשבים שונים תגיע למחשבי היעד בזמנים שונים ובסדר אקראי – אקראיות שניתן להקביל, ל "שעונים שלא מכוונים אותו הדבר".
כלומר: אם נניח מראש שהשעונים אינם מכוונים, הסיכוי שלנו להיכשל בהנחות שגויות לגבי התנהגות המערכת – יפחת, ולכן אולי כדאי לנו להניח שזה המצב הנתון.
מחשב אישי (PC) הוא לא מערכת מבוזרת בדיוק מהסיבה הזו.
חשבו על זה: המחשב האישי בנוי מכמה רכיבים דיי חכמים ("מחשבים"?), שפועלים במקביל – ויכולים להיכשל באופן בלתי תלוי: CPU, GPU (מעבד או "כרטיס" גראפי), כונן כשיח, כרטיס רשת, וכו'.

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

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

בעיות "אקדמיות" של מערכות מבוזרות

אלו לרוב בעיות שהן יותר low level, בליבה של המערכת המבוזרת – שאולי רבים מהמשתמשים של המערכות הללו יכולים לא להכיר.

Multicast – שליחת הודעה לכל המחשבים בקבוצה

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

את בעיית ה Multicast פותרים לרוב ב-2 דרכים עיקריות:

  • Messaging – כאשר יש מתווך (Message Bus, Message Broker וכו') שדואג לכך שההודעות יגיעו גם למחשב שכרגע לא זמין. המתווך צריך להיות Highly Available ובעל יכולת גישה לכל המחשבים במערכת – על מנת לספק רמת שירות גבוהה.
  • Gossip-based transmitting (נקרא גם Gossip Protocol) – משפחה של פרוטוקולים שמעבירים את ההודעה כחיקוי הדפוס של התפשטות מגיפות (אפידמיה): כל מחשב שולח הודעה לחבר אקראי, אחת לכמה זמן, לאורך טווח זמן שנקבע. סטטיסטית, בסבירות גבוהה מאוד – ניתן להגדיר התנהגות שבסופה כל המחשבים ברשת יקבלו לבסוף את ההודעה, על אף כשלים, שינוי בתוואי הרשת, או קושי להגיע למחשבים מסוימים. החיסרון של גישה זו היא שטווח הזמן של פעפוע ההודעות איננו מהיר, ויש "בזבוז" של משאבי הרשת בהעברת הודעות כפולות. לרוב משתמשים בדפוס זה להעברת הודעות קטנות.
    יישומים מקובלים הם עדכון קונפיגורציה או איתור ועדכון על כשלים במערכת.
Remote Procedure Call (בקיצור RPC)
הבעיה של הפעלת מתודה על מחשב מרוחק, בצורה אמינה וקלה – גם היא נחשבת לסוג של בעיה של מערכות מבוזרות, אבל מכיוון שהדומיין הזה מפותח למדי – לא אכנס להסברים.
Naming – היכולת של כל המחשבים במערכת לתת שם אחיד למשאב מסוים
דוגמה נפוצה אחת יכולה להיות הכרת כל המחשבים במערכת (למשל hostnames), בעוד מחשבים עולים ונופלים כל הזמן.
דוגמה נפוצה אחרת היא כאשר נתונים זזים לאורך הזמן (בשל רפליקציה, כשלים, ו partitioning) בין אמצעי אכסון שונים – ואנו רוצים להיות מסוגלים לאתר אותם.
יש גישות רבות לפתרון הבעיה, אציין כמה מהן בזריזות:
  • Naming Server מרכזי (או מבוזר עם רפליקציות לקריאה-בלבד, דמוי DNS -שהוא גם היררכי) – שמנהל את השמות במערכת וכולם פונים אליו.
  • Client Side Directory (קליינט = מחשב במערכת) – גישה בה משכפלים את ה Directory לכל המחשבים במערכת ע"י multi-cast (למשל עבור client-side load balancing – כאשר כל מחשב ברשת בוחר אקראית למי לפנות בכדי לבקש שירות. סוג של פתרון לבעיית ה Fault Tolerance).
  • Home-Based Approaches – כאשר הקונפיגורציה היא כבדה מדי מכדי לשכפל כל הזמן בין כל המחשבים במערכת, ניתן לשכפל (ב multi-cast) רק את רשימת שרתי הקונפיגורציה (עם אורנטצייה גאוגרפית, או של קרבה – ומכאן המונח "Home"). אם שרת קונפיגורציה אחד לא זמין (כשל בשרת או בתוואי הרשת) – ניתן לפנות לשרת קונפיגורציה חלופי.
  • Distributed Hash Table (בקיצור DHT) – שזה בעצם השם האקדמי למבנה הנתונים הבסיסי של שרת Naming משתכפל עצמית – לכמה עותקים, בצורה אמינה, scalable, וכו'. בסיסי-נתונים מבוזרים מסוג K/V כמו Cassandra או Riak – נחשבים למשתייכים לקטגוריה זו, ויכולים (ואכן משמשים) כפתרון לבעיית Naming במערכת מבוזרת.
    למיטב הבנתי, Cassandra מתבססת על מבנה מעט שונה שנקרא Consistent Hashing, שיעיל ביותר בפעולות ה Lookup של keys, על חשבון מחיר שינויים ברפליקציה שהם יקרים יותר. הנה מצגת שמצאתי שמסבירה את ההבדל בין מבני-נתונים.
סנכרון שעונים 
אמנם אמרתי בהקדמה שהנחת היסוד של מערכות מבוזרות היא שהשעונים אינם מסונכרנים – אך זה לא אומר שלא ניתן לשאוף למצב כזה, או לקירוב שלו. העניין הוא גם שכל המחשבים במערכת יגיעו להסכמה על שעון אחיד (בעיית הקונצנזוס – נדון בה מיד) וגם היכולת לקחת בחשבון את ה Latency של הרשת ו"לבטל אותו", כלומר: בקירוב מסוים.
לעתים קרובות, הידיעה שלמחשבים במערכת יש שעות אחיד בסטייה ידועה (נניח עד 100ns, בסבירות של 99.99%) – יכול להיות בסיס לפישוט תהליכים משמעותי. גישות להתמודדות בעיה זו:
  • הפתרון הבסיסי ביותר הוא פתרון כמו Network Time Protocol (בקיצור NTP) שמקובל על מערכות לינוקס / יוניקס – שקורא את השעה משרת זמן מרכזי, ותוך כדי הקריאה מבצע מדידות של ה latency לשרת המרכזי. באופן זה, הוא יכול להסתנכרן, בסטייה שניתנת לחישוב – לזמן של השרת המרכזי.
  • פרוטוקולים משוכללים יותר, כמו Reference broadcast synchronization (בקיצור RBS) אינם מניחים על קיומו של שרת זמן מרכזי, אלא מודדים latencies לשאר המחשבים במערכת – וכך מייצרים זמן משוערך יחסית למחשבים אחרים במערכת.
  • חשוב לציין שהשעון לא חייב להיות שעון של בני-אדם (שעה, דקה, שנייה, מיקרו שנייה), אלא יכול להיות שעון לוגי, שהוא בעצם counter עולה של אירועים. כל פעם שיש אירוע בעל משמעות – מעלים את ה counter באחד ומסנכרים את כלל המערכת על רצף האירועים (ולאו דווקא זמן ההתרחשות המדויק שלהם). מימוש מקובל: Lamport's Clock.
    הרעיון דומה לרעיון ה Captain's log stardate הדמיוני מסדרת המדע הבדיוני StarTrek: מכיוון שמסע במהירות האור (או קרוב לו) משפיע על מרחב הזמן, לא ניתן להסתמך על שעון עקבי עם שאר היקום, ולכן הקפטן מנהל יומן ע"פ תאריכים לוגיים. (סיבה נוספת למנגנון התאריכים בסדרה: לנתק את הצופה מרצף הזמן המוכר לו, סיבה קולנועית גרידא).
יצירת קונצנזוס
בעיית הקונצנזוס היא הבעיה לייצר תמונת עולם אחידה, לגבי פריט מידע כלשהו – עבור כל המחשבים במערכת.
ברמה הבסיסית יש את בעיית ה Leader Election: כיצד בוחרים מחשב יחיד בקבוצה (נקרא "מנהיג" – אבל עושה את העבודה השחורה) לבצע פעולה כלשהי (למשל: לשלוח מייל למשתמש, על אירוע שכל המחשבים במערכת יודעים עליו).
אנו רוצים שרק מחשב יחיד יבצע את הפעולה בכדי למנוע כפילויות.
כיצד בוחרים מנהיג?
  • לנסות לדמות בחירות אנושיות – זה לא כיוון פעולה מוצלח. נדלג. 🙂
  • אלגוריתם פשוט (The Bully Algorithm) מציע לכל מחשב במערכת לשלוח ב broadcast מספר אקראי שהגדיר, ובעל המספר הגבוה ביותר – הוא המנהיג.
  • ישנם מספר אלגוריתמים מורכבים יותר (מבוססי Ring או DHT) – שהם אמינים יותר, ומתאימים יותר למערכות עם מחשבים רבים.
  • בכל מקרה, אחד העקרונות המקובלים הוא לבחור מנהיג קבוע (כלומר: עד שהוא מת) – בכדי לחסוך את הפעלת האלגוריתם שוב ושוב. זה יכול להיות "מנהיג אזורי" או "מנהיג גלובאלי".
סוג אחר יש יצירת קנוזנזוס נעשית ע"י טרנזקציה מבוזרת. בד"כ מבצעים טרנזקציה מבוזרת ע"י רעיון / אלגוריתם שנקרא two-phase commit (או בקיצור 2PC). האלגוריתם עובד כך:

  1. ה coordinator הוא מי שיוזם את הפעולה. הוא שולח vote request לכל המחשבים המיועדים להשתתף בטרנזקציה המבוזרת.
  2. כל מחשב שמקבל את ה vote request עונה vote-commit (אם הוא מסוגל לבצע את הפעולה) או vote-abort (אם הוא לא מסוגל לבצע אותה).
  3. ה coordinator אוסף את כל הקולות. אם יש לפחות מישהו אחד שענה vote-abort, או פשוט לא ענה – הוא מפרסם global-abort וכל המחשבים מבצעים rollback.
  4. אם כולם ענו ב vote-commit – ה coordinator שולח הודעת global-commit – וכל המחשבים מבצעים commit מקומי על הטרנזצקיה.
יש גם גרסה מתוחכמת יותר של הפרוטוקול, בשם three-phase-commit (בקיצור 3PC) – המתמודדת עם מצב ביניים בו ה coordinator כשל. מעולם לא שמעתי מערכת שממשת את 3PC, וכנראה ש 2PC טוב לרוב הגדול של השימושים.
עוד אלגוריתם מקובל להשגת קונצנזוס הוא paxos שדורש שרוב (quorum) של המחשבים במערכת יסכים על עובדה – ואז הם מעדכנים את השאר המחשבים על ההחלטה.סוג אחרון של קונזנזוס, או תאום הוא Distributed Mutual Exclusion (שנקרא בתעשייה פשוט Distributed Lock) בו כמה מחשבים מתאימים ביניהם מי ייגש למשאב מסוים בלעדית, ייתכן ובמצב שלמשאב הזה יש כמה עותקים במערכת (בשל רפליקציה).

Distributed Fault Tolerance
הכלל השחוק כמעט בעניין זה הוא "Avoid a single point of failure" – על מנת שמערכת מבוצרת תהיה אמינה, יש לדאוג שאין שום רכיב יחיד במערכת שכישלון שלו – מכשיל את כלל המערכת.
תחום זה כולל נושאים של:
  • גילוי כשלים במערכת. אם מחשב א' לא מצליח ליצור קשר עם מחשב ב' – האם זו בעיה במחשב א', במחשב ב'? או בשילוב שלהם (גרסאות לא מתואמות, בעיות רשת)?
    • הבסיס לפתרונות הוא לרוב בעזרת coordinator (או קבוצה של coordinators) שדוגמים את כלל המחשבים במערכת בעזרת הודעות heartbeat (או alive) – בהן כל מחשב מדווח על מצבו, וע"פ כללים מסוימים מגיעים להחלטה.
    • גישות מתוחכמות יותר מתבססות על מידע שמגיע ממחשבים על חברים שלהם במערכת – ואז לקיחת החלטה מבוזרת (למשל: הצבעה בנוסח paxos האם מחשב מסוים הוא תקין או לא).
  • תקשורת P2P שהיא כבר פתרון לקשיים בין מחשבים במערכת לתקשר זה עם זה. כאשר מתקינים מערכת ברשת ארגונית – ייתכנו מצבים (ע"פ הגדרה) בהם אין לכל המחשבים ברשת תקשורת ישירה זה עם זה. מצב בעייתי אחר שאיתו P2P מתמודד הוא כשלים ברשת האינטרנט (ויעילות בהעברת כמויות גדולות של מידע, למשל: סרטים).
  • נושא נוסף הוא אלגוריתמים מתייצבים עצמית, שזה הרעיון של כתיבת אלגוריתמים שבהינתן מצב לא תקין רנדומלי באחד המחשבים – האלגוריתם, תוך כדי פעולה, יפצה על הכשל ויתקן אותו עם הזמן. פרוטוקולי תקשורת, ופרוטוקולים של רפליקציית נתונים – כוללים אלמנטים של התייצבות-עצמית.
כיום, ניתן למצוא כמה מערכות שמספקות את ה primitives הנ"ל. מערכות כמו:
  • Zookeeper (של Hadoop)
  • Consul (מבית היוצר של Vagrant)
  • Etcd
  • Eureka (של נטפליקס)
מספקות בבסיסן מערכת Naming מבוזרת (הם קוראים לזה לרוב Service Discovery) וקונפיגורציה מבוזרת (משהו בין Naming מבוזר לניהול קונצנזוס), אבל חלקן כוללות גם פרימיטיביים של Leader Election, Voting, 2 Phase-Commit וכו'.
אם אתם מזהים במערכת שלכם את הצורך באחד מהפרימיטיביים הללו – שווה אפשר לשקול לקחת מערכת מוכנה.
האמת: פעם בחיים פיתחנו Leader Election, ופעם 2PC – ודווקא היה בסדר (בהקשר לפוסט "התשתית הטובה ביותר").

בעיות "מעשיות" של מערכות מבוזרות

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

רק בשלבים מאוחרים יותר של ההתמודדות עם המערכת, אנו נדרשים ל fine-tuning שחושף אותנו להתמודדות עם הבעיות "האקדמיות" שהצגתי.

הבעיות המעשיות עליהן אני מדבר הן:

בעיות Scale

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

  1. שכפול "כוח-עבודה זול" – מעבר ל cluster של מחשבים שיבצע את העבודה (דאא). מה שנקרא "ציר-x" של ה Scale Cube.
  2. העברת עבודה ל Clients (למשל קוד javaScript). מכיוון שעל כל שרת יש עשרות עד אלפי מחשבי Client – יש הגיון רב לנצל את כח החישוב שלהם או האכסון שלהם.
  3. יצירת עותקים נוספים לקריאה-בלבד (Read Replicas) כפי שנהוג ב MySQL, למשל.
    1. וריאציה אחרת של רעיון זה הוא שימוש ב caches.
  4. פירוק המערכת למספר תתי-מערכות שכל אחת מבצעת תת-פונקציה. מה שנקרא "ציר ה-y" של ה Scale cube. חלוקת מערכת "מונוליטית" למיקרו-שירותים היא סוג של פירוק מערכת לתתי-מערכות.
  5. ביצוע Partioning (של מידע או עבודת חישוב). למשל: מחשבים בקבוצה א' יעבדו את הלקוחות ששמם מתחיל באותיות א-י, והמחשבים בקבוצה ב' יעבדו את הלקוחות ששמת מתחיל באות כ-ת. חלוקה זו מסומנת כ "ציר-z" ב Scale Cube, ובד"כ מגיעים אליה לאחר ששאר המאמצים מוצו.
    שימוש ב Partitioning מקל על ה Scalability, אבל יוצר בעיות חדשות של Consistency וסנכרון.
ה Scale Cube הוא מודל תאורטי, אך שימושי – המאפיין את הדרכים להגדיל את אופן פעולתה של מערכת (בעיקר צד-שרת).
בד"כ מתחילים לגדול בציר x, לאחר מכן ציר y, ורק לבסוף בציר z (המפרך יותר).

כאשר עוסקים ב Scale גדול המעבר ל Partitioning הוא כנראה בלתי-נמנע, מה שמציב כמה בעיות אחרות.
בעיה אחת לדוגמה היא הבעיה הידועה כ CAP theorem. הטענה שבהינתן Partitioning, לא ניתן לקבל גם Availability (זמינות) ו Consistency (עקביות הנתונים) באותו הזמן – ויש לבחור ביניהם.

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

הערה1: בסיס נתונים רלציוני נחשב כ "CA" כי הבחירה שלו היא ב Consistency ו Availability – אך ללא יכולת ל Partitioning (ולכן אומרים ש RDBMs הוא "לא Sacalable").
הערה2: פרויקט Cassandra הצליח לכאורה "לרמות" את ה CAP Theorem. כיצד? הנה הסבר מהיר: כל פריט מידע נשמר (נניח) כ 5 פעמים במערכת. בכל פעולת קריאה אנו מציינים כמה עותקים נדרשים לצורך הקריאה. אם נבחר 1 – נקבל AP, אם נבחר 5 – נקבל CP – ויש לנו את הטווח באמצע. כלומר: לא באמת רימינו את ה CAP Theorem, אבל אפשרנו לכל פעולת קריאה לבצע את ה trafeoff עבור עצמה.

עוד הקבלה מעניינת של ה CAP Theorem נמצאת בפעולת multi-casting השונות:

פרוטוקול Gossip הוא סוג של AP, פרוטוקול Paxos הוא סוג של CP, ופרוטוקול 2PC הוא סוג של CA – הוא לא יכול להתבצע על חלק מהמחשבים במערכת ולהשיג את התוצאה.

עוד עניינים מעשיים, שכתבתי עליהם בפוסטים קודמים:

סיכום

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

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

—-

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

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

—–

[א] אני מכיר דווקא בחור אחד שלא. אצלו "מכוונים לאותה השעה" הוא הפרש של עד חצי שנייה – ואל תשאלו איך הוא מכוון אותם….

משרת ארכיטקט ב Gett

היי,

כפי שרבים מכם בוודאי שמתם לב, עברתי לפני מספר חודשים (הממ… בעצם חצי שנה – הזמן עובר מהר!) לחברת Gett (לשעבר GetTaxi).

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

בקיצור: אני מגייס עוד ארכיטקט לצוות.

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

חברת Gett היא חברה בעלת הכנסות גבוהות, רווחית, ושנמצאת בצמיחה מהירה מאוד.

שנה שעברה (על פי פרסומים זרים) החברה הכניסה כ 200 מיליון דולר, והשנה הכוונה היא להכניס 500 מיליון דולר – פי 2 וחצי.

צוות השרת, ביום שהגעתי, כלל כ 6 מפתחים – והיום יש בו כבר 12 מפתחים ועוד 3 קבוצות outsource המגבות את חוסר היכולת שלנו לגייס לצוות בקצב בו היינו רוצים. וכן – אנחנו מגייסים:

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

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

התנועה במערכת הולכת וגדלה ובעיות Scale ו Stability צצות באופן תדיר – בעיות שיש לפתור, לעומק.

קבוצת הפיתוח של Gett (חלק גדול ממנה) במשחק באולינג

הארכיטקט שאני מחפש ידרש:

  • לתמוך וללוות את צוותי הפיתוח בעת פיתוח פיצ\'רים מורכבים / מרכזיים לחברה.
  • לעזור לבנות ב R&D תמונת עולם משותפת, אודות הארכיטקטורה של המוצר – ולאן היא הולכת.
  • מדי פעם, לעשות Designs מפורטים לפיצ\'רים כאלו ואחרים – שדורשים זאת.
  • לעזור לשפר את קבוצת הפיתוח – לתרום לרמת הידע והמקצועיות של הצוותים.
  • לתרום למאמץ הכללי, ליצור תשתית Highly Scalable, Highly Available ו Highly Maintainable (מלים גדולות – אבל זה המצב) המתהווה בתקופה זו – תשתית שמריצה את הביזנס של חברת Gett. זה אומר גם מעורבות ב DevOps, באיכות המוצר, ובתהליכי הפיתוח בכלל.

העולם הטכנולוגי שלנו מבוסס בעיקר על AWS, רובי on ריילס, שפת Go, פיתוח אפליקציות נייטב לאנדואיד ואייפון, PostgreSQL,  רדיס, וקצת Hadoop.
אנו באמצע תהליך של שבירת אפליקציה \"מונוליטית\" (סוג של MVC) – למיקרו-שירותים. יש לנו כבר כ 14 מיקרו-שירותים, ונוצרים אחד או שניים חדשים כל חודש, מאז שהגעתי.

אני מחפש מישהו:

  • עם לפחות 5 שנות ניסיון בפיתוח משמעותי – עדיף שכבר ממלא תפקיד של ארכיטקט כמה שנים.
  • בעל עומק מקצועי: מישהו שנכנס לעומק העניין, ומבין את הבעיות הטכנולוגיות (או הארגוניות) עד השורש.
  • מישהו בעל רקע טוב בצד השרת (שם רוב האתגרים שלנו, כרגע): מבין בסיסי נתונים, ותכנון של מערכות.
    • ייתרון משמעותי הוא הכרות מעמיקה עם ה Stack הטכנולוגי של ריילס.
    • ייתרון נוסף יכול לנבוע מניסיון עבודה במערכות מבוזרות (דברים כמו leader election, sharding או circuit breakers), במובייל, במערכות Big Data (אנחנו עוד רק בהתחלה), ופיתוח צד לקוח וובי.
  • מישהו שמתאים ונהנה לעבוד בסביבה אינטנסיבית, בה לוקחים החלטות מהר, ולעתים בוחרים בפתרונות פחות מקיפים – בכדי להתקדם מהר יותר / לללמוד מהר יותר.
  • מישהו שיכול לזקק בעיה טכנולוגית מורכבת להסבר פשוט – ולהנחיל אותו בקרב הסובבים אותו.
  • מישהו בעל יחסי-אנוש טובים, צנוע (לפחות, במידה), ושיודע לשמוח שהדבר הנכון נעשה – גם אם לא הוא יזם אותו. זוהי תרבות ארגונית לא שגרתית שקיימת ב Gett – ומאוד מנסים לשמר אותה.
הכוונה היא לא לחפש רק מישהו ותיק ומנוסה שיודע לעשות הכל (זה יהיה נחמד – אבל לא חובה), אלא גם מישהו באמצע הדרך שיכול ללמוד ולהתפתח בתוך החברה. הכוונה היא להשקיע במועמד (כנסים, יש לנו מסלול \"הכשרת ארכיטקטים\" פנימי, וכו\'). סה\"כ נראה לי שהסביבה ב Gett היא סביבה לימודית מצויינת בלי קשר – בגלל כל ההתרחשויות בחברה.

אם אתם מעוניינים באחת המשרות, ובמיוחד במשרת הארכיטקט – פנו אלי במייל: liorb[at]gett.com. כנ\"ל לגבי חברים / מכרים.
כשאתם כותבים אלי אתם יכולים להיות קצת פחות רשמיים – ולספק קורות חיים \"freestyle\". אפשר גם לשלוח מייל רק בכדי להתייעץ. עבורכם או עבור חברים.

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

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

ליאור