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

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

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

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

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

  • מוצרים: אפליקציית לקוח של טורנט, 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 – הוא לא יכול להתבצע על חלק מהמחשבים במערכת ולהשיג את התוצאה.

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

סיכום

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

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

—-

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

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

—–

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

על Circuit Breakers ויציבות של מערכות מבוזרות

דמיינו 2 חנויות מכולת כמעט זהות: “המכולת של שמואל” ו”המכולת של הלל”.

בשתי חנויות המכולת יש מוצרים איכותיים שאתם אוהבים, מוזיקה טובה מתנגנת, השירות מצוין, הן פתוחות 24/7, והמחירים – הוגנים. אתם אוהבים את המכולות הללו, ואתם אוהבים לקנות בהן.

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

במכולת של הלל – המצב הפוך. הם מוכנים לכך שלא יהיו מוצרים מסוימים זמינים. הם קוראים למצב הזה partial service או degraded service (הם קצת גיקים עם המונחים שלהם). המכולת פתוחה 24/7 למעט מקרים נדירים בהם הם סוגרים אותה (כאשר הלל צריך לצאת לאיזה סידור ואין לו מחליף), אבל כאשר אני הולך לקנות משהו – ייתכן ואחזור ללא קוטג’ הביתה. לפעמים לא אכפת לי, אבל לפעמים זה מבאס. אם זה ממש חשוב לי אפילו אקפוץ למכולת אחרת להביא את הקוטג’. אבל בגלל שאני אוהב את החוויה במכולת – אני עדיין אחזור אליה בקנייה הבאה.
במקום לדאוג ל”אפס חוסרים במלאי”, החבר’ה של הלל עסוקים בכך שחוסר של מוצר מסוים במלאי – לא יגרום למכולת להיסגר מעצמה. זה מאוד מוזר אבל הם מספרים שאם לא יעשו שום דבר, מחסור בטונה פתאום יגרום למכולת להסגר מעצמה. גיקים – אמרנו כבר?

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

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

בצורה בלתי מובנת, האינטואיציה של רובנו היא ללכת בגישה הראשונה.
הסבר אחד הוא שגישה זו ישימה בעת שהמערכת עוד קטנה – וקשה לנו להתרגל לרעיון שכללי המשחק השתנו עם הזמן.
הסבר שני הוא שאנו “מהנדסים באופי” שחושבים מנקודת מבט של “המוצר שאו בנינו ויש לנו אחריות עליו” וקצת פחות מנקודת המבט של חווית השירות שמתקבלת בסוף. אם היינו צרכנים של המוצר – קרוב לוודאי שהיינו מעדיפים את חווית השימוש השנייה (בד”כ. ישנם כמובן גם מקרים בהם הראשונה עדיפה, למשל: בניתוח לב פתוח, לא היינו רוצים להתחיל ניתוח אם חסר משהו… 🙂 ).

Circuit Breakers

המנגנון הבסיסי המאפשר את מודל “המכולת של הלל” הוא ה Circuit Breaker.
לצורך הדיון אניח שמדובר במערכת בארכיטקטורת Micro-Services (בקיצור: MSA) והשירותים השונים הם הרזולוציה לבידוד בפני תקלות. ניתן לעשות אותו הדבר במערכת “מונוליטית”, אם כי המשמעת העצמית הנדרשת היא לעתים מעט גבוהה יותר.

ה Circuit Breaker הוא מן Proxy לשירות מרוחק – אשר רק דרכו עושים את הקריאות. השירות המרוחק יכול להיות:
א. זמין
ב. לא זמין
ג. זמין – אך כושל (יש errors)
ד. זמין אבל אטי

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

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

מקורות: “דפוס העיצוב” של ה Circuit Breaker הוצג לראשונה ב 2007 ע”י מייקל נייגארד, בספר המצויין (!!): !Release It.
ב 2014 מרטין פאולר כתב פוסט בבלוג על דפוס העיצוב (הוא לא חידש שם, רק הסביר יותר פשוט), ומאז זהו ה reference המקובל.

המימוש הפנימי של ה Circuit Breaker הוא לרוב כמכונת מצבים פשוטה:

מקור: orgsync/stoplight (מימוש ברובי של Circuit Breaker)
  • המצב ההתחלתי הוא “מעגל סגור” (ירוק).
  • אם יש מספר מסוים של תקלות (מוצג בתרשים כ “fail”) – עוברים למצב “אדום”.
    • בד”כ לא מדובר בתקלה יחידה אלא threshold של סדרת תקלות. למשל: רצף של 5 exceptions, כאשר ה threshold הוא של 5.
  • במצב “אדום” – כל ניסיון קריאה לשירות המרוחק ייענה בערך החזרה מסוים (או Exception) מצדו של ה Circuit Breaker שאומר “תסתדרו בלי!” (כלומר: בלי השירות).
  • לאחר זמן מה (נניח: 120 שניות) של מצב אדום, ה circuit breaker יעבור למצב צהוב – ניסיון להחזיר שירות.
    • הוא יאפשר למספר נתון של קריאות לעבור לשירות המרוחק כדי לבדוק את התגובה. למשל: 10 קריאות. לכל שאר הקריאות הוא עדיין יחזיר את התשובה “תסתדרו בלי!”.
    • אם ב 10 הקריאות הללו, הוא מזהה treshhold מסוים בעייתי, למשל: רצף של 3 exceptions מצד השירות המרוחק (לרוב ה threshold של המצב הצהוב הוא יותר מחמיר מזה של המצב הירוק) – הוא חוזר למצב אדום.
    • אחרת – הוא מחזיר את המערכת למצב ירוק.
כמו שאתם מבינים – יש המון וריאציות אפשריות של התנהגות של Circuit Breakers:
  • אפשר לשים thresholds שונים ומשונים. למשל, משהו שלא הזכרנו: שניסיונות חזרה למצב הצהוב יקרו בתדירות משתנה: יש גישה של המתנה של 2 דקות ואז כל 30 שניות (כאשר השירות המרוחק הוא חשוב) ויש גישה של להאריך את זמני הניסיון, למשל: פעם ראשונה 2 דקות, אח”כ 5 דקות, וכל פעם נוספת – 10 דקות (כאשר השירות המרוחק פחות חשוב ודווקא שגיאה ממנו היא לא נעימה).
  • כאשר circuit breaker מופעל – כנראה שתרצו שסוג של alert יעלה ל monitoring, הרי מדובר בהחלטה לתת שירות פחות טוב. מתי ואיך להעלות את ה alert – עניין לבחירה.
  • לעתים יש אפשרות של override ידני – אפשרות לקבע מצב ירוק/אדום/צהוב של ה circuit breaker בהתערבות ידנית.
  • אולי הכי חשוב: כיצד ה circuit breaker מאתר שגיאה של השירות המרוחק?
    • האם ע”י ניטור ה responses של ההודעות שחזרו מהשירות המרוחק (למשל: HTTP 5xx)?
    • האם ע”י ביצוע בדיקה יזומה (proactive) לשרת המרוחק (למשל: שליחת pinging או בדיקת health-check)?
    • אם מדובר על אטיות, ה circuit breaker יכול למדוד את מהירות החזרה של קריאות מהשרת המרוחק. אפשר להגיב לממוצע של קריאות אטיות, או לעתים להסתכל על אחוזון מסוים. למשל: אם 5% מהקריאות אטיות מ 10 שניות, אנו רוצים לנתק – מכיוון שזה אומר שירות גרוע למשתמש הקצה. האם מנתקים רק את הקריאות האטיות או את כולן?!
    • וכו’
תוכלו למצוא מספר מימושים שונים של Circuit Breakers, בכל שפת תכנות כמעט (לא ראיתי באסמבלי ;-)), אבל לא נדיר המקרה בהם תרצו לממש גם וריאציה משלכם – במידה ויש לכם מקרה חשוב שלא מטופל ע”י המימושים הזמינים.

לא נדיר גם מצב בו אתם משתמשים בכמה circuit breakers שונים בתוך המערכת. תלוי כמה גדולה ומורכבת היא.
ההתעסקות עם circuit breakers גם היא יכולה להיות משמעותית, וכדאי לשים לב ש:

  • אתם לא נסחפים לאזור ה over-optimization שאיננו משתלם מבחינת ההשקעה.
  • אתם יוצרים מערכת של circuit breaker שהיא מורכבת מדי לניטור ושליטה בזמן אירוע אמת ב Production (כאשר אתם לא יכולים לענות על שאלות כמו: “מדוע x התנתק”? או “אילו ניתוקים היו בזמן נתון”).

אלו דוגמאות מהעולם האמיתי של ל Partial Service ניתן לתת? הנה כמה שאני נתקלתי בהן:

  • לוגים, לוגים, לוגים! בעם הייתה לנו מערכת עם שירות ירוד כמעט יומיים (!!) עד שהבנו שהיא נופלת כל הזמן כי הדיסק מלא ופעולות כתיבה ללוג נכשלות. אם כתיבה ללוג נכשלת – עדיף לא לכתוב לוגים, מאשר לגרום ל IO exceptions שמשבשים תהליכים שלמים במערכת.
    • בווריאציה אחרת מערכת מרוחקת לדיווח של בעיות (סוג של alerts) הגיבה ב latency של 4 שניות, וגרמה לשיבושים רבים בשירות שדיווח לה על בעיות זניחות, יחסית.
  • שירות ש”מצייר” מסלול נסיעה על המפה של החשבונית (בעולם המוניות). אם הוא לא זמין / מגיב היטב – שלח חשבוניות בלי ציור של המסלול, מה הבעיה?
  • שירות שמבצע סליקה של תשלומים. עדיף לשמור את סכום העסקה ולנסות לחייב כמה דקות מאוחר יותר (תחת סיכון של חיובים, עד סכום מסוים, ללא כיסוי) – מאשר לדחות על הסף את כל העסקאות, אפילו בפרק זמן קצר יחסית (כמה דקות).
  • וכו’ וכו’

Throttling

עוד וריאציה דומה של Circuit Breaker היא מנגנון throttling (“להחזיק אצבע על הקשית כך שלא יהיה זרם חזק מדי”).

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

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

המנגנון הוא דיי פשוט (אתאר את המימוש הספציפי) – עוקבים אחר מספר הבקשות הפעילות לשרת המרוחק בעזרת distributed, non-blocking lock (קל יותר באנגלית) על בסיס רדיס: לפני קריאה לשרת המרוחק מנסים “לקבל” Lock – אם מצליחים – מבצעים את הקריאה. אם לא מצליחים (כי כבר יש 10 קריאות פתוחות) – מחזירים הודעת “תסתדרו בלי!” לפונקציה שביקשה את השירות, ונותנים לה לספק שירות חלקי.

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

TCP timeouts

עוד נקודה ששווה להזכיר היא ה timeouts שאנו קובעים לתקשורת.
Timeouts הם מנגנון הגנה ממעלה ראשונה: אם שרת מרוחק לא מגיב – לא נרצה לתקוע thread (או אפילו request) לאורך זמן. תסריט נפוץ הוא שירות אטי, שגורם לשירות שקורא לו גם להיות אטי (כי הוא עסוק בלהמתין), שגורם גם הוא לשירותים האחרים התלויים בו – להיות אטיים באותו האופן.

למרות ש TCP הוא “reliable protocol”, גם לו יש תקלות: בעיקר התנתקויות. כאשר קצב הקריאות בין שירותים הולך גדל – אנו נחווה תקלות אלו יותר ויותר.

התנהגות נפוצה היא לספק אותו ה timeout ליצירת ה connection וביצוע הקריאה עצמה (פעולת “קריאה”). אבל:

  • פעולת ה connection היא פעולה פשוטה ומהירה – היא אורכת, באופן טיפוסי, שבריר של שנייה (בניכוי network latency).
  • פעולת הקריאה לרוב גורמת לשירות השני לעבוד: לקרוא מידע מבסיס הנתונים, לקרוא לשירותים אחרים, לבצע עבודת CPU משמעותית וכו. זמן מקובל לקריאה שכזו הוא כ 100ms וגם לא נדיר להיתקל במצב של 1000ms ויותר (שוב: בניכוי network latency).

לכן, אם נגדיר timeouts באופן הבא:

  • עבור פעולת ה connection של ה TCP – נגדיר timeout בסך ה: tolerable latency
  • עבור פעולת ה read של ה TCP – נגדיר timeout בסך: tolerable latency + tolerable server time

נוכל לצמצם בצורה מורגשת זמני המתנה מיותרים.

כמו כן, אם יש לנו timeout על connection – כדאי לשקול בחיוב פעולת retry מיידית.
ניתן להראות ש timeout קצר ליצירת connection + ביצוע retry יחיד – היא מדיניות שברוב המקרים תהיה עדיפה בהרבה על יצירת connection עם timeout ארוך.

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

אחד הנתונים הידועים ב AWS הוא ש latency בין AZs יכול להיות עד 10ms. אם מסתכלים על הנתונים עצמם רואים ש 10ms הוא לא ממוצע, אלא אירוע נדיר יחסית: בבדיקות שערך Matthew Barlocker (חברת Lucid Software) – הוא ראה שבאחוזון ה 99.85% מקבלים latency בין AZ של 3ms בלבד:

הערה: אמזון מתעדפת נמוך (de-prioritize) קריאות ICMP (פרוטוקול השליטה של TCP/IP, הכולל גם את פקודת ה ping) – ולכן לא כדאי להסתמך על Ping להערכת ה latency ב AWS.

מסקנה אפשרית אחת היא שסביר לקבוע TCP Timeout של 3ms כאשר ליצירת connection באותו ה AWS region.
על פעולות HTTP GET ניתן לשקול (תלוי במקרה) מדיניות דומה של מתן timeouts קצרים יחסית (יש לקחת בחשבון את זמן השרת + network latency) – עם אפשרות ל retry.

“Be Resilient” vs. “Fail Fast”

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

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

מדיניות טובה אחרת אומרת: Fail Fast – אם המערכת מזהה שגיאה או סבירות גבוהה לשגיאה, עליה “לשבור את הכלים באותו הרגע” – וכך יהיה קל יותר לאתר את התקלה. אם התקלה נגררת ונגררת – ה”שבר” צפוי להיות בשלב מאוחר יותר במערכת, בשלב שיהיה קשה מאוד להבין ממנו מה מקור הבעיה האמיתי (root cause).

כיצד 2 גישות חכמות אלו משתלבות זו עם זו בעולם של מערכות מבוזרות, השאופות ליציבות גבוהה?

למרות הסתירה הבסיסית, ניתן לשלב את שתי הגישות:

  • אם מזהים כשל שנראה שניתן “לספוג” – אפשר לבצע Alert Fast, בשאיפה שה Alert מספיק ברור ומשמעותי כדי שיהיה ניתן לנטר אותו ולטפל בו בזמן סביר.
  • אם מזהים כשל שנראה שלא ניתן “לספוג” אותו – זהו באמת מצב שכדאי לעשות Fail Fast.
פעמים רבות, מה שמתחיל כ”כשל חלקי” שניתן לספוג אותו – מתגלגל בהמשך לכשל משמעותי יותר שלא ניתן עוד לספוג (“כמות הופכת לאיכות”  – של הכשל). אם אתם בוחרים בכיוון של Alerts – כדאי שאלו יהיו Alerts שתוכלו, ואכן תתייחסו אליהם – בזמן סביר.

סיכום

בעוד שמערכת יחידה (“מונוליטית”) מתעסקים באופטימיזציה של ה up-time (מדד ברור למדי) – במערכות מבוזרות, המציאות יכולה להיות מורכבת / גמישה ברבה.

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

היכולת של מערכת להימצא במצבים שונים של כשלים-חלקיים מוסיפה מורכבות למערכת – אך מאפשרת להגיע לזמינות (availability) גבוהה יותר כאשר מדובר במערכת מורכבת.
העצה של מרטין פאוולר ל MonolithFirst היא טובה בהיבט הזה: מערכת מבוזרת היא בהחלט מורכבת יותר – ומבחינת זמינות  משתלם לעבור למודל מבוזר רק כאשר כלל המערכת הגיע לסף מסוים של מורכבות.
זמינות גבוהה של מערכת מושגת לא רק על ידי תכנון נבון, אלא גם ע”י אימונים ושיפור תמידי.
Chaos Monkey, למשל, הוא כלי Open Source ש”יפיל” לכם בצורה יזומה אך אקראית שירותים (או שרתים) במערכת – כדי שתוכלו לבדוק את ההתמודדות שלכם, ולשפר את צורות התגובה שלכם לכישלון שכזה.
בשלב ראשון ניתן להפעיל אותו בסביבת בדיקות – בה נפילה היא למידה ללא נזק, ובהדרגה ניתן להתקדם לסביבות יותר ויותר מציאויתיות ומחייבות. אם אתם מסוגלים להפעיל אותו ב production, בשעות העומס שלכם, ולשרוד עם שירות סביר – אז אתם בליגה העולמית!
שיהיה בהצלחה!

AWS: להכיר את S3 מקרוב

בהמשך לפוסט \”הצצה ראשונית ל AWS\” התחלתי לכתוב פוסט על ה Big Data Stack של AWS, אך מהר מאוד נתקעתי: הבנתי שחסר רקע ב Hadoop, ורקע קצת יותר מקיף על שירותי הבסיס של AWS.

שירותי הבסיס של AWS? מלבד EC2 (שהוא מרכזי מאוד, אבל לא כ\”כ מפתיע), שירות חשוב מאוד הוא S3, ולו החלטתי להקדיש את הפוסט הבא.

S3 (קיצור של Simple Storage Service, להזכיר) הוא שירות האכסון הבסיסי של אמזון, והוא שימושי מאוד בתסריטים רבים. הממשק שלו דומה למערכת קבצים מבוזרת (אם כי הוא קצת יותר מורכב).

את הקבצים מארגנים ב Buckets – שהם סוג של Root Folders, עליו ניתן להגדיר הרשאות ועוד מספר תכונות.
לכל קובץ ניתן לגשת ישירות בעזרת HTTP, בצורה:

s3://bucket/folder/filename

כאשר קבצים יכולים להיות בגודל של עד 5TB.

למרות הממשק הדומה למערכת קבצים, כדאי לזכור שבגישה ל S3 יש latency של רשת + ה latency הפנימי של S3 (שיכול להיות עוד 100-200ms בממוצע). אל תנסו לרוץ בלולאה על קבצים ב S3 בזה אחר זה, כמו שאולי אתם עשויים לעשות עם מערכת קבצים מקומית. להזכיר: זמן גישה חציוני למערכת קבצים מקומית עשויה להיות משהו באזור ה 10ms…, ואין לה את המורכבות הנוספת של הרשת.

Bucket, הסמל של S3

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

  • רמת רגילה, המספקת durability של 99.999999999% (11 תשיעיות, וואהו!)
  • Reduced Durability (נקראת RRS, קיצור של Reduced Redundancy Storage) – המספקת durability של 99.99% בלבד. כלומר: סיכון של 0.01% אחוז, כל שנה, לאבד את המידע. המחיר שלו נמוך ב 15-20% מהתצורה הסטנדרטית. תצורה זו מתאימה למידע לא קריטי / שניתן לשחזר.

בכל מקרה ה availability של s3 הוא 99.99%, כלומר: לעתים לא תוכלו לגשת לקובץ (availability), למרות שהוא עדיין קיים (durability). תוכלו לגשת אליו זמן קצר מאוחר יותר.

מה אני יכול לעשות עם S3?

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

  • הנגשה של תוכן סטטי (קבצי HTML/CSS/תמונות וכו\’, לעתים קבצים JSON או XML עם נתונים) על גבי HTTP. ל S3 ניתן לגשרת ע\”י REST ו/או SOAP.
  • שיתוף של נתונים, בצורה מבוזרת, בין כמה שרתים. לעתים כאשר קצב הקריאה הוא גבוה מאוד.
  • שמירה של כמות גדולה של נתונים סטטיסטיים לא מעובדים / מעובדים קלות – עבור עיבוד עתידי (מה שנקרא \”Data Lake\”)

את הפעולות הבסיסיות ניתן לעשות דרך ה UI של אמזון:

  • יצירת Bucket, וקביעת הגדרות שונות שלו.
  • העלאת קבצים
  • ניהול קבצים
  • וכו\’

ניתן להשתמש ב awscli בכדי לבצע פעולות על s3 מתוך command line:

  • ליצור bucket (פקודת mb), להציג את רשימת הקבצים שנמצאים ב s3 (פקודת ls) או להעתיק קבצים בין s3 למחשב המקומי (פקודת cp), וכו\’…
  • פקודת sync – לסנכרן תיקיה מקומית מול bucket של s3. הפקודה תגרום רק לקבצים חדשים, בעלי גודל שונה, או תאריך עדכון חדש יותר מאשר ב s3 – להיות מועלים ל s3. הפרמטר delete– יגרום לפקודה לנקות מ s3 קבצים שנמחקו מהמחשב המקומי.
דרך שלישית ומקובלת היא להשתמש ב Programmatic APIs.

אם אתם עובדים על \”חלונות\”, יש כלי UI נחמד בשם S3 Browser, המאפשר לראות את ה Bucket ולבצע עליו פעולות בצורה נוחה (משהו כמו כלי FTP נוח).

ה UI של S3

על כל bucket יש מספר תכונות:

  • הרשאות: מי רשאי לגשת לקבצים ב bucket? ניתן לאפשר גישה ציבורית (ע\”י HTTP url). ניתן גם לקבוע הרשאות ברמת הקובץ הבודד.
  • ניהול גרסאות: כל שינוי לקובץ ב bucket ינוהל בגרסה (כולל מחיקה). ריבוי העותקים יגביר את העלויות, וניתן לקבוע מדיניות (\”lifecycle rules\”) מתי ניתן למחוק עותקים ישנים או להעביר אותם ל AWS Glacier (אכסון זול בהרבה, במחיר גישה אטית לקבצים – יכול לקחת גם כמה שעות בכדי לעשות checkout לקובץ).
  • האזנה לאירועים: ניתן להאזין להוספה / מחיקה / עדכון קבצים ב bucket, ולשלוח הודעה לאחד משלושה שירותים של AWS:
    • Simple Notification Service (בקיצור SNS) – שירות ה publish/subscribe של אמזון. מאפשר לכמה לקוחות להירשם ל topic, ושולח את ההודעות ב push (ל HTTP endpoint, דוא\”ל, או SMS).
    • Simple Queue Service (בקיצור SQS) – שירות queues. על הלקוח לשלוף ביוזמתו את ההודעה מה queue, ומרגע זה – ההודעה כבר לא קיימת יותר. לעתים קרובות מחברים את SNS שישלח הודעות למספר SQS queues – אחד לכל נמען.
    • AWS Lambda – בכדי להריץ פונקציה על בסיס השירות.
  • אירוח אתרים סטטיים (Web Hosting) – על בסיס קבצי html/css/javascript שמועלים ל s3, בשילוב עם Route 53 (שירות ה DNS של אמזון).
  • הצפנה (server side encryption): אמזון יכולה להצפין עבורנו את הנתונים הנשמרים ב s3, ע\”פ מפתחות שאנו מספקים לה. אם מישהו פרץ לתשתיות של אמזון (או קיבל צו חיפוש פדרלי בארה\”ב – למשל), הוא מוצא קבצים מוצפנים שאין בהם הרבה שימוש.
  • אינטגרציה ל Cloudfront – שירות ה CDN של אמזון, המאפשר להנגיש קבצים המאוחסנים ב S3 בעלויות נמוכות יותר, ו latency קצר יותר (על חשבון: עד כמה מעודכנים הקבצים שניגשים אליהם).
  • Multipart upload – המאפשר לחתוך קבצים גדולים לכמה parts ולטעון אותם במקבלים על גבי כמה HTTP connections.
  • Logging – שמירת לוג של הפעולות שנעשו על ה Bucket.

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

    היבטים של Landscape

    S3 הוא שירות ברמת ה Region, ובאופן אוטומטי תהיה רפליקציה של הנתונים בין ה Availability Zones השונים. הרפליקציה מתבצעת תוך כדי כתיבה, כך שאם קיבלתם OK על הפעולה – המידע שם ומסונכרן (בניגוד ל offline replication המתבצע מאוחר יותר).

    שם של Bucket צריך להיות ייחודי ברמת כל ה Regions של AWS (כלומר: Globally unique ב Account).
    שם האובייקט משפיע על האופן שבו S3 תעשה partition לנתונים, ולכן אם אתם זקוקים ל tps גבוה – כדאי לדאוג לשמות בעלות שונות גבוהה, ושאינם תלויים בדפוסים שלא מייצגים את דפוסי הגישה (למשל: לא להתחיל שם של אובייקט ב timestamp – כי יהיו קבצים רבים המתחילים באופן דומה). הנה פוסט בנושא, אם הנושא רלוונטי עבורכם.

    Policies

    על Bucket ניתן לקבוע policies, המוגדרים מכמה אלמנטים:

    • Resources – על אילו משאבים (קבצים) ב bucket אנו רוצים להחיל את ה policy.
    • Actions – אלו פעולות על הקבצים אנו רוצים להשפיע (upload, list, delete, וכו\’)
    • Conditions – באלו תנאים יחול ה Policy: שעות פעילות מסוימות, regions של AWS, מצב של הקובץ, וכו\’. בעצם ב conditions נמצא הכוח האמיתי של ה Policy.
    • Effect – משמעות: allow/deny. אם יש סתירה בין policy של deny ל policy של allow – ה deny policy הוא זה שיכריע.
    • Principal – החשבון ב AWS או IAM user עליו חל ה policy.
    Policy לדוגמה יכול להיות הכנסת ססמה לפני מחיקה של קבצים מה Bucket, בכדי לצמצם את הסיכון שמישהו מוחק נתונים קריטיים, בטעות. ה Durability הגבוה של S3 הופך את הגורם האנושי לחלק המסוכן בשמירת המידע.
    את ה policy מגדירים כקובץ json ע\”פ מבנה מסוים, ועושים לו copy-paste לתוך ה UI (ב properties של bucket / הרשאות / policy). אמזון (בצדק) לא יצרו את ה UI המורכב שהיה נדרש בכדי לאפשר להגדיר policies בתוך ה UI של ה bucket. ניתן להשתמש ב AWS Policy Generator בכדי לייצר Policies (אך לא לערוך או למחוק) ואז להדביק את התוצאה ב UI של ה bucket properties.

    Policy לדוגמה. מקור: אמזון

    סיכום

    הנה עברנו בחינה מעמיקה של S3. היא לא הייתה ארוכה, בעיקר בגלל ש S3 הוא באמת… די פשוט.
    S3 הוא מאבני הבניין היסודיות ביותר של AWS – וכדאי להכיר אותו היטב.

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

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

    FAQ של אמזון על S3
    המחירון של S3
    Data encryption on S3

    מיקרו-שירותים: API Facade

    בפוסט זה אני רוצה לדבר על דפוס עיצוב נפוץ למדי בעולם ה micro-services – ה API Gateway.
    בדפוס העיצוב הזה נתקלתי מכמה מקורות, כאשר כולם מפנים בסוף לבלוג של נטפליקס – משם נראה שדפוס העיצוב התפרסם.
    מקור: microservices.io

    לפני שנתחיל – אני רוצה להצביע על אלמנט מבלבל במינוח של תבנית העיצוב:

    Facade הוא הוא אובייקט המספק תמונה (View) פשוטה של רכיבים פנימיים מורכבים (״complex internals״ – מתוך ההגדרה של GoF) – למשל כמו ב Layered Architecture.
    Gateway הוא אובייקט שמספק הכמסה לגישה למערכות או משאבים חיצוניים – למשל כמו רכיב הרשת Proxy שנקרא לפעמים גם Gateway.
    Proxy הוא אובייקט שמספק אותו ממשק כמו אובייקט אחר, אך מספק ערך מוסף בגישה לאובייקט (למשל Laziness או Caching).

    יש משהו לא מדויק ומבלבל בשם ״API Gateway״. כפי שנראה, דפוס העיצוב הוא בעצם שילוב של שלושת הדפוסים הנ״ל.

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

    להזכיר: דפוס עיצוב, ככלל, הוא מקור השראה יותר משהוא “אמת מוחלטת” שכדאי להיצמד אליה בלהבלהבלה…..

    דפוס העיצוב

    API Facade החל בשל צורך של נטפליקס לארגון ה APIs שלהם: המערכת המונוליטית הפכה לעוד ועוד שירותים – עוד שירותים שעל ה clients היה להכיר. הצורך בהיכרות עם כל השירותים יצר dependency ב deployment שהפך את משימת ה deployment לקשה יותר. למשל: פיצול שירות לשני שירותים חדשים דרש עדכון של כל ה clients ו deploy של גרסה חדשה – גרסה שמודעת לכך ש API X עכשיו שייך לשירות B ולא לשירות A (מה אכפת ל Clients בכלל מהחלוקה הפנימית לשירותים?!).

    ה API Facade הוא רכיב שיושב בין ה Client לשירותים, ומסתיר את פרטיהם הלא מעניינים. הוא מציג ל Client סדרת API פשוטים, כאשר מאחורי כל אחד מהם יש flow = סדרת קריאות ל APIs של שירותים שונים.

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

    עוד בעיה שצצה היא עניין ה Latency:
    כאשר ה Client ביצע יותר ויותר קריאות (כי יש יותר ויותר שירותים) עלויות ה latency של הרשת גברו:

    מקור: הבלוג של נטפליקס

    Latency לאמזון יכול בקלות להגיע ל 100-200ms. במקום לשלם על latency של קריאה אחת – שילמו על latency של הרבה קריאות. חלק מהקריאות הן טוריות (4 בתרשים לעיל) – ואז ה Latency האפקטיבי הוא פי 4 מ latency של קריאה יחידה.

    בעזרת הצבת API Facade שמקבל קריאה יחידה, ואז מבצע סדרה של קריאות בתוך הרשת הפנימית (שהן זולות בהרבה: פחות מ 10ms באמזון, ופחות מ 1ms ב Data Center רגיל) – ניתן לקצר באמת את ה Latency של הרשת שעל ה Client “לשלם”:

    מקור: הבלוג של נטפליקס

    חשוב לממש את ה API Facade כך, שהוא לא יגרע מהמקביליות שהייתה קיימת קודם לכן ב Client – אחרת הוא יכול לגרום ליותר נזק מתועלת. לצורך כך ממשים את ה API Facade בעזרת שפה/כלים שקל לבצע בהם מקביליות וסנכרון שיהיו גם קלים לתחזוקה, וגם יעילים מבחינת ביצועים. נטפליקס בחרה למשימה ב Closure (תכנות פונקציונלי) או Groovy עם מודל ראקטיבי (RX observables). אחרים בחרו בשפת Go או סתם #C או ג’אווה.

    תפקיד זה, של ניהול קישוריות בצורה יעילה, היא תפקיד קלאסי של Gateway.
    וריאציה אחרת של תפקיד Gateway היא כאשר ה API Facade יודע לתרגם את פרוטוקול ה Client האחיד (נאמר HTTP) לפרוטוקולים שונים של השירותים השונים (נאמר SOAP, ODATA, ו RPC) – וכך לסייע בקישוריות.

    עניין אחרון שבו ה API Facade מסייע הוא עניין של העשרת התקשורת. למשל: אתם רוצים שכל ה clients יבצעו SSO (קיצור של Single Sign-On, דוגמת SAML 2.0) או Audit על הגישה לשירותים שלכם. מה יותר נוח מלעשות זאת במקום אחד מרכזי (מאשר בכל אחד מהשירותים, ואז להתמודד עם פערי-גרסאות)?

    כנקודת גישה מרכזית, ה API Facade יכול לרכז פעולות אלו ואחרות. זהו תפקיד קלאסי של Proxy.

    סכנה!

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

    עצה חוזרת ונשנה היא להשאיר את ה API Facade רזה ומינימלי ככל האפשר – כדי שלא יסתבך.
    מדוע?
    משום שה API Facade הוא ריכוז של תלויות:

    • הוא תלוי בכל ה Services אותם הוא מסתיר
    • הוא תלוי בכל ה Clients אותם הוא משרת

    את כל התלויות מהן ניסינו להיפטר – העמסנו עליו, מה שאומר שהוא הופך להיות ל “Single Point of Deployment”. הרבה שינויים במערכת (שינוי API של שירות, או שינוי API של Client) – יחייבו לעשות גם לו Deploy.
    אם עושים למישהו Deploy תכוף מאוד, כדאי מאוד שהוא יהיה פשוט, אמין, ולא “ייתפס באמצע פיתוח” שיעכב את ה deploy. מצד כזה בדיוק יכול להיות לרכיב שמטפל גם בתרגום פרוטוקולים (Gateway), וגם בסיפוק שירותי אבטחה (Proxy).

    לכן, ה Best Practice הוא לבצע extraction של כל לוגיקה אפשרית מה API Facade לשירותי-עזר שיספקו שירותים שכאלה (תרגום, SSO, וכו’), בעוד ה API Facade הוא רק נקודת החיבור של שירותי העזר האלו ל API של ה Client.
    התוצאה: ה API Facade נשאר “easily deployable” – הוא מתמחה רק בהחזקת תלויות רבות ובקריאה לרצפים (flows) של APIs של שירותים עסקיים, בעוד שירותי העזר שלו הם בעלי הלוגיקה, ולהם ניתן לבצע עדכון / deploy – רק ע”פ הצורך ובקצב שנוח לעשות כן.

    כך בערך זה נראה:

    איך תקשרו את זה לצוות שלכם? את העובדה שאתם רוצים שה API Facade יהיה רזה ומינימליסטי, ולא יכלול שום דבר אחר? שאתם לא רוצים “ליפול בפח” של יצירת רכיב מורכב שתלוי כמעט-בכולם, ובאופן זה הוא מערער את יציבות המערכת?

    נכון! ע”י כך שתקראו לו “API Facade”, ולא “API Gateway” או “API Proxy”.
    מכיוון ש “Facade”, בהגדרה, הוא “שלד ועצמות”, רזה ומינימליסטי, בעוד “Gateway” או “Proxy” – הם לא.

    הנה עוד המלצה לאופן שיצמצם את התלויות שה-API Facade נושא עליו:
    לבצע “partitioning” של ה API Facade לכך שיהיו כמה API Facades במערכת – אחד לכל Client.
    נטפליקס גילו שהקמת צוות ייעודי לתחזוקת ה API Facade יצרה צוואר-בקבוק וחוסר תקשורת מול צוותי ה client – הלקוח העיקרי של התוצר. הם העבירו את האחריות לקוד ה API Facade לצוות ה Client – כאשר יש API Facade לכל Client שקיים. התוצאות היו חיוביות – וגם חברות אחרות אימצו גישה זו.
    מאוחר יותר, נטפליקס איחדו את התשתית של כל ה API Facades לתשתית אחידה – עבור יעילות ואחידות גבוהות יותר. עדיין כל צוות אחראי להגדרת ה flows שלו (flow = קריאה נכנסת מה Client, שמתתרגמת לסדרת קריאות מול השירותים השונים).

    סיכום

    בתבנית ה API Gateway Facade נתקלתי כבר מספר פעמים. בקרוב, אנחנו ננסה גם ליישם אותה אצלנו ב GetTaxi.
    התבנית נראית כמו חלק טבעי במחזור החיים של ארכיטקטורה: ארכיטקטורת Micro-Services הופכת לפופולרית, ואיתה צצות כמה בעיות חדשות. מה עושים? מתארים Best Practices, או דפוסים – שמציעים פתרונות לבעיות הללו, ומתקשרים אותם כ”דפוסי עיצוב”.
    עם הזמן, הפתרונות המוצלחים שביניהם יהפכו לחלק מהגדרת הארכיטקטורה של MSA, ויפנו את מקומם ל Best Practices חדשים…

    API Facade הוא דפוס שעוזר להתמודד עם בעיה ספציפית שנוצרת בעקבות ריבוי השירותים של Micro-Services Architecture, והצורך של ה Client לתקשר עם כל “הסבך הזה” (יש כאלו שהחלו לצייר את השירותים כ”ענן שירותים”). API-Facade מקל על הבעיה, אך גם יוצר פיתוי מסוים ליצור “גוש מונוליטי של פונקציונליות, מלא תלויות” ממנו כדאי להיזהר…

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

    —-

    מקורות: