2019-05-20

JavaScript ES6/7/8 - להשלים פערים, ומהר - חלק ב'


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

נוספו הרבה Utilities לשפה ב ES6: מבני נתונים הם היום iterable, וניתן להפעיל עליהם פונקציות
()find(), sort(), filter(), foreach(), map וכו'

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

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

הספריות הסטנדרטיות  עברו מודרניזציה. נוספו יכולות internationalization כגון Intl.DateTimeFormat או Intl.NumberFormat. מי שזקוק בוודאי יבין מיד את היתרונות.

נוספו מבני נתונים כמו Map או Set.

כאן אולי עולה השאלה, למה צריך Map עם הקלות של ייצוג Dictionary על אובייקט {}?

בגדול Map הוא iterable וניתן לעשות על ה entries שלו פעולות כמו map/filter, ויש לו כמה פונקציות/תכונות שימושיות שלא זמינות לאובייקט. אבל הייתרון הגדול בשימוש ב Map לדעתי - היא בקריאות בקוד, כאשר שימוש ב Map מדגיש את הכוונה: פשוט לאחסן זוגות איברים, ותו לא.

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

מטרת הפוסט היא להתמקד בתחביר חדש / ייחודי ב ES6 שקשה להבין לבד.
לספק לכם כלים להבין את התחביר המורכב, ומה שמתרחש מאחוריו.

נצא לדרך!






השלמות


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

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


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

ביצירת אובייקטים ב ES6, אם ה key וה value בעלי שמות זהים, במקום לכתוב height: height - אפשר פשוט לכתוב height.

הנה ההפעלה:


אני מקווה שזה פשוט הגיוני.


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

  1. ההגדרה:
    1. אנו מציבים את הערך h של ה destructed object בתוך המשתנה height.
      הייתי רוצה להמליץ ליוצרי השפה על תחביר חץ כגון: height <- h = 6  שהיה אולי יותר ברור - אבל כבר מאוחר מדי.
    2. סיפקנו ערכי ברירת-מחדל, ב-2 רמות: גם אובייקט, וגם ערכים לשדות.
  2. ההפעלה:
    1. כשלא סופק אובייקט, ברירת המחדל היא האובייקט שסופק.
    2. סופק אובייקט, אך properties החסרים בערך - יקבלו ערך ברירת-מחדל.
    3. השם הארגומנט הוא h ולא height, ולכן שליחת אובייקט עם property בשם height - הוא חסר משמעות (אם כי ניתן להתבלבל).

שתי דוגמאות אחרונות לסיום ההשלמות:

  1. יש לנו Arrow Function שמחזירה אובייקט. המפרשן של ג'אווהסקריפט עשוי לא להבין בצורה חד-ערכית למה התכוונו (?! אולי זה destruction של אובייקט). הפתרון התחבירי - לעטוף את גוף הפונקציה בסוגריים.
    אם היה מדובר ב object deconstruction - היינו עוטפים בסוגריים את כל הביטוי (אגף ימין + שמאל של ההשמה).
  2. מה זו שרשרת החצים הזו? מאוד הגיוני: פונקציה שמחזירה פונקציה. אתם כנראה תתקלו בכאלו. 
    1. הנה ההפעלה: ההפעלה הראשונה (basePort = 80) מקבלת פונקציה, וההפעלה השנייה (distance = 100) מפעילה את הפונקציה שהתקבלה. אוי, יצא מספר מוכר!

זהו. סיימנו את ההשלמות ואפשר להמשיך הלאה.


Classes


ES6 הציגה Syntactic sugar להגדרת Classes. כלומר: נוספה מילה שמורה class שעוזרת להגדיר class, אבל זה אינו מבנה שמציג יכולות חדשות בשפה - אלא רק מקצר כתיבה, וחוסך התעסקות עם prototype. מאחורי הקלעים נוצר קוד שיכולנו לכתוב גם ב ES5:


באופן דומה, יש גם הורשה:


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

כלומר: לא קיבלנו Full-fledged classes מובנים בשפה - אך קיבלנו כלי שבהחלט כדאי להשתמש בו.

עוד 2 דברים שניתן להגדיר על מחלקות הם getters/setters, ו static members - הזמינים רק מתוך המחלקה, ולא מתוך המופע (כלומר: ב ES5 אלו properties שיושבים על המצביע ל constructor ולא על ה prototype):


זהו. עכשיו אתם מכירים classes ב ES6. מזל טוב!


Modules


ההפרדה ב JavaScript בין קטעי קוד מקבצים שונים ("מודולים") צמחה מ 2 תקנים: CommonJs הסינכרוני (NodeJs) ו AMD האסינכרונית (בדפדפן, המימוש הנפוץ נקרא Require.js - כתבתי עליו פוסט בזמנו).

הגדרות המודולים הבשילו - והיום הם חלק מהתקן של השפה. הם נמצאים במלואם בתקן - אבל לא כל פרטי המימוש זמינים לרוחב כל המנועים השונים. הדפדפנים המודרניים תומכים היום בטעינה של מודולים בנוסח <script type=”module”> וחלקם גם ב dynamic import. עדיין מדובר ב 75-85% מהמשתמשים בלבד (בעת כתיבת הפוסט, ע"פ caniuse) - משהו שקשה מאוד להסתמך עליו.

הפתרון הפשוט היום הוא להשתמש בכלי להרכבת קבצי ה source ל bundle - כמו WebPack או Parcel, ע"מ לקבל תמיכה במודולים בדפדפן - משהו שרבים מאיתנו כבר עושים היום, בכל מקרה.

בצד השרת (NodeJs) התמיכה הרשמית במודולים החלה בגרסה 12.
בגרסאות ישנות של Node, זהו פיצ'ר ניסיוני שאפשר להדליק עם feature flag - או שאפשר לקבל אותו מספריות צד-שלישי.

בכל מקרה, הכיוון הוא לאמץ מודולים, וגם אם אתם לא משתמשים בהם כרגע - סביר שתשתמשו בקרוב. את דוגמאות הקוד בפוסט אני מריץ על node 12.

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

ב ES6 כל קובץ הוא מודול. אנו יכולים להחצין (export) משתנים, פונקציות, מחלקות, וכו' - ומי שמעוניין להשתמש בהם, יצטרך להצהיר על רצונו זה במשפט import. מה שלא הוחצן - איננו נגיש למודולים (קבצים) אחרים, ולא ניתן לעשות לו import. פשוט!

הנה האופנים בהם ניתן להחצין אלמנטים במודול, שימו לב שענייני ה import/export הם עניין שבו נוטים להתבלבל:

  1. אנו מוסיפים את המילה השמורה export לפני ההגדרה (משתנה, פונקציה, וכו') כדי להחצין את ההגדרה. פעולה זו נקראת named export.
  2. אנו מחצינים שורה של אלמנטים בפקודה בודדת. הסוגריים המסולסלים עוטפים את האלמנטים. זהו גם named export.
  3. אנו משתמשים בפקודה מיוחדת בשם default export המחצינה אובייקט - ויכולה להיות מוגדרת לכל היותר פעם אחת בקובץ. 
    1. מי שיבצע import למודול יוכל לתת איזה שם שירצה לאובייקט הזה.
    2. הסוגריים המסולסלים מגדירים אובייקט בו במקום. יכולנו גם לקרוא ל export default עם רפרנס לאובייקט שנוצר קודם לכן.
  4. אפשר לערבב את ה default export בתוך פעולת export של מספר איברים אחרים. גישה זו איננה מומלצת! 
פרקטיקה מקובלת היא להשתמש ב default export בלבד, על מנת שיהיה מקום אחד ברור שמציין מה בדיוק מוחצן מהמודול - ולשים את פעולת ה default export בסוף הקובץ. זוהי פרקטיקה הלקוחה מ CommonJS.

דרך אחרת היא להחליט להשתמש ב export על כל אלמנט שאנו רוצים להחצין, כך שרמת הגישה תהיה מוגדרת "על הרכיב". הערבוב בין הגישות - עשוי להיות מבלבל, ולכן אינו מומלץ.



באופן דומה, ניתן לבצע import:

  1. צורה זו מייבאת את כל הקובץ, והיא תריץ מחדש את כל הקוד בקובץ (כמו import בשפת C). צורה זו איננה מומלצת לשימוש, ואיננה חלק ממערכת המודולים!
  2. הצורה המומלצת היא לייבא את ה default export - ולתת לו שם מקומי. פשוט.
  3. צורה זו היא named import בו אנו מציינים את שמות האלמנטים בהם אנו רוצים לעשות שימוש.
    התחביר מזכיר תחביר של deconstructing assignment.
  4. אפשר להשתמש ב named import אך לתת שמות אחרים מקומיים לאלמנטים שלהם עשינו import.
  5. אפשר לייבא את אל האלמנטים שהוחצנו, מה שנקרא namespace import.
    1. אפשר לבצע deconstructing assignment בכדי להגיע לתוצאה דומה ל named import.
  6. אפשר לבצע import מעורב (אם היה גם export מעורב). foo  (מחוץ לסוגריים המסולסלים) הוא השם לאובייקט ברירת המחדל שהוחצן. הגישה הזו יוצרת מקום לבלבול בכמה רמות שונות - ולכן אני ממליץ להימנע ממנה.



Promises


אני מניח שאני לא צריך להכיר ולהסביר מה הם Promises - אבל אולי אני טועה. Promises הוא Pattern המאפשר לבצע ניתוק בין הרצה של לוגיקה לקבלת התוצאה שלה. סה"כ זהו כלי שימושי מאוד - שהפך לחלק מרכזי מאוד בשפה.
אני מניח שרובכם מכירים את הרעיון משפות תכנות אחרות, או מספריות כמו Q, Bluebird  או מ jQuery.deferred (אותו כיסיתי בפוסט עבר).

מחמת הספק, ומחמת החשיבות של היכולת - אסכם אותה בקצרה.

Promise הוא הבטחה לקבלת תוצאה עתידית של הפעלת לוגיקה כלשהי, לרוב הפעלה אסינכרונית. שם נרדף, ולא פחות מוצלח הוא "Deferred" - תשובה המגיעה מאוחר יותר. ה Promise הוא ה handler להפעלה, המאפשר לאסוף את התשובה:
  • במקום אחר (להעביר את ה promise/handler לאורך כמה פונקציות - ורק אז לקרוא את התשובה)
  • ובזמן אחר (להספיק לעשות פעולה נוספת בזמן שהפעולה האסינכרונית רצה).

יתרון נוסף וחשוב של Promises הוא כתיבת קוד למסודר יותר - יחסית ל callbacks.

"אז מה ההבדל? במקום callbacks מקוננים, אני כותבת את השורות בזו אחר זו?"

  1. כן - זה שיפור מורגש. 
  2. callbacks עם error handling הוא קוד שנוטה להסתבך במיוחד. נראה בהמשך כמה יותר אלגנטי הוא הפתרון של Promises.


הנה דוגמה פשוטה:

  1. יצרתי Promise והעברתי להרצה פונקציה המבצעת קריאה אסינכרונית ברשת (request הוא מודול של NodeJs). כשתחזור התשובה - הפונקציה תמשיך לפעול ותקבע תשובה ב Promise, תשובה שיהיה אפשר לאסוף מרגע שנקבעה.
    1. אם הבקשה מצליחה - אני מאפשר החזרת ערך בעזרת ה Promise - בסימן הצלחה (להלן "resolve").
    2. אם החלטתי שהבקשה נכשלה - אני מאפשר החזרת הסבר בסימן כישלון (להלן "reject").
  2. במקום / שלב מאוחר יותר - אני שולף את הנתונים מה Promise
    1. then - אם הייתה הצלחה.
    2. ה Promise שלי, יכול היה להחזיר Promise בעצמו - וכאן הייתי יכול להמשיך ולשרשר את הטיפול בתשובה שלו. במקרה שלנו, לא הגדרנו תשובה בפעולת הסעיף הקודם, ולכן הערך הוא undefined.
    3. catch - יתבצע אם היה כישלון (במקרה שלנו - הייתה הצלחה). "ערוץ" ה catch משתשרשר כל ה promises שבדרך ("then"), כך שאם נכשל ה promise המקורי (למשל: נקלקל את ה url) - יופעל ה catch.
    4. finally - קוד שירוץ בכל מקרה. 

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

  1. Promise.all יוצר Promise חדש, שיהיה resolved (או rejected) כאשר כל ה promises ברשימה יחזירו תשובה. זהו כלי חשוב מאוד להרצה של מספר פעולות אסינכרוניות במקביל - ואז טיפול בתשובות.
    1. אפשר לשים לב שהתקן הוגדר לפני שהיה Rest Parameter לפונקציות...
  2. הפעולה ההופכית, race, שיכולה הייתה גם להיקרא any - מחזירה promise שיחזיר לנו תשובה כלשהי (שחזרה) מה promises שנשלחו כפרמטרים. פעולה פחות נפוצה - אך עדיין חשובה.
  3. ניתן להשתמש ב Promises גם לפעולות סינכרוניות. פשוט יוצרים promise כבר עם "התשובה בפנים" בעזרת הפונקציות reject או resolve.



סיכום


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

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

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

אני מלא כוונה טובה לסיים פוסט נוסף (ואחרון?) בסדרה - ולכסות גם את הנושאים הללו.


שיהיה בצלחה!



2019-05-14

Javascript ES6/7/8 - להשלים פערים, ומהר

ג'אווהסקריפט היא אחת השפות הנפוצות בעולם: קל ללמוד אותה*, יש לה מונופול בסביבת הדפדפנים, וכמעט לכל מערכת חשובה היום בעולם - יש ייצוג וובי. ג׳אווהסקריפט גם פופולארית למדי בצד-השרת (node.js), היא נחשבת לשפה אוניברסלית שרצה בכל מקום - ויש לה מעט מאוד מתנגדים, כי היא לא "שייכת" לשום קבוצה.

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

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

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

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

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

לא אכנס לעומק שמות-הקוד, הכינויים, והגרסאות השונות, אבל אפשר לומר שהמהדורה השישית של השפה, שנקראת ECMAScript 6 (בקיצור ES6) או ECMAScript 2015 - היא כנראה הקפיצה המשמעותית ביותר. במהדורה זו נוספו "מחלקות" ו"מודולים" לשפה - והפכו אותה דומה הרבה יותר לשפות OO מוכרות כמו Java, TypeScript או #C. הרבה יותר - אבל עדיין, בפרטים יש הבדלים רבים.

פעם נאמר שההבדל בין JavaScript ל Java שקול להבדל בין Carpet ל Car.
היום כבר אפשר לומר שההבדל בין JavaScript ל Java שקול להבדלים בין Carrier ו Car - כבר באותו האזור.

עוד שתי מהדורות חשובות של JavaScript (או ECMAScript - בשם הפורמאלי) הן מהדורות 7 ו 8 - להלן ES7 ו ES8, כל אחת הוסיפה סדרה של כלים משניים (אך עדיין משמעותיים) לשפה.

אימוץ התקנים של ECMAScript ע"י מנועי ההרצה (כמו V8 או Chakra) לא נעשה כמקשה אחת בנוסח "מעכשיו אנחנו תומכים ב 100% ב ES5.1" אלא התמיכה נעשית feature by feature - כך התקן מאפשר.
לכן לא חשוב כ"כ איזה פיצ'ר שייך לאיזו מהדורה של התקן - אלא חשוב יותר להסתכל על התמונה הכוללת: כמה פיצ׳רים זמינים אל איזה אחוז מהמנועים.

רמת התמיכה ב ES6 ע"י מנועי-ההרצה השונים. מקור.

אם עוד לפני שנה-שנתיים התמיכה בדפדפנים עדיין לא הייתה טובה מספיק - והשימוש העיקרי ב ES6 היה בסביבות בהן אנו שולטים על הגרסה (כמו NodeJs), היום המצב כבר השתנה ובגרסאות הדפדפנים האחרונות התמיכה כבר טובה למדי!
התמונה למעלה מציגה את התמונה עבור ES6, אך המצב גם כבר דיי טוב עבור ES7 ו ES8.
לפני שאתם משתמשים בפיצ'ר מתקדם כדאי לבדוק את רמת התמיכה שלו באתר CanIUse.

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

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

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

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

מטרת הפוסט היא לא לכסות את כל פינות ES6 וכל הפיצ׳רים - אלא בעיקר לכסות את התחביר החדש - ומשמעויותיו. לאפשר לכם לקרוא ולהבין קוד ES6. את התשובות לשאולת בנוסח ״איך עושים .... ב ES6״ - אני אשאיר לגוגל ו Stack Overflow.

נצא לדרך!


let ו const מחליפים את var


ל var (הגדרת משתנה) של ג׳אווהסקריפט יש כמה בעיות מהותיות:
  • אם שכחנו להשתמש במילה השמורה var בהגדרת משתנה - אין בעיה! המשתנה יוגדר על המרחב הגלובלי (או אובייקט שמייצג אותו, למשל window בדפדפן).
  • אם הגדרנו משתנה פעמיים - אין בעיה! הוא יוגדר מחדש (על חשבון הקודם). הגדרה כפולה של משתנה היא כנראה באג ולא כוונת המתכנת הסביר. 
  • ה scope של הגדרת var הוא scope הפונקציה - ולאו דווקא הבלוק העוטף (כלומר: {}), זה גם מבלבל (שונה משפות תחביר-C האחרות) - וגם פחות שימושי: משתנים שאורך החיים שלהם מתאים יותר ל block ״זולגים״ החוצה ל scope של הפונקציה.
  • קוד שבא לפני הגדרה של משתנה שהוגדר כ var - עדיין יכול להשתמש במשתנה. זו מן התנהגות של מנגנון שנקרא hoisting בו כל הגדרות ה var (וגם function או class) מקודמות לתחילת ה scope שבהן הוגדרו לפני שהקוד מבוצע במפרשן. 
    • עצה נפוצה ב ES5 היא לבצע את כל ההגדרות בתחילת ה scope - בכדי להימנע מהתנהגות לא-צפויה של הקוד. כלומר: לכתוב את הקוד כפי שאכן ירוץ.


מה ההבדל בין let ל const?

let הוא משתנה שיכול להשתנות, ו const הוא משתנה שערכו מוגדר רק פעם אחת (כמו const ב Kotlin, כמו final בג׳אווה או readonly ב #C).

const יגן רק על הרפרנס עצמו. אם משתמשים בו על אובייקט, הוא רק יגן בפני החלפת המצביע לאובייקט - אך לא יגן בפני שינויים על האובייקט עצמו:



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


אז איך מתמודדים עם מגבלות עבר (כלומר: "ה var")?

עם בעיות ה var של ג׳אווהסקריפט, החלו להתמודד עוד ב ES5 בעזרת מנגנון שנקרא "strict mode״: אם בתחילת ה scope (פונקציה או גלובאלי) כתבתם את השורה "use strict" - אזי המפרשן יהיה סלחן פחות לטעויות.

בהקשר של var: כאשר אנחנו נמצאים ב Strict mode - אנו מחויבים להגדיר משתנה בעזרת var / let / const.


ב ES6:
  • מוגדר תמיד strict mode בתוך מודולים - אלמנט חדש בשפה (פוסט הבא?), שהוא דיי נפוץ. בשל תאימות לאחור לא החילו strict mode על המרחב הגובאלי / פונקציות רגילות - וההמלצה היא להמשיך ולהגדיר בהם ״use strict״.
  • שימוש ב let / const לא מאפשר להגדיר מחדש משתנה שכבר הוגדר.
  • ה scope של let / const הוא הבלוק {} בו הם הוגדרו - ולא רק הפונקציה. זה כנראה השיפור המורגש ביותר.
  • לכאורה let / const לא עוברים תהליך של Hoisting ולא ניתן לגשת אליהם לפני שהוגדרו.
    • למען הדיוק, כן מתרחש Hoisting (מגבלות טכניות?) - אבל המפרשן מוסיף גם בדיקה בעת הגישה, ואם יש גישה למשתנה לפני שאותחל - הוא יזרוק Reference Error:

  1. מדפיסה ״global x״ מכיוון ש x לא  c ב scope הפונקציה, הולכים ל scope החיצוני - ומוצאים אותו שם. זו התנהגות ES5.
  2. השורה השנייה תזרוק ReferenceError בעת הפענוח.המשתנה y לא אותחל - זו הבדיקה שדיברנו עליה. היה hoisting ולכן המפרשן יודע על קיומו, אבל לא ניתן לגשת אליו.
הערה: בשל הבדיקה שנוספה לשפה שמשתנה לא יקרא לפני שהוגדר, ייתכן והחלפה גורפת של var ל const/let של ES6 יגרמו ל Errors חדשים שלא נזרקו בשימוש ב var. שווה לעשות את המעבר - אבל להיות גם מודעים לאפשרות לתקלות.

הבלוק שאתם רואים (סוגריים מסולסלים צהובים) הוא התחליף המקובל ב ES6 ל Immediately Invoked Function Expressions - הגדרה של פונקציה שמיד מפעילים אותה. זה בעצם היה תרגיל לצורך "סגירת" משתנים מסוימים ב scope מצומצם יוצר, מה שאנו מקבלים ב ES6 מבלוק רגיל - כאשר אנחנו משתמשים ב let/const.




Arrow Functions



על פניו, Arrow Functions (בקיצור: AF) הם דרך מינימלית יותר להעביר פונקציה כארגומנט.


  1. התחביר הקלאסי (הפונקציה אנונימית ומצביע אליה מושם למשתנה).
  2. תחביר AF כאשר יש פרמטרים.
  3. תחביר AF ללא פרמטרים.

אלמנט יותר חשוב הוא ש AF  נוצלו על מנת לעשות תיקון היסטורי בהגדרת ה this בג'אווהסקריפט. ב ES6 "הרכיבו" כמה תיקונים היסטוריים על שינויים חדשים - מה שמעודד אפילו יותר להשתמש בפיצ'רים הללו.

כפי שאתם בוודאי זוכרים, this בג'אווהסקריפט מתייחס ל context הקורא לפונקציה, ולא ל scope החיצוני - כפי שמקובל ברוב שפות התכנות. זה מבלבל (כתבתי על זה פוסט שלם, בזמנו) - ותרגיל מקובל הוא להוסיף ב scope החיצוני את השורה:

var that = this; // Store the context of this

על מנת להיות מסוגלים לגשת ל this של ה scope העוטף.

ב AF - זו ההתנהגות הטבעית. הפרקטיקה היום ב ES6 היא להשתדל ולהשתמש ב Arrow functions ככל האפשר.



כמה תכונות חדשות של פונקציות (ומסביב) - ששינו את תחביר השפה



Rest Parameter

Rest Parameter הוא המקביל של varargs של ג'אווה / params של #C:



כמו בשפות אחרות - על ה rest param להיות אחרון ברשימת הפרמטרים (הגיוני).

אתם בוודאי תתקלו גם template strings במוקדם או במאוחר. המירכאות הבודדות והכפולות כבר תפוסות בשפה - אז בחרו מירכאות נוטות-לאחור. כל ביטוי בתוך המחרוזת שסגור ב {}$ - יוערך (eval) ע"י המפרשן.

ברוכה הבאה, ג'אווהסקריפט, למשפחת השפות המודרניות!


Spread Operator

Spread Operator (בקיצור: SO) הוא כלי חדש וחשוב בשפה. התחביר שלו זהה ל Rest Parameter (שלוש נקודות) - מה שהזכיר לי אותו בהקשר לאייטם הקודם.

ה SO מקבל אובייקט iterable (כמו מערך או מחרוזת)  - ו"מפזר" את הערכים שלו. הקונספט הזה קיים גם בשפות אחרות.

חשוב לציין שאי אפשר להשתמש בו סתם כך, למשל: להציב את התוצאה שלו למשתנה. יש להשתמש בו בהקשר שמוכן לקבל iterable מהסוג הנכון.

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


  1. בצורה הזו x מקבל את הרשימה, בעוד y ו z - לא מקבלים ערכים, ולכן הם undefined.
  2. אם ״פיזרנו״ את הרשימה - כל הפרמטרים מקבלים ערכים מהרשימה (כי הרשימה ארוכה דיה).
  3. אי אפשר להשתמש ב SO להשמה פשוטה. זה לא הגיוני. אפשר להשתמש ב SO רק בהקשרים המוכנים לקבל iterable.


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


פעולת sort בעצם משנה את האובייקט עליו היא מופעלת. עוולה ישנה של ג'אווהסקריפט.

אבל - יש פתרון:

  1. ה SO מאפשר לנו לייצר בקלות עותק חדש של המערך - ורק עליו נבצע מיון.
  2. אנחנו יכולים להשתמש ב SO גם בכדי להרכיב בקלות ובצורה דינאמית - מערכים חדשים.
  3. SO עובד גם על אובייקטים, ומאפשר מן תחביר חדש וקל "להרחיב" אותם.
  4. אפשר גם הפוך - אפשר לצמצם עותק של האובייקט. כמובן, אבל חשבתי שיהיה שימושי להזכיר.

בקיצור: כלי שימושי!



Deconstructing


אם אתם עובדים עם שפות מודרניות, סיכוי טוב שה Spread Operator הזכיר לכם פעולת Deconstruction, המפרקת איברים ממערך או אובייקט - בהשמה. גם פעולה זו נוספה לשפה:

  1. בדוגמה הפשוטה ביותר, אנו מציבים כמה ערכים, במקרה שלנו a ו b - בפעולה אחת, מתוך מערך.
  2. אפשר להשתמש ב deconstruction בכדי לבצע swap, למשל.
  3. אפשר לשלב Rest Operator בתוך Deconstruction ולהציב את כל הערכים הנותרים בתוך המשתנה שקראנו לו rest.
    1. שימו לב שאם אני לא זקוק לערך מסוים, אני יכול לדלג עליו עם פסיק ללא משתנה. נחמד.

Deconstruction עובד גם על אובייקטים:


  1. זהו התחביר. אנחנו שולפים לתוך משתנה בשם x את הערך למפתח x מתוך האובייקט.
  2. מה עושים כאשר המשתנה כבר מוגדר, אך אנו רוצים להציב בו שוב?
    1. תקלה: אסור להגדיר מחדש משתנה בעזרת let.
    2. תקלה: ג׳אווהסקריפט לא יודע לזהות שמדובר בפעולת deconstruction.
    3. הפתרון התחבירי: לעטוף את השמת ה deconstruction בסוגריים. אני מקווה שעכשיו זה נראה הגיוני.
  3. השימוש הנפוץ ל deconstruction, מן הסתם - הוא בהשמה לריבוי ערכים. מה קורה כאשר אין התאמה בין שם המשתנה למפתח באובייקט? - אנו מקבלים undefined.
    1. מה עושים אם לאובייקט יש מפתחות בשמות שלא מתאימים לנו? - אנחנו יכולים להשתמש בתחביר הזה בכדי לבחור באלו שמות משתנים להציב אותם. אנו רוצים שערך המפתח x יכנס למשתנה a, וערך המפתח y למשתנה b.
    2. האם אפשר לספק שמות שונים רק לחלק מהאיברים? אפשר.
      אני מסתכל על השורה ותוהה הזו מה הסיכוי לנחש מה היא עושה אותה מבלי להכיר את הכללים?!
  4. גם כאן אפשר להשתמש ב rest operator (כרגע פיצ׳ר בהרצה), rest הפעם הוא מטיפוס אובייקט (ולא מערך). 



ערך ברירת מחדל

כן! אנחנו יכולים לקבוע ערכי ברירת מחדל לפרמטרים בפונקציות (ובעוד כמה מקרים).
  • ערך ברירת מחדל לפרמטר בפונקציה - שימושי להרחבת פונקציה בצורה תואמת-לאחור או צמצום החתימה שלה - עבור השימושים הנפוצים.
    • ערך ברירת המחדל הוא תחליף מרכזי תחביר ה x = x || 10 שמאוד היה מקובל בשפה. היום - כמעט ולא תראו אותו.
    • ערך ברירת המחדל הוא תחליף מסוים ל function overloading - יכולת שלא קיימת בשפה.
  1. שימוש פשוט בערכי ברירת מחדל.
  2. אם מעבירים undefined לפרמטר עם ערך ברירת-מחדל, אזי יתקבל ערך ברירת המחדל - ולא undefined. ערך null יעבור כרגיל.
  3. הנה, אפשר להשתמש בערך ברירת-מחדל גם בהשמת deconstruction.
  4. גם כאן, כללי ה null וה undefined - תקפים.


סיכום



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

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


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



---

רוצים להנסות בזריזות ב ES6, מסביבה מעט יותר נוחה מה console של הדפדפן? ה https://es6console.com - הוא אופציה ראויה. רק על תשכחו להדליק את ה flags של ES7/8 - פשוט אין ES8 console...

2019-04-10

בחירות בתוכנה: איך לבחור נכון?

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

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

מצד שני, מבנה-של-מערכת הוא לא צחוק. שינויי מבנה הם שינויים קשים וארוכים. "אין לנו זמן לזה!"


האם נכון ״להרכין את הראש״, ולהכניס את הפיצ׳ר תוך יצירת עיוות במערכת?

או אולי יותר נכון ״לעמוד על הרגליים האחוריות״ ולדרוש לבצע שינוי במבנה המערכת - כך שלא ייווצר עיוות? שינוי במבנה המערכת יכול להאריך את זמן העבודה על הפיצ׳ר בימים או שבועות - וגם זה לא ממש צחוק.





אם אתם מתכנתים בארגון גדול - כנראה שיש לכם ״גב״ להחלטות שכאלו. הולכים לארכיטקט / מנהל טכני - והוא יחליט.

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



איך בוחרים?


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

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

"אם מדובר במודול קריטי של בקרה על כור גרעיני - אסור להתפשר בכלום על האיכות!" היא דוגמה מלומדת ששמעתי כמה פעמים בהקשר זה. מעולם לא פגשתי במתכנת שפיתח מודול קריטי של מערכת שתלויים בתקינותה חיים רבים. למתכנתים הללו יש כנראה guideline דיי ברור. מה איתנו, כל שאר 99.999% מאנשי התוכנה?

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

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

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

עיוותים במודל של המערכת הם לא סתם לא-אסתטיים. הם מדרדרים את המערכת למקום בו שינויים הופכים קשים יותר וההתקדמות - אטית יותר ומרובת-תקלות. אף אחד חוץ מהמהנדסים לא יחוש בהתדרדרות ויבין אותה - עד שכבר נהיה ב Technical Debt עמוק ומשמעותי.


אז מה עושים?
איך אתם לוקחים החלטה שכזו?

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

התשובה היא כמובן: It depends.

זה תלוי בארגון ובשלב שבו הוא נמצא: בארגון בוגר, רב לקוחות - נרצה לקחת פחות סיכונים, לפחות בפיצ'רים שבהם תלויים מספר רב של לקוחות. בארגון / מוצר חדש עם מעט לקוחות - זה הזמן לקחת יותר סיכונים. Lean Startup FTW.

זה תלוי ברוח הארגון, וב DNA שלו: ישנם ארגונים, גם לא קטנים, המקדשים את ההתקדמות והיכולת לבצע שינויים, גם במחיר סיכונים מסוימים. "Move fast and break things".
הערה: אני פותח פה פתח ל Self-Suggestion לקורא שרוצה להימנע מעימותים ופשוט יאמר לעצמו "אצלנו ב DNA הארגוני רוצים רק פיצ'רים - אז אני אזרום עם ה DNA הארגוני". אני לא רוצה לעודד סוג כזה של מחשבה - אלא שתחשבו מה נכון לארגון שלכם באמת. עד כמה הדחיפה לפיצ'רים באמת עובדת טוב, וכמה זמן פיתוח באמת "שוקע" בגלל עיוותים במערכת.

זה תלוי בגודל העיוות: אי אפשר לכמת עיוות, אבל אפשר לומר ש"לשמור גם אובייקט Y בטבלה שנועדה לשמור אובייקטים מסוג X" [א] הוא ככל הנראה עיוות חמור יותר מלהחזיר null או 1- בכדי לחוות על מצב לא-תקין ב API (פעם זה היה best practice, אפילו).

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


חטאים...

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

העבודה ב SAP סיפקה לי עוד חוויה עוצמתית, שהצלחתי לעכל רק לאורך השנים:
בנקודות הקיצון, ההסתכלות על "עיוותים במערכת" ו Technical Debt הוא כאוטי ולא צפוי / מובן.
במקרה אחד הייתה לנו במערכת במצב טכני טוב, אבל בשל נסיבות ניתנו לנו חודש וחצי, של כל קבוצת הפיתוח, לשפר את המצב הטכני של המערכת. השקענו הרבה עבודה, הסתבכנו עם חלק מהשיפורים ולא סיימנו אותם, וגם עם אלו שסיימנו - לא ברור אם באמת הבאנו את המערכת למצב טוב יותר.
  • כשאין לקוחות, ואין בעיות אמיתיות וכואבות - קשה מאוד להעריך אם מצב X1 הוא עדיף או לא על מצב X2 אחר. לרגע אחד נראה שכן, ורגע אחריו - שלא.
  • שינויים במבנה המערכת נוטים להסתבך ולארוך זמן ארוך מהצפוי, תוך כדי לעתים שהם מספקים תמורה קטנה מהמצופה.

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


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

חשוב שיהיה לכם יחס טוב בין Signal / Noise. אם על כל ארבע אזהרות שלכם - רק אחת מתממשת, הייתי אומר שזה המקום לצמצם קצת באזהרות. לעצמי אני אומר שלא הייתי רוצה לעלות על שלוש אזהרות לכל אחת שמתממשת. זהו כמובן Trade-off - בחרו לעצמכם את היחס שמתאים לכם - אבל זה בהחלט יחס שיש לחשוב עליו ולקחת אותו בחשבון.



סיכום


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

אז מה אני מציע לעשות כאשר יש דילמה בין הוספת פי'צר למערכת במחיר עיוות למבנה המערכת?

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

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


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



----

[א] נתקלתי במקרה כזה כי רצו לחסוך יצירה של טבלה בבסיס הנתונים (?!). כמה שבועות מאוחר יותר הבינו שזה היה חיסכון דבילי (לא קיבלו את דעתי הראשונית) - ויצרו לאובייקט Y טבלה משלו.

[ב] נקודתית, בשעתו - לא יודע כיצד הוא עכשיו.


2019-03-16

קוברנטיס: Deployments ו ReplicaSets


בפוסט הקודם על קוברנטיס, הגדרנו Pod - יחידת ההרצה הקטנה ביותר בקוברנטיס.
סיימנו בכך ש Pod שנוצר בגפו, מה שקרוי ״Naked Pod״ - ואיננו בעל שרידות גבוהה: אם הוא קרס, או ה node שבו הוא רץ קרס / נסגר - קוברנטיס לא יחדש אותו.

בפוסט הזה נציג סמנטיקה מרכזית נוספת בעבודה עם קוברנטיס: ה Deployment - המכמיסה בתוכה סמנטיקה בשם ReplicaSet אותה נסביר גם כן. הסמנטיקות הללו הן מה שמאפשרות להגדיר/לעדכן pods כך שיהיו resilient. זוהי תכונה קריטית שאנו מקבלים מקוברנטיס, ולא היינו מקבלים מ Dokcer לבדו.

בואו נתחיל.




Resilient Pods



בכדי ליהנות מיכולת מתקדמות של קוברנטיס כגון Self-Healing ו Auto-Scaling - עלינו להימנע מהגדרה של naked pods, ולהשתמש בסמנטיקה בשם ReplicaSet.

ReplicaSet הוא המקבילה של AWS Auto-Scaling-Group, מנגנון המוודא שמספר ה Pods שרצים:
  • לא יורד ממספר מסוים (Auto-Healing)
  • עולה בצורה דינאמית כאשר יש עומס על ה Pods (למשל: CPU utilization מעל 60%) - עד גבול מסוים שהגדרנו, ויורד בחזרה - כאשר העומס חולף.
למי שעבד עם שירותי-ענן, הצורך הזה אמור להיות ברור כשמש.

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


תרשים קונספטואלי ולא מדויק. נדייק אותו בהמשך.
מה Deployment מוסיף על ReplicaSet? במחשבה ראשונה נשמע שאנו זקוקים רק ל ReplicaSet.
ובכן, בואו ניקח תסריט מאוד נפוץ ויומיומי: עדכון Pod לגרסה חדשה יותר. ביצענו שינוי במיקרו-שירות הרץ כ Pod, ואנו רוצים לעדכן את הקוד שלו.
כפי שאנחנו זוכרים, צורת העבודה (המומלצת) עם קוברנטיס היא הגדרה דקלרטיבית:
  • אנו מגדירים מצב רצוי
  • קוברנטיס דוגם כל הזמן את המצב המערכת, ואם יש פעם בין המצב הנוכחי למצב הרצוי - היא פועלת לסגירת הפער. 
נניח שיש לנו 3 Pod replicas של מיקרו-שירות, הרצים כולם בגרסה 11 (למשל: build number). אנו רוצים לעדכן אותם לגרסה 12.

אם פשוט נעדכן את קוברניס שאנו רוצים את ה Pod replicas בגרסה 12, עלול לקרות המקרה המאוד-לא-רצוי הבא:
  1. קוברנטיס רואה שלא רוצים יותר את גרסה 11 - הוא מכבה את כל ה Pod replicas בגרסה הזו.
  2. לא טוב! מרגע זה אנחנו ב downtime.
  3. קוברנטיס רואה שרוצים את גרסה 12 - הוא מתחיל להריץ Pod replicas של הגרסה הזו.
  4. אבוי! בגרסה 12 יש באג בקוד, והמיקרו-שירות לא מצליח לרוץ.
  5. קוברנטיס יוצר Log מפורט וברור של הבעיה - אבל עד שלא נטפל בה בעצמנו - אנחנו ב Full Downtime.

זה בהחלט לא תסריט שאנו רוצים שיהיה אפשרי בסביבת Production!


Deployment, לשמחתנו, מוסיף את היכולות / האחריות הבאה:
  • ביצוע סדר הפעולות בצורה נכונה - כך שתמיד יהיו מספיק Pods שרצים.
  • בחינת (probing) ה Pod replicas החדשים - ו rollback במקרה של בעיה.
    • למשל: בדוק שה pod replica החדש שעלה הוא תקין (ע״פ תנאים מסוימים, מה שנקרא Readiness check) - לפני שאתה מעלה עוד pod replicas מהגרסה החדשה או מוריד עוד ישנים.
    • ישנן מגוון הגדרות בכדי לשלוט בהתנהגות המדויקת.

בתרשים למעלה ערבבתי, לצורך הפשטות, בין entities שהם קונפיגורציה (באפור: deployment, replica set) לבין entities של זמן-ריצה (בכתום: Pod replica). אני רוצה כעת לדייק את הדברים בעזרת התרשים הבא:




קונספט ארכיטקטוני מרכזי בקוברנטיס הוא ה Controllers, שהם בעצם מעין Modules או Plug-Ins של סביבת הניהול של קוברנטיס (ה Control Plane). ה Controllers מאזינים ל API Server הפנימי של קוברנטיס אחר שינויים על ה Resource הרלוונטי להם (Deployment, Service, וכו') או שינויים בדרישות - ואז מחילים את שינויים נדרשים.

בעיקרון ה Controllers רצים ב Reconciliation loop (כמו event loop) שנקרא כך בגלל שכל פעם שיש אירוע (שינוי שנדרש) הם מבצעים פעולה על מנת "ליישר קו" (reconcile) וכך, לעתים בצעדים, ה loop דואג שכל הזמן המצב בשטח יגיע במהרה למצב שהוגדר. מדי כמה זמן הם בודקים את סה"כ ההגדרות ומגיבים לפער - אם קיים. כלומר: גם אם event מסוים פוספס / לא טופל - תוך זמן קצר ה controller יגלה את הפער וישלים אותו.


בניגוד להפשטה המוצגת בתרשים למעלה, Controllers לעולם לא יתקשרו ישירות אחד עם השני (אחרת: הם לא יהיו Pluggable). כל קריאה היא ל API Server, למשל "צור משאב מסוג Replication", שבתורו יפעיל אירוע מתאים שייקלט ע"י ה Replication Controller ויוביל לפעולה.

אנחנו נראה בהמשך את ההגדרות של ה Deployment ואת ה template המדובר.

הנה תיאור דינאמי של תהליך ה deployment:



ה Deployment ישמור את ה replica set הקודם (ועליו כל ההגדרות), על מנת לאפשר תהליך של Rollback.
כמובן שההתנהגות המדויקת של שלב ה Mid (ליתר דיוק: סדרת שלבי ה mid) תלויה מאוד באסטרטגיית ה Deployment שנבחרת.

אני מניח שאתם מכירים את 2 האסטרטגיות המרכזיות, המקובלות בכלל בתעשייה: Rolling deployment ו Blue/Green deployment.
קוברנטיס לא תומך היום (בעת כתיבת הפוסט) ב Blue/Green deployments כיכולת-ליבה (אם כי יש מדריכים שמראים כיצד ניתן להשיג זאת, על בסיס יכולות קיימות של קוברנטיס - הנה דוגמה לאחד).

בכלל, כל נושא ה deployments הוא GA רק מגרסה 1.9 (תחילת 2018). כיום קוברנטיס תומך רק באסטרטגיות: "Recreate" (אסטרטגיה סופר פשוטה, בה יש downtime בהגדרה) ו "RollingUpdate" (ברירת המחדל).

הנה שני מקורות, המציגים כ"א כמה תסריטי deployment אפשריים, בהתבסס על יכולות הליבה של קוברנטיס:
  • מצגת של Cloud Native Computing Foundation (בקיצור: CNCF) - הארגון שהוקם על מנת לקחת אחריות על פרויקט קוברנטיס, ולנהל אותו כ Open Source בלתי-תלוי. יש גם סיכום one-pager של האסטרטגיות שהוא מציג. 
  • פוסט של ארגון בשם Container Solutions - שגם הוא מציג בצורה ויזואלית יפה, את משמעות האסטרטגיות השונות (סט דומה, אך לא זהה לקודם).



הגדרת Deployment בפועל


בלי לקשקש הרבה, נגדיר manifest של deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deploy
  labels:
    app: hello-world-deployment
spec:
  replicas: 4
  selector:
    matchLabels:
      app: hello-world
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  minReadySeconds: 5
  template:
    metadata:
      labels:
        app: hello-world
        env: dev
        version: v1
    spec:
      containers:
      - name: hello-pod
        image: hello-world:latest
        ports:
        - containerPort: 8080
          protocol: TCP
אתם כבר אמורים להכיר את 3 המפתחות הראשונים, ולכן נתמקד ב spec:
  • את הגדרת ה pod ניתן למצוא תחת ה template והן זהות ל Pod שהגדרנו בפוסט הקודם (מלבד label אחד שהוספנו). שימו לב שה metadata של ה Pod מופיע תחת template, ולא במפתח ה metadata של ה manifest.
  • replicas הוא מספר העותקים של ה pod שאנו רוצים להריץ. 
  • ה selector משמש לזיהוי חד משמעי של סוג ה pod שאנו מתארים. חשוב שניתן label "יציב" שבעזרתו קוברנטיס יידע לקשר ולהשוות אם היו שינויים בין pod templates. אם היה שינוי - עליו לבצע deploy.
    • למשל: אם שינינו גם את label הגרסה וגם את ה image - איך אפשר לקשר בוודאות שמדובר באותו האפליקציה, ולא בחדשה?
    • מקובל להשתמש ב label בשם app או app.kubernetes.io/name. 
  • על אסטרטגיית ה deployment כבר דיברנו. הנה 2 פרמטרים חשובים של אסטרטגית rolling deployment:
    • maxUnavailable - הכמות המרבית המותרת של pods שאינם זמינים תוך כדי פעולת deploy. הדבר משפיע כמה pods קוברנטיס יכול "להוריד במכה" כאשר הוא מתקין גרסה חדשה. המספר מתייחס למספר ה pods התקינים בגרסה הישנה + החדשה ביחד, וברירת המחדל היא 25%. ניתן גם לקבוע 0.
    • maxSurge - הוא פחות או יותר הפרמטר ההופכי: כמה pods חדשים ניתן להרים "במכה". ככל שהמספר גדול יותר - כך ה rolling deployment עשוי להיות מהיר יותר. גם כאן ברירת המחדל היא 25%.
      בקיצור גמור: אם יש לנו 4 pod replicas, הערכים שקבענו ב manifest יבטיחו שה cluster תמיד יכיל בין 3 ל 5 pod replicas בזמן deployment.
  • minReadySeconds - שדה רשות (ערך ברירת מחדל = 0) שמציין כמה שניות לחכות לאחר שה pod מוכן ("ready") לפני שמעבירים לו תעבורה. ההמתנה הזו היא פרקטיקה מקובלת, מכיוון שהנזק מ pod בעייתי שמחובר ל production traffic - עשוי להיות משמעותי. אפשר להיתקל גם בערכים של 20 ו 30 שניות. חשוב להזכיר שערך גבוה יאט את תהליך ה rolling deployment מכיוון שאנו ממתינים ל minReadySeconds - לפני שאנו ממשיכים להחליף עוד pods.
    • כאן שווה להזכיר את ה Readiness & Liveliness Probes של קוברנטיס. קוברנטיס מריץ ב nodes רכיב טכני הרץ כ container ומבצע בדיקות Health-check על ה pods השונים ב node ומדווח את התוצאות הלאה. כל pod צריך לענות ל2 קריאות: well-known/live./ ו well-known/ready./
      • מצב ה live הוא אות חיים בסיסי. המימוש המומלץ הוא פשוט להחזיר HTTP 200 OK מבלי לבצע פעולות נוספות. אם התשובה המתקבלת היא לא 2xx - קוברנטיס יאתחל את ה pod הזה מיד.
      • מצב ה ready אמור להיות עשיר יותר, בד"כ בודקים גישה לבסיס הנתונים ("SELECT 1") או גישה למשאבים קריטיים אחרים ל Pod (למשל: גישה לרדיס, או שירותים אחרים הקריטיים לפעילות ה pod). אם האפליקציה עוברת עדכון (למשל: הטמעת קונפיגורציה חדשה / עדכון caches ארוך) - הדרך הנכונה היא להגדיר אותה כ "לא ready" בזמן הזה.
        אם התשובה ל ready היא שלילית, קוברנטיס עשויה לנתק אותו מתעבורה נכנסת עד שיסתדר. אם המצב מתמשך (ברירת המחדל = 3 כישלונות רצופים) - ה pod יעבור restart.
        • שגיאה נפוצה היא להגדיר את live ו ready אותו הדבר - אבל אז מערבבים פעולות live יקרות מדי, ו restarts מיותרים של pods (כי עשינו restart בעקבות live אחד שכשל, נניח - מתקלת רשת נקודתית ואקראית).





את המניפסט, כמובן, מחילים כרגיל:
$ kubectl apply -f my-deployment.yaml
נוכל לעקוב אחר מצב ה deployment בעזרת הפקודה:

$ kubectl get deployment hello-deploy
אם אנו מגלים שהגרסה לא טובה (נניח: עלה באג חדש ומשמעותי לפרודקשיין), אנו יכולים לבצע rollback. הפקודה:
$ kubectl rollout history deployment hello-deploy
תציג לנו רשימה של revisions של ה deployment. נניח שאנו רוצים לחזור לגרסה 1, עלינו לפקוד:
$ kubectl rollout undo deployment hello-deploy --to-revision=1
כאשר מדובר ב RollingDeployment, ה rollback כמובן הוא deploy בפני עצמו שייקח זמן, ויעבוד לפי אותם הכללים של deploy חדש. תאורטית אנו יכולים פשוט לשנות את ה deployment.yaml בחזרה  למצב הקודם ולבצע deploy חדש - אי כי זה פחות מומלץ, אפילו רק מטעמי תיעוד.



סיכום


סקרנו את משאבי ה ReplicaSet וה Deployment בקוברנטיס - הדרך העיקרית לעדכן את המערכת ב pods חדשים / מעודכנים, בצורה Resilient.
על הדרך, הרחבנו מעט את ההבנה כיצד קוברנטיס עובד.

עדיין, לאחר שביצענו deployment לא נוכל לגשת ל pods שלנו (בקלות הרצויה). לשם כך עלינו להגדיר עוד משאב-ליבה בקוברנטיס בשם Service, המתייחס ל"קבוצה של Pod replicas בעלי אותו הממשק".

סמנטיקת ה Service מתוכננת להיות הנושא לפוסט הבא בסדרה.

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




2019-02-16

לקבל מושג ירוק על קוברנטיס (Kubernetes)


הפוסט הזה נכתב לאנשי-תוכנה מנוסים, המעוניינים להבין את Kubernetes - בהשקעת זמן קצרה.
הבאזז מסביב ל Docker ו Docker Orchestration הוא כרגע רב מאוד. דברי שבח רבים מסופרים על הטכנולוגיות הללו, מבלתי להתייחס לפרטים ועם מעט מאוד ראיה עניינית וביקורתית. כאנשי-תוכנה ותיקים אתם בוודאי מבינים שהעולם הטכנולוגיה מלא Trade-offs, וכדאי לגשת לטכנולוגיות חדשות עם מעט פחות התלהבות עיוורת - וקצת יותר הבנה.

אני הולך לספק את הידע בפוסט לא בפורמט ה "From A to Z" כפי שדיי מקובל - אבל ע"פ סדר שנראה לי יותר נכון והגיוני. אני רוצה לדלג על דיון ארוך על ההתקנה, ועוד דיון ארוך על הארכיטקטורה - עוד לפני שאנחנו מבינים מהי קוברנטיס. אני הולך לדלג על כמה פרטים - היכן שנראה לי שהדבר יתרום יותר להבנה הכללית.


בואו נתחיל.


אז מהי בעצם קוברנטיס?


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

בכדי לפשט את הדברים, ניתן פשוט לומר שקוברנטיס היא סוג של סביבת ענן: 
  • אנו אומרים לה מה להריץ - והיא מריצה. אנו ״מזינים״ אותה ב Containers (מסוג Docker או rkt) והגדרות - והיא ״דואגת לשאר״.
  • אנו מקצים לקוברנטיס כמה שרתים (להלן ״worker nodes״ או פשוט "nodes") שהם השרתים שעליהם ירוצו הקונטיינרים שלנו. בנוסף יש להקצות עוד כמה שרתים (בד״כ - קטנים יותר) בכדי להריץ את ה master nodes - ה״מוח״ מאחורי ה cluster.
  • קוברנטיס תדאג ל containers שלנו:
    • היא תדאג להרים כמה containers בכדי לאפשר high availability - ולהציב אותם על worker nodes שונים.
    • אם יש עומס עבודה, היא תדאג להריץ עוד עותקים של ה containers, וכשהעומס יחלוף - לצמצם את המספר. מה שנקרא auto-scaling.
    • אם container קורס, קוברנטיס תדאג להחליף אותו ב container תקין, מה שנקרא גם auto-healing.
    • קוברנטיס מספקת כלים נוחים לעדכון ה containers לגרסה חדשה יותר, בצורה שתצמצם למינימום את הפגיעה בעבודה השוטפת - מה שנקרא deployment
      • כפי שראינו בפוסט על Docker - פעולת restart של Container תהיה מהירה משמעותית מ VM, שזה גם אומר לרוב deployments מהירים יותר.
    • לשימוש בקוברנטיס יש יתרון בצמצום משמעותי של ה Lock-In ל Cloud Vendor [א], והיכולת להריץ את אותה תצורת ״הענן״ גם On-Premises.
      • הסתמכות על קוד פתוח, ולא קוד של ספק ספציפי - הוא גם יתרון, לאורך זמן, וכאשר הספק עשוי להיקלע לקשיים או לשנות מדיניות כלפי הלקוחות.
  • קוברנטיס גם מספקת לנו יכולות ליבה של ניהול Infrastructure as Code, היכולת להגדיר תצורה רצויה לתשתיות רשת, אבטחה בצורה הצהרתית ופשוטה - מה שמייתר כלי ניהול תצורה (Provisioning) כגון Chef, Puppet או Ansible - ויכול לחסוך עבודה משמעותית.




יעילות


עם כל היעילות המוגברת שהתרגלנו אליה מריצה בענן בעזרת שירותים כמו AWS EC2 או Azure Virtual Machines (להלן ״ענן של מכונות וירטואליות״) - קוברנטיס מאפשרת רמה חדשה וגבוהה יותר של יעילות בניצול משאבי-חומרה.

בתצורה קלאסית של מיקרו-שירותים, הפופולרית כיום - ייתכן ומדובר בניצולת חומרה טובה בכמה מונים. הכל תלוי בתצורה, אבל דיי טיפוסי להריץ את אותו ה workload של מיקרו-שירותים בעזרת קוברנטיס על גבי 20-50% בלבד מהחומרה שהייתה נדרשת על גבי ״ענן ציבורי של מכונות וירטואליות״.

איך זה קורה? למכונה וירטואלית יש overhead גבוה של זיכרון (הרצת מערכת ההפעלה + hypervisor) על כל VM שאנו מריצים. זה לא כ״כ משמעותי כשמריצים שרת גדול (כיום נקרא בבוז: Monolith) - אך זה מאוד משמעותי כאשר מריצים שרתים קטנים (להלן: מיקרו-שירותים).

מעבר לתקורה הישירה שעתה ציינו, יש תקורה עקיפה וגדולה יותר: כאשר אני מריץ על שרת 4 מיקרו-שירותים בעזרת VMs ומקצה לכל אחד מהמיקרו-שירותים 25% מהזיכרון וה CPU ההגבלה היא קשיחה. אם בזמן נתון שלושה מיקרו-שירותים משתמשים ב 10% ממשאבי המכונה כ״א, אבל המיקרו-שירות הרביעי זקוק ל 50% ממשאבי המכונה - הוא לא יכול לקבל אותם. ההקצאה של 25% היא קשיחה ואינה ניתנת להתגמשות, אפילו זמנית [ב].

בסביבת קוברנטיס ההגבלה היא לא קשיחה: ניתן לקבוע גבולות מינימום / מקסימום ולאפשר מצב בו 3 מיקרו-שירותים משתמשים ב 10% CPU ו/או זיכרון כ״א, והרביעי משתמש ב 50%. אפשר שגם 10 דקות אח״כ המיקרו-שירות הרביעי יהיה idle - ומיקרו-שירות אחר ישתמש ב 50% מהמשאבים.





הכרה חברתית


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

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

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


מחירים


כמובן שיש לכל הטוב הזה גם מחירים:
  • קוברנטיס היא טכנולוגיה חדשה שיש ללמוד - וכמות / מאמץ הלמידה הנדרש הוא לרוב גבוה ממה שאנשים מצפים לו.
    • התסריטים הפשוטים על גבי קוברנטיס נראים דיי פשוטים ואוטומטים. כאשר נכנסת לתמונה גם אבטחה, הגדרות רשת, ותעדוף בין מיקרו-שירות אחד על האחר - הדברים הופכים למורכבים יותר!
      Troubleshooting - עשוי להיות גם דבר לא פשוט, מכיוון ש"מתחת למכסה המנוע" של קוברנטיס - יש מנגנונים רבים.
    • ברוב המקרים נרצה להריץ את קוברנטיס על שירות ענן, ולכן נידרש עדיין לשמר מידה של מומחיות כפולה בשני השירותים: לשירות הענן ולקוברנטיס יש שירותים חופפים כמו Auto-Scaling, הרשאות ו Service Discovery (בד"כ: DNS). 
  • הטכנולוגיה אמנם לא ממש ״צעירה״, והיא בהחלט מוכחת ב Production - אך עדיין בסיסי הידע והקהילה התומכת עדיין לא גדולה כמו פתרונות ענן מסחריים אחרים. יש הרבה מאוד אינטגרציות, אך מעט פחות תיעוד איכותי וקל להבנה.
  • כמו פעמים רבות בשימוש ב Open Source - אין תמיכה מוסדרת. יש קהילה משמעותית ופעילה - אבל עדיין הדרך לפתרון בעיות עשויה להיות קשה יותר מהתבססות על פתרון מסחרי. 
    • גם בשימוש ב״קוברנטיס מנוהל״ (EKS, AKS, ו GKE), החלק המנוהל הוא החלק הקטן, והשאר - באחריותנו.
    • האם החיסכון הצפוי מניהול משאבים יעיל יותר, יצדיק במקרה שלכם שימוש בסביבה שדורשת מכם יותר תפעול והבנה?
      • במקרה של ניהול מאות או אלפי שרתים - קרוב לוודאי שזה ישתלם.
      • שימוש בקוברנטיס עשוי לפשט את סביבת התפעול, וה Deployment Pipeline. ההשקעה הנדרשת היא מיידית - בעוד התשואה עשויה להגיע רק לאחר זמן ניכר, כאשר היישום הספציפי באמת הגיע לבגרות.
      • במקרים לא מעטים, ארגונים נקלעים לשרשרת של החלטות שנגזרות מצו האופנה ובניגוד לאינטרס הישיר שלהם: עוברים למיקרו-שירותים כי ״כך כולם עושים״ / ״סיפורי הצלחה״ שנשמעים, משם נגררים לקוברנטיס - כי יש להם הרבה מאוד שרתים לנהל, שכבר נהיה דיי יקר.
        לו היינו עושים שיקולי עלות/תועלת מול המצב הסופי - כנראה שהרבה פעמים היה נכון לחלק את המערכת למודולים פשוטים, או להשתמש במיקרו-שירותים גדולים ("midi-services") - וכך לשלוט טוב יותר בעלויות והמורכבויות האחרות.





קוברנטיס בפועל



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

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

לצורך הדיון, נניח שאנו עובדים על AWS ו EKS ואמזון מנהלים עבורנו את ה Masters nodes. ה Worker nodes שלנו נמצאים ב Auto-Scaling Group - מה שאומר שאמזון תנהל עבורנו את ה nodes מבחינת עומס (תוסיף ותוריד מכונות ע״פ הצורך) והחלפת שרתים שכשלו. זה חשוב!

אנחנו גם משתמשים ב ECR (קרי Container Registry מנוהל) , ואנו משתמשים בכל האינטגרציות האפשריות של קוברנטיס לענן של אמזון (VPC, IAM, ELB, וכו׳). התצורה מקונפגת ועובדת היטב - ונותר רק להשתמש בה.

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






יצירת Pod


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

אני מניח שאנחנו מבינים מהו Container (אם לא - שווה לחזור צעד אחורה, ולהבין. למשל: הפוסט שלי בנושא), ויש לנו כבר Image שאנו רוצים להריץ ב ECR.

בכדי להריץ Container, עלינו לעדכן את קוברנטיס ב manifest file המתאר Pod חדש מצביע ל container image. קוברנטיס ירשום את ה Pod ויתזמן אותו לרוץ על אחד מה nodes שזמינים לו.

כאשר ה node מקבל הוראה להריץ Pod עליו להוריד את ה container image - אם אין לו אותו כבר. כל node מחזיק עותקים עצמאיים של ה container images משיקולים של high availability.

הנה קובץ ה manifest שהרכבנו:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  labels:
    env: dev
    version: v1
spec:
  containers:
  - name: hello-world-ctr
    image: hello-world:latest
    ports:
    - containerPort: 8080
      protocol: TCP

קובץ ה manifest בקוברנטיס מורכב מ 4 חלקים סטנדרטיים:
  1. גרסת ה API
    1. הפורמט הוא לרוב <api group>/<version> אבל כמה הפקודות הבסיסיות ביותר בקוברנטיס נמצאות תחת API Group שנקרא core - ולא צריך לציין אותו.
    2. ה API של קוברנטיס נמצא (בעת כתיבת הפוסט) בגרסה 1.13 - אז למה גרסה 1?
      ניהול הגרסאות בקוברנטיס הוא ברזולוציה של משאב. הקריאה לייצור pod היא עדיין בגרסה 1 (כמו כמעט כל ה APIs. בעת כתיבת הפוסט אין עדיין גרסת v2 לשום API, מלבד v2alpha או v2beta - כלומר גרסאות v2 שעדיין אינן GA).
  2. סוג (kind) - הצהרה על סוג האובייקט המדובר. במקרה שלנו: Pod.
  3. metadata - הכולל שם ו labels שיעזרו לנו לזהות את ה pod שיצרנו.
    1. ה labels הם פשוט זוגות key/value שאנחנו בוחרים. הם חשובים מאוד לצורך ניהול Cattle של אובייקטים, והם בלב העבודה בקוברנטיס.
  4. spec - החלק המכיל הגדרות ספציפיות של המשאב שהוגדר כ "Type". 
    1. name - השם שניתן ל container בתוך ה Pod, וצריך להיות ייחודי. במקרה של container יחיד בתוך ה pod - אין בעיה כזו. 
    2. image - כמו בפקודת docker run...
    3. ports - ה port שיהיה published.
      TCP הוא ערך ברירת-המחדל, אך הוספתי אותו בכדי לעשות את ה Yaml לקריא יותר.



תזכורת קצרה על Yaml:

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

רשימה ב Yaml נראית כך:
mylist:
    - 100
    - 200
"mylist" הוא ה key, והערך שלו הוא רשימה של הערכים 100 ו 200.
כל האיברים שלפניהם הסימן " - " ובעלי עימוד זהה - הם חברים ברשימה.
סימן ה Tab ברוב ה Editors מתתרגם ל 2 או 4 רווחים. ב Yaml הוא שקול לרווח אחד, ולכן שימוש בו הוא מקור לבעיות ובלבולים. הימנעו משימוש ב Tab בתוך קבצי Yaml!

המבנה הבא, שגם מופיע ב manifest (ועשוי לבלבל) הוא בעצם רשימה של Maps:
channels:
  - name: '#mychannel'
    password: ''
  - name: '#myprivatechannel'
    password: 'mypassword'
"channel" הוא המפתח הראשי, הכולל רשימה.
כאשר יש "מפתח: ערך" מתחת ל"מפתח: ערך" באותו העימוד - משמע שמדובר ב Map.
כלומר, המפתח "channel" מחזיק ברשימה של שני איברים, כל אחד מהם הוא מפה הכוללת שני מפתחות "name" ו "password".

אם נחזור לדוגמה של הגדרת ה container ב manifest למעלה, בעצם מדובר במפתח "containers" המכיל רשימה של איבר אחד.
בתוך הרשימה יש מפה עם 3 מפתחות ("image", "name", ו "ports") כאשר המפתח האחרון "ports" מכיל רשימה עם ערך יחיד, ובה מפה בעלת 2 entries.

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





עכשיו כשיש לנו manifest, אנחנו יכולים להריץ את ה Pod:
$ kubectl apply -f my-manifest-file.yml

kubectl הוא כלי ה command line של קוברנטיס. פקודות מסוימות בו יזכירו לכם את ה command line של docker.
במקרה הזה אנו במקרה הזה אנו מורים לקוברנטיס להחיל קונפיגורציה. הפרמטר f- מציין שאנו מספקים שם של קובץ.

תוך כמה עשרות שניות, לכל היות, ה pod שהגדרנו אמור כבר לרוץ על אחד ה nodes של ה cluster של קוברנטיס.

אנו יכולים לבדוק אלו Pods רצים בעזרת הפקודה הבאה:
$ kubectl get pods
עמודה חשובה שמוצגת כתוצאה, היא עמודת הסטטוס - המציגה את הסטטוס הנוכחי של ה pod. אמנם יש רשימה סגורה של מצבים בו עשוי להיות pod, אולי עדיין הסטטוס המדווח יכול להיות שונה.
למשל: הסטטוס ContainerCreating יופיע בזמן שה docker image יורד ל node. זה מצב נפוץ - אך לא מתועד היטב. את הסטטוס ניתן למצוא בעיקר... בקוד המקור של קוברנטיס.

הפקודה הבאה (וריאציות), בדומה לפקודת ה Docker המקבילה - תציג את הלוגים של ה Container ב Pod :
$ kubectl logs my-pod 
אם ב Pod יש יותר מ-2 containers (מצב שלא אכסה בפוסט), הפקודה תציג לוגים של ה container הראשון שהוגדר ב manifest. אפשר לציין את שם ה container כפי שצוין ב manifest - וכך להגיע ל container נתון בתוך Pod-מרובה containers.

עבור תקלות יותר בסיסיות (למשל: ה pod תקוע על מצב ContainerCreating וכנראה שה node לא מצליח להוריד את container image) - כדאי להשתמש בפקודה:
$ kubectl describe pods my-pod
התוצאה תהיה ססטוס מפורט שיכיל את הפרטים העיקריים מתוך ה manifest, רשימה של conditions של ה pod, ורשימת כל אירועי-המערכת שעברו על ה pod מרגע שהורנו על יצירתו. הנה דוגמה להפעלה הפקודה (מקור):


Name:  nginx-deployment-1006230814-6winp
Node:  kubernetes-node-wul5/10.240.0.9
Start Time: Thu, 24 Mar 2016 01:39:49 +0000
...
Status:  Running
IP:  10.244.0.6
Controllers: ReplicaSet/nginx-deployment-1006230814
Containers:
  nginx:
    Container ID: docker://90315cc9f513c750f244a355eb1149
    Image:  nginx
    Image ID:  docker://6f623fa05180298c351cce53963707
    Port:  80/TCP
    Limits:
      cpu: 500m
      memory: 128Mi
    State:  Running
      Started:  Thu, 24 Mar 2016 01:39:51 +0000
    Ready:  True
    Restart Count: 0
    Environment:        <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-5kdvl (ro)
Conditions:
  Type          Status
  Initialized   True
  Ready         True
  PodScheduled  True
Volumes:
  default-token-4bcbi:
    Type: Secret (a volume populated by a Secret)
    SecretName: default-token-4bcbi
    Optional:   false
...
Events:
  FirstSeen LastSeen Count From     SubobjectPath  Type  Reason  Message
  --------- -------- ----- ----     -------------  -------- ------  -------
  54s  54s  1 {default-scheduler }      Normal  Scheduled Successfully assigned nginx-deployment-1006230814-6winp to kubernetes-node-wul5
  54s  54s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Pulling  pulling image "nginx"
  53s  53s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Pulled  Successfully pulled image "nginx"
  53s  53s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Created  Created container with docker id 90315cc9f513
  53s  53s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Started  Started container with docker id 90315cc9f513

ניתן גם, בדומה ל Docker, לגשת ישירות ל console של ה container שרץ ב pod שלנו בעזרת הפקודה:
$ kubectl exec my-pod -c hello-world-ctr -it -- bash
במקרה הזה ציינתי את שם ה container, אם כי לא הייתי חייב.


נראה לי שזה מספיק, לבינתיים. בואו נסגור את העניינים:
$ kubectl delete -f my-manifest-file.yml
הפעולה הזו עלולה להיראות מוזרה ברגע ראשון. הסרנו את הקונפיגורציה - ולכן גם ה Pod ייסגר?

לשימוש בקוברנטיס יש שתי גישות עיקריות:

  • גישה אימפרטיבית - בה מורים לקוברנטיס איזה שינוי לבצע: להוסיף משאב, לשנות משאב, או להוריד משאב.
    • פקודות כגון kube ctl create או kubectl replace הן בבסיס הגישה האימפרטיבית.
  • גישה דקלרטיבית - בה מורים לקוברנטיס מה המצב הרצוי - והוא יגיע עליו בעצמו.
    • פקודת kubectl apply - היא בבסיס הגישה הדקלרטיבית. אפשר להגדיר כמעט הכל, רק באמצעותה.
    • החלת patch על גבי קונפיגורציה קיימת הוא משהו באמצע: זו פקודה דקלרטיבית, אך מעט חורגת מה lifecycle המסודר של הגישה הדקלרטיבית הקלאסית. סוג של תרגיל נינג'ה.
כמובן שהגישה הדקלרטיבית נחשבת קלה יותר לשימוש ולתחזוקה לאורך זמן - והיא הגישה הנפוצה והשלטת.

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



סיכום


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

לא פעם קראתי מאמרים שמתארים את קוברנטיס כמעבר מהובלת מסע על גבי פרד - לנסיעה ברכב שטח יעיל!
אני חושב שההנחה הסמויה ברוב התיאורים הללו היא שהקוראים עדיין עובדים על גבי מערכות On-Premises ללא טכנולוגיות ענן. מעבר משם לקוברנטיס - היא באמת התקדמות אדירה.

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


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

חשוב לציין שה Pod שהגדרנו הוא עצמאי ו"חסר גיבוי" - מה שנקרא "naked pod".
אם naked pod כשל מסיבה כלשהי (הקוד שהוא מריץ קרס, או ה node שעליו הוא רץ קרס/נסגר) - הוא לא יתוזמן לרוץ מחדש. מנגנון ה auto-healing של קוברנטיס שייך לאובייקט / אבסטרקציה גבוהה יותר בשם ReplicaSet. אבסטרקציה מעט יותר גבוהה, שבה בדרך כלל משתמשים - נקראת Deployment.

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


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




----

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

רפרנס של פקודות ה kubectl


----

[א] אל דאגה! לספקי הענן יש אינטרס עליון לגרום לנו ל Lock-In גם על סביבת קוברנטיס. לאחר שהניסיונות להציע חלופות ״מקומיות״ לקוברנטיס כשלו - רק טבעי שהם יתמקדו בלהציע יכולות שיפשטו את השימוש בקוברנטיס, אך גם יוסיפו סוגים חדשים של Lock-In. בכל מקרה, ברוב הפעמים אנו כבר תלויים בתשתיות כמו RDS, Athena, S3 ועוד - ולכן כבר יש Lock-In מסוים גם בלי קשר לקוברנטיס.
"Cloud Agnostic Architecture״ הוא בסה״כ מיתוס, השאלה היא רק מידת התלות.

[ב] שווה לציין שזה המצב בענן ציבורי. כששכן שלנו למכונה ב AWS רוצה יותר CPU - למה שנסכים לתת לו? אנחנו משלמים על ה ״slice״ שלנו במכונה - שהוגדר בתנאי השירות.
בפתרונות של ענן פרטי (כמו VMWare) ישנן יכולות ״ללמוד״ על brusts של שימוש בקרב VM ולהתאים את המשאבים בצורה יעילה יותר. כלומר: המערכת רואה ש VM מספר 4 דורש יותר CPU אז היא משנה, בצורה מנוהלת, את ההגדרות הקשיחות כך שלזמן מסוים - הוא יקבל יותר CPU מה VMs האחרים הרצים על אותה המכונה. טכנולוגית ה VM עדיין מקצה משאבים בצורה קשיחה - אך תכנון דינמי יכול להגביר יעילות השימוש בהם. זה יכול לעבוד רק כאשר כל ה VMs על המכונה שייכים לאותו הארגון / יש ביניהם הסכמה.
T3/T2 instances ב EC2 הם VMs שעובדים על עיקרון דומה: ב״חוזה״ שלנו רשום שה instance יכול לעבוד ב burst ולקבל יותר משאבים - אך עדיין יש פה עבודה לפי חוזה, ולא אופטימיזציה גלובלית של המשאבים על המכונה (מכיוון שה VMs שייכים לארגונים שונים).