Bulkheads – דפוס עיצוב של חוסן (Resiliency Design Pattern)

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

ירידה לעומקו של עניין

רעיון ה bulkhead[א] הוא רעיון עקרוני ליציבות של מערכות, שאותו ניתן לראות בשימוש גם בעולם התוכנה.

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

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

הנה שתי דוגמאות מוכרות ליישום של bulkhead שאנו מכירים מהיום-יום:

    • availability zones ב AWS (או המקבילה בעננים אחרים) – כשל של AZ יחיד יפגע בשירות (בטווח הקצר) – אך יאפשר לנו להמשיך את השירות כרגיל ב AZs האחרים.
      • לצורך כך מושקעים ב Amazon מאמצים רבים על מנת לוודא ש AZ אינם תלויים זה בזה, ושכשל באחד ה AZ (הצפה, נפילת מתח, בעיית תוכנה, וכו’) – לא יגרור כשל של ה AZ האחרים.
      • כמובן שבתכנון מערכת המשתמשת ב AWS עלינו ליצור יתירות של שירותים חיוניים (למשל: NAT gateway או בסיס-נתונים) על מנת שנוכל להמשיך ולרוץ בזמן ש AZ אחד כשל.
    • תהליכים במערכת ההפעלה – מערכת ההפעלה יוצרת הפרדה בין תהליכים (processes) שונים כך שכשל בתהליך אחד לא ישפיע על תהליכים אחרים: תהליך אחד קורס – והשאר יכולים להמשיך לרוץ ללא הפרעה.
    • למען הדיוק הטכני שווה לציין שההפרדה הזו אינה bullet proof כאשר מדובר בגישה למשאבים משותפים.
      למשל: תהליך שגוזל 100% CPU עלול להיות מתוזמן (לחלופין) על כל ה cores של המכונה ולשתק בפועל את כולה. עלינו להצמיד את התהליך (בעזרת CPU binding / affinity) ל core מסוים – בכדי לקבל הגנה טובה בפני תסריט ה “CPU 100%”. עניין דומה קיים לגבי זיכרון, גישה ל I/O, או כל משאב משותף אחר.

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

יישום בסיסי של bulkheads: להפריד את השרתים שלנו לשני clusters שונים (ומבודדים זה-מזה) ולנתב בקשות שונות ל cluster שונה. החלוקה יכולה להיות עבור microservice בודד, קבוצה של microservices, או אולי אפילו כל המערכת.

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

החכמה ביישום bulkhead מוצלח היא חלוקה סלקטיבית ע”פ שני קריטריונים:

  • מאפייני כשל (failure conditions) – כך שתעבורה מסוג I עלולה לכשול בעוד תעבורה מסוג II עשויה לעבוד כרגיל.
  • יתרון עסקי (financial benefit) – כאשר יש חשיבות עסקית מאחורי סוגי התעבורה השונים שעשויה להצדיק מצב בו תעבורה סוג I שורדת בעוד תעבורה סוג II כושלת.
Bulkhead מוצלח עשוי להיות על בסיס שני הקריטריונים, או רק אחד מהם.
הנה כמה דוגמאות ליישום של Bulkhead ברמה האפליקטיבית:
 
הדוגמה הקלאסית היא כנראה הפרדה בין לקוחות משלמים ללקוחות לא-משלמים. 
נניח: אתר שנותן שירות מוגבל בחינם, אך שירות משופר בתשלום (freemium).
שימו לב שהחלוקה היא עסקית.
וריאציה מקובלת: שני clusters:

  • Cluster A – ללקוחות משלמים
  • Cluster B – ללקוחות שאינם משלמים.
אם יש בעיה בפיצ’ר של לקוחות לא-משלמים שגורם לבעיה – לקוחות משלמים יכולים (ובצדק!) להמשיך ליהנות משירות תקין.
אפשר לשים יותר חומרה ומשאבים, קונפיגרציות יותר אמינות (גם אם עולות יותר) – ב cluster של הלקוחות המשלמים.החולשה של המודל היא במאפייני הכשל: דווקא הלקוחות המשלמים מקבלים כנראה יותר יכולות, ולכן יש סבירות גבוהה יותר שדווקא הטראפיק שלהם ייתקל בבאג כלשהו – שלא יקרה ללקוחות ה”חינמיים”.
קצת פדיחה אם Cluster A נפל – בעוד cluster B עובד כרגיל…

תת וריאציה היא ש Cluster B יקבל תעבורה של שני סוגי הלקוחות: משלמים ולא-משלמים.
במקרה של תקלה – אפשר לדחות לקוחות לא-משלמים כליל מהמערכת. אם יש משהו שיציל את התעבורה של לקוחות משלמים (נניח: עוד חומרה) – אדרבא!
אם יש כשל שנובע מ”פיצ’ר חינמי” (נניח: פרסומות) – יש הגיון עסקי רב לבודד את הכשל מלקוחות משלמים.
הוריאציה הזו הגיונית ככל ש Cluster B גדול מ Cluster A (נניח: פי כמה מונים).

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

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

בדוגמה הזו יש ל bulkheads פוטנציאל גדול יותר להשיג שיפור ממשי מהדוגמה הקודמת.

דוגמה: הפרדה לפי שווקים

למשל:

  • Cluster ללקוחות ארה”ב
  • Cluster ללקוחות מערב אירופה
  • Cluster ללקוחות מזרח-אירופה
  • Cluster ללקוחות אנגליה
בהנחה שעבור כל מדינה יש חלקי קוד ייחודים המתאימים לרגולציה ו/או settings מעט שונים שהם מקובלים יותר (בגלל הבדלים בין השירות במדינות) – העלולים לגרום לתנאי כשל שונים.
ייתכן ויש בעצם 20 מדינות בהן עובדים, כאשר לכל מדינה יש תצורת עבודה מעט שונה. אבל – מאוד יקר לנהל 20 clusters, וגם אחוז המשאבים המבוזבז (כי לא משתפים אותם) – יגדל ויתעצם.
ניתוח של תנאי הכשל (אלו מדינות משתמשות בפיצ’רים שונים –> חשיפה לתנאי כשל פוטנציאלים שונים) והמשמעות העסקית מובילה אותנו לחלוקה ל-4 clusters.
במידה וכל השווקים (לאחר ה clustering) הם בעלי חשיבות עסקית דומה, הפוטנציאל של bulkheads המתואר זה תלוי בעצם בתנאי-כשל שונים משמעותית בין ה clusters. ככל שתנאי הכשל שונים בין ה clusters – כך ההצדקה להצבת bulkheads הולכת ועולה.להזכיר: כאשר אותו מצב כשל מתרחש בכל התסריטים – כל ה clusters ייפגעו, וההפרדה לא תעזור.

דוגמה אחרונה: מנגנון חדש מול מנגנון ישן (“canary release”)
כאשר יש שכתוב של חלקים משמעותיים של המערכת, ובמיוחד כאשר המערכות הללו תלויות גם בשירותי צד-שלישי חדשים (= תנאי כשל נוספים) – ייתכן ויש הצדקה להפריד את התעבורה שעוברת במנגנון החדש והישן לזמן מסוים.
היום, בעידן הענן, לא קשה לעשות הפרדה כזו – אפילו אם היא תחיה, נאמר, לחודשים בודדים.
ה bulkheads יאפשרו שכשל מתגלגל במנגנון החדש, לא יפגע במאסה של הביזנס — שפועל עדיין על המנגנון הישן.

אמנם כל הדוגמאות שנתתי הן ברמת ה cluster האפליקטיבי, אבל הרעיון של Bulkhead הוא כללי ויכול להיות מיושם ברמות שונות. למשל: ברמת ה thread pool או רמת הסכמה בבסיס הנתונים.

אזהרת Patterns!!! (גנרית)

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

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

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

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

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

[א] בעולם הספנות bulkheads נקראים גם partitions. המונח “partitions” בעולם התוכנה הוא מאוד מוכר ומתייחס בעולם לרעיון מעט אחר, ולכן בהקשר לתוכנה משתמשים רק במונח bulkheads על מנת לתאר … bulkheads.

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

כשאתה אומר "Event-Driven Architecture", למה אתה מתכוון?

אחד הכנסים האהובים עלי, שאני נוהג לעקוב אחרי התוכן שמוצג בו הוא כנס ;goto

ה Keynote ב Goto Chicago שהתקיים לפני מספר ימים היה של מרטין פאוולר, בו הוא ביצע סיווג של סגנונות של \”Event-Driven Architecture\” (או בקיצור: EDA).

יש לי באופן אישי בעיה עם ההגדרה \”Event-Driven Architecture\”.

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

לומר ש \”יש לנו ארכיטקטורה שהיא Event-Driven\” זה כמעט כמו לומר \”יש לנו ארכיטקטורה שהיא Database Driven\” או \”ארכיטקטורה שהיא Java Driven\”. כלומר:

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

בכלל, ככל שמערכת גדלה, ובמיוחד כאשר היא נעשית מבוזרת – יותר סביר שנשתמש באירועים איפשהו במערכת. האם זה הופך את המערכת שלנו ל Event-Driven?

ובכן… מדוע אנשים מציינים שיש להם \”Event Driven Architecture\”? כנראה כי:
א. זה נשמע \”טוב\”. באזז מרשים.
ב. כי אותם אנשים מרוצים מהשימוש ב events – ונתונים לתכונה הזו של המערכת דגש מיוחד.

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

הייתי קורא למערכת כזו \”Event Based\”, ומנסה להבין מה מניע את המערכת והארכיטקטורה שלה. האם מדובר ב Scalability? האם חלוקה של תהליך עיבוד ליחידות קטנות ופשוטות יותר?

אז למה באמת מתכוונים ב \”Event-Driven Architecture\”?

אם נחפש קצת בספרות מקצועית, נוכל למצוא הגדרות כמו זו:
מתוך הספר Event-Driven Architecture: How SOA Enables the Real-Time Enterprise
אני לא רוצה להיות מרושע, אבל באמת זו חתיכת פסקה ארוכה שלא אומרת הרבה…
בספרון של מארק ריצ\’רדס בשם Software Architecture Patterns הוא מתאר שני סוגים עיקריים של EDA:
  • Mediator Topology – בה יש \”מח\” מרכזי שיודע איזה שלבים יש לכל event, אלו ניתן למקבל וכו\’- והוא זה שמנהל אותם
  • Broker Topology – בה החוכמה היא \”מבוזרת\” וכל רכיב שמטפל ב event יודע מה היעד הבא של ה Event / או אילו events חדשים יש לשלוח.
הוא בוחן את נקודות החוזק והחולשה בכל גישה – וזה נחמד, אך הוא עדיין לא ממש מספק תמונה שמכסה את השימושיים העיקריים של events במערכות תוכנה.

קצת עזרה??

כאן באמת הסשן של פאוולר היה מוצלח, ולכן החלטתי שהוא שווה פוסט. פאוולר הוא אולי לא המתכנת המבריק ביותר, אולי לא הארכיטקט הראשון לתכנון מערכות – אבל קשה לי לחשוב על דמות טכנולוגית שיודעת לקחת חומר טכני ולהנגיש אותה לקהל – בצורה טובה כמוהו.
פאוולר מגדיר ארבעה צורות עיקריות לשימוש ב Events במערכת. אם נשתמש במונחים הללו, ולא במונח הכללי \”Event-Driven Architecture\” – הרי נוכל להבין טוב יותר אחד את השני.
Event Notification

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

למשל: 
  1. אפליקציית הנהג מודיעה שהסיעה הסתיימה.
  2. המערכת לניהול נסיעות סוגרת את הנסיעה ושולחת הודעה שהנסיעה הסתיימה.
    הקריאה היא אסינכרונית בפועל: אין צורך להמתין לחיוב שהתבצע – אלא רק ל Queue שהוא אכן קיבל את ההודעה. אם מערכת החיוב פונה לחברות האשראי ייתכן שייקח לה מספר שניות לבצע את החיוב, בעוד Queue יקבל את ההודעה ב מילישניות בודדות.
  3. מערכת החיוב בודקת את ה Queue כל הזמן אם יש הודעת. כאשר היא שולפת הודעות – היא מטפלת בהן ב\”זמנה החופשי\”.
המודל של Event Notification הוא פשוט ושימושי.
  • הוא מתאים כאשר מישהו רוצה לשלוח הודעה והוא לא מצפה לתשובה.
  • הוא מתאים כאשר הצד מרוחק יבצע את הפעולה בקצב שונה. למשל: בשל תקלה בחברת כרטיסי האשראי ייתכן שהחיוב יתבצע רק לאחר שעה (דוגמה קיצונית).
  • הוא מאפשר למערכת ניהול הנסיעות להיות בלתי תלויה במערכת החיוב: אם יום אחד רכיב אחר יטפל בחיוב / הטיפול יחולק בין כמה רכיבים – מערכת ניהול הנסיעות לא תשתנה כתוצאה מכך.
    • חוסר התלות הזו היא לא מושלמת: מה קורה כאשר מערכת החיוב זקוקה לנתון נוסף לצורך החיוב? (על כך בהמשך)
    • כלל טוב לצמצום התלות הוא לשגר אירוע \”כללי\” שמתאר את מה שקרה במערכת \”נסיעה הסתיימה\” ולא פקודה כמו \”חייב נסיעה!\”. שימוש במינוחים של פקודה גורם לנו לקבל בצורה עמוקה יותר את התלות בין המודולים – ולהעצים אותה לאורך הזמן.
  • כאשר יש ריבוי של Events Notifications במערכת – קשה יותר לעקוב אחרי ה flow, במיוחד כאשר events מסוימים מתרחשים רק לפעמים ו/או במקביל.
    Mitigation אפשרי הוא מערכת לוגים מרכזית ופעפוע \”request id\” (ואולי גם hop counter) על גבי ה events. כל כתיבה ללוג תציין את ה request id – וכך יהיה אפשר לפלטר את כל מה שהתרחש במערכת במערכת הלוגים ולראות תמונה שלמה. בערך.
עד כאן טוב ויפה. 
מה קורה כאשר מערכת החיוב דורשת עוד נתון ממערכת הנסיעות? למשל: האם הנסיעה התחילה בשדה תעופה או לא?
בגישת קיצון אחת, להלן הגישה העצלה – ניתן לשלוח ב event רק את id של הנסיעה שהסתיימה. מערכת החיוב תשלים את הנתונים החסרים ממערכת הנסיעות / מערכות אחרות.
בגישת קיצון שנייה, להלן הגישה הנלהבת (eager) – אנחנו מעדכנים את ה event לכלול את כל הנתונים שמערכת החיוב זקוקה להם.
בשני המקרים, יש לנו תלות מסוימת במערכת החיוב: שינוי במערכת החיוב עלול לדרוש שינוי במערכת ניהול הנסיעות – בגרסה אחת להוסיף שדה ב API של מערכת ניהול הנסיעות, בגרסה שניה – נתון על ה event.
איזו גישה עדיפה? – אין לי תשובה חד משמעית.
בד\”כ אני מעדיף את הגישה הנלהבת, כי:
  • יותר קל לעקוב מה מתרחש – כי אפשר להסתכל על ה event ולהבין אלו נתונים נשלחו – ובמקום אחד.
  • הערכים שעברו על ה event הם קונסיסטנטים ל event – לא יקרה מצב שבחצי שנייה (או דקה) שלוקח למערכת החיוב לבצע קריאה – אחד הערכים השתנה.
  • יש פחות קריאות ברשת, ומעט פחות דברים שעשויים להשתבש.
כמובן שאם יש נתונים שצריכים להיות fresh, כדאי לערבב בין הגישות – ולקרוא אותם ברגע האמת בעזרת API.
Event-Carried State Transfer
גישה זו היא וריאציה של גישת ה Event Notification, אבל שינוי אחד בה – משנה בצורה משמעותית את כללי המשחק:
  1. אפליקציית הנהג מודיעה שהנסיעה הסתיימה.
  2. מערכת ניהול הנסיעות שולחת את כל הנתונים שיש לה על ה event.
    איך שולחים את כל הנתונים? בד\”כ לוקחים את אובייקט המודל של ה ORM – ועושים לו serialization. 
  3. מערכת החיוב בודקת כל הזמן אחר הודעות. כאשר יש הודעה – היא קוראת ממנה רק את הנתונים שהיא זקוקה להם. היא עשויה לשמור עותק מקומי שלהם.
בעצם במקום להעביר הודעה, אנחנו מעבירים את ה state השלם של האובייקט בין המערכות השונות.

לגישה זו כמה יתרונות:

  • השגנו isolation גדול יותר (במימד אחד): שינוי במערכת החיוב לא ידרוש שינוי במערכת ניהול הנסיעות.
  • Availability – אם מערכת ניהול הנסיעות קרסה, ניתן להמשיך לבצע חיובים, כי למערכת החיוב יש את כל הנתונים שהיא צריכה.
לגישה יש גם כמה חסרונות:
  • שברנו את ה encapsulation: מערכת החיוב מכירה את מבנה הנתונים הפנימי של מערכת ניהול הנסיעות. מעכשיו יהיה קשה הרבה יותר לבצע שינויים במבנה הנתונים, ויש גם סכנה שהמתכנת של מערכת החיוב לא יבין את השדות נכון – ויפעל ע\”פ נתונים מוטעים.
  • העברנו הרבה נתונים מיותרים ברשת – בד\”כ זו בעיה משנית.
  • יצרנו עותקים שונים של הנתונים ברשת, מה שפוטנציאלית יוצר בעיה של Consistency בין הנתונים. נתונים שכן צריכים להיות up-to-date לצורך הפעולה – יהיו לא מעודכנים ויתכן שיהיה צורך בליישם מנגנון של eventual consistency.
שבירת ה encapsulation היא מבחינתי השבר העיקרי.
מרטין פאוולר ביטא זאת יפה, בנחמדות בריטית אופיינית: \”זו גישה שטוב שתהיה בתחתית ארגז הכלים שלכם\”. אפשר להשתמש בה – מדי פעם.

מה קורה כאשר מידע נוסף נמצא על מערכת שלישית? למשל, את הפרט אם נסיעה התחילה בשדה תעופה (ואז יש לתת 30% הנחה ;-)) ניתן להסיק רק כאשר יש נתונים נוספים ממערכת האזורים?

גם כאן יש בחירה: אפשר לתת את המשימה למערכת ששולחת את ה event, או לזו שמקבלת אותו. כנראה הבחירה תעשה ע\”פ איזו תלות בין המערכות – היא סבירה יותר.

Event Sourcing

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

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

מה התועלת בגישה הזו?

  • אם אנחנו מקבלים עדכוני state מכמה מקורות – אין לנו race condition בו מקור א\’ מוחק את השינוי של מקור ב\’.
    זה יכול להתרחש אם מקור א\’ קרא עותק ישן של האובייקט, לפני השינוי שמקור ב\’ ביצוע – אבל ההודעה ממנו הגיעה אחריו.
  • אנחנו שומרים היסטוריה של האובייקט ושינויי המצב שלו. אם נרצה – נוכל להרכיב את מצב האובייקט בכל רגע נתון (בהנחה ששמרנו את הזמן שבו קיבלנו את ההודעה).
    • זה יכול להיות שימושי לצורך debugging / הסבר התנהגות המערכת.
  • ההודעות שנשלחות הן קטנות (רלוונטי כאשר האובייקט השלם הוא גדול)
יש גם כמה חסרונות:
  • הרכבה של האובייקט, במיוחד אם קיבל רשימה ארוכה של עדכונים – היא פעולה יקרה יחסית.
    • החיסרון הזה הוא בעייתי במיוחד אם הרכבה של האובייקט דורשת נתונים נוספים ממערכות אחרות.
    • החיסרון הזה מתמתן אם אנחנו מחזיקים עותק של הנתונים בזיכרון.
  • מה קורה כאשר הסכמה משתנה? כלומר: מבנה הנתונים?

פה עשויה לעלות שאלה פילוסופית: אם אני מקבל את העדכונים כ delta, אבל אז בכל עדכון עושה merge עם האובייקט שאני מחזיק אצלי – האם זה עדיין Event Sourcing?

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

Command Query Responsibility Segregation (בקיצור: CQRS)

מספרים שאם הייתם מחפשים בגוגל \”CQRS\” לפני עשור, הוא היה שואל: \”`?did you mean `Cars\”
הרעיון הוא דיי ישן, ומקורו בשנות השמונים, אבל רק בשנים האחרונות הוא הפך למאוד-מוכר.

אני מניח שהרוב הגדול של הקוראים מכיר את השם, אבל לא בהכרח מכיר את הרעיון מאחוריו. לרוב האנשים CQRS מתקשר ל \”high performance\”.

האמת שהרעיון של CQRS אינו קשור קשר ישיר ל events, אבל פעמים רבות – משתמשים בו כך.

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

מתי זה שימושי?

כאשר דפוס הקריאה ודפוס הכתיבה שונים זה מזה.

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

כאשר משתמש אחר נכנס לטוויטר, הוא רוצה לראות את הפיד של הטוויטים מיד. אם נתחיל לעשות Query אחר כל האנשים שהוא עוקב אחריהם, וכל הטוויטים שלהם, ואז נמיין אותם ע\”פ הסדר -זה ייקח הרבה זמן! יותר מדי.

במקום זה, לאחר שכתבתי את ה tweet יש Background Processor שמעתיק את הטוויט שלי לפיד של כל העוקבים.
כלומר:

  • מודל \”הכתיבה\” הוא רשימה של טוויטים ע\”פ מחבר.
  • מודל \”הקריאה\” הוא הפיד של כל משתמש בנפרד.
זה אומר שיש הרבה שכפול נתונים במערכת, ושטח האחסון הנדרש הוא אדיר. אם יש למישהו מיליון עוקבים – כל טוויט ישוכפל מיליון פעמים.
מצד שני, זה גם אומר שגם אם אני עוקב אחרי 1000 פרופילים ויותר – הפיד שלי ייטען ב (O(1.
במקרה של טוויטר סביר שמודל הכתיבה ומודל הקריאה הן בכלל מערכות שונות – כאשר כל הוספה של טוויט למודל הכתיבה – שולחת אירוע של עדכון (event notification) למודל הקריאה – שם נמצא ה Background processor.
מכיוון שלא ממש משנה אם אני מקבל את הטוויטים מיד או אחרי דקה או שתיים מרגע שצויצו, ומכיוון שלא כ\”כ משנה עם הסדר של הטוויטים אצלי הוא אותו סדר כמו אצל חבר שלי העוקב אחרי בדיוק אותם פרופילים – המודל הזה עובד.

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

שימוש אחר של CQRS הוא לא דווקא מבחינת ביצועים גבוהים, אלא פשטות של הקוד. אם המודלים סבוכים למדי – החלוקה ל\”מודל כתיבה\” ו\”מודל קריאה\” יקטינו את ה code base בחצי, בערך.

כאן שווה לציין ש Event Sourcing ו CQRS הולכים יד ביד זה עם זה:
מודל הכתיבה הוא ה State Log – אבל יש מודל קריאה שהוא המצב העדכני. זה יכול להיות בסיס נתונים או טבלה אחרת בה שומרים את המצב העדכני, וזה יכול להיות מודל שעובד מעל אותם נתונים – ורק מכיל את הקוד של \”השטחת\” העדכונים בזמן ה Query.

סיכום

התלהבתי מהסדר שפאוולר עשה בנושא ה Event-Driven, ולכן כתבתי את הפוסט והוספתי עוד כמה תובנות משלי.
השארתי קצת אפור בהגדרות של הגישות השונות. תמיד ניתן לערבב ולייצר וריאציות. למשל:

הייתי מעורב בבניית מערכת שמבצעים בה רפליקציה של נתונים לשירותים אחרים, כמו בגישת ה Event-Carried State Transfer – בכדי להשיג High Availability. מצד שני, כמות הנתונים שמועתקת היא קטנה ומדודה מאוד, והנתונים הם ברמת הפשטה של ממשק ולא מבנה נתונים פנימי – כך שאין פגיעה בהכמסה של המערכת.

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

אז מה הופך מערכת להיות Event-Driven? – לא תצאו עם תשובה חד משמעית מהפוסט.
יצא לי לעבוד על מערכת בה השתמשנו בכל ארבעת הגישות, ולרגע לא חשבתי לקרוא למערכת \”Event Driven\”…. מוזר.

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

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

ההרצאה של פאוולר ב Goto; Chicago
פוסט של פאוולר בנושא

על העיקרון הפתוח-סגור (ארכיטקטורה)

פוסט זה הוא סוג של המשך לפוסט: חלוקת המערכת למודוליםהעיקרון הפתוח-סגור אומר ש:

Software Entities (classes, modules, functions, etc.)
should be open for extension, but closed for modification.

העיקרון הפתוח-סגור (בקיצור: OCP, שזה ה Open-Closed Principle), הוא חלק ממערכת ה S.O.L.I.D – מערכת כללים לבחינת איכות של עיצוב (Design) תוכנה שהגדיר הדוד בוב (Robert C. Martin) בתחילת שנות ה 2000.

כמו עקרונות אחרים במערכת, ואולי יותר מכולם – ה OCP איננו ברור כ”כ במבט ראשון.
גם לא במבט שני.

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

לא עזרתם בהרבה! מקור: http://www.tomdalling.com
בגרסה היותר ידידותית, העיקרון אומר משהו כזה:

  • כתבו את הקוד כך, שרכיבי התוכנה (מחלקות, מודולים, פונקציות, וכו’) לא ישתנו לאחר שסיימו לכתוב אותם – להלן: “Closed for modification”.
  • תוספת של פונקציונליות – יש להוסיף ברכיבי תוכנה חדשים (עוד מחלקות, עוד מודולים, או עוד פונקציות) – להלן “Open for Extension”.

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

———

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

שאלות ראשונות

מדוע לא לשנות קוד שכבר נכתב?

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

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

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

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

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

מדוע “הרחבה” של קוד היא בסדר?

בשלב הראשון אנו כותבים את ה “Basic Functionality”. הקוד עובד ומתייצב עם הזמן.
כאשר אנחנו נדרשים להרחיב את הפונקצינליות אנו מוסיפים את תוספות 1, 2, ואז 3.

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

כמו כן – הרחבה תכופה תאפשר לכל איזור קוד להיות קטן ופשוט מספיק בכדי להבין אותו: “Scalability” של כתיבת קוד.

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

מדוע עדיף לנו שבאגים יופיעו באיזור החדש של הקוד, ולא באיזור הישן?

זה לא בדיוק מה שנאמר.
חלק גדול מהיתרון של יישום OCP הוא שהקוד שלנו מחולק ל”חתיכות” קטנות של קוד שלא מסתבכות יותר מדי. קטן => פשוט (או יותר נכון: גדול => מסובך).

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

כיצד ממשים OCP בפועל?

יש לי מערכת. אני רוצה ליישם את העקרון הנפלא של OCP. כיצד עושים זאת?

יש כמה וכמה דרכים. אציג דרך אחת שיצא לי לדון בה לאחרונה:

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

אני בונה את המנגנון כך: Broker שמקבל את הבקשה ואז מעביר את רשימת ההתאמות האפשריות לרכיב “Base Condition” – הרכיב ידרג את ההתאמות מהטובה ביותר, להכי פחות טובה.

התוצאה של ה Matching System עוברת לרכיב התוכנה הבא שינסה ליישם בפועל את ההתאמה. קודם את האופציה הטובה ביותר, ואם לא מסתדר – ימשיך לאופציות הבאות עד שיצליח.

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

מנגנון ה Broker קורא מקובץ קונפיגורציה – באיזה סדר להפעיל את התנאים.

ובאמת לאחר כחצי שנה המנגנון נראה כך:

בכל שלב הוספתי עוד תנאי למנגנון ה Matching (העדפות של נהג, העדפות של נוסע, וכו’).
הקוד החדש נכתב ללא נגיעה בקוד שהיה קיים קודם לכן.

בפבואר הוספתי את Condition 2 – ועד אפריל הוא התייצב בצורה טובה.
במרץ הוספתי את Condition 3 – ועד מאי הוא התייצב בצורה טובה.
במאי הוספתי את Condition 4 – ועד יולי הוא התייצב בצורה טובה.

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

ביישום הספציפי הזה, ה Broker בנוי כך שהוא יכול להכניס את התנאים החדשים לפעולה בצורה הדרגתית:
כהוספתי את Condition 2 בתחילת פבואר – הוא פעל במוד בדיקה, על 100 נסיעות בלבד ביום.
באמצע פבואר כבר שיניתי את הקונפיגורציה שיעבוד על 3000 נסיעות ביום (בכדי לבדוק טוב יותר), ורק בסוף פבואר, לאחר שתוקנו עיקרי הבאגים – הפעלתי אותו על כלל המערכת.

ה Design הזה כמובן הוא לא “מושלם” ואני לא ממליץ להעתיק אותו כי הוא מופיע כדוגמה לעיקרון חשוב של תכנון מערכות. הוא מתאים לצרכים מאוד ספציפיים.
ככל שיהיו יותר תנאים של Matching שלא משתלבים ב Pattern הנ”ל – הוא ילך ויסתבך. אין לו תוכנית ל “חריגים לא מוכרים” (כי הם לא מוכרים, דאא) ויהיה צורך לבצע חשיבה מחדש ברגע שיגיעו כאלו.

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

איכות המבנה / העיצוב של ה Matching System (או כל מודול אחר) תלוי בעיצוב הראשוני, ומידה לא פחותה – ברמת ההחלטות וההתאמות שידרשו עם הזמן.

ספקות לגבי OCP

ה OCP הוגדר בצורה לא ברורה. את זה כבר הזכרנו. היו כמה מאמרים שהתפרסמו בביקורת הזו (למשל: Say “No” to the Open/Closed pattern של Marco Cecconi ו The Open-Closed Principle, in review של Jon Skeet)

במאמר מאוחר של הדוד בוב מ 2013 הוא סיפק את ההגדרה הבאה ל OCP:

What it means is that you should strive to get your code into a position such that, when behavior changes in expected ways, you don’t have to make sweeping changes to all the modules of the system. Ideally, you will be able to add the new behavior by adding new code, and changing little or no old code.

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

ספק מרכזי שיש לגבי OCP, הוא האם התקדמות בטכניקות של הנדסת תוכנה בשנים האחרונות – שינו משהו לגבי הנכונות שלו?

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

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

שינוי/הוספה של קוד לא הייתה תמיד מסוכנת, אבל כאשר יש אוטומציה – היא מסוכנת הרבה פחות.

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

האם בהכרח זהו קוד שכדאי להימנע ממנו?
לא בטוח. אם במשך 5 שנים של אורך חיי במערכת יצטברו במתודה 3 או 4 צורות – לא נראה לי שזה יהיה קוד רע. בואו לא נהיה פאנטיים. האם חשוב להוסיף final על כל משתנה בג’אווה? אפשר – אבל ה impact המעשי הוא כנראה באמת קטן.

אם המערכת שלנו תתרחב ל 10 צורות, וכמו המתודה DrawAllShapes יש מתודות כמו EditAllShapes, ResizeAllShapes ועוד – אז ברור לי שבסיס הקוד שלי הולך וגדל ללא היכר. זה מקרה ברור בו כן היינו רוצים ליישם את ה OCP.
בסיס קוד גדול יותר => קוד שקשה יותר לשלוט בו => קוד שקשה יותר לשנות / יותר באגים.

ברור לחלוטין שלא ניתן, ולא כדאי, להגיע ל 100% יישום של OCP במערכת.
בניגוד להוספת “final” למשתנים בג’אווה, פרקטיקה שמוסיפה מעט ערך אבל גם העלות שלה שולית – OCP משמעה כתיבת עוד קוד ולקיחת “סיכונים” בעצם הגדרת ההפשטה (למשל: ה Condition בדוגמה למעלה).
כאשר דרישה חדשה משתלבת יפה בהפשטה שיצרנו – השגנו ייתרון, אך כל דרישה חדשה שלא תתאים להפשטה – תקשה ותסבך את המערכת.

OCP מתבטא ב Patterns כמו Plug-ins, State, Strategy, ועוד. אולי גם Composite ו Delegate.
זוהי רשימה קלאסית של Patterns שאנשים נטו ונוטים לעשות בהם שימוש מוגזם: השימוש עשוי להרגיש “טוב” (המערכת שלנו היא “כמו בספרים”), אך בפועל ייתכן ויצרנו מערכת יותר מסובכת ויותר יקרה לשינוי / תחזוקה.

OCP הוא במיטבו כאשר ממשים אותו במידה, אבל מהי המידה הנכונה?

מקור: https://www.slideshare.net/PaulBlundell2/open-closedprinciple-kata

קרייג לרמן, ו PV

ובכן, אם לא הכרתם עד עכשיו – אז שווה להכיר:
במקביל לדוד בוב שהגדיר את עקרונות ה S.O.L.I.D, עבד בחור קנדי בשם קרייג לרמן על מערכת חוקים מאוד מאוד דומה בשם GRASP. המערכת של לרמן הרבה יותר פשוטה להבנה! מה שהופך אותה לעדיפה בעיני.

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

העיקרון המקביל של GRASP ל OCP נקרא Predicted Variations או בקיצור PV. והוא מוגדר כך:

Identify points of predicted variation and create a stable interface around them

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

Predicted מצוין כ “educated guesses” מכיוון שלא סביר שבאמת נוכל לחזות כיצד תתפתח המערכת לאורך חייה.
כלומר: עלינו לחשוב לאילו כיוונים סביר שהמערכת תתפתח, ולבנות את הממשק באיזור הזה כך שיהיה ניתן להוסיף קוד ע”י הוספה של יחידות קוד עצמאיות, ולא ע”י שילוב של תוספות קוד בקוד הקיים. בהמשך המאמר מוסבר:

We can prioritize our goals and strategies as follows:
1. We wish to save time and money, reduce the introduction of new defects, and reduce the pain and suffering inflicted on overworked developers.
2. To achieve this, we design to minimize the impact of change.
3. To minimize change impact, we design with the goal of low coupling.
4. To design for low coupling, we design for PVs.

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

אנסה לתמצת את הרעיונות במילים שלי:

  1. זהו במערכת מקומות שבהם אתם צופים תוספת של קוד, ונסו להכין ממשקים ידועים להרחבות הללו.
  2. בעצם מדובר ב”ניהול סיכונים”: הסבירות שאכן המערכת תתרחב לשם (תזכורת = בני-אדם נוטים לבצע הערכת יתר בציר הזה)  X  כמה קוד וסיבוכיות אתם צופים שתתווסף.
  3. עבור שינויים קלים (שורות קוד בודדות) – הערך ביישום OCP הוא גם נמוך. נסו לכוון למקומות משמעותיים.
  4. קלעתם טוב – השגתם ערך; לא קלעתם טוב – השקעתם זמן מיותר. זה בעצם העניין כאן.
  5. אפשר לנקוט בגישה אגי’לית: כשאתם רואים שאכן המערכת מתפתחת לכיוון מסוים, בצעו Refactoring והוסיפו ממשק מתאים / PV. אם יש לכם בדיקות יחידה – אין סיבה לפחד מ Refactoring.

אני מניח שאם GRASP הייתה השיטה השלטת – הפוסט שלי היה מתקצר בחצי: המטאפורה של Predicated Variations הרבה יותר קולעת מה Open-Closed Principle (סליחה, הדוד בוב).

לא מצאתי את הציוץ של הבחור שהציע: “בואו נחליף את OCP ב PV – ונתחיל להשתמש ב SPLID”.

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

—-

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

ארכיטקטורות ללא שרת (Serverless Architectures) – מה זה השטויות האלה?!

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

“עכשיו תעשה לי את זה בלי שרת” – מבקש המראיין.
ווב, בלי שרת? כלומר… Rich Client שלא מתקשר עם אף אחד?

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

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

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

הרי קשה להתעלם מהבאזז המוזר הזה של “Serverless Architectures”, באזז שכולל Frameworks, ספרים, ואפילו Conference שמוקדש לנושא.

בפוסט הזה אנסה להסביר את הנושא. נראה שבעצם כל התשובות נכונות.

הכרזה של ServerlessConf בניו-יורק

אז מה בעצם הכוונה ב “Serverless”?

כמו באזזים שונים שנתקלתי בהם בעולם התוכנה לאורך השנים (“SOA”, “MVC”, או “Big Data”), חוסר בהירות או אחידות בהבנת העקרונות הבסיסיים מאחורי הבאזז – לא מונעים מהבאזז לפרוח (אולי אפילו להיפך). זה קצת כמו הברקזיט: קודם מצביעים – ואז הולכים להבין במה מדובר 😉

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

BaaS (קיצור של Backend as a Service) – שירות המאפשר למפתחי צד-לקוח (בעיקר מובייל, ולכן לעתים נקרא גם MBaaS) לבצע bootstrapping[א] מהיר ללא הצורך בידע והשקעה בצד השרת.
שירותים כמו parse.com (שנקנתה ע”י פייסבוק ואח”כ נסגרה), Firebase, או Kinvey הפכו פופולריים בעולם הסטארטאפים, וקיימים גם פתרונות פופלארים בקהילות הסגורות של עולם ה Enterprise (יבמ, אוקרל, סאפ) שבוודאי יהיו פחות מוכרים לאיש התוכנה הממוצע.

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

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

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

FaaS (קיצור של Functions as a Service) היא הגל השני של ה Serverless Architectures, שעלה לכותרות בעיקר בעקבות ההצגה של שירות AWS Lambda. למרות שהשירות שוחרר בגרסתו הראשונית עם מגבלות רבות, הוא הגיע משחקן מוכר ומוערך מאוד (אמזון), עם מחיר כניסה אפסי (אפשר להתנסות בו במחיר של סנטים), ובמרחק נגיעה – כי למאות אלפי (?) מתכנתים יש כרטיס אשראי כבר מוזן באמזון, והם יכולים להפעיל את השירות במרחק של קליק.

הפתרון, בגדול, מאפשר לכתוב ולעשות deploy לפונקציה בודדות – שתופעל כתגובה לקריאה ל endpoint (נקראים: “event sources”) שנגדיר.
תפעול הפונקציות הללו (ניהול עומס, monitoring, ו High Availability) מנוהל ע”י אמזון. רק מעדכנים את קוד הפונקציה – ואמזון דואגת לשאר.
בעצם FaaS הוא סוג של PaaS (כלומר: Platform as a Service, סליחה על השימוש התכוף בבאזזים) ברזולוציה קטנה הרבה יותר: במקום אפליקציה – פונקציה.

מאז הצגת AWS Lambda הזדרזו המתחרים להציג שירותים מקבילים, והיום יש לנו גם את Google Cloud Functions (עדיין באלפא, בעת כתיבת פוסט זה), את Azure Functions (גם הוא חדש), ואת OpenWhisk של יבמ (שרצה על פלטפורמת Bluemix של יבמ, פלטפורמה מוכרת בכנסים – וקצת פחות מפתרונות בפרודקשיין).

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

האם ב FaaS אין שרת? – יש קוד צד-שרת, אבל בעצם מדובר בסט של פונקציות עצמאיות המנוהלות ע”י מישהו אחר. יש צד-שרת, למרות שאין “שרת”.
האם זו נישה? – כרגע כן. הרבה משחקים באופציה החדשה, מתלהבים (“יו! פאקינג אינסוף Scalability!!”), אבל גם מתפכחים (“אהה.. אבל ה DB הוא צוואר הבקבוק של ה scale” / “קצת נהיה בלאגן” / “זה יוצא דווקא דיי יקר…”).
האם זו גישה חדשנית או אפילו מהפכנית? – אולי. נהיה חכמים יותר עוד שנתיים – שלוש. עדיין יש פה גישה חדשה שמאתגרת הרבה מאוד ממה שהתרגלנו אליו עד עכשיו בעולם צד-השרת.

תת-וריאציה (מעט משונה) של FaaS היא DBFaaS, הגישה הזו גורסת שלעתים קרובות אנו זקוקים ב FaaS לפנוקציות שמבצעות עיבוד קל על נתונים. היכן יותר יעיל לשים את הפונקציות הללו – אם לא ב Database עצמו?
לחברת SAP יש מוצר בשם HANA, בסיס נתונים in-memory, שמאפשר ממשק וובי ישיר ל stored procedures שלו.
גישה זו שוברת כמעט כל מוסכמה של Layered Architecture או MVC, ומציבה דאגות רבות (Scalability? אבטחה? יכולת debugging ותהליכי תוכנה נכונים?). אני עדיין לא מצאתי איזון בריא לגישה הזו – אך אין ספק שהיא מערערת מוסכמות וגורמת לחשיבה מחודשת של הדברים. למה בעצם לא??

טרנפורמציה של ארכיטקטורת Client-Server לארכיטקטורת Serverless. מקור: Bliki

Serverless Web Site (אין קיצור מגניב)

תצורה אחרונה, וקצת פחות מהותית מהאחרות אליה מתייחסים כ Serverless Architecture היא לארח אתר אינטרנט (לרוב עם פונקציונליות מוגבלת, ובעיקר חוסר-תקשורת בין המשתמשים השונים) כ Static Content, ובניית כל הפונקציונליות ב JavaScript.
את קובצי ה HTML/CSS/JavaScript מארחים בשירות אכסון כמו S3 או אפילו עדיף – CDN.

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

כמה שהגישות השונות שהצגתי הן שונות זו מזו – יש ביניהן סינרגיה.

כאשר אנו משתמשים ב BaaS, תחת היכולות הגנריות שמציע השירות הספציפי – מה יותר טבעי מלהוסיף קצת custom server functionality כ FaaS?

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

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

מקור: אמזון. לטעמי השימוש כאן במונח “Microservice” הוא לא ממש מדויק.
אם היו גם מציינים גם Big Data, דוקר, ו IoT – זה היה כבר יותר הגיוני 🙂

אז מה הם באמת התכונות העיקריות של ארכיטקטורה “ללא-שרת” (קרי: מבוססת FaaS)?

אפשר לייחס ל Serverless Applications את התכונות הבאות:

  • התבססות במידת האפשר על שירותי צד-שלישי.
  • ה Flows העיקריים של האפליקציה מנוהלים בצד-הלקוח. צד השרת מספק בעיקר “פונקציות”.
  • בניגוד לשרת שחי לאורך זמן, וטיפל בבקשות שונות – ב Serverless Architectures המופע של צד-השרת חי לאורך טיפול בבקשה. יכולים להיות אלפים כאלו שמופעלים ונדומים בשנייה.
    • זו תכונה טכנית יחסית, שעשויה להשתנות. ייתכן והספקים יתחילו לספק הנחות למי שמבטיח כמות מסוימת של שימוש בשעה – והדרך להוזיל עלויות תהיה כן להחזיק כמות מסוימת של instances קבועים.
    • יתרונות נוספים של instances קבועים הם cache “חם” (ברמות השונות) וביטול / קיצור ה initialization latency.
  • ארכיטקטורות Serverless לוקחות את רעיון ה Continuous Deployment לאקסטרים חדש: כעת ניתן, בכל רגע, לבצע דיפלוי ברזולוציה של פונקציה בודדת.
יתרונות:
  • חסכון בעלויות, כי משלמים רק על מה שרץ – במיוחד כאשר יש traffic דליל. בסקייל בינוני ומעלה – זה יותר יקר.
  • יותר אופרציה מנוהלת ע”י ספק השירותים, או קצת בהקצנה: noOps#. רעיון ה Platform as a Service (בו כמעט כל היבטי ניהול אפליקציות מבוצע ע”י הספק) לא הצליח כ”כ בפועל, אך יש סיכוי שמודל ה FaaS יצליח יותר.
  • כאשר כל פונקציה (עם מאפייני הביצועים הייחודיים שלה) היא מבודדת – קל יותר לעשות Scaling. טעות נפוצה היא ש FaaS הוא “infinitely scalable”. כאשר יש לפונקציה state משותף כזה או אחר – צווארי הבקבוק של ה scalability יהיו שם – כפי שהיה תמיד.
  • אפשרות לקרב את הפונקציה למשאב אחר, למשל: קרוב מאוד לשירות צד-שלישי ולבצע חישובים שם, קרוב לנתונים, או אולי קרוב ללקוחות (למשל: לשים פונקציות שירוצות אצל ספק ה CDN וכך יספקו תשובות מאוד מהירות למשתמשי הקצה שלי, בכל רחבי העולם).
חסרונות:
  • חלוקה של מערכת להרבה פונקציות קטנות דיי מהר הופכת לאתגר תפעולי מצד המתכנתים. רבים מהמימושים המוצלחים של FaaS כרגע – מבוססים על פונקציות מעטות.
  • חלוקת המערכת להרבה פונקציות קטנות עלולה ליצור קשיים בהשגת אמינות גבוהה: הרשת יכולה לשבש קריאות בין פונקציות שונות – ואם אנו זקוקים ל”אפס פספוסים”, אנו נצטרך להתגונן בפני כשל של הרבה מאוד נקודות אינטגרציה.
  • Lock-In ל Vendor המספק לכם את השירותים. אולי הפונקציות עצמן מבודדות (isolated), אך סביר שהן משתמשות ב infrastructure נוסף (הרשאות, API Gateway, אכסון נתונים, וכו’) שכובל אתכם לספק הספציפי.
  • כאשר כל פונקציה רצה בפני עצמה בשרת צד-שלישי, ולא ניתן להריץ את הקוד לוקאלית – הפיתוח נעשה מורכב יותר.
    • לא יפתיע אותי אם נראה בקרוב יותר ויותר אמולטורים שמאפשרים להריץ את פתרונות ה FaaS (בצורה פחות יעילה / אמינה – רק לצורך פיתוח) על המחשב המקומי.
  • לפחות כרגע, לפתרונות כמו AWS Lambda יש Initialization latency לפונקציה – כלומר: אתחול הפונקציה מוסיף לזמני התגובה כ 100-600 מילישניות (ע”פ דיווחים שונים). זה בסדר ל Event Processing, אבל קצת פחות טוב ל UI או מערכות semi-realtime.

איזה סוג של מערכות אם כן, מתאימות יותר ל Serverless Architectures?

  • UI-Centric Apps – אפליקציות בהן חווית המשתמש היא המרכזית, והצרכים מצד השרת הם מצומצמים. אלו יהיו בעיקר אפליקציות מובייל – אך לעתים גם מערכות ווב.
  • Message-Driven Systems – מערכות שעיקר עיסוקן הוא טיפול באירועים, למשל: טיפול בטרנזקציות בנקאיות, איתור Fraud, או מערכת שמחליטה איזו מודעה להתאים למשתמש. מערכות אלו ניתן לפרק בקלות יחסית לסדרה של פונקציות, כאשר יש הגיון לשפר כל פונקציה בנפרד: הן מבחינת ביצועי תוכנה = לרוץ מהר יותר, והן מבחינת ביצועים עסקיים = לתת תשובה קצת יותר מדויקת.
    • אם המבנה הלוגיקה העסקית הוא של Pipes and Filters – מבנה ועקרונות של Serverless Architectures עשויים בהחלט להיות רלוונטיים.
    • באזז נוסף שמתקשר ל Message-Driver Systems ו FaaS הוא IoT (קיצור של Internet of Things): אירועים שמגיעים מהמון מכשירים קטנים.
  • חיזוק לנאמר לעיל: מערכות בהן אין (או כמעט ואין) State בצד השרת.
    • בשפה של תכנות פונקציונלי: הפונקציות של FaaS הן Pure Functions.
  • (קצת תיאורטי) יש טיעון שמערכות שחוות spikes גדולים מתאימות ל Serverless Architecture בגלל הציפיה לתשלום ע”פ גרנולריות מדויקת של שימוש, ויכולת Scalability גבוהה כאשר הפונקציות הן stateless.
    • כאשר AMI של אמזון (שנבנה בצורה המתאימה) יכול לעלות ולהיכנס לשימוש בטווח של דקה או שתיים – רוב ה spikes יכולים להיות מטופלים בצורה סבירה עם autoscaling. קשה לי להאמין שהעלויות של FaaS יוכלו להתחרות בעלויות של ניהול התשתית בעצמנו (IaaS) – כאשר מדובר ב Scale גבוה. אין ארוחות חינם.

לגבי העלויות ארצה לציין זאת במפורש: יש אינסוף אזכורים לכמה AWS Lambda עשויה זולה יותר מ EC2. שמעתי לא פעם את המספר “80% חסכון!”. זה סיפור טוב. כשצוללים לפרטים, ניתן לראות סיפורים על חסכון משמעותי – כאשר יש 3 קריאות ביום (מול שרת t2.micro).

יותר קשה למצוא ניתוחים של עלויות AWS Lambda ב scale גבוה. כל הניתוחים הללו שראיתי (הנה דוגמה) מראים מחיר גבוה יותר בשימוש ב Lambda מאשר בשימוש ב EC2.

סיכום

“כמה זמן יעבור עד ש Serverless Architectures יהיו הסטנדרט?” – היא שאלה שכבר שמעתי.
“כמו שהענן הולך להעלים את תצורות ה On-Premises… הרי Serverless יעלימו את ה Client-Server”

האמת – שאני לא יודע.
ייתכן וזה באזז שיחלוף, וייתכן שלגישת ה Serverless Architectures תהיה השפעה רבה על עיצוב מערכות מכאן ואילך.

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

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

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

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

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

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

[א] קרי – להעמיד מוצר ראשוני / התחלתי

דפוסי ארכיטקטורה: מיקרו-שירותים (Micro-Services Architecture)

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

מיקרו-שירותים הוא “הטרנד החם” בנושא הארכיטקטורה בשנים האחרונות, ונראה שהוא כבר עכשיו הדיח את ארכיטקטורת השכבות ממקומה והפך ל “ארכיטקטורת ברירת המחדל למערכות ווב” (או מה שאוהבים לקרוא לו היום Twelve-Factor App)

יש בזה הגיון רב: בסאפ יצא לי לראות מערכות בהן Layered Architecture התאימה בצורה נהדרת לאתגרים והדרישות. מדובר במערכות עסקיות, גדולות ומורכבות בהן ה UI הוא שיקוף של הסכמה בבסיס הנתונים.
במערכות מודרניות יותר, בהן ה UI הוא עשיר יותר ובעצם בסיס הנתונים הוא שיקוף של חווית השימוש שתוכננה, חלק מההנחות של ארכיטקטורת השכבות (למשל: “ה UI משתנה ללא שינויים ב Business Logic ו/או ה Persistence”) – פשוט לא משקפות את המציאות.

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

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

הסבירו לי נא, בדקה, מה “הקטע” של מיקרו-שירותים?

אנחנו עוד נחזור ונעמיק בדברים, אבל אנסה קודם להסביר בקצרה את עיקרי-הדברים.

הגורם הדוחף העיקרי לארכיטקטורה של מיקרו-שירותים הוא כנראה הנושא של Continuous Deployment: כיצד אנו מרכיבים את המערכת שלנו כך שנוכל לעשות deploy לגרסה חדשה 10 פעמים ביום – ומשם מאיצים את הקצב עוד ועוד?
ארכיטקטורה של שכבות בכלל לא מסייעת לעניין זה, אלא אם אתם הולכים לעשות deploy כל פעם ל Layer בודד אחר.

בפועל, רוב היכולות של מערכות הווב המודרניות מרכיבות גם UI, גם לוגיקה, וגם Persistence – כך שכל deploy דורש את כל שכבות המערכת.

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

  • ניתן לעשות לכל שירות deploy באופן בלתי-תלוי באחרים.
    לאמזון יש עשרות אלפי מיקרו-שירותים, והיא עושה deploy כל 10 שניות (בממוצע). איך ייתכן? היא פשוט עושה deploy כל פעם לחתיכה קטנה אחרת של קוד.
    בדרך כלל מיקרו-שירותים מורצים כתהליכים נפרדים של מערכת ההפעלה, המתקשרים זה עם זה על גבי HTTP / REST.
  • ניתן לפתח כל שירות באופן בלתי תלוי: CI, בדיקות, וסביבת העבודה היא בלתי-תלויה. אולי גם שפת התכנות.
    תכונה זו היא מרכזית מאוד, ומטרתה לשפר את ה scalability של הפיתוח: כמה מפתחים בצורה יעילה במקביל על הקוד. ההנחה כאן שהיא שיותר קל לפתח הרבה רכיבים קטנים – מאשר רכיב אחד גדול.
  • לכל שירות יש Persistence בלתי-תלוי.
    תנאי זה בא לשרת את שני העקרונות הקודמים – והוא לא מטרה בפני עצמה. Persistence היא תלות בין רכיבים במערכת, אם כי פחות מורגשת. אם שני שירותים ישתמשו באותן טבלאות בבסיס הנתונים – יהיה לכם הרבה יותר קשה להגיע ל deployment בלתי תלוי. גם אי-התלות בסביבת הפיתוח תשתפר מתנאי זה.
  • מיקרו-שירותים הם קטנים.
    “כמה קטנים?” – זהו דיון חם עליו אפשר לכתוב פוסט בפני עצמו. הנה כמה מדדים מקובלים (ולא-מתואמים) לגודל המומלץ למיקרו-שירותים. למרות שהם שונים – כולם מרמזים על “קטן למדי”:

    • מספיק קטן כך שאדם אחד יכול להכיר היטב את כל הקוד של השירות (“כזה שיכנס כולו לראש שלי” – הגדיר ג’יימס לואיס, חלוץ בתחום ה MSA)
    • מספיק קטן שצוות אחד (שניתן להאכיל בעזרת 2 פיצות אמריקאיות גדולות) – יכול לפתח ולתחזק אותו.
    • כמה מאות שורות של קוד.
    • מספיק קטן שלא יהיה קשה לארגון “לזרוק” אותו – ולכתוב אותו מחדש.
      מספרים שדף הבית של אמזון מפעיל כ 100-150 שירותים שונים (תלוי במקרה המדויק) בכדי לשרת כניסה של משתמש.
  • שירות עושה “דבר אחד בלבד” – Single Responsibility Principle.
    כמובן שגם “מתפעל את הלוגיסטיקה של וולמארט” הוא דבר אחד בלבד – ולא דבר קטן בכלל!
    זהו כמובן כלל קונספטואלי, שלא ניתן לאמוד אותו “מתמטית” – ושיהיה עליכם ליצור קונצנזוס ארגוני לגביו. כלל זה בא לשרת ולסייע לכם לקיים את כל התנאים הנ”ל.
היתרונות העיקריים של ארכיטקטורת המיקרו-שירותים הם:
  • ארכיטקטורה שתומכת ב Continuous Deployment. שניה! האם CD הוא מטרה או אמצעי? ובכן… גם וגם. CD משרת מטרות עסקיות (feedback מהיר ולמידה מהירה) והוא מנבא מוצלח של ההצלחה העסקית של הארגון (מקור).
  • Scalability של הפיתוח, הרבה מפתחים יכולים לעשות הרבה שינויים במקביל – ולפחד פחות.
    הנחה סמויה – יש לכם לא רק את הארכיטקטורה של מיקרו-שירותים, אלא גם מערך מקיף של בדיקות אוטומטיות שהכרחיות לקיום של CD סביר.
  • High Availability – קריסה של שירות אחד (או חמישה) – לא משביתים את המערכת שלכם, אם הם לא קריטיים ל flows המרכזיים של המערכת. שירות קרס בצורה בלתי צפויה? יש memory leak? מכיוון שכל שירות רץ כתהליך נפרד של מערכת ההפעלה – הוא עשוי לא להשפיע על השירותים האחרים. קריסה של קוד ב Layered Architecture / Monolith – סביר יותר שתגרום להשבתה.
  • אפשור / קידום שכתוב הדרגתי של המערכת.
    אם למשל, החלטתם לעבור מרובי לסקאלה (עוד מופע של “Ruby doesn’t scale“) או מ Framework אחד ל Framework שני – המשמעות בארכיטקטורה Layered היא דרמטית: עצירה ארוכה בכדי לאפשר שינוי שכזה.
    מצד שני, זה הרבה יותר פשוט בארכיטקטורת מיקרו-שירותים: ניתן להתחיל להעביר שירותים ל Stack החדש בזה אחר זה – וחלק מהם לא להעביר לעולם. רק שימו לב שאתם לא נגררים לתפעול של stacks רבים רק כי “אפשר”, או מחוסר תשומת לב – זו טעות קלאסית.
    תכונה זו של הארכיטקטורה היא שימושית במיוחד כאשר אתם חיים בעולם שבו המערכת שלכם נמצאת ב Refactoring תמידי (כי העסק דינאמי ומבנה השירות הישן כבר לא עושה את העבודה אחרי שנה-שנתיים…).
  • Scalability של ה production – מכיוון שאתם יכולים “לשכפל” ו/או לבצע אופטימיזציות על שירות X מבלי להתמודד עם שכפול או השפעות הביצועים של שירות Y שמתקשה להשתכפל. בעיות Scalability מטופלות בשיטה “פרה, פרה”.
    נושא זה הוא גם, בד בבד, חולשה של מיקרו-שירותים, נסביר מיד.
באופן טבעי, יש לארכיטקטורת מיקרו-השירותים גם כמה חולשות:
  • קושי ב monitoring ושחזור בעיות ב production. כאשר יש לכם עשרות (שלא לדבר על מאות) שירותים שונים – קשה יותר לשחזר בעיות ולנתח. למשל: לאסוף ולסנכרן לוגים של שירותים שונים, במיוחד אם כמה שירותים התעדכנו לגרסאות שונות מאז. צפו השקעה בניהול והפצה של ה session id בין כל הקריאות. מיקרו שירותים “מפרקים את התמונה” להרבה תמונות קטנות, וכדי להבין מה קורה במערכת – יש להשקיע ב “להרכיב את התמונה בחזרה”.
  • Scalability של ה production, ויעילות בכלל – מכיוון ששירותים שיכלו עד עכשיו לתקשר בזיכרון, מתקשרים כעת על גבי HTTP  – לכל קריאה נוסף overhead מסוים. מכיוון שיש הרבה מיקרו-שירותים, שיגרמו להרבה קריאות וה overhead הזה ילך ויגבר.
    פעמים רבות, ה overhead הזה מחייב מעבר ל I/O אסינכרוני – מה שמסבך את הקוד.
  • Refracting יכול להיות מורכב – אם הוא חוצה-שירותים.
  • Operational Complexity – בעוד monitoring הוא האתגר הראשון, הוא לא האחרון. תפעול של שירות בודד אולי הוא קל יותר, אך לתפעל מערכת של מאות שירותים (גרסאות, היכן רצים, כיצד לשדרג) – היא משימה לא קלה לכל הדעות. אומרים שאימוץ של מיקרו-שירותים מעביר מורכבות מהפיתוח ל Operations. סביר להניח שאימוץ ארכיטקטורה של מיקרו-שירותים תאלץ את גוף ה Operations שלכם “לעלות מדרגה” מבחינת היכולות שלו. אם הוא לא יצליח – זה הולך להיות כואב מאוד…

באופן מעט מפתיע, המונח Microservice Envy נוסף כמשהו שכדאי להיזהר ממנו, בדו”ח של Technology Radar Jan2015 של חברת Thoghtworks. למה מפתיע? כי Micro-Services הוא משהו שמדברים עליו קצת יותר משנה, אז הגענו מהר למדי למצב בו מתריעים משימוש יתר / שימוש ללא הבנה מספיקה. הנה הציטוט המדוייק:

We remain convinced that microservices can offer significant advantages to organizations, in terms of improving team autonomy and faster frequency of change. The additional complexity that comes from distributed systems requires an additional level of maturity and investment. We are concerned that some teams are rushing in to adopting microservices without understanding the changes to development, test, and operations that are required to do them well. Our general advice remains simple. Avoid microservice envy and start with one or two services before rushing headlong into developing more, to allow your teams time to adjust and understand the right level of granularity.

אז מה ההבדל בין מיקרו-שרותים ל SOA?

זו כנראה השאלה השנייה הכי נפוצה ששמעתי בכל דיון על מיקרו-שירותים (הראשונה היא: “אז מה הם בעצם מיקרו-שירותים?”).

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

הדרך היחידה בה אני יכול לענות על השאלה הזו, היא להניח כל פעם על פרשנות שונה של “SOA” – ואז לנסות ולענות.

אם SOA עבורכם הם SOAP ותקני ה *-WS
אז התשובה היא פשוטה: אתם יכולים להשתמש ב *-WS גם במיקרו שירותים, אבל סביר שתקורת הפיתוח ותקורת הביצועים – יהרגו אתכם.
אחת מהנחות היסוד של *-WS היא “coarse grained services” – וזה היהפך המוחלט בהנחת היסוד של מיקרו-שירותים שהם קטנים. חוץ מזה – אין ספק שיש גם הרבה עקרונות משותפים.

אם SOA עבורכם היא ניהול (“mix and match”) של שירותים בדמות ESB או CMDB
ESB (קיצור של Enterprise Service Bus) הוא הרעיון בו ניתן יהיה לעשות שינויים במערכת לא בקוד, אלא בקונפיגורציה – וכנראה ע”י power business users. יהיו הרבה שירותים שעושים פעולות טכניות (חישוב עלות טיסה, רישום הזמנה, וכו’) – אך כל flow בעל משמעות יורכב בעצם ב ESB ע”י חיווט של השירותים הנתונים בקונפיגורציות שונות.

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

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

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

אם SOA עבורכם הוא מידול של המערכת לשירותים stateless ופונקציונליים
כלומר: אתם מאמינים שניתן לחלק את המערכת לפונקציות (“רכיב לכל משימה”) – ולא דווקא ע”פ חלוקה OO. את השירותים עצמם – ניתן כמובן לפתח ב OO. כמו כן – אי-תלות מובנה בין השירותים.

במקרה כזה, ההבדל בין SOA למיקרו-שירותים הוא סמנטי, כלומר: יש להתאמץ מעט בכדי למצוא אותו. לא בכדי קוראים אנשי המיקרו-שירותים לפרשנות כזו של SOA בשם “SOA done right” (דוגמה, במסגרת הכתומה).

ההבדלים שניתן למצוא (כי חיפשנו) הם:

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

האנטומיה של מיקרו-שירות

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

עד עכשיו סיפקתי סקירה high level של הנושא – אך יש עוד פרטים רבים שהייתי רוצה לגעת בהם.
למשל: כיצד מתחילים? כיצד “מפרקים” מערכת למיקרו-שירותים (להזכיר: MSA = Micro-Services Architecture)?

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

על השירות להיות self-contained: להכיל את ה deployment script העצמאי משלו, את הפרוייקט ב IDE משלו, את הבדיקות שלו, את התלויות שלו (ספריות צד-שלישי, בגרסאותיהן השונות) – וכו’.
מבחן טוב לעצמאות הזו היא מפתח חדש שמקנפג סביבה רק עבור השירות הזה, פותח רק אותו ב IDE – ומצליח לעשות שינויים ולהעביר אותם ל production מבלי להסתבך. השאיפה היא ליצור מערכת גדולה – מתוך הרבה רכיבים קטנים ופשוטים, ושפיתוח של כל רכיב קטן שכזה – יהיה פשוט כמו כתיבה של רכיב פשוט עצמאי (נאמר: תרגיל בקורס באוניברסיטה).

השלכה משמעותית של רעיון זה, הוא שאת השירות צריכה לכתוב קבוצת אנשים שיכולה לבצע את כל המטלות הנדרשות מהם (UI, Operations, DB, ולוגיקה עסקית). ע”פ Conway’s law, אם בארגון שלכם יש “צוותי UI”, “צוותי DB”, ו”צוותי server” (ולא “צוותים פונקציונליים”) – אזי סביר שהיישום של השירותים אצלכם יהיה: שירותי UI, שירותי DB, ושירותי Server – שזה נחשב מידול לא טוב של מיקרו-שירותים. ייתכן ויהיה עליכם לשנות את המבנה הארגוני בכדי להגיע ל MSA מוצלח.

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

הנה כמה תשובות לדילמות נפוצות. שימו לב שהן רק guideline – מכיוון שהן נענות “למקרה הממוצע”, ולא לכל מקרה באשר הוא.

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

   האם על כל שירות להיות מנוהל ב Git Repository עצמאי?
זה עניין של טעם. למשל: גוגל מנהלים 90% מהשירותים שלהם ב repository יחיד. Git repository לכל שירות – גם הוא סביר בעיני.

   מה עושים עם ספריות שכל השירותים צריכים? למשל Logging או אבטחה?

מה שאתם עושים עם כל ספריית Open Source שאתם משתמשים בה הרבה – הוסיפו אותה כ dependency לכל השירותים שזקוקים לה. זה אמנם לא שירות – אבל לא כל הקוד חייב להתמפות לשירותים, יכולים להיות גם “סתם רכיבים לשימוש חוזר”. הרגישו נוח לייצר וריאציה שלכם על דפוס הארכיטקטורה.
אפשר לדמיין מיקרו-שירות שעובר ל production כתא חי: יש את פנים התא שהוא ייחודי, ועוד קצת infrastructure שיש לשלוח עם כל תא ל production בכדי לקיים אותו. וכן – יהיו הרבה instances של השירותים הללו חיים (מה שמקשה על ניטור ו root cause analysis).

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

   מה??? יהיו לי במערכת גרסאות שונות של 3rd Parties? יהיה עלי להתמודד גם עם באגים של SpringFW 3.0.1 וגם עם אלו של SpringFW 4.1.2?
ובכן – היכולת להשתמש בגרסאות שונות של ספריות הוא דווקא יתרון גדול של MSA. כשיש מערכת גדולה ועותק יחיד של הספריה – אנחנו נאלצים לרוב “להיתקע” עם גרסה ישנה לאורך זמן, או לצאת ל”מסע צלב” בכדי לשדרג אותה. הפחד מ”ריבוי באגים” הוא לא תמיד כ”כ ריאלי. בכל מקרה – ברור שיש תקורה בשימוש במספר רב של גרסאות של אותה הספרייה – הייתי ממליץ לנהל “רשימת גרסאות מאושרת” שניתן יהיה לבחור רק ממנה במוצר. לאזן בין גמישות לשליטה.

יש מצבים בהם יש בעיה טכנית להשתמש בגרסאות שונות של אותה הספרייה (למשל: כאשר השירותים הם לא תהליכים שונים של מערכת ההפעלה אלא למשל wars ב JEE application server, או שימוש ב jQuery כאשר UI של שירותים שונים מוצג זה לצד זה על אותו דף של דפדפן. מה עושים? מתפשרים, או מחליפים טכנולוגיה – כמו תמיד.

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

הייתי מסכם זאת כך: RDBMS (בסיסי נתונים רלציוניים) הם בעלי התאמה גבוהה לארכיטקטורת Layers, אך בעלי התאמה פחות טובה – ל MSA. אם אתם עובדים עם MSA (או רק CD) – הבעיות יהיו קטנות יותר אם תעבדו עם בסיסי נתונים שהם schema-less כמו K/V DB או Document DB.
האמת שהיכולות המתקדמות של RDBMS עודדו הפרות משמעותיות של עקרונות ארכיטקטוניים חשובים (הכמסה, שבירת ה Layering שהוגדר, וכו’). איך קיבלנו זאת כל השנים? – זה פשוט עבד.

   איך מנהלים Transactions אם הנתונים שלנו מבוזרים לטבלאות שונות?
פשוט לא עושים. לא מגיעים ל Internet Scale כשעובדים עם ACID. נוקטים מדיניות של Eventually Consistent.

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

   כיצד שירותים מתקשרים זה עם זה? כיצד הם יודעים על קיומו אחד של השני?
בדרך כלל לכל שירות יש URL וניתן להשתמש ב DNS ו Load Balancer בכדי לנהל כמה עותקים – ולמצוא עותק זמין. הכי נפוץ הוא להשתמש בתקשורת REST, הרי – למה להמציא את הגלגל מחדש. אפשר גם על גבי TCP/UDP, אולי RPC או Thrift – הכל ע”פ הצרכים שלכם. בד”כ יהיו לכם גרסאות שונות של ה APIs של השירות, וכמה גרסאות של API שיחיו במקביל – עד שתוכלו באמת להשבית אותם.

דפוס נפוץ בארגון APIs של מיקרו-שירותים הוא ה API Gateway (מסמך: כיצד נטפליקס עושים זאת): שירות נוסף שכל תפקידו לקבל בקשה מה Client (ב URL אחד וב HTTP request אחד) – ולקרוא לכמה שירותים, להרכיב את התוצאות, ולהחזיר הכל באותה הקריאה. שירות זה הוא סוג של Facade שיסתיר מ clients חלק מהשינויים במבנה השירותים (פיצול, שינויי API, וכו’)

תקשורת על גבי HTTP היא סינכרונית, ובהחלט אפשר למצוא שירותים שמשתמשים ב Messaging (למשל AMQP) בכדי לתקשר בין השירותים. אפשר לראות פעמים רבות שימוש בדפוס הארכיטקטורה CQRS בהקשר זה כאשר מפעילים שירות שלא משנים את ה persistence בצורה סינכרונית, אך שירותים שיגררו “כתיבות” בעזרת הודעות א-סינכרוניות.

עקרון ההפרדה (isolation) הוא מספיק חשוב בכדי להצדיק תקשורת דרך הרשת. מצד אחד – זה מוסיף ל resilience של המערכת, מצד שני זו תקורה ברורה בפיתוח ובזמן הריצה.

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

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

   האם אין תשתית כזאת, שתפתור לי את כל הבעיות שנובעות מ MSA – ותשאיר אותי עם כל התהילה, והיכולת להתמקד בכתיבת הקוד העסקי?
יש כל-מיני ניסיונות. יש את סנסה ל node.js, את rodent לרובי, או DropWizard של Yammer לעולם ה JVM. משום מה כולם עוסקים בפיתוח של המיקרו-שירותים (החלק הקל) ולא בתפעול שלהם (החלק היותר מורכב).
בכל מקרה… אם אתם קוראים את הבלוג – אתם אמורים לדעת מה דעתי האישית בנושא.

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

Bounded Contexts

רעיון שחוזר על עצמו שוב ושוב בהקשר ל MSA הוא רעיון ה Bounded Context, רעיון של מתודולוגיית ה Domain-Driven Design (בקיצור: DDD) של אריק אוונס. הרעיון מזהה שה Domain Model שלנו (תיאור האובייקטים בעולם והקשרים ביניהם. לא בתוכנה – ב”ביזנס”) הוא לא אבסולוטי – אלא תלוי הקשר.

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

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

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

בכל מודל תחום (BC) יש רק את אוסף האובייקטים והתכונות שרלוונטיים לעולם הזה. המתכנתים ואנשי הביזנס מתקשרים על עולם מושגים שהם יכולים להכיל ולשלוט בו. הגבלנו בכוונה את “טווח הראיה” שלהם לטווח ניתן לשליטה.

מה קורה כאשר אותו אובייקט (נאמר הזמנה) קיים באופן שונה במודלים שונים? למשל “הזמנה” היא בעלת תכונות מסוימות ב BC של אנשי המכירות, ובעלת תכונות שונות ב BC של הנהלת חשבונות?

יש לכך כמה פתרונות: אפשר לנהל Shared Context שמכיל את ההזמנה ומשותף לשני העולמות, ניתן לעשות “המרה” של אובייקט מ BC אחד ל BC שני, אפשר שה BC יראה רק view – אבל בבסיס הנתונים (שכבה נמוכה יותר) יהיה מודל שהוא aggregation של כל התכונות מכל המודלים, וכו’ וכו’.

הקשר בין BC ל MSA הוא פשוט: אותן בעיות שהתמודדו איתן במשך שנים ב DDD, ובפרט בעקבות רעיון ה BC – צצות עכשיו בעולם ה MSA. בשני העולמות מאמינים שחלוקת המודל למודלים מקומיים וקטנים / פשוטים יותר – היא הדרך הנכונה להצליח.

 
 
 
 
 

בדיקות

בעוד אנו חווים ב MSA עלות לינארית להוספת שירותים (דבר טוב!), קל להגיע מהר לעלות מעריכית בכתיבת וניהול בדיקות אינטגרציה. אנו רוצים לבדוק את כל השירותים בכל התסריטים, בכל deploy – ויש יותר ויותר כאלו.
דווקא בדיקות יחידה (פשוטות, או של קבוצות של מחלקות) – הן דווקא פשוטות ועובדות היטב. מקסימום “זורקים” כמה Mock Objects לדמות את השירותים בהם השירות שנבדק תלוי – וזה אמור להסתדר בצורה קלה.
הבעיה היא יותר בבדיקה של אינטגרציה של כמה שירותים. כיצד והיכן מרימים סביבה? היכן מנהלים את הקונפיגורציה? האם יש לבדוק גם את התסריטים שתלויים בשירות שלנו – או רק את ה API של השירות עצמו? האם לבדוק עם שירותים אחרים חיים או רק mocks שלהם?
השימוש ב MSA פותח כל מיני שאלות על Testing שלא היינו צריכים להתמודד איתן קודם לכן.
טובי קלמסון ניסה לסכם את כל אפשרויות הבדיקה במצגת דיי מקיפה. קשה לי לתמצת את מה שהוא אמר – כי הוא חוזר על הרבה רעיונות מוכרים ומנסה להתאים אותם לסביבה של MSA:
  • נסו להריץ כמה שיותר בדיקות בזיכרון / עם mocks – שירוצו מהר.
  • כתבו יותר בדיקות יחידה ואינטגרציה ופחות בדיקות End-to-End שקשה לתחזק (“פירמידת הבדיקות”).
  • בדקו לוגיקה בעזרת בדיקות יחידה, וקוד יותר “משעמם” עם בדיקות אינטגרציה או בדיקות-רכיב (בעיה: יש פרשנויות שונות לכמעט כל סוג בדיקה שנציין).
גישה אחרת (אציין: מקובלת) היא להשקיע פחות בבדיקות אינטגרציה – ויותר בניטור המערכת והיכולת לבצע root cause analysis מהיר. במקום prevention (בדיקות) – להתמקד בטיפול הבעיה ברגע שהיא צצה. יש הגיון כלכלי ברור לגישה זו, אם כי לוקח זמן להגיע לרמה “מספיק טובה” של ניטור והתאוששות בכדי שנסמוך על המערכת הזו להחליף את תפקידם של בדיקות האינטגרציה.
אם אתם יודעים לכתוב בדיקות אוטומציה – כנראה שתוכלו למצוא את הרכב הבדיקות שיעבוד למערכת שלכם. פשוט קחו בחשבון זמן לתכנון מחדש של מתודולוגית הבדיקות שלכם בעקבות MSA.

סיכום

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

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

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

החלטנו לעבור למיקרו שירותים – אבל הייתה הסתייגות רבה מניהול תהליכים נפרדים המתקשרים ב REST, בעיקר בגלל שזה קוד רב “ומשעמם” שיש להוסיף למערכת. המערכת כיום (כמו רוב מערכות ה Layered Architecture) מלאה בקשרים רוחביים בתוך ה Layers שקשה מאוד לנתק. באמת התרנו לעצמנו לקרוא מכל מקום ב Layer לכל פונקציה אחרת ב Layer – מה שגרם לריבוי תלויות. כמובן שגם ברמת ה DTO בעיות רבות נפתרו בעזרת “joins” – מה שקשר את הפונקציות השונות של המערכת גם בשכבה זו. ניתוח תלויות ראשוני נראה קצת מבהיל במחשבה שכל תלות הופכת לקריאת HTTP.

לכן…החלטנו לוותר על התכונה של “isolation by process” של MSA – ויצרנו וריאציה קצת חדשה: כל שירות מקבל פרויקט נפרד (pom.xml במייבן משלו) עם build משלו, בדיקות משלו, וכו’ – והוא נארז לתוך קובץ jar. (המערכת כתובה בשפת ג’אווה + UI בשפת ג’אווהסקריפט). את כל השירותים אנו אח”כ אורזים לתוך קובץ war. גדול – ואז עושים deploy.

כיצד אם כן עושים deploy לשירות בודד?
שומרים תמיד את קובץ ה war. האחרון שעבר deploy, מעדכנים רק את קבצי ה jar. של השירות הרלוונטי (הוא עצמו + תלויות ישירות) – ועושים deploy מחדש.

איך מוודאים isolation בין השירותים?
הגדרנו שבכל שירות יהיה package מיוחד בג’אווה בשם facade המתאר את הממשק החיצוני של השירות – ורק לו שירותים אחרים יכולים לקרוא. אנו מתכוונים לאכוף התנהגות זו בעזרת JDepend שירוץ בעת ה build ויפיל אותו בעת חריגות.
בעת build של כל שירות – ה jar. שנוצר מתעדכן ב maven repository של הארגון, ושירותים אחרים שתלויים בו פשוט מגדירים תלות ל jar הזה ומקבלים גרסה עדכנית בעת ה build שלהם.

ל facade יכולים לקרוא שירותים אחרים או שכבה שנמצאת בתוך השירות (בשם webaccess) – במידה והשירות גם זמין על גבי HTTP.

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

עד כמה וריאציה זו של MSA מוצלחת? ייקח זמן לדעת. בוודאי נעשה עליה עוד שינויים והתאמות.

אז… אל תפחדו ללמוד את החוקים, להבין אותם – ואז לשנות אותם קצת.

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

—–

לינקים מעניינים

תיאור של חברת SoundCloud על האתגרים במעבר מאפליקציה אחת גדולה (“Monolith”) לארכיטקטורה של מיקרו שירותים. מומלץ!
חלק א’: http://goo.gl/1ciqbS, חלק ב’: http://goo.gl/31Gmy7, חלק ג’: http://goo.gl/0aeq3u.

פוסט ידוע, ומצוטט רבות, של ג’יימס לואיס ומרטין פאוולר על מיקרו-שירותים: http://martinfowler.com/articles/microservices.html.
גם שווה קריאה. הם עוסקים בכמה היבטים בהם לא נגעתי בפוסט.

הקלטה של סשן מ Buraco 2012 של Fred George על מיקור-שירותים מיקרו-שירותים: https://www.youtube.com/watch?v=2rKEveL55TY

יש גם מצגת של ג’יימס לואיס בנושא (איך לא?): http://www.infoq.com/presentations/Micro-Services

מצגת מעניינת: http://www.slideshare.net/fuglylogic/microservices-26369481

פרק בפודקאסט “Software Engineering Radio” שעוסק במיקרו-שירותים: http://www.se-radio.net/2014/10/episode-213-james-lewis-on-microservices/