להבין Virtualization

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

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

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

  • זיכרון וירטואלי – הוא מימוש של וירטואליזציה לזיכרון. הוא מבצע multiplexing (חלוקת משאב פיסי אמיתי לכמה תהליכים אורחים במקביל) וגם emulation (בכך שהוא מנהל כתובות מדומות של הזיכרון הוירטואלי עמם התהליכים האורחים עובדים).
  • מנגנון ה LVM של לינוקס (קיצור של Logical Volume Manager) הוא דוגמה לוירטואליזציה של הדיסק. האופי שלו הוא אחר: הוא מבצע aggregation בכך שהוא גורם לכמה דיסקים פיסיים להראות כמו דיסק לוגי יחיד. זו עדיין וירטואליזציה.
  • מנגנון ה NAT (קיצור של Network Address Translation) של פרוטוקול IP, המשמש רכיבי רשת כמו proxy או reverse proxy בכדי להסתיר כתובות רשת אמיתיות של מחשבים – הוא דוגמה לוירטואליזציה של הרשת.
  • X-Server או VNC של לינוקס / Remote Desktop של חלונות – הם דוגמאות לוירטואליציה של ה Desktop. זהו עולם דיי עשיר עם פתרונות כמו App-V של מייקורוסופט ש"מזרים" אפליקציה שרצה בשרת מרוחק – למחשב השולחני המקומי (זו בעצם Application Virtualization).
בפוסט זה ארצה להתמקד בויאטואליזציה של החומרה כולה (זיכרון, דיסק, רשת, וכו') – מה שרוב האנשים מכירים פשוט כ "Virtual Machine" או "VM".

במהלך הפוסט אנסה לענות על שאלות כגון:

  1. כיצד, בפועל, טכנולוגיות הוירטואליזציה חוסכות לארגון ה IT כסף רב?
  2. מה ההבדל בין hypervisors מ"טיפוס 1" ל hypervisors מ"טיפוס 2"? וכיצד בוחרים את העדיף לסיטואציה?
  3. מהי Paravirtuallization ומהם Linux Containers (שעושים כ"כ הרבה רעש בשנתיים האחרונות)??
  4. למה לקח כ"כ הרבה זמן לויטרטואליזציה לתפוס מקום מרכזי בתעשייה? אתם יודעים, כמעט 40 שנה?
  5. מי הם Popek ו Goldberg – ומה הקשר שלהם לכל הסיפור?

מוטיבציה

אנסה להעביר את המוטיבציה בעזרת סיפור קצר. בסיפור שלנו יהיה ארגון עם Data Center שכולל כמה שרתים שונים:

  • שרת קבצים (shares)
  • שרת ווב
  • שרת e-commerce
  • שרת דואר (Exchange)
(לצורך פשטות הסיפור אתאר רק 4 שרתים. בסביבה אמיתית סביר שיהיו הרבה יותר).

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

  • Sandboxing: תקלה בשרת הדואר יכולה לגרום לקריסה של מערכת ההפעלה – ולא רוצים ששרת הווב או המסחר האלקטרוני לא יהיו זמינים בגללו. אולי שרת הדואר דורש עדכון של מערכת ההפעלה שגורם לבעיה חדשה בשרת הקבצים?
  • אמינות: כאשר כל שרת רץ על עותק שלו של מערכת ההפעלה – פשוט יש פחות תקלות. שני שרתים דורשים הגדרות תצורה מעט שונות – שלא אופטימליות או מוכרות עבור תוכנת השרת השנייה.
המציאות בפועל הייתה שאנשי ה IT דרשו מהארגון "קופסה לכל שרת" כתנאי עבודה בסיסי.
הקצאה של "קופסה לכל תוכנת שרת" סיפקה את היתרונות המצופים:
  • זמינות: שדרוג / עדכון של תוכנת שרת אחד, לא משפיעה על שרתים אחרים.
  • דרישות סביבה שונות: שרת הדואר רץ על "חלונות", שרת המסחר על איזו גרסה של UNIX ושאר השרתים רצים על לינוקס בכלל. דרישות חומרה בד"כ אפשר לכנס בקלות רבה יותר.
  • יתרונות אבטחה: קבלת הרשאות root על מחשב אחד, לא משפיעה על הגישה למחשבים האחרים.
כל זה טוב ויפה – אבל יקר.
יקר בכלל הכפילות של החומרה שחלק גדול מהזמן נמצאת ב Utilization נמוך. אם שרת הווב, דורש לשעה בחודש כח עיבוד X, יש לספק לו כח עיבוד זה גם אם שאר החודש הוא זקוק לכח עיבוד של X/12 או אפילו X/60.
בנוסף: כשקונים שרת פיסי עבור תוכנה חדשה – כמה חומרה יש לקנות? האם מתכננים לחודש הראשון של השימוש או לשנה אח"כ? בתעשייה נוצרה פרקטיקה בשם Sizing שמטרתה להעריך כמה חומרה תידרש, פרקטיקה מאוד לא מדויקת בה טעות של עד פי 2 – נחשבת כהצלחה.
שרתים פיסיים שרוב זמנם עובדים על כ CPU 10% – היו תמונה מקובלת.

לעלות החומרה יש עלויות נגזרות נוספות:

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

הערך של וירטואליזציה

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

מה קורה כאשר השרת הפיסי כושל? האם לא פגענו בזמינות של השרתים שלנו? ברגע שחומרת המחשב המשותף קורסת – יושבתו מיידית כל ה VMs שרצים עליה!!

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

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

אתחול של VM הוא מהיר מאתחול של שרת פיסי, מה שמפחית את ה Mean Time To Recover (בקיצור: MTTR) של המערכת. האתחולים המהירים (בעת תקלות תוכנה) מתקזזים עם תקלות החומרה (הנדירות יחסית) של חומרת השרת המשותף – כך שבדרך כלל, וירטואליזציה דווקא מוסיפה לזמינות של כלל המערכת.

הרצה של תוכנה על וירטואליזציה מספקת יתרונות נוספים:

  • היכולת "להרים" שרתים בקלות מפשטת את עבודת ה IT.
  • בזמן שדרוג, ניתן בקלות להרים VM עם גרסה חדשה יותר של התוכנה, במקביל לגרסה הישנה. ניתן להפעיל את המערכות זמן-מה במקביל ו"לכבות" את הגרסה הישנה רק לאחר שנצפתה עבודה תקינה של הגרסה החדשה.
  • שיפור הזמינות ע"י חלוקה: אם ניקח מערכת שהיא easily scalable, למשל שרת ווב, ובמקום VM אחד נציב שני VMs שלו על אותה מכונה פיסית – נקבל זמינות משופרת. אם VM אחד קרס בשל בעיית תוכנה, ה VM השני יכול להמשיך ולספק שירות. כל זאת – עם אותה החומרה (אבל קצת יותר זיכרון).
  • Checkpoints: טכנולוגיית הוירטואליזציה מאפשרת לקחת snapshot של VM מסוים, ואז לשמור את ההעתק בצד – או לשכפל אותו ל VM נוסף. יכולות אלו מקלות מאוד על עבודת ה IT. יכולות אלו מקלות מאוד גם על פיתוח תוכנה: לבדוק את התוכנה על מערכות הפעלה שונות בגרסאות שונות, או מפתח אחד שמתקין את הגרסה החדשה ביותר – ומשכפל אותה שתהיה זמינה לכל חבריו לצוות.

איך וירטואליזציה עובדת

הסביבה שמריצה וירטואליזציה של מכונה נקראת Virtual Machine Monitor (בקיצור: VMM) או Hypervisor. מקור השם Hypervisor טמון בהרשאות מערכת ההפעלה שניתנות לה: בעוד ההרשאות הגבוהות במערכת ההפעלה שייכות למשתמש העל (supervisor) – ל VMM נדרשו הרשאות גבוהות אפילו יותר, דרגה מעל ה supervisor, כלומר hypervisor.

הציפייה מ Hypervisor היא שהוא ימלא 3 דרישות עיקריות:

  1. Fidelity – התוכנה תרוץ על ה VM בדיוק כפי שהיא רצה על מערכת הפעלה שמותקנת ישירות על החומרה.
  2. Safety – תוכנה שרצה על VM תהיה מבודדת מתוכנה שרצה על VM אחר על אותו מחשב פיסי. פעולות שיעשה VM אחד (ניהול משאבים של מערכת ההפעלה, למשל) – לא יפגע בפעילות התקינה של ה VM השני.
  3. Efficiency – התוכנה על ה VM לא תרוץ "הרבה יותר לאט" מאשר על הרצה ישירות על החומרה הפיסית.

מקורה של הגדרה זו היא במסמך שפרסמו עוד ב 1974 Popek ו Goldberg – חוקרים אמריקאים שהיו חלוצים בנושא הוירטואליזציה. הם גם הגדירו את התנאים שמאפשרים וירטואליזציה. בגדול הם הגדירו 2 קבוצות של פקודות המעבד (instructions):

  • sensitive – פקודות שמבצעיות פעולות IO.
  • privileged – פקודות שיגרמו ל trap אם נקראו ב user mode.

כידוע לכם (אני מניח), למעבדים יש 2 מצבי עבודה: user mode ו kernel mode. ה user mode נועד לתוכנות שרצות על מערכת ההפעלה (ולחלקים של מערכת ההפעלה) וה kernel mode נועד ל kernel של מערכת ההפעלה ורק בו ניתן לגשת ל I/O או לזיכרון ישירות. לכל mode יש סט פקודות זמין מעט שונה. אם פעולה של kernel mode נקראת ב user mode מתרחש אירוע בשם trap שגורם למעבד לעבור מצב ל kernel mode ואז להפעיל את הפקודה המבוקשת. מנגנון זה מאפשר לתוכנה לרוץ באופן ישיר על המעבד, בלי מעורבות של מערכת ההפעלה, ועדיין למערכת ההפעלה – לשלוט על כל הגישות לחומרה.

Popek ו Goldberg הראו ש Fidelity בוירטואליזציה יתרחש רק כאשר הפקודות שהן sensitive הם subset של הפקודות שהן privileged (מצב שלא היה קיים במעבדי אינטל – נושא עליו נדבר בהמשך).

דרך פשוטה ראשונה למשמש hypervisor היא כ interpreter שיפרשן כל פקודת שפת-מכונה של הקוד שרץ, ויחליט מה "בטוח" ולשלוח למעבד / מערכת ההפעלה – ובמה הוא רוצה להתערב ולשנות אותו. למשל: פקודת INC (כלומר: Increment) היא בטוחה, פקודה להשבתה של interrupt מסוים מהחומרה – היא לא, כי ייתכן שה interrupt הזה נועד ל VM אחר. בעזרת interpreter נוכל להשיג את דרישות הבטיחות והבידוד – אבל הביצועים יהיו גרועים.

דרך יותר מעשית, והיא בעצם הדרך המקובלת היא להגדיר את ה Hypervisor כ"מערכת ההפעלה של מערכת ההפעלה":

הדוגמה הספציפית היא ל type1 hypervisors אבל העקרונות נכונים גם עבור type 2 hypervisors עליהם נדבר בהמשך.
ה hypervisor "משתלט" על ה kernel mode של המעבד – כי עליו לתפוס את כל הגישות לחומרה, ומריץ את מערכת ההפעלה המתארחת (Guest OS) ב user mode. פנימית עושים הבחנה בין מערכת ההפעלה שזוכה ל virtual kernel mode (כלומר: מדמים לה שהיא רצה ב kernel mode – למרות שהיא לא) ול virtual user mode – מה שמערכת ההפעלה המתארחת מצפה שיהיה user mode.
הערה: במעבדי אינטל ספציפית יש 4 modes של ריצה שנקראים ring 0 (המקביל ל kernel mode) עד ring 3 (המקביל ל user mode). מימושים רבים של VMM מעל ארכיטקטורת אינטל פשוט משתמשים ב ring 1 בכדי לתאר את ה virtual kernel mode, כאשר virtual user model פשוט נותר ring 3. יש כאלו שמתארים את ה hypervisor כ ring -1 – אבל לא נכנס לפינה הזו בפוסט….
כאשר הקוד ב virtual kernel mode (שחושב שהוא ב kernel mode) מפעיל פקודה שהיא privileged אזי יש trap שמעביר את השליטה ל hypervisor.
כאשר הקוד ב virtual kernel mode מפעיל פקודה שהיא sensitive אבל לא privileged (כלומר: לא גורמת ל trap) אזי תהיה שגיאה וסביר שמערכת ההפעלה תקרוס. אאוץ!

מצב זה (בו יש פקודות sensitive שניגשות לחומרה, אך לא privileged – לא גורמות ל trap) היה קיים בכמה מארכיטקטורות המעבדים הזמינים בעולם, בניהן הארכיטקטורה הנפוצה ביותר בעולם – i386 של אינטל. אחד מהערכים של אינטל הוא תאימות לאחור (שנוכל על המעבד החדש להריץ אותן תוכנות שרצו על מעבדים ישנים) – מה שגרם למצב בו ארכיטקטורה זו שלא מתאימה לוירטואליזציה השתמרה במשך כמעט 30 שנה. ואיפה סביר יותר שתצמח טכנולוגיית הוירטואליזציה – על מחשבים פשוטים וזולים (אינטל) או מערכות מחשבי-ענק של IBM?

הבעיה נפתרה לבסוף בשנת 2005 כאשר אינטל, בשיתוף פעולה לא-שגרתי עם AMD, הוסיפו סט פקודות למעבד שפותר את המצב. באינטל מעבדים אלו מסומנים כ VT (כלומר: Virtualization Technology) וב AMD השם הוא AMD-V – אבל בעצם זה אותו הדבר.

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

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

מה עשו בעולם האינטל עד הופעת ה VT (ספציפית VT-x)?
חברת VMWare הציעה פתרון וירטואליזציה כבר בשנת 1999. הפתרון של VMWare סרק את קוד המכונה שנטען ל VM ושינה אותו כך שלא יקראו פקודות sensitive ב user space. אולי זה נעשה ע"י הרצת פקודה privileged תפלה קודם לכן – אולי בדרכים אחרות. פתרון נחשב פחות יעיל – אך הוא אפשר להריץ וירטואליזציה על מעבדי אינטל ללא VT (לא היו כאלה ב 1999).

חלק גדול מפתרונות הוירטואליזציה היום (VMWare, VirtualBox, KVM, וכו') יודעים לעבוד גם עם VT-x וגם בלי יכולת זו – ואז הם מבצעים fallback של שכתוב קוד המכונה למצב שמאפשר וירטואליזציה.

הפתרון לשכתב את קוד המכונה לאי-שימוש בפקודות sensitive נחשב (מסיבות הגיונית) לפוגע Efficiency של הקוד הרץ על ה VM.
אם תשאלו בפורום מקצועי את השאלה: "מה קורה אם אני מריץ את ה VMM שלי ללא VT-x", סביר שתשובה שתקבלו היא "אז ה VM יזחל כמו עגלה!".

שני חבר'ה מ VMWare הראו במאמר שפרסמו ב 2006 שמסקנה זו איננה כ"כ נכונה. לכתוב קוד המכונה לפקודות לא sensitive יש אמנם עלות, אך הוא מפחית את השימוש ב traps במעבד. אירועי ה trap במעבד גורמים ל context switch שמייתר חלק מה caches של המעבד ומשבש חלק ממנגנוני ה prediction, והפחתה שלהם – יכולה להאיץ את הריצה.

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

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

סוגי Hypervisors

Goldberg, אותו הזכרנו קודם, הוא גם זה שהגדיר את ההבחנה בין Type 1 Hypervisors ל Type 2 Hypervisors.


Type 1 Hypervisors משמשים כמעין מערכת הפעלה והם התוכנה היחידה שרצה מעל החומרה / kernel mode. הם פשוטים ובד"כ אמינים יותר ממערכות ההפעלה[ב] (תכונה שנגזרת מכך יש להם הרבה פחות שורות קוד), והמפתחים שלהם יכולים לבצע אופטימיזציות שלא ניתן לעשות ב Type 2 Hypervisors. לרוב נראה אותם בסביבת השרת – היכן שאמינות ויעילות חשובות יותר

Type 2 Hypervisors הם תוכנה רגילה שרצה על מערכת הפעלה מסוימת (לינוקס, חלונות, וכו'), אולי במקביל לתוכנות אחרות, ועדיין מספקת הפשטה של החומרה למערכת הפעלה המתארחת. Hypervisors אלו תלויים (לטובה, ולעתים כמגבלה) בשירותים שזמינים במערכת ההפעלה המארחת (Host OS). כאשר אנו כמפתחים משתמשים בתוכנת וירטואליזציה כדי להריץ VM – זהו כנראה Type 2 Hypervisors.

למשל, בעולם של VMWare קל למפות את שני סוגי ה Hypervisors השונים:

  • ESX Server (ה hypervisor של חבילת vSphere) – הוא בעצם Type 1 Hypervisors.
  • VMWare Workstation / Player ו Fusion (למק) – הם בעצם Type 2 Hypervisors.

באופן דומה ניתן לומר שגם VirtualBox (של סאן, עכשיו אורקל) הוא Type 2 hypervisors ו Hyper-V של מייקורוספט נחשב כ Type 1 hypervisor (הוא משולב במערכת ההפעלה).

KVM (קיצור של Kernel based Virtual Machine), שהוא VMM בשימוש נרחב בעולם הלינוקס, הוא שנוי-במחלוקת מבחינת ההגדרה. הוא נארז כחבילה במערכת ההפעלה (כמו תוכנה hosted) ומקבל ממנה שירותים, אך יש לו גישה ישירה לחומרה (כמו type 1 hypervisor) – וכנראה שהוא נהנה מיתרונות דומים. ההגדרה, כמו הרבה הגדרות – היא טובה עד נקודה מסוימת.

Paravirtualization

עוד סוג של Hypervisors שקיימים הוא Hypervisors של Paravirtualization ("מעבר-לוירטואליזציה"). סוג זה של Hypervisors מנסה לשפר את ה Efficiency של הרצת ה VM, על חשבון מעט מה Fidelity. הרעיון הוא שהתוכנה שרצה על ה VM תהיה מודעת לכך שהיא שרצה בוירטואליזציה. ידע זה יתורגם לכמה קריאות API (נקראות Hypercalls) שבהן היא תשתמש במקרים מסוימים – שיאפשרו תהליך יעיל יותר של ההרצה. לרוב, מי שמודע לכך שהוא מתארח על סביבת ה Paravirtualization הוא מערכת ההפעלה המתארחת (ולא התוכנה שרצה עליה) והיא משתמש ב hypercalls לפעולות כמו עדכון הזיכרון הוירטואלי – פעולות שיכולות להיות פשוטות ומהירות יותר במידה והן נעשות בתיאום עם ה hypervisor.

עוד נושא שמנסים להתמודד איתו ב Paravirtualization הוא העניין של "זמן אמיתי". מערכת ההפעלה המתארחת חושבת שהיא רצה על החומרה, ולא מודעת לכך שהיא משתפת את המעבד / I/O עם מערכות הפעלה אחרות – מה שגורם לפער בתפיסת הזמן (time) שלה. אם היא מסנכרת את תפיסת הזמן שלה בעזרת ה API (היעיל) של ה Paravirtualization Hypervisor – בעיה זו יכולה להיפתר.

Hypervisor ידוע שעובד ב Paravirtualization הוא Xen של חברת Citrix, הזמין גם בגרסה חינמית (אך ללא עדכונים אוטומטיים).

Operating System-level virtualization

בנוסף לנושא ה Efficiency של ה VM, שתמיד נמוך מהרצה ישירה על החומרה, עניין שמתסכל את המשתמשים בוירטואליזציה הוא נושא ניצולת המשאבים. הבעיה לא גדולה אם אני מריץ VM אחד על המחשב הפיסי, אך מה אם אני מריץ עשרה?
נאמר שאני מריץ 10 VMs של לינוקס בגרסה זהה – זה אומר שכל השירותים של מערכת ההפעלה רצים 10 פעמים, הקוד שלהם נטען לזיכרון 10 פעמים, ומבני הנתונים שהם מנהלים – מנוהלים 10 פעמים. זהו מחיר ה isolation בין ה VMs – וזה מחיר יקר.

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

את הניסיונות לפתור את בעיית המשאבים – מנסים לפתור ה Linux Containers: להיות משהו יותר רזה מ VMM, אך יותר מקיף מניהול תהליכים במערכת ההפעלה, שה isolation שלהם מוגבל, וחלוקת המשאבים (למשל priority של תהליך) – לא מדויקת.

המסע ל Linux Containers התחיל במנגנון בשם chroot שהוצג על Unix7 בשנת 1979 (כבר אמרנו בפתיחה שוירטואליזציה היא לא דבר חדש). chroot יצר סביבת ריצה מעט שונה לכל תהליך – והתמקד בשיקולי אבטחה בין תהליכים. הוא בעצם שינה את ה root folder של כל תהליך כך שיראה רק אותו שלו של התיקיות, ולא יוכל להשפיע על תהליכים אחרים (ע"י גישה לקבצים שלהם, למשל). הסביבה שנוצרה לתהליך נקראה chroot jail, שם שליווה עוד פתרונות דומים אחרים (למשל FreeBSD Jail – "כלא BSD החופשי" 🙂 )

עם השנים, היכולות התפתחו, והמיצוב נעשה ידידותי יותר: לא עוד jail אלא container.
מערכת ההפעלה לינוקס פיתחה יכולות ברמת הקרנל בכדי לתמוך באותם containers: בהתחלה cgroups (אריזת כמה תהליכים לקבוצה והקצאת משאבים מבוקרת לקבוצה) ולאחרונה LXC (קיצור של LinuX Containers, מבוסס על cgroups).

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

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

תמורת פשרות הללו מקבלים Efficiency כמעט של מערכת ללא וירטואליזציה בכלל, וניצולת משאבים טובה הרבה יותר (דיסק, זיכרון, וכו') מכיוון שרצה רק מערכת הפעלה אחת. ה mitigation לקריסת מערכת ההפעלה הוא בד"כ להריץ על containers יישומים ב cluster על גבי כמה מחשבים פיסיים שונים, או להריץ תהליכים שה availability שלהם הוא לא קריטי.

Linux Containers הוא לא הפתרון היחידי לוירטואליזציה ברמת מערכת ההפעלה בלינוקס. יש פתרונות מעט שונים כגון OpenVZ או Linux-VServer.

Docker

LXC הוא בסיס טוב ל"וירטואליזציה רזה", אך הוא עדיין בסיסי לשימוש. מעליו צמחו פתרונות שונים לניהול workflows וניהול שוטף של העבודה. ללא ספק, הכלי שצמח הכי מהר בתחום זה הוא Docker. אם אתם עובדים בסביבת ענן / DevOps ולא שמעתם את השם הזה עד עכשיו – תבדקו את המקורות מהם אתם מתעדכנים.

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

כ Benchmark (הוגן, אני מקווה) בחרתי ב Chef – פרויקט באותו הדומיין פחות או יותר (אוטומציה של ניהול תצורה), פופולרי למדי, ושקיים כבר כ 5 שנים:

אין דברים כאלו!

ההתלהבות מ Docker היא קצת מעבר ל Github עצמו:

  • האגדה מספרת, שחברי הפרויקט החלו לעבוד ב Github בצורה ציבורית – ופתאום שמו לב שיש להם עשרות תורמים שהם לא מכירים (לרוב פרויקטי ה Open Source יש תורם משמעותי אחד).
  • יש כבר סטארט-אפים שסובבים סביב Docker: רשימת 10 הסטארט-אפים הטובים ביותר הבנויים מסביב ל Docker.
  • יש כבר כמה חברות שהחליטו לאמץ אותו כרכיב מרכזי בתשתית שלהן (למשל eBay, Rackspace, או Cloudflare).

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

Docker מאפשר:

  • לבנות containers של הקוד שלכם מתוך סקריפט.
  • לנהל את הגרסאות השונות של ה containers ב repositories שונים.
  • לבצע deploy קל container על שרת שהוגדר.
  • להעביר בקלות container רץ משרת אחד לשרת אחר (עבוד Continuous Delivery, פיזור עומסים, או סתם כדי להעביר אותו לסביבה מעודכנת יותר).

Docker עוזר לנהל כמה מהמגבלות של Linux Containers ולהפוך אותן למגבלות קטנות יותר. סביבות ניהול משוכללות כבר קיימות לפתרונות וירטואליזציה (למשל VMWare vSphere), אך לא כ"כ ל Linux Containers – ובחינם. Docker הוא הבסיס לכזו סביבה, ויש לא מעט סטארט-אפים שמנסים להפוך אותו לסביבת ניהול שלמה. כפי שאמרנו Docker מתבסס היום על LXC, אבל בפרויקט כבר מדברים על אפשור היכולות שלו עבור סביבות וירטואליזציה שונות כגון Xen, KVM, או אפילו Hyper-V (של מייקרוסופט).

הציפיות מ Docker הן דומות לאלו שהיינו מצפים היו ממכולות התובלה: לשנות את פני ניהול ה Data Center בצורה שתהפוך אותו לדבר שונה ממה שאנו מכירים אותו היום.

סיכום

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

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

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

עוד נושא קרוב, שלא דיברנו עליו בכלל הוא SDN – קיצור של Software Defined Network, תחום שעוסק בוירטואליזציה ואוטומציה של הניהול (המורכב) של הרשת. נשאיר משהו לפוסטים עתידיים.

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

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

[ב] מאמר מפורסם בנושא הוא "?Are Virtual Machine Monitors Microkernels Done Right"

להבין Dependency Injection [מעודכן]

עדכון: ספטמבר 2014 – הפוסט חודש כליל. הפוסט המקורי פורסם בספטמבר 2012.

לשימוש ב Dependency Injection (בקיצור: DI) יש שני מניעים עיקריים:

  • הקפדה על ה Single Responsibility Principle[א] (בקיצור: SRP).
  • ביצוע בדיקות יחידה / בדיקות אינטגרציה.
לעתים, השימוש ב DI הופך מוטמע כ"כ עמוק בפרויקט / בתהליכים (״התשתית שלנו היא Spring״) – עד שניתן לשכוח מהיכן הכל התחיל. כאשר מקפידים מאוד על שימוש ב DI, אך מחלקות בפרויקט עושות מטלות שונות – זה נראה סוג של "פספוס": זה כמו להאכיל את הילדים בקינואה וברוקולי בצהריים, אבל צ'יפס וקרמבו בשאר הארוחות. מותר… אבל לא מומלץ.
DI הוא גם פתרון ממשי ומעשי לבעיות שצצות בביצוע בדיקות יחידה, ואפילו יותר: בביצוע בדיקות אינטגרציה. אני עבדתי עם קבוצות פיתוח שהוסיפו DI בכדי לבצע בדיקות יחידה, וללא קשר ל SRP (ל SRP היה לנו כלי אחר).
כיום, נראה שביצוע בדיקות יחידה/אינטגרציה הוא יותר נפוץ מהקפדה מלאה על SRP. רוב הארגונים עושים היום בדיקות יחידה, אבל חלק קטן מהם (מניסיוני האישי) באמת מקפיד באמת על SRP. ויש עוד אחוז מסוים של ארגונים שמשתמשים בכלי DI – "כדי שיעשה סדר", אך ללא יכולת להסביר מהו בדיוק הסדר הזה שהם מצפים לו וכיצד הוא יקרה. לצפות ל"פחות באגים" בעקבות שימוש בכלי DI זה נחמד, אבל ניתן באותה מידה לצפות לכזו תוצאה משימוש ב Log4J… ללא שימוש נכון ויעיל – הציפיה היא תקוות שווא.

בפוסט זה אני מנסה גישה חדשה: פרסמתי את דוגמאות הקוד (העובדות) מהפוסט בגיטהאב: https://github.com/baronlior/di-post
בואו נראה איך זה עובד…

DI בהיבט של SRP

בואו נתבונן על הקוד הבא:

הממשק
ספק השירות
צרכן השירות

הנה השקענו, בהגדרת Interface (ע"פ עקרון ה Program to an interface), בהפרדה בין נותן שירות לצרכן השירות. הפרדה זו ניתן לקטלג כ"ניהול תלויות" / "Low Coupling" – ערכים בסיסיים בהנדסת תוכנה.

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

אמנם המשתנה המקומי artist הוא מסוג הממשק Artist – אך השימוש ב new מחייב שיהיה import ותלות למחלקה הקונקרטית. זרעי הפרת-התלויות – נזרעו.

מדוע הפסימיות? המתכנת ישתמש בממשק מבלי קשר ל imports הזמינים…

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

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

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

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

איך אפשר לעשות זאת?
– פשוט "נעיף" את ה import של המחלקה הקונקרטית מצרכן השירות ונוסיף מחלקה שהכרת המחלקות הקונקרטיות הוא ייעודה. Repository בו מחלקות יכולות להירשם, ולמצוא אחת את השנייה ע"פ "סימנים" מסוימים (שם, שם התפקיד, ממשק, וכו'). נקרא לו Service Locator:

דוגמא לשימוש ב Service Locator

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

עם השנים התפתחו ספריות של Service Locator שמקלות על המפתח, ועם הזמן החליטו להחליף את סדר האחריויות: במקום שמחלקה תגדיר למה היא זקוקה ("the artist"), מחלקת ה Service Locator תתפוס פיקוד והיא "תזריק" לצרכן השירות את מה שהוא צריך. מימושים אלו נקראו בהתחלה "Inverse of Control" (בקיצור: IoC), עד שמרטין פאוולר טבע את המונח "Dependency Injection" (בקיצור: DI) אותו אנו מכירים היום.

הנה דוגמה לשימוש ב JSR-330 (הסטנדרט של ג'אווה ל DI):

ה Annotation של Inject@ מסמנת שלבנאי של Michelangelo "יוזרק" המימוש הקונקרטי של המחלקה מסוג ה Painter לה הוא זקוק – מה שמותיר את הקוד של Michelangelo נקי למדי מכל עניין של ניהול הקשרים בין המחלקות.

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

מחלקה זו אינה מוגבלת בתלויות שלה – התפקיד שלה הוא "לשאת בנטל" התלויות של האפליקציה.

הקישור בין Inject@ והפונקציה ()bind נעשית בספריית ה DI בשם Google Guice – ה Reference Implementation הרשמי של JSR-330.

אי אפשר לא להזכיר את Spring – הספרייה שבעצם הפכה את DI לכ"כ נפוץ, והופיעה זמן מה לפני JSR-330 או Guice. ספרינג היא עדיין כנראה ספריית ה DI הנפוצה ביותר בעולם הג'אווה.

ההיסטוריה של ה DI/IoC. מקור

הערה: בעוד המינוח "Dependency Injection" היה ברור יותר מ "IoC" המעורפל-משהו, בד בדד הוא כנראה הותיר גם לאנשים מספיק מרחב לדמיין מהי "הזרקה". הזרקה, למשל, יכולה להיות גם השמה ב Reflection של Private member של מחלקה, לא? למשל כך:

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

מצד אחד חסכנו שורת קוד משעממת, מצד שני "עקפנו" את ה encapsulation של ג'אווה והתרנו לגשת ל private member של המחלקה.

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

  1. טשטוש הגבולות של מושג ה private בג'אווה.
  2. בעוד בנאי עם הרבה (נאמר 10) פרמטרים יציק לנו, וידרבן אותנו לנהל תלויות בצורה קפדנית / לחלק את המחלקה לכמה מחלקות קטנות יותר ועם התמחות מוגדרת יותר – (מניסיון) לא כ"כ מפריע לנו שיש מחלקה עם 10 פרמטרים עליהם יש Inject@.
  3. שימוש מוגבר ב class members ולא משתנים עם scope מצומצם יותר – כי כ"כ קל להגדיר בדרך זו משתנים.
סה"כ "הזרקה" של משתנים לבנאי המחלקה נחשבת הדרך המועדפת לניהול תלויות.
הנחת היסוד הוא ששימוש ב DI בא להבטיח ניהול תלויות מדויק וקפדני, ולא רק "לקצר את כתיבת הקוד". הדרך הכי קצרה לנהל תלויות היא כנראה שימוש ב new, בכל מקום שרק רוצים.
ולכן, השמה ל private member נראית קצת מוזרה: כמו אכילת קינואה וברוקולי עם צ'יפס מטוגן בצד. מותר, אבל מוזר.

ריבוי צורות (Polymorphism)

אם שמתם לב, הדרך להגדיר קשר בין הממשק למחלקה הקונקרטית (פקודת ()bind) היא על בסיס הטיפוס (class) בג'אווה. בתחילת הדרך נהוג היה דווקא לרשום מחלקה תחת שם (מחרוזת מסוימת) ואז לבקש אותה בחזרה ב Service Lookup / בהזרקה. עם הזמן הגיעו ל 2 תובנות:

  • שם המחלקה הוא כנראה התיאור הברור והצפוי ביותר שלה.
  • השימוש ב class object בשילוב עם Generics מאפשר ליצור ספריית DI / Service Lookup ללא הצורך המרגיז בביצוע downcasting.
מה קורה כאשר רוצים להשתמש בריבוי צורות?

ברור לכם שברגע שהמיפוי הוא לא חד-חד ערכי, ה framework לא יצליח לבצע resolving נכון ב runtime. יש frameworks שמתירים מצבים שהם לא חד-חד ערכיים, בהנחה שהם יצליחו לנחש נכון, ויש כאלו שפשוט זורקים שגיאה בזמן ריצה, למשל: "binding xxx was already configured".

מה עושים? יש לכך כמה פתרונות (ל Spring Framework, למשל, יש כמה דרכים משלה), אך הפתרון המקובל ב JSR-330 הוא להוסיף Qualifiers (מעין תגיות) על ה binding ועל הבקשה (ועל צרכן השירות – בהתאמה), וכך לבצע את ההתאמה: התאמה ע"פ תיאור הכוונה.

רישום של 2 מימושים לממשק Painter, תחת שמות שונים

הנה המימוש המחלקה Michelangelo שמקבלת שני מימושים שונים של Painter:

הדוגמה היא רק בכדי להמחיש את נושא ה DI

DI בהיבט של בדיקות יחידה/אינטגרציה

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

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

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

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

אזור ירוק – הכנסת הסביבה הבדיקה; אזור הכתום – הרצת הבדיקה.

  1. אנו בודקים את מחלקת ה Artist ולכן צריכים מופע שלה. כדי שה DI Framework יוכל לעבוד, אנו צריכים ליצור את Michelangelo בעצמו בעזרת ה Framework.
    יש פה הזרקה ל member. מה שאני לא מרשה לעצמי לעשות בקוד production, אני מרשה לעצמי לעשות בקוד של בדיקות – כדי לכתוב קוד קצר יותר.
  2. אני יוצר mock ל Painter. לא רציתי להתעסק עם ספריית mocking (כמו Mockito או PowerMock) ולכן יצרתי mock כ Anonymous Class.
  3. פונקציה זו היא בעצם inline של הקונפיגורציה. בעוד שבקוד production אנו רוצים להפריד את ה wiring מהקוד, דווקא בבדיקה ה wiring הוא חשוב להבנת הבדיקה, ולכן אני מעדיף שיהיה באותו הקובץ. שוב השתמשתי ב Anonymous Class.
  4. זו הבדיקה עצמה, הפעלה של המתודה ()createArt מול התוצאה הצפויה (הבדיקה עוברת).

סיכום

בפוסט זה סקרנו את נושא ה Dependency Injection ושורשיו, וניסינו להפוך "שימוש אוטומטי" בכלי DI, לשימוש מושכל יותר.

מה השתנה בשנתיים האחרונות שגרם לי לחדש את הפוסט?
אני חושב שבאופן אישי יצא לי להיחשף בעבר ל DI בעיקר ככלי לביצוע בדיקות יחידה – את ניהול התלויות העיקרי עשינו במערכת (הישנה) על גבי JNDI של JEE (סוג של Service Locator). את תמונת עולם זו שיקפתי בפוסט בצורה דיי נחרצת – כפי שהגיב והעיר ויטלי.

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

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

  • אין צורך ב Framework בכדי לבצע DI. בסה"כ, DI הוא רעיון, וה Frameworks השונים רק יוצרים אוטומציה של התהליך.
  • באוטומציה (או Framework) יש יתרון שהוא גם בעיה: היא חוסכת מאיתנו את הצורך בהבנה עמוקה. ללא הבנה עמוקה ניתן לשכוח מדוע אנו עושים מה שאנו עושים (במקרה שלנו – DI) – ולפתע למצוא את עצמנו עושים משהו אחר מהכוונה המקורית.
    אני נוטה בשנים האחרונות להטמיע רעיונות ראשית ללא Framework, בכדי להבין לעומק את הנושא – ורק בסיבוב השני להוסיף את ה Framework שמקל על העבודה. זה כמו ללמוד "חשבון" (מתמטיקה בסיסית) עם מחשבון ביד מהרגע הראשון: האם לא עדיף ללמוד כמה חודשים ללא מחשבון – ורק אז להוסיף אותו?
  • DI הוא לרוב רק חלק קטן במערך המודולריזציה של המערכת. שימוש ב DI לא מבטיח איכות או מודולריות, בפני עצמו. הוא רק כלי (נוסף) להשיג מטרות אלו.
  • אני לא שולל את הרעיון של אי-שימוש ב DI. למשל: להפסיק ליצור interface לכל מחלקה (מלבד מקרים של ריבוי צורות), להסתמך על public vs. private להגדרת הממשק, ולצמצם את השימוש ב DI למינימום האפשרי (למשל: עבור בדיקות אינטגרציה, או ניהול תלויות ברמת השירותים בלבד). בסופו של דבר אנו משלמים לא מעט עבור ההגנה שמספקת גישת ה Program to Interface (בצורה הדקדקנית שלה) וצוותים מסוימים / פרוייקטים מסוימים – יכולים להסתדר מספיק טוב עם פחות הגנות, ועדיף לתת להם להתקדם מהר יותר.
את דוגמאות הקוד כתבתי ב Guice ולא בספריה הנפוצה יותר Spring Framework בעיקר בגלל ש Guice מצומצם ופשוט יותר. ספרינג מספקת הרבה מאוד יכולות – שיכולות מאוד שימושיות למערכת גדולה ומסובכת, אך לא היו תורמות דבר להבנת הרעיונות הבסיסיים.
שיהיה בהצלחה!

—-

[א] עקרון זה מוכר גם כ Separation of Concerns או Segregation of Duties. הכוונה היא שכל יחידת קוד (פונקציה, מחלקה, מודול) יעשו דבר אחד, ודבר אחד בלבד. צד שני של אותו המטבע הוא שכל אחריות תהיה בידיים של יחידת קוד אחת גם כן. כלל זה עוזר ליצור סדר במערכת, ולוודא שמי שכותב את הקוד יראה לנגד עיניו את התמונה המלאה ולא חלקים ממנה (כי כל האחריות נמצאת באותו המקום).

קישורים:

DI קליל ברובי
הפסיכולוגיה של בדיקות תוכנה – מישקו הארווי (היוצר של AngularJS ו jsTestDriver).

שפת Ruby: מה זה השטויות האלה?!

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

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

רובי דומה בהיבטים רבים לשפת Python, מכיוון שהיא:
  • שפת high level, כמעט scripting language
  • שפת general purpose
  • שפת Dynamic typing
  • התחביר שלה דומה
החוויה הראשונה בהתקלות עם רובי, שלי לפחות, היא שהיא מעט מוזרה – דומה ל Perl (\"יש אצלנו יותר מדרך אחת לעשות כל דבר\"). בתחביר יש הרבה קיצורים, רחוקים מהשפה האנגלית ולא כ\"כ אינטואטיביים למי שלא רגיל אליהם , למשל:

%w(abc def ghi)
(ללא מרכאות) הוא קיצור מקובל ל
[\"abc\", \"def\", \"ghi\"]

דוגמה נוספת היא הבחירה במונחים:

  • string.downcase במקום string.lowercase 
  • string.gsub במקום string.replace (יש מתודה בשם replace, אך היא עושה משהו אחר)
  • to_s במקום toString, כאשר יש גם מתודה בשם to_str  אבל לא נכון להשתמש בה, אם המחלקה היא לא סוג של string בפני עצמו.
דבר אחרון שעשוי להיות דיי מוזר הוא השימוש הרב בסימני פיסוק: סימני \"?\" או \"!\" משמשים כקונבנציה על שמות של מתודות/פונקציות. יש שימוש ב <=, גם ב @, או $ בשמות של משתנים, ויש גם שימוש ב :|, &, וסימנים אחרים.
אני רואה מדריכים של רובי שמצהירים בגאווה: \"הצלחנו ליצור קוד שהוא ממש כמו פסקה בשפה האנגלית!\".
בפועל, יש בדוגמאות הללו כ\"כ הרבה סימני פיסוק בקוד, שזה לא נראה לי כמו קוד ספרותי – אלא הרבה יותר כמו חיבור של גיק מתבגר ולא-מתקשר. 
\"@הצלחנו: ליצור (!), קוד :שהוא |ממש| כמו => פסקה? בשפה! האנגלית!\" – כן, בטח!
רובי, בניגוד ל Python, היא שפת OO \"אמיתית\" עם classes כפי שאנו מכירים אותם בג\'אווה / #C.
רובי הושפעה גם מ SmallTalk, משפת LISP ומ Perl. כן,… Perl השפה שעליה נאמר שהיא שפה לכתיבה-בלבד.
לזכותה של רובי יאמר שהיא אחת השפות שצמחו במהירות הגדולות ביותר, ויש לה קהילה גדולה, איכותית, ופעילה. חוזק גדול מאוד של הקהילה הוא תשתית ה FullStack Web Development בשם Ruby on Rails (בקיצור: RoR), אבל יש לה גם כח ונוכחות מעבר לכך. ניתן לציין את RSpec, Chef, Puppet, Vagrant, Cucumber – שהשפעתם על תעשיית התוכנה היא רבה.
מתכנת ששולט ב Ruby יכול לכתוב בה בקצב מהיר מאוד, יותר מאשר ב Python – כך טוענים.
המטרה של פוסט זה היא לאפשר, בזמן קצר, למתכנת של #C או ג\'אווה גם להיות מסוגל לקרוא קוד זה.

התקנה

התקנת רובי פשוטה הרבה יותר על לינוקס.
משתמשי חלונות: כדאי להשתמש בלינק הזה, ולהשתמש בגרסת 32 bit, כי גרסת ה 64 היא בעייתית.
כמו כן כדאי להשתמש ברובי 2. רובי לא מזמן עשתה את המעבר מ 1.9 לגרסה 2. למרות החלפת הספרה לא היה פה שינוי דרמטי, אלא רק שינוי הדרגתי, קטן כנראה מהמעבר בין גרסה 1.8 לגרסה 1.9.
אם אתם רוצים לפתח (דאא!) אז תזדקקו דיי מהר גם ל Devkit. קובץ ההתקנה הוא בסך הכל ZIP שנפתח לאיזו תיקיה. בתיקיה זו (אני עובד בחלונות) הקלידו ב command line:
> ruby dk.rb init
> ruby dk.rb install
ה IDE החביב עלי הוא (כמובן) RubyMine.
התקנת רובי תוסיף לכם אפליקציה בשם irb (שהוא ה Interactive Ruby Shell, להפעלה מה commandline / shell) שבו תוכלו לבדוק במחזור feedback של שניות פקודות שלא ברור לכם בדיוק כיצד הן מתנהגות. אפשר להתקין את wirble שמאפשר autocomplete ו syntax highlighting ב irb.
כחלק מהתקנת רובי מגיעה גם אפליקציה בשם gem, שהוא ה Package manager של רובי (ממש כמו אלו של לינוקס, ממנה הושפעו הכלים של רובי לא-מעט). בעזרת gem מתקינים חבילות רובי. החבילה הראשונה שאני ממליץ עליה היא rspec ספריית בדיקות היחידה / BDD (שבדמותה נוצרה Jasmine של ג\'אווהסקריפט – למי שמכיר). פשוט הקלידו בשורת הפקודה:

> gem install rspec

בואו נצא ידי חובה וניצור hello world קטן:

ברובי יש 2 מתודות להדפסה:
print שהיא המקבילה ל System.out.print בג\'אווה.
ו puts (קיצור של put string. כמה טוב שלא בחרו לקרוא לה disuninject_string) שהיא המקבילה של System.out.println בג\'אווה, ובה נשתמש בד\"כ.

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

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

טבילת אש ראשונה

  1. שמות מחלקות חייבים להתחיל באות גדולה.
    כאשר אנו משתמשים באות גדולה בתחילת שם משתנה – הוא הופך לקבוע. אפשר לומר שגם מחלקה היא סוג של קבוע, כי הרי המחלקה (לא המופעים שלה) – לא משתנה מרגע שהוגדרה.
    המחלקה שלנו יורשת מהמחלקה LivingCreature.
  2. ה Constructor הוא מתודה (מתודות מוגרות ע\"י def, קיצור של define) בשם initialize. אין צורך להגדיר טיפוסים (int, string וכו\') של משתנים ברובי – זו שפה דינמית.
  3. משתני מופע (נקראים instance variables, מה שנקרא בג\'אווה \"members\") מוגדרים ע\"י סימן @ בתחילת השם. שימו לב שאת משתני המחלקה מגדירים רק בתוך מתודות, ולא בתוך המחלקה – כפי שמקובל בג\'אווה. עצם השימוש במשתנה עם שם שמתחיל ב@ – מגדיר אותו כ member של המחלקה.
  4. משתני מחלקה (המקבילים ל static members בג\'אווה) מוגדרים ע\"י 2 סימני @.
    שימוש במשתני מחלקה נחשב כפרקטיקה לא מומלצת ברובי, בדומה לג\'אווה.
  5. שמות מתודות, משתנים וקבצים ברובי נעשים ע\"י שימוש ב snake_case (בהקבלה ל camelCase שבשימוש בג\'אווה).
    מקובל מאוד שמתודה או פונקציה ללא פרמטרים – לעולם לא תופיע עם סוגריים.
    משתמשים ב PascalCase (אות גדולה ראשונה) להגדרות שמות מחלקות ומודולים.
    עבור קבועים, מקובל מאוד להשתמש ב SCREAMING_SNAKE_CASE (בדומה לג\'אווה).
  6. אוקיי… הנה קוד קצת מוזר. זהו התחביר (המוזר) ברובי למשפט if מקוצר. משפטי if רגילים דורשים שימוש ב end.
    התחביר של המשפט הוא: אם , במקרה שלנו: \"עשה את הפרש הגילאים\" אם \"הגיל קטן מה lifespan הממוצע\".
    אבל ביצוע חישוב וזריקה של התשובה לאוויר – היא \"עשייה\"?
    כן. מכיוון שהשורה האחרונה במתודה / פונקציה ברובי – היא ערך ההחזרה (אפשר להשתמש ב return כדי להחזיר ערך מאמצע הפונקציה).
    אין פונקציות מסוג void ברובי – תמיד פונקציה תחזיר ערך. אם לא הגדרנו אותו / אין ערך בשורה האחרונה – אזי יחזור nil.
  7. נעבור להרצה:
    אנו יוצרים מופע חדש של המחלקה: ה new נמצא בצד ההפוך לזה שאנו רגילים בגא\'ווה, והוא מקבל את הארגומנטים עבור ה constructor.
    הפעלה של מתודות – מקובל מאוד לשרשר ברובי, והרבה פונקציות בספריות הסטנדרטיות של רובי מחזירות reference לעצמן לצורך כך.
    כפי שכבר ציינו, אין צורך בסוגריים בהפעלת time_to_live, כי היא לא מקבלת ארגומנטים.
הערך שנותר לבחור בן 21 לחיות (ע\"פ התוכנה) – הוא שישים שנה.
כאשר שואלים את התוכנה על בחור בן 90, היא לא מבצעת כלום (כי התנאי במשפט ה if לא מתקיים) – ולכן חוזר nil.
כשנשלח nil ל puts – הוא מדפיס שורה ריקה.

שונות

רובי מספקת דרך מובנה לבצע formatting בסיסי של מחרוזות. ע\"י שימוש ב {}# (בתוך מחרוזת עם גרשיים), רובי תשתול במחרוזת ערכים שזמינים ב scope. אהבתי!

בדומה לפייטון, ניתן לבצע ברובי השמה מרובה (a,b = c,d).

שפות high level כמו רובי מעודדות את המתכנתים להשתמש בחופשיות במחרוזות בכדי לתאר \"מצבים חוזרים\". בשפת C היינו משתמשים בצורך זה ב const int, בג\'אווה היינו משתמשים ב enum. ל JVM יש גם מנגנון של String Pooling שמאפשר לייצר אותה מחרוזת כמה פעמים – אך לנהל רק עותק אחד בזיכרון. אני מניח שיכולת זו מבוססת על תהליך הקומפילציה של ג\'אווה.

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

הערה: symbols הם לא ממש מחרוזות, ולא ניתן להפעיל עליהם מתודות של מחרוזות, אולם נשמר הקשר ש:

:hello.object_id == hello.object_id 
בעוד
\"hello\".object_id != \"hello\".object_id 

בדוגמה למעלה a ו b הן מחרוזות, בעוד c ו d הם symbols
בשורת ה puts השתמשתי בכפל עם מחרוזת, שזהו קיצור לפקודת join על מערך. כך אני גורם ל puts להדפיס שורה אחת עם פסיקים – ממש כמו שמופיע בהערה באותה השורה.

עוד דבר קטן שאנו רואים בדוגמה הוא את קונבנציית השמות למתודות של \"!\": בעוד המתודה chop מחזירה עותק של המחרוזת a שהוסר ממנה הסימן האחרון (אנו לא עושים כלום עם הערך שחזר בדוגמה), המתודה !chop מסירה מהמחרוזת b את הערך האחרון ומחזירה nil.

לא ניתן להשתמש ב\"!\" בכל מתודה שרוצים: מי שכתב את המחלקה string יצר שתי מתודות: chop ו !chop.

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

לולאות ברובי דומות מאוד לפייטון או ג\'אווהסקריפט: פורמט for … in.
בדומה לג\'אווהסקריפט (שם לולאה עוברת גם על פרמטרים של אובייקט האב), יש בלולאת for רגילה – התנהגות שלרוב איננה רצויה: הערך שהוגדר (item במקרה שלנו) – ממשיך לחיות מעבר ל scope של המערך.

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

השימוש של התחביר השני רחב יותר מאשר סתם לולאות והוא נקרא block, הוא בעצם סוג של inline function שמעובר למתודה (each או select).

הנה דוגמה של שימוש במתודה select של מערך (סוג של filter: ערך ההחזרה הוא רק משתנים עבורם היה ערך חיובי). התחביר ~= הוא קיצור להתאמה של מחרוזת (color) ל regular expression (שאותה כותבים בין קווים נטויים, במקרה שלנו הופעת האות e).
כפי שאתם זוכרים, השורה האחרונה בפונקציה – היא ערך ההחזרה.

עוד מבנה נתונים \"טבעי\" בשפת רבי הוא ה Hash (המקבילה ל Map או HashTable בג\'אווה).
התחביר הוא key => value. כשנתקלתי לראשונה בקטע קוד שכזה – לא שיערתי שמדובר בעצם ב Map: החץ נראה \"חשוב מדי\" מכדי לשמש כסימן הפרדה פשוט.

מאז גרסה 1.9 רובי הגדירה תחביר מקוצר חדש ל Hash שהמפתחות שלו הם symbols (שזה סוג של best practice). הרי הוא לפניכם.

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

כמה השלמות חשובות

self ברובי יכול להזכיר this בג\'אווה, אבל ההתנהגות שלו שונה. אם הוגדר בתוך מתודה – הוא יצביע על המתודה, אם בתוך מחלקה ומחוץ למתודה – אזי על המחלקה, ואם מחוץ למחלקה – הוא יצביע על המרחב הגלובלי \"main\".

ברובי ניתן לארגן את המחלקות / קבועים / וכו\' בתוך modules (דומה ל packages ב ++C או #C)
כדי לנווט בתוך ה module – אנו משתמשים בסימן ::

:: מאפשר גישה למה שנחשב \"קבועים\" ברובי: מודולים, מחלקות, ומשתנים קבועים (כולם מתחילים באות גדולה).

בלוקים הם \"חתיכות של קוד\" שאנו שולחים בפרמטר לפונקציה (שימושים נפוצים הם פונקציות כמו each, filter, וכו\')
הנה התחביר של הבלוק:

מקור

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

סיכום

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

שפת רובי היא מורכבת ועשירה, והיכולת \"לקרוא כל קוד ברובי\" – דורשת כנראה למידה ארוכה בהרבה מפוסט זה (שלא לדבר על היכולת \"לכתוב כל קוד ברובי\"). לרובי יש ספריות ספציפיות שיש להכיר, טכניקות של metaprogramming – שנמצאות בשימוש לא מובטל, ועוד.

מפרשן (Interpreter) ברירת המחדל של רובי נקרא MRI (קיצור של Matz\'s Ruby Interpreter). הוא הסטנדרט, הוא עושה את העבודה – אבל נחשב אטי, אטי יותר מ Python – לדוגמה.

האם זה באמת נכון ומתי? קשה ממש לומר. יש הרבה מבחני ביצועים שמראים דברים שונים. לכל הפחות: זהו נושא בדיון.
עוד מגבלה של MRI היא שאין לו באמת multi-threading, ואין לו parallelism (קרי: שימוש ב GPU, SIMD, וכו\'). אלטרנטיבה מקובלת לסביבה שכזו היא JRuby – המפרשן של רובי שרץ על JVM (מהיר יותר, multi-threaded וכו\').

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

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

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

—-

קישורים:

Ruby tricks to make your code more fun and less readable – השלמה טובה לפוסט. חבל לי לשכתב את מה שנכתב כאן בצורה יפה.

מדריך Coding Style לרובי, שימושי ומלמד.

סשן ביוטיוב: Functional Development in Ruby

דפוסי ארכיטקטורה (Architectural Patterns): דפוסי עיצוב, בגדול

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

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

אתן לכם מטאפורה לדפוסי ארכיטקטורה:

דפוסי ארכיטקטורה הם כמו מתכון למרק

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

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

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

הדפוסים עליהם נעבור בפוסט

  • כדור גדול של בוץ (Big Ball of Mud)
  • "הדר המסודר" (Hadar Amesudar)
  • "שכבות עוגה" (Layered Architecture)
  • MVC (קיצור של Model-View-Controller)
  • "דודות מרכלות" (Event-Driven Architecture)
  •  Command-Query Separation (בקיצור: CQRS או CQS) 
  • "גרעין קטן" (MicroKernel) ודרכו נזכיר גם Plug-in Architecture
  • "מיקרו-שירותים" (Micro-Services Architecture, בקיצור: MSA)

דפוסים נפוצים אחרים שלא נעבור עליהם הם, אך שווה להזכיר:

  • צינורות ומסננים (Pipes & Filters) – אותו כיסיתי בפירוט בפוסט משלו.
  • BlackBoard (רלוונטי בעולם של בינה מלאכותית)
  • MVP, MVVM או Presentation-Abstraction-Control (דומים למדי ל MVC)
  • Broker (רלוונטי במערכות מבוזרות)
  • Model-Driven Architecture (מן "רעיון גדול" שלא ממש "תפס") או Domain-Specific Languages (בקיצור DSL – רעיון שהצליח חלקית).
  • וכו'

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

בגדול אזכיר ש:

  • לדפוסים יש ערך רב כשפה משותפת וכמקור השראה.
  • הצמדות לדפוסים "כפי שהוגדרו" – יכולה להיות הרסנית.
  • כדאי ליצור וריאציות שלכם של הדפוסים שיתאימו לכם יותר, או לשלב רעיונות (Mix and Match) מדפוסים שונים.

בואו נצא לדרך!

כדור גדול של בוץ (Big Ball of Mud)

השם ה(לא) מחמיא הזה כמובן ניתן ל Anti-Pattern. כדור גדול של בוץ הוא מצב בו כל המחלקות במערכת שלנו נמצאות ב Pool אחד משותף, ללא חלוקה ברורה ומסודרת.
מצב זה אולי נוח כאשר יש 10 מחלקות, אך מאוד לא נוח כאשר יש כ 1000 מחלקות במערכת.
אפשר בהחלט להתחיל במערכת עם "כדור גדול של בוץ", לגלות את התכונות והמבנה והרצוי של המערכת – ואז לחלק. ככלל אצבע הייתי מצביע על כמה עשרות מחלקות, לכל היותר, כנקודה בה כדאי מאוד שהיה כבר מבנה מוגדר.

מדוע כדאי לנו לחלק את המערכת לחלקים? בגלל:

  • בעיית ההתמצאות (Orientation)
  • בעיית ה Development Scalability
  • יצירת מסגרת מנטלית של סדר וחוקיות במערכת. זו לא בדיחה: כמה כללים פשוטים מה "נכון" ומה "לא נכון" עוזרים לאנשים בפרויקט להרגיש בטוחים יותר במה שהם עושים. אי קיום הכללים הפשוטים הללו / חוסר סדר בסיסי כלשהו בפרויקט – משפיע על אנשים רבים (לא כולם) בצורה שלילית וברורה
    קשה להבין את ההשפעה עד שלא רואים את המעבר של פרויקט ממצב של "אי-סדר", לאפילו מצב של "סדר בסיסי".
את 2 הבעיות הראשונות תיארתי בפירוט בפוסט מדוע אנו זקוקים ל Software Design?

"הדר המסודר"

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

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

  • מחלקות עד 1000 שורות קוד
  • מחלקות מתחת ל 1000 שורות קוד
  • קבועים, enums, או data objects (שהם בד"כ מאוד קטנים)

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

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

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

  • דואר נכנס
  • דואר יוצא
  • אנשי קשר
  • קונפיגורציה
  • אחר

החלוקה הזו נשמעת יותר הגיונית, אבל גם בה יש בעיות עקרוניות. למשל: הקבוצה "אחר" יכולה להכיל לא-מעט מחלקות שלא מתקשרות ישירות לפונקציה "עסקית" (persistence, helpers, פקדי UI שבשימוש חוזר, וכו'). יצרנו שם "כדור בינוני של בוץ" בפני עצמו. ההתמצאות עצמה היא בעיקר ברמת האזורים ("שכונות בעיר") אך לא מכסה רזולוציה קטנה יותר ("רחובות").
דבר נוסף הוא שכדי להשיג Developer Scalability (היכולת של הרבה אנשים לעבוד על המערכת במקביל) אנו נרצה להטיל מגבלות על היכולת של החלקים השונים של המערכת לתקשר זה עם זה.

מדוע אנו מעוניינים בסדר וניהול תלויות?

נמשיל את העניין לבעיה פשוטה מאוד: הפרדה של קוד בין 2 מחלקות:

את קוד ה Parser אנו מחלקים ל2 חלקים. מדוע?

  • כי יותר קל לנהל קוד בחתיכות קטנות ופשוטות, מחתיכה גדולה ומורכבת יותר.
  • כי אז ניתן לבצע שינוי באזור אחד, עם סבירות גבוהה* שלא יהיה צורך לשנות אזור אחר. פחות בדיקות רגרסיה, פחות "פחד" לשבור דברים, והיכולת של 2 מפתחים לעבוד במקביל בצורה יותר יעילה.
* כל עוד החלוקה היא אפקטיבית
לא מספיק שחילקנו את ה Parser ל 2 מחלקות, אנו רוצים להטיל מגבלות על התלויות ביניהן:
  • הכמסה (encapsulation) – מגבלה מאוד מקובלת בעולם ה OO, שמפרידה בין חלקים ציבוריים (בהם ניתן ליצור תלות) ופרטיים (בהם לא ניתן ליצור תלות). עצם כך שצמצמנו את התלויות – הגברנו את הסיכוי ששינוי בקוד (באזור הפרטי) לא ידרוש שינויים או ישפיע על מחלקות שתלויות במחלקה זו – שוב: Developer Scalability, תחזוקה קל היותר, ויכולת לבצע שינויים בצורה פשוטה יחסית (באזורים הפרטיים).
  • ניהול תלויות (dependency management) – מגבלה שאומרת שמחלקת ה Document Parser רשאית לקרוא ל Paragraph Parse – אך לא להיפך. צמצמנו כך את התלויות עוד יותר.

אציין שבניהול תלויות המטרה היא לצמצם תלויות, לא רק "לנהל/לעקוב" אחריהן.

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

הכללים המגבילים, מציבים בפנינו שקלול-תמורות מסוים:

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

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

Layered Architecture

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

כללי החלוקה של דפוס השכבות הוא כדלהלן:

  • נפריד את מאגר המחלקות במערכת שלנו לכמה שכבות: שכבות עליונות לטיפול בעניינים ברמת הפשטה גבוהה, כאשר רמת ההפשטה יורדת ככל שיורדים בשכבות.
  • לוגיקה בשכבה גבוהה (n) תהיה מורכבת מהפעלה של לוגיקות (או פונקציות) בשכבה שמתחתיה (n-1). כמו כן, לוגיקה בשכבה גבוהה (n) תעזור להרכיב את הפונקציות בשכבה הגבוהה ממנה (n+1).
  • בד"כ השכבה הגבוהה ביותר היא זו שמתקשרת עם העולם החיצון: משתמש או מערכות אחרות.
  • החלוקה של המחלקות לשכבות היא שלב אחד, ובד"כ יש חלוקות נוספות בתוך השכבה למודולים / תתי-מערכות (sub-systems).
  • בד"כ בכל שכבה יש תת-שכבת API (או "Facade") מנוהלת – בה, ורק בה השכבה העליונה יכולה להשתמש. לא נרצה שכל ה APIs שזמינים בתוך השכבה יהיו זמינים לשכבה מעל – אחרת, איזו משמעות יש להפרדה שהגדרנו?!

בהצצה לתוך השכבה הבודדת, המבנה עשוי להראות דומה למבנה הבא:

פרשנות נפוצה של מודל השכבות במערכות ווב, מערכות מידע, או Enterprise Systems יש מן מתכון מקובל של לחלוקה המערכת לשכבות.
אין לכם כח לקבוע רמות הפשטה שונות ושימושיות של המערכת? – השתמשו במתכון מוכן (מתכון של מתכון):

  • UI – ממשק המשתמש
  • Business Logic – הפונקציה העיקרית שהמערכת ממלאה (ניהול ספקים, רכש, מלאי, וכו')
  • Data Access – בשכבה זו פעם היה הרבה קוד מתוחכם, אך היא הוחלפה חלקית ע"י מערכות ORM – ומה שנשאר בה הם האובייקטים של המערכת שמתארים את הנתונים (Entities, Collections, וכו').
  • Persistency – שכבה זו מתייחסת לבסיס הנתונים (בהנחה שיש כזה, בד"כ רלציוני) – סקריפטים ליצירת הטבלאות (DDL) ולעתים גם קוד שרץ בתוך בסיס הנתונים (stored procedures) [ב].

לחלוקה זו יש וריאציות שונות (הוספת שכבת Service מעל ה Business Logic, או שכבת Presentation Logic מתחת ל UI, וכו') – אבל סה"כ היא מאוד מקובלת.

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

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

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

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

אציין, שבמערכות Enterprise כדוגמת ERP של סאפ – יש ערך ממשי בחלוקה הנ"ך לשכבות: מערכות אלו מבוססות במידה רבה על מידול (modeling) של נתונים, ואז באמת יש פיתוחים ב Business Layer שלא צריכים לעדכן את ה UI (כי הוא נבנה מתוך מודל הנתונים שלא השתנה), ולא צריכים לעדכן כמעט את הטבלאות בבסיס הנתונים. שכבת ה Business Layer היא משמעותית ומשתנה תדיר – ויש ערך רב בהפרדה זו. כמו כן ניתן לשנות את ה UI (כיצד מרנדרים את המודל) – מבלי לשנות את שכבת ה Business Logic בכלל, וכו'.

עוד בעיה נפוצה היא פרשנות של המודל ע"פ פונקציה אותה ממלאת המחלקה / מודול ולא ע"פ רמת הפשטה, כך שלמשל, כל פעולת גישה לדיסק נכנסת לשכבת ה Persistence באופן אוטומטי – "כי דיסק הוא Persistence" ואף אחד לא שוקל מהי רמת ההפשטה של הפעולה. למשל: קריאה של קובץ מהדיסק ששקול ל Input מהמשתמש – נכון יותר, כנראה, שהיה בשכבת ה UI. זו אותה רמה הפשטה. שכבת ה Persistence הוגדרה מכיוון שיש הרבה מאוד פעולות דיסק שהם ברמה נמוכה – אך לא כולן הן כאלה.
התעלמות מהעיקרון של רמות ההפשטה השונות – גורם ל"פספוס" ביישום של מודל השכבות.

"הב לי עלים ירוקים…"

Model-View-Controller

בתיאוריה, MVC הוא דפוס נהדר לחלוקה של שכבת ה UI.
תאוריה בצד. בפועל: יש לו בעיה מהותית – שמונעת ממנו לעתים "להמריא".

דפוס MVC בא לחלק אפליקציית UI לחלקים מוגדרים היטב – ולהגדיר תלויות ביניהן.
המקור של MVC הוא ב framework של שפת Smalltalk-80 משנת 1980 בשם "Model-View-Controller Editor".

ה Framework הזה היה מיועד לאפליקציות command line (שיא ה UI באותה תקופה), והחלוקה הייתה בין:

  • Model – הנתונים של האפליקציה / מבני נתונים
  • View – תצוגת הפלט, מה לרשום על המסך
  • Control – קבל ה Input מהמשתמש וטיפול בקלט הזה – בעצם ה Business Logic.
MVC "קלאסי" / "המקורי"

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

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

אם כולם היו מכירים את MVC ומשתמשים בו באותו האופן – לדפוס זה היה ערך רב יותר. אולם, יש 2 בעיות:

  • MVC המקורי לא כ"כ מתאים לאפליקציות Desktop, Web או כל מה שאינו command line.
  • יש המון וריאציות של MVC: חלקן קיבלו שמות נבדלים (יחסית) כמו MVP, MVVM או PAC – אבל רוב הווריאציות פשוט משתמש בשם "MVC", תוך כדי ביצוע שינויים במודל "המקורי".
התוצאה המידית היא בלבול וחוסר תקשורת –  FUD. אם דיברנו על כך שדפוסים משמשים כשפה משותפת, אזי MVC משמש לרוב כאנטי-שפה-משותפת: מתכנת אחד אומר "Controller" והשני שומע "Kotroller" (כלומר: מה שהוא מכיר כ Controller). שני המתכנתים רבים כמה ימים, אולי שבועות – עד שהם מצליחים להסכים שבעצם השני לא אידיוט/מפגר/לא מבין – שניהם פשוט מכירים שתי וריאציות שונות של MVC.
בפועל, אוותר על הניסיון לתאר ולאפיין את כל הווריאציות התאורטיות של MVC (להזכיר: יש כאלו שהם Client-Side, כאלו שהם MVC צד שרת, כאלו שהם מערבים צד-לקוח וצד-שרת, וכאלו שמתאימים לאפליקציות Desktop או Native Mobile).
אני ממליץ לוותר על ההגדרות, ופשוט להיצמד לספרייה הרלוונטית שבה אתם משתמשים – והכללים שהיא מכתיבה: בסוף, התועלת העיקרית היא מכללים ברורים שכולם מצייתים להם – קצת פחות אם כלל מסוים מנוסח בווריאציה 2א' או בווריאציה 3ג'.
יש ספריות יש רבות הממשות (וריאציה של) דפוס זה: Struts, Play!, Grails, Ruby on Rails, Django, ASP.NET MVC , Angular.js, Backbone.js ועוד….
מבנה של MVC צד-לקוח מודרני טיפוסי אפשרי
העיקרון המשמעותי ביותר, והמוסכם ביותר בקרב ספריות "MVC" מודרניות הוא ההפרדה בין UI (כיצד משהו נראה על המסך) ל Presentation Logic (כיצד להחליט מה יש להציג על המסך). למשל (דוגמה סופר-פשוטה):
במקום לקבוע כלל ש:
  • אדם עם +500 חברים מקבל icon מיוחד.
מחלקים את המימוש ל2 אזורים:
  • Presentation Logic – אדם עם 500+ מתויג כ "אדם פופולרי במיוחד" (What)
  • UI – אדם המתויג כ "אדם פופולרי במיוחד" מקבל icon מיוחד.
הפרדה זו יוצרת מעט overhead על הפיתוח, אך היא מאפשרת להחליף את ה presentation (כלומר: אלמנטים ב UI / כל ה UI) בצורה קלה יחסית, מבלי "לאבד" או לשנות את ה Presentation Logic שהוא יותר יציב, או לחלופין להוסיף presentation נוסף (נאמר: mobile version), מבלי לשכפל את הקוד לגמרי.
החלפת / רענון שכבת ה UI – היא פעולה תדירה מאוד במערכות ווב. ארגונים גדולים עושים זאת כל 2-3 שנים, ואתרים קטנים מבצעים שינויים לפעמים על בסיס חודשי.

סיכום

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

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

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

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

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

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

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

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

הדוד בוב בביקורת עוקצנית על MVC (וידאו): "MVC is not an Architecture"

OpenStack – ענן בקוד פתוח, חלק ב\'

בפוסט זה נדבר על שירותי OpenStack (בקיצור: OSK) שאינם שירותי ליבה. בכל זאת – חלקם מאוד שימושיים!

את השירותים אחלק ל-2 קבוצות:

  • שירותי אחסון (Swift, Cinder ו Trove)
  • שירותים אחרים (Heat ו Ceilometer)
הקשרים בין השירותים השונים של OpenStack

שירותי האחסון (Storage)

כשירות בסיסי, OpenStack מספק 3 צורות אכסון:

  • Ephemeral (קצר-טווח, מיושם ע\"י Cinder) 
  • Block (מיושם ע\"י Cinder)
  • Object (מיושם ע\"י Swift)
להזכיר: Cinder הוא המקביל ל (EBS (Elastic Block Storage של אמזון, ו Swift הוא המקביל של S3.

שירות נוסף שכללי בקטגוריה הוא שירות Trove שהוא בעצם רק storage-related: הוא אחראי על provisioning אוטומטי של instances של בסיסי-נתונים.

Block Storage (שם קוד: Cinder)

משמעות השם Cinder הוא גחלת – פחם שחדל לבעור, אך הוא עדיין חם.

Cinder הוא שירות הפשטה וניהול של Backends של אחסון. בצד אחד הוא מנהל מערכות אחסון נתונים קיימות (כמו netapp או EMC) ומצד שני הוא חושף API של הקצאת שטח אחסון (מופיע כדיסק SCSI) ל VM שרץ על נובה.

ה Ephemeral Storage מוקצה למכונה כ\"דיסק מקומי\" וזמני. ברגע שה VM \"יורד\", התוכן שנשמר בבלוק שהוקצה – יימחק.
ה Block Storage גם הוא מוקצה ל VM ספציפי, אולם אם נותק מה VM – הנתונים שעליו עדיין יישמרו. עליו נשמור את הנתונים הייחודיים / בעלי המשמעות במכונה. בדומה ל EBS, ניתן בכל שלב ליצור snapshot של הבלוק, עבור גיבוי או שכפול.

מדוע הוא שימושי?

נניח אנו רוצים לעבור ממכונה m1.small למכונה גדולה יותר m1.large, או בין תמונת-דיסק (\"Image\") של Padora ל RHEL (הפצות שונות של לינוקס). ברגע שאנו משתמשים ב Block Storage אנו יכולים לנתק אותו מ VM אחד – ולהקצות אותו מיד ל VM חדש, מבלי לבצע העתקה ארוכה. יש בעיות עם RHEL? נקים מחדש VM של Padora ונחזיר אליו את ה Storage.

לסינדר יש דרייברים לספקי אחסון שונים (פתרונות NAS או SAN) התומכים בפיצר\'ים השונים, עבור גרסאות שונות של OSK). ניתן למצוא כאן את טבלת הדרייברים הזמינים והיכולות הנתמכות.
ניתן להשתמש במקביל בכמה Storage Backends.

בכל פעם שאנו מבקשים בלוק שיוקצה ל VM, התהליך Cinder-Scheduler מחפש את הבלוק המתאים ביותר הזמין במערכת – ומקצה אותו.

Object Storage (שם קוד: Swift)

בעוד Cinder הוא יחסית פשוט (הוא בעיקר מנהל מערכות Storage חיצוניות), שירות Swift הוא מורכב יותר. לפעמים מריצים את Swift כשירות עצמאי, ללא שימוש ב Stack המלא של OpenStack.
Swift היא הטכנולוגיה מאחורי שירות ה cloud-files, של Rackspace.

Swift היא מערכת מבוזרת, fault-tolerant, אלסטית, ו eventually consistent לאכסון אובייקטים. מלבד העובדה שגודל האובייקטים הוא עד 5GB (ניתן לקונפיגורציה) – היא דיי דומה לכמה בסיסי-נתונים NoSQL מסוג K/V מוכרים.

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

כל התקשורת מול הצרכנים של סוויפט נעשית דרך שרת(י) ה Proxy של סוויפט, בעזרת REST-ful APIs (כלומר: על גבי HTTP). ניתן לחבר ל Proxy שירות Caching (בד\"כ Memcached) לטיפול מיידי בבקשות הנפוצות ביותר.

סוויפט הוא multi-tenant בצורה מובנה, וכל tenant מקבל הפרדה מגישה של tenants אחרים. מלבד יכולת (יחסית ייחודית?) של multi-tenancy, סוויפט גם נחשב קל / יעיל מאוד לניהול.

לפזר מידע על דיסקים, דרך RESTful API זה לא דבר כ\"כ מורכב. החלק המורכב באמת בסוויפט הוא גילוי והתאוששות מתקלות (בעיות consistency, תקלות חומרה וכו\').

סוויפט מחזיק כמה סוגי של שרתי (או תהליכי) consistency לצורך זה:

  • Auditors רצים ברקע על כל שרת של סוויפט וסורקים את הדיסקים לוודא את תקינות המידע עליהם. אם יש תקלות – הקובץ מועבר ל quarantine area, ועותק \"בריא\" של הקובץ (זוכרים? יש יתירות) – מועתק במקומו.
  • Updaters מוודאים שה metadata על הקבצים הוא נכון ועקבי. הם גם מנהלים counters על כמות האכסון, ומספר האובייקטים, וכו\' ברמות הניהול השונות. בגדול, סוופיט מגדיר accounts (שהם ה tenants), בתוכם ניתן להגדיר containers (כמו תקיות) ובתוכם מנהלים את האובייקטים.
  • Replicators מוודאים שיש מספיק יתירות של קבצים במערכת. מספיק? ע\"פ הגדרות SLA – כמובן. שמירת היתירות היא משימה עקבית, כי מדי-פעם קבצים מתגלים כשגויים או שסתם שרתים \"נופלים\".

על סוויפט לבדו, ניתן לכתוב ספר שלם. בעצם… כבר כתבועוד אחד, ועוד אחד)

DBaaS (שם קוד: Trove)

כפי שציינתי, שירות Trove (\"אוצר בלום\") הוא לא שירות אחסון, אלא שירות הקשור לאחסון. הוא אמור לפשט את המשימה של התקנה של בסיס נתונים. יש המתארים אותו כ \"Database as a Service\".

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

מייד אפרט את היתרונות שיש בשירות כמו Trove, אולם הסיבה שאלו דווקא בסיסי נתונים כנראה נעוצה בכך שבסיסי נתונים הם נפוצים מאוד. באותה המידה היה הגיוני לעשות שירות דומה ל Applications Servers. אולי… במידה, זה מה ש PaaS עושה. אז מה Trove מסייע לנו לעשות ב instance של בסיס הנתונים שהוקם?

  • ניהול משתמשים בבסיס הנתונים / ניהול הרשאות על אובייקטים בצורה מרכזית.
  • Backup / Restore מנוהל בקנה מידה גדול, או Patching.
  • ניהול Quota.
  • Vertical Scalability פשוט לניהול: שינוי גודל הזיכרון / כח מחשוב / דיסק – ע\"פ ה workload באותו הרגע.
  • ניטור מצרפי ו diagnostics.
ניתן למצוא היום את Trove בכמה מוצרים בפרודקשן:

כרגע Trove תומך רק ב MySQL ותואמים שלו (MariaDB, Percona), אולם יש תוכניות קונקרטיות לתמוך ב MongoDb, Cassandra, Redis, CouchBase ועוד, וכן לתמוך בקונפיגורציות High Availability של MySQL. יש להזכיר שהוא רק שוחרר בגרסת (IceHouse (2014 – ויש לו עוד הרבה לאן להתפתח.

מקור: Mirantis

שירותים אחרים

OpenStack Orchestration (שם קוד: Heat)

Heat הוא הגרסה של OSK ל CloudFormation של אמזון.
שירות זה דומה במטרתו לשירות תמונות-הדיסק (שם קוד: Glance), אך הרזולוציה שבה הוא עובד היא של landscapes שלמים.
ה landscape הרצוי מתואר בקובץ בפורמט HOT (קיצור של Heat Orchestration Template. נקרא גם בקיצור Template), קובץ שהוא human readable, ושניתן לערוך ידנית.

הנה דוגמה של קובץ HOT בסיסי ביותר, המבקש שירות של nova להרצת image מסוים על מכונה קטנה:

heat_template_version: 2013-05-23

description: Simple template to deploy a single compute instance

resources:
my_instance:
type: OS::Nova::Server
properties:
key_name: my_key
image: F18-x86_64-cfntools
flavor: m1.small

בעוד Glance מדבר ברזולוציה של תמונת-דיסק, Heat מקשר אותה גם לחומרה, רשת, הגדרות משתמשים וקונפיגורציה. בקובץ ה HOT ניתן להשאיר משתנים חסרים – שאותם יתבקש המשתמש למלא בזמן שהוא מבקש הקמה של Landscape ע\"י התבנית המדוברת, בכדי להפריד בין הגדרת ה HOT file, לבין הפעלה שלו בוריאציות שונות.

OpenStack Monitoring & Billing (שם קוד: Ceilometer)

ה Ceilometer הוא מכשיר שמודד בעזרת לייזר את גובה העננים (לצורך חזאות). אני מניח שמקור השם משרבב בתוכו את המילה Ceiling (תקרה).
שירות זה הוא מעט יותר SaaS-י באופיו: הוא אוסף את נתוני השימוש של לקוחות (פנימיים או חיצוניים) במשאבי הענן לצורך חיוב. את מידע ה billing ניתן להזין למערכות חיצוניות (למשל: מערכות ERP).

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

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

שימוש חשוב של Celiometer ו Heat ביחד הוא Auto-Scaling:

  • Ceilometer עוקב אחרי ה nodes השונים ומזהה את עומס העבודה שלהם.
  • ברגע שיש treshhold מסוים של שימוש – הוא מדווח על כך ל Heat.
  • ב HOT file של HEAT יש הוראות כיצד לבצע up/down scaling – ו HEAT פועל ע\"פ ההנחיות בכדי להפעיל / לכבות VMs ולהתאים את השירותים הרצים ל Scale הנדרש.

סיכום + הקשר ה PaaS-י

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

  • Designate – שירותי DNS (כלומר: Domain Name Server)
  • Marconi – שירותי Message Queuing
  • Barbican – שירותי Secure Storage
  • Sahara – שירותי Data Processing / Big Data מעל Hadoop
  • Mistral – שירותי תזמון משימות / Workflows
אתם בוודאי יכולים לזהות דפוס דיי ברור:
מלבד Designate (וביחד עם Trove ואולי גם Ceilometer) – אלו הם בעיקרון שירותים שמתאימים יותר להגדרה של PaaS מאשר IaaS.
כבר כמה שנים שהגבול (התאורטי) בין PaaS ל IaaS מתערער. אמזון, למשל, היא ספק IaaS מובהק שצומח כבר כמה שנים לעולם ה PaaS. הדפוס הזה הוא חוזר: מרגע ששירותי ה IaaS מסופקים בצורה סבירה – השטח הנרחב יותר לצמוח אליו הוא יכולות PaaS ואז אפילו SaaS.
יכולת מרכזית שחסרה בתמונה, שבלעדיה OpenStack לא יחשב כ PaaS היא הרזולוציה של ה deployment וניהול ה \"Compute\": הרזולוציה היא עדיין של VM ולא של אפליקציה.
ובכן, OpenStack מריצה ברקע עוד שני פרויקטים Solum ו Murano שעושים ממש את זה: Solum הוא יותר ברמת המפתח שרוצה לעשות Deploy לאפליקציה כלשהי, ו Murano הוא יותר קטלוג של אפליקציות ספציפיות מקונפגות מראש – להן יהיה ניתן לעשות deploy \"בלחיצת כפתור\".
כרגע הפרויקטים (Solum הוא המשמעותי בין השניים) הם בשלבים ראשוניים, ולא נחשבים לשחקנים חשובים בעולם ה PaaS. מי שרוצה PaaS על גבי OSK משתמש לרוב ב OpenShift או CloudFoundry.
בכל זאת, כאשר משתמשים בפתרונות כמו CloudFoundry או OpenShift יש כפילויות פונקציונלית בין המערכות: 
  • Authentication
  • DNS & Messaging (בקרוב)
  • Load Balancing
  • Dashboard & Monitoring
  • וכו\'
כפילות זו ברורה לכל משתמש עסקי.

מצד שני, בעת הפיתוח של Solum, קהילת OSK לא מוכנה לקבל ב Solum כפילויות מול פונקציונליות שקיימת כבר (+ באינקובציה) ב OSK. להישאר DRY (קרי: Don\'t Repeat Yourself).
Solum מקבל המון מ OSK, אבל הוא גם נאלץ להשתמש בשירותים שיועדו להיות אופטימליים ל IaaS ולא ל PaaS – וכנראה שלא תמיד כ\"כ מתאימים.

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

מה יהיה?
כבר כמה אלפי שנים שאין נביאים בארץ ישראל. נחכה ונראה.

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

—–

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

\"CloudFoundry ו OpenStack – זיווג מגן-עדן\"
\"OpenShift ו OpenStack – זיווג מהעננים\"

מה עדיף?! עננים או גן-עדן?