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

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

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

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

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

הכלל הראשון שארצה להדגיש הוא זה:

כלל חשוב לחיים המקצועיים!

כדי להסביר את הכלל, אפתח בדוגמה משעשעת של אלגוריתם חיפוש בשם Sleep Sort:

ע״פ האלגוריתם הזה, יש רשימת מקור (הקלט) ורשימת יעד (התוצאה). בעת ההרצה אנו סורקים את רשימת המקור ולכל איבר מפעילים פונקציה (למשל: co-routine) שתכניס את האיבר לרשימת היעד בעוד n שניות, כאשר n הוא גודל האיבר.

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

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

למשל, עבור קלט של המספרים 2 ו 1,000,000,000 האלגוריתם ירוץ במשך כמעט 32 שנים – מה שבהחלט פחות יעיל אפילו מ Bubble Sort. 

מה היה לנו כאן? מחיר אמיתי מאוד, ומשמעותי מאוד, שלא לקחנו בחשבון בהערכת הזמן של האלגוריתם. יכולנו בהתאם לשכלל את התאוריה שלנו ולנסח זמן ריצה בנוסח:
(O(max(item_size) * sec) +n – כאשר בעיקרון אפשר להזניח את n.

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

  • HashTable
  • LinkedList
  • Binary Search Tree
  • Graphs

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

חוק המספרים הקטנים

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

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

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

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

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

חשוב לציין שלא כל "פקודת מחשב" מתבצעת ע"י cycle בודד של מעבד. ליהפך: תנאי if פשוט לכאורה, עלול בקלות להתתרגם לעשרות ומאות CPU cycles. כאמצעי ביטחון אפשר לחשוב על ה CPU כמבצע עשרות-מיליוני פעולות בשנייה בלבד.

"מספרים שכל מתכנת חייב להכיר" (2012). איפה הייתם בקורס מבני-נתונים?!

המחיר הנוסף

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

  • גישה לדיסק היא מאוד מאוד יקרה (לפני עידן ה SSD היא הייתה מאוד מאוד מאוד יקרה).
  • גישה לזיכרון היא יקרה, ובהחלט לא אקראית – על אף השם "Random Access Memory = RAM".
  • נתונים, גם מהרשת, גם מהדיסק, וגם משכבות הזיכרון השונות מוגשות בבלוקים רציפים. העלות להביא את הבלוק היא החלק היקר, בעוד ההבדל בין סריקה של בית בודד או את כל הבלוק – הוא לרוב משני עד זניח.
    • אפשר לראות את ההתנהגות הזו בנתונים למעלה, כאשר קריאה של בית בודד מ SSD תיקח 0.15ms בעוד קריאה של מיליון בתים מ SSD (עשרות עד מאות בלוקים) – תיקח רק מעט יותר: כ 1.0ms.
    • שווה מאוד לקרוא ולטפל בנתונים ברציפות, ולא בסדר אקראי
  • למעבדים יש היררכיה של זיכרונות Cache. נתון שלא נמצא ב Cache יובא קודם ל L3 ואז ל L2 ואז ל L1, ולכל הבאה שכזו, יקדם חיפוש ברחבי ה Cache Level שהנתון אכן לא שם.
    • זה בזבוז להביא מילה בודדת בזיכרון, ולכן כל פעם מביאים בלוק זיכרון רציף. גודל הבלוק – תלוי בארכיטקטורת המעבד.
נתחיל בתכלס, ונראה איך זה משפיע עלינו.

קרב ראשון: Vector/ArrayList מול LinkedList

אנחנו מעונינים להוסיף דינאמית איברית לרשימה. איזה מבנה-נתונים הכי מתאים? Vector (אשתמש בשם הקדום ב ++C) או רשימה משורשרת?

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

רשימה משורשרת פשוט מוסיפה עוד ועוד ערכים במחיר (O(1 לכל פעולה.

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

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

בואו נראה מה קורה בפועל:

הממ… לא בדיוק.

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

  • כל אלמנט ברשימה המשורשרת תופס יותר זיכרון: צריך להכניס לזיכרון גם את הערך וגם את המצביע לאלמנט הבא. כנ"ל אם הערך הוא מצביע לאובייקט ולא Int. זה אותו הדבר.
  • בתאוריה: העתקה של מרחבי זיכרון רציפים היא עלות (O(n או משהו יקר אחר.
    בפועל: במעבדים מודרניים יש פקודה יעילה למדי להעתקה של בלוקים של זיכרון. זו איננה פעולה יקרה כ"כ.
  • חיפוש המקום להכנסה ברשימה משורשרת הוא הנקודה החלשה הבולטת של הרשימה המשורשרת: ה nodes של הרשימה מפוזרים אקראית במרחב הזיכרון. כשאנו סורקים את הרשימה (בממוצע n/4 איברים בכל פעם) אנחנו "חוטפים" עוד ועוד cache misses הדורשים לעדכן את זיכרון המטמון.
    • כאשר אנחנו סורקים את הוקטור, ככמט כל בלוק של זיכרון שנביא ל cache – ינוצל במלואו.
    • במקרה של שימוש ב virtual memory (לא נכלל בגרף) – המקרה גרוע הרבה יותר: ייתכן ובגלל קפיצות בין דפים שונים של ה main memory "נחטוף" Page Fault ויהיה עלינו להביא את דף הזיכרון מהדיסק, רחמנא ליצלן!

שיפור עמודות לטובת הרשימה-המשורשרת

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

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

עד כמה עומדת הרשימה המשורשרת למחוץ את הוקטור? בתיאוריה זה אמור להיות אכזרי כלפי הוקטור. בואו נראה בפועל:

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

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

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

בעולם הג'אווה – המשחק משתנה

הדוגמאות מלמעלה הן ב ++C, וכנראה מייצגות גם את מה שמתרחש ב Rust או Go. שפות שעובדות עם הזיכרון מול מערכת ההפעלה ומושפעות ישירות מארכיטקטורת המעבד.
בג'אווה (או #C) ואולי גם בפייטון / רובי – הדברים מעט שונים. יש מפרשן או JVM/CLR שנמצאים בין מערכת ההפעלה לקוד. יש שימוש מאסיבי ב Heap – שאינו רציף.
איך הדברים נראים בג'אווה? אולי שם ההבדל בין וקטור לרשימה משורשרת נמחק?
בואו נבדוק שוב את המקרה של הכנסת איבר למקום הממוין ברשימה.

אין לי גרף מתאים (הנה המקור לנתונים), אבל מאיסוף ידני של הנתונים ממבחן ב ++C, ב 20,000 איברים היו התוצאות 617 מילישניות לרשימה משורשרת, ו 234 מילישניות לוקטור – שזה יחס דומה.

ה DynamicIntArray, אם תהיתם, הוא מימוש של ArrayList המאחסן איברים מסוג int (פרמיטיב) ולא ב Integer (אובייקט) – ולכן הוא ידידותי באמת ל cache. הנתונים באמת שמורים במערך רציף (כמו ב ++C) והם לא reference לאובייקט שתלוי ב Heap (שאותו ג'אווה אכן משתדלת למלא בצורה רציפה).

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

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

את זה לא סיפרו לי בקורס מבני-נתונים!

סיכום ביניים

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

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

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

מה עושים?

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

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

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

פעם נתקלתי במימוש שבאמת הייתה בו חשיבות לשימוש ברשימה משורשרת. מה עשינו? הקצנו את הזיכרון עבור כל הרשימה בצורה רציפה (כמו וקטור) והשתמשנו רק בו. דבר דומה עשה חבר שדיברתי איתו – מה שגרם לי להבין שזה common sense ולא המצאה גדולה. חיפוש פשוט בגוגל העלה מימוש שכזה כקוד פתוח.
אלטרנטיבה אחרת, וקצת יותר ידועה: Unrolled Linked List.

מה עושים בגרפים? אני לא מכיר באופן אישי, אבל אני מניח שגם Neo4J או AWS Neptune מצאו את הדרך שלהם לבנות את מבנה הנתונים החשוב והנהדר הזה Graph – בצורה שאיננה עוינת לזיכרון. או לפחות: עוינת פחות ככל האפשר.

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

שיהיה בצלחה!

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

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

ג'אווה 11 – 9: שאלות ותשובות – שכדאי להכיר

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

החברה שפיתחה את ג'אווה במקור (Sun Microsystems) כשלה עסקית ונרכשה ע"י Oracle בשנת 2010 – שקיבלה את הנכס הנדיר שנקרא ג'אווה, אך נכס שגם דורש השקעה רבה ומחולק בחינם בעולם.

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

כשאנו מדברים על שינוים אנחנו לא מדברים רק על שינוים בשפת ג'אווה עצמה – אלא בעיקר על שינויים בפלטפורמת ה JVM, פלטפורמה שמשרתת את Scala, Closure, קוטלין, Groovy, JRuby, ועוד. חשוב לזכור שה JVM הוא גדול יותר מג'אווה.

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

  • Module System – מודולריזציה בתוך ה JDK, ולראשונה הסרה של קוד ישן (בכמויות לא-מבוטלות). המהלך מציע כמה יתרונות טכניים חשובים – אך משמעותו שלראשונה ה JDK הופך ל non-backward-compatible, מה שעשוי לפגוע לאורך שנים בקצב האימוץ של הגרסאות החדשות. הסרת קוד ישן מה JDK מתוכננת להמשיך בגרסאות עתידיות של ג'אווה גם כן.
  • New release cycle – הכולל releases "קטנים" כל חצי שנה, וגרסאות LTS.
  • גביית דמי-שימוש עבור עדכוני באגים ועדכוני-אבטחה – אורקל עושה מהלך משמעותי על מנת לסבסד את תחזוקת ג'אווה ע"י משתמשיה, ואולי אף יוביל לרווחים משמעותיים לאורקל עצמה.
    • יישור קו בין OracleJDK ל OpenJDK – צעד משנה למהלך הנ"ל.

גרסה 9 הציגה לראשונה התחלה של כל השינויים הנ"ל, אך הוכרז מראש שזו גרסה שתתמך לחצי-שנה בלבד, וגרסת היעד לתמיכה ארוכה (LTS, כלומר Long-term support) תהיה גרסה 11.
מאז יצאה גם ג'אווה 10 – גם היא גרסה מינורית ולא כ"כ נוספת, שסיפקה עוד כמה התקדמויות בנושאים הנ"ל (ומספר תוספות קטנות לשפה) – אבל חשיבות הגרסה הייתה בהיותה הכנה גנרלית לשחרור גרסה 11.

גרסה 11 היא זו שתעמת את קהילת ה JVM בפעם הראשונה באמת עם מציאות חדשה, והדילמות שנובעות ממנה. זו גרסת LTS שתתמך עכשיו עד 8 שנים קדימה, גרסת ג'אווה המשמעותית שתהיה זמינה בשנים הקרובות.

  • האם קהילת הג'אווה שהייתה רגילה לג'אווה חינמית תתחיל לשלם ל Oracle עבור עדכונים ותמיכה?
  • האם החלק הארי של הקהילה יישאר על גרסה 8, או שנראה אימוץ משמעותי גם של גרסאות 11 והלאה, כבר בשנים הקרובות?

נותר רק להמתין ולראות כיצד הדברים יתפתחו.

בואו נתחיל לסגור פינות…

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

מה ההבדל בעצם בין OpenJDK ו OracleJDK? ומה השתנה?

החל מגרסה 7 החלה להיות משוחררת ההפצה של OpenJDK, שהיא הפצה תחת רישיון GNU GPL v2 עם החרגה ל linking – רישיון Open Source חופשי, המאפשר שימוש בג'אווה בכדי לכתוב תוכנה מסחרית ולמכור אותה.

היוזמה הייתה עוד של חברת Sun, והגנה על האופי הפתוח של ג'אווה.

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

החל מגרסה 9, הפצות ה OpenJDK וה OracleJDK מתאחדות. זה לא יקרה ע"י הסרה של תוכן מתוך ה OracleJDK אלא בעיקר ע"י פתיחה של התוספות שאורקל סיפקה להיות OpenSource. כמה תכונות עם רישיון מסחרי (כמו ה Java Web Start) אכן יוסרו מה JDK, אבל בעיקר בגלל שהאימוץ שלהן נמוך למדי. התהליך מסתיים עכשיו בשחרור גרסה 11, בו תוכן ההפצות הוא זהה – וההבדל ביניהן הוא רק הרישיון.

למה לי לשלם לאורקל כסף על מה שזמין בחינם וזוכה לתמיכה של הקהילה (כלומר: OpenJDK)?

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

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

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

ג'אווה 9 הציגה מחזור חדש של שחרור גרסאות שנראה כך:

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

החל מגרסה 9, אורקל מתחייבת שחרר גרסה פעמיים בשנה, מה שיאפשר לשחרר תוספות לשפה וה JVM בתדירות גבוהה יותר.
הבחירה במספרים שלמים עבור הגרסאות הללו לא מצביע על כמות התוכן שיתווסף, אלא על אופנה של השנים האחרונות ״לרוץ״ עם מספרי גרסאות (למשל: ראקט הייתה בגרסה 0.14 לפני שלוש שנים – אך כיום היא כבר בגרסה 16.5. אנגולר הייתה בגרסה 2.0 ב 2014, אך עוד מעט משתחררת גרסה 7.0).

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

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

  • לעדכן את גרסת ה JVM/ג'אווה שלנו כל 6 חודשים כמו שעון. כל יום שאנו לא על הגרסה האחרונה הוא יום שבוא יכול להשתחרר עדכון אבטחה קריטי שלא נקבל.
  • להשתמש ב OracleJDK בתשלום ולעבוד על גרסאות LTS על מנת לקבל עדכוני אבטחה שוטפים. התשלום הוא כ $2.5 דולר לחודש ל Desktop/Laptop ו $25 לחודש לכל Core פיסי של שרת [1].
  • לקנות תמיכה מספק צד-שלישי, כגון Azul או RedHat כאשר הם אלו שיספקו תיקונים לבאגים ובעיות אבטחה. כל חברה – עם המדיניות שלה.
    • למשל, ל Azul יש תוכנית בשם Medium Term Support (בקיצור MTS), שבה היא תתמוך לאורך שנתיים וחצי בכל גרסה שניה של ג'אווה שאיננה LTS. יתרון גדול בגישה הזו היא חפיפה בתמיכה בין גרסאות ה MTS – מה שלא קיים בתמיכה ה"חינמית" של אורקל .
    • חשוב לספקי צד-שלישי יספקו גם תמיכה לג'אווה גרסה 8 לעוד כמה שנים – היכן שבכל מקרה רוב התעשייה צפויה להיות בזמן הזה. הנה למשל הצהרה של יבמ.
עוד פרט שכדאי לציין הוא שהרישיון של OracleJDK (לפחות לגרסה 11) מאפשר שימוש ללא הגבלה לצורך פיתוח. התשלום הוא רק עבור שימוש production (שאותו ישלם לקוח המריץ את הקוד On-Premises, או ספק SaaS – עבור המכונות שהוא מריץ).

האם ההצהרה על שחרור גרסת ג'אווה כל 6 חודשים היא לא מטעה? בעצם נראה שגרסאות ה LTS הן אלו שמשנות – והן ימשיכו בקצב אטי יחסית של כל 3 שנים?

באמת היא כנראה איפשהו באמצע. אורקל מצהירה שכל גרסה שתשוחרר תעבור Quality cycle מפרך כמו של גרסה מ'אגורית עד היום. כמו כן יש הצהרה שהיא תשחרר לפחות שני עדכונים (תיקוני באגים ואבטחה) מינוריים – לכל גרסה שתצא. גרסאות שאינן LTS  הן לא גרסאות "בטא" – ע"פ ההצהרה.
סביר להניח ששחרור גרסאות תכוף יאפשר לג'אווה להתפתח מהר יותר – אך במידה. מי שצפוי לעבוד באמת עם גרסאות שאינן LTS הוא רק פלח צר של Early adopters (למשל: סטארטאפ בתחילת דרכו), במיוחד בשנים הראשונות.

האם מה שאורקל עושה הוא "בסדר"? האם זה לא "מחטף מרושע"?

אין לי תשובה ברורה. בד"כ כשארגון רוצה לקבל הכנסות מפרויקט Open Source – הוא מציע ערך נוסף על הקיים בכדי לקבל עליו תשלום: Premium support או יכולות נוספות (למשל: Redis modules).
אורקל בעצם ממשיכה לגבות תשלום על תמיכה, תוך כדי שהיא מפסיקה בפועל את התמיכה-בחינם שהיא סיפקה לאורך שנים רבות. אם היה מדובר בפרויקט קטן – זה אולי לא היה מתקבל בשמחה, אך זה לא היה עושה הרבה רעש. ג'אווה היא אחת מהתשתיות (ביחד עם לינוקס, MySQL ועוד כמה) המשמעותיות ביותר המסופקות בחינם. ההשפעה – היא משמעותית!

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

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

כאב ראש לאנשים רבים בעולם – ככל הנראה ייגרם בכל זאת.

"איך אתה מתכנן להגיב לשינוי בתהליך שחרור הגרסאות של ג'אווה" מתוך סקר של ה JVM Ecosystem

דיברנו על איזו בעיה של תאימות לאחור, במה בדיוק מדובר?

ג'אווה 9 הציגה לראשונה את ה Module System (הידוע לשעבר כ Project Jigsaw או super packages) – כלי מודולריזציה בשפה.

packages, כלי ארגון הקוד עד עכשיו, הוא כלי נחמד – אבל עם מגבלה עיקרית: הוא עוזר לארגן את תוך ה package (למשל: ניתן להגדיר מחלקות כ private ל package) אך הוא לא מתמודד עם ניהול תלויות בין packages.

  • אין לי יכולת אמיתית לחשוף מחלקות מסוימות בצורה סלקטיבית ל packages אחרים – הרזולוציה היא רק private או public.
  • אין ניהול רציני של התלויות בין ה packages.
    • כל מחלקה יכולה להוסיף כל תלות שהיא רוצה ל package אחר. כלומר: התלויות בין ה packages מגודרות שוב ושוב, ומפוזרות בין ה classes השונים של המחלקה – שזה לא DRY, וקשה מאוד לבקרה.
    • כל מחלקה יכולה להוסיף את עצמה לכל package – כך שאין שליטה ממה מורכב בדיוק ה package. זו בעיה משנית.
ה Module System (בקיצור: MDS) של ג'אווה מאפשר להגדיר קובץ בשם module-info.java המגדיר מודול ואת התלויות שלו. הנה דוגמה לכזו הגדרה:
module monitor.rest {
    requires spark.core;
    requires monitor.statistics;
    exports monitor.rest;
}
הקומפיילר יתייחס להגדרות ויאכוף אותן (יופי!).
חשוב לציין שמילים שמורות חדשות, כמו module ו requires הן בעלות משמעות רק בקובץ ה module-info, ולא ישפיעו על שאר קוד הג'אווה.

קישורים: Java 9 modules cheat sheet, מדריך, ועוד כמה מקורות טובים

ג'אווה השתמשה ב MDS בעצמה עבור ה JDK. במידה מסוימת זה היה סוג של "eat your own dog food", אבל מצד שני – זה היה צורך מרכזי של ה JDK עצמו, שעם השנים התלויות הרבות והמיותרות בו הפכו אותו לקשה מאוד לתחזוקה. באורקל טוענים שזו אחת הסיבות לשחרור הכל-כך איטי של שינויים בג'אווה.

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

  • corba – לזקנים ביננו, שזכו להכיר את המפלצת.
  • java.transaction (מדובר בטרנזקציות אפליקטיביות, לא של JDBC. כאלו כמו שהיו ב EJB גרסה 1).
  • java.xml.ws וספריות נוספות של טיפול ב XML (למשל JAXB). בתחילת שנות ה 2000 התייחסו ל XML כ "פורמט שהולך לתאר את כל המידע בעולם". תזכרו את זה כשאתם מתחילים להתלהב מטכנולוגיה חדשה.
  • JavaFX – טכנולוגיית Desktop UI של ג'אווה שלא ממש הצליחה. לג'אווה יש היסטוריה מרשימה ביותר של טכנולוגיות UI כושלות (אפשר לדבר בהזדמנות גם למה…)
    • JavaFX תמשיך להיות זמינה כמודול עצמאי שלא קשור ל JDK.
  • Applets
  • Java WebStart (טכנולוגיה להתקנה ועדכון אוטומטי של אפליקציות ג'אווה)
  • Nashorn שהחליף את Rhino כמפרשן JavaScript שרץ על גבי ג'אווה ומסופק כחלק מה JDK.
    • נשהורן הוא חדש, אך שפת ג'אווהסקריפט מתפתחת בקצב כ"כ מהיר, שהייתה מספיקה גרסה אחת בכדי להבין שלא ישים לתחזק עוד מפרשן JavaScript כחלק מה JDK.

בנוסף, בג'אווה 9 עד 11 הסירו כמה פונקציות שהיו deprecated לאורך זמן רב. זו הפעם הראשונה בהיסטוריה של ג'אווה שקוד באמת מוסר מה JDK.
אם אתם משתמשים באחת מהטכנולוגיות או ה APIs שהוסרו מה JDK – יהיה עליכם ליבא אותן בצורה מפורשת (למשל: באמצעות מייבן) או למצוא חלופה (למשל ל Applets או Nashron – שלא ייתמכו יותר).

המודולריזציה של ה JDK תסייע גם להלחם בצריכת הזיכרון הגבוהה של ג'אווה, ובגודל ה distributables. ג'אווה עובדת עם Dynamic linking (קישור של קוד בזמן ריצה, ולא בזמן קומפילציה). יש בכך כמה יתרונות (קל מאוד לייצר Plugin architecture, ניתן לבצע אופטימזציות מסוימות) אבל המחיר הוא קבצי jars גדולים מהנדרש (אנו כוללים את כל הקוד בספריה, אפילו אם אנו זקוקים רק ל 5% ממנה) וגם צריכת זיכרון גבוהה יותר.

בעולם ה micro-services וה FaaS (למשל: AWS lambda) – התכונות הללו הן מגבלה רצינית של ג'אווה. למבדה קטנה בג'אווה יכולה בקלות לדרוש jar file של עשרות MB ולצרוך מאות MBs של זיכרון. למבדה דומה ב ++C או Go תדרוש שבריר מהמשאבים של ג'אווה. כנ"ל לגבי ג'אווהסקריפט או פייטון – אבל מסיבות קצת שונות.

ג'אווה 9 הוסיפה כלי בשם jlink המאפשר לבנות אפליקציית ג'אווה עם static linking. השימוש העיקרי הוא FaaS או הפצה של ג'אווה למכשירים עם מגבלות במשאבים. החיסרון של jlink הוא שהתוצר לא ניתן לעדכון ללא החלפה מלאה של התוצר הבינרי. היום ניתן לעדכן גרסאת JRE ולקבל עדכוני אבטחה / באגים קריטיים מבלי לעדכן את קוד האפליקציה. אפליקציה שנבנתה עם jlink תדרוש בנייה מחדש ו deploy מחדש – על מנת להחיל את אותם העדכונים.

הסכנה שבשינוי

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

המעבר בין ג'אווה 8 לג'אווה 11, או בין JDK 8 ל JDK 11 (למי שמשתמש בשפת JVM שאיננה ג'אווה) – עומד להיות עניין גדול יותר.

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

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

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

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

בואו ניזכר שנייה מה קרה בפייטון:

ראשי הקהילה תיארו זאת כך:

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

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

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

וכך – כל העגלה נתקעה. קריאה לקהילה לבצע את המעבר לא נענתה בחיוב, או לפחות לא בקצב מהיר. הוקמו אתרים כמו http://py3readiness.org ו http://python3wos.appspot.com שמדדו ועודדו – את האימוץ של פייטון 3.

פייטון 3 שוחררה ב 2008, אבל לקח בערך עשור עד שרוב הקהילה הצליחה לעשות את המעבר.

—————–

השינויים הנדרשים במעבר מג'אווה 8 לג'אווה 11 – הם קטנים יותר משמעותית מאלו שהיו בפייטון 3. בעיקר צריך לעדכן תלויות של מודולים ומחלקות, בעוד בפייטון היו נדרשים שינויים בקוד עצמו = הרבה יותר עבודה.
המודעות לסיכונים, כנראה גדולה – הסיפור של פייטון 3 הדהד בכל התעשייה במשך שנים ככישלון צורב.

האם המעבר לג'אווה יהיה מהיר יחסית, או שיגרר לאורך שנים?

נחכה ונראה.

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

קישורים: מדריך לעדכון פרויקט מייבן לג'אווה 11.

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

—-

[1] החישוב הוא באמת יותר מורכב, ופחות קל להבנה. לארכיטקטורות שונות של חומרה, יש הגדרה שונה למהו "Processor". רישיונות הוא בד"כ נושא סבוך בחברות Enterprise – אז אל תניחו שהסיבוכיות הזו היא "תרגיל" של אורקל.

קוברנטיס (Kubernetes) עומד להיות הענן-בתוך-הענן של רובנו

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

מדוע אני כותב את הפוסט דווקא עכשיו? שום דבר לא קרה בשבוע האחרון. רק יצא יום כיפור (אין קשר), אמנם הכריזו על ליין חדש של אייפונס, ויש גם את ארכיטקטורת Turing החדשה של nvidia – אבל לא באמת היה השבוע משהו גדול ורועש בגזרת ה Containers ו/או Container Orchestration Framework (בקיצור עבור הפוסט: COF).
בכל זאת, לפני כשנה כתבתי פוסט שעסק ב COFs והשווה קצת בין מזוס, ECS, קוברנטיס, Swarm ו Nomad – ובאותה עת נראה היה שהתחרות בין כמה מה COFs שהוזכרו עדיין פתוחה.
כשאני מסתכל על ההתרחשויות של השנה האחרונה – נראה שהעניין נסגר. אם "קולות החיילים" לא יבצעו מהפך של הרגע האחרון – Kubernetes הוכרזה בחודשים האחרונים כ COF שתכתיב את הטון בעתיד הנראה לעין.
תזכורת קצרה:
  • קוברנטיס (לעתים נכתב בקיצור K8s) היא פריימוורק ששוחרר כפרויקט קוד פתוח ע״י גוגל באמצע שנת 2014 לניהול Orchestrations של Containers. בסה"כ בת ארבע שנים.
    • לגוגל היה ניסיון קודם בפרויקטים פנימיים דומים: הראשון בשם Borg והמחליף שלו – Omegaֿ, להרצת קונטיינרים. הם לא הריצו Docker או Rkt – אלא קונטיינרים פרי פיתוח מוקדם של גוגל. הניסיון הזה הוכיח את עצמו.
  • קוברנטיס (בדומה ל COFs אחרים) מספקת יכולות Load Balancing, Discovery ו Auto-Scaling ובעצם מהווה סוג של ״מיני-ענן״ בו יחידת ה compute היא Container. האיום הזה לא נחבא מעיניהם של ספקי-הענן הגדולים, והם ניסו לפתח COFs מקבילים שישמרו את ה Lock-In לענן שלהם.
  • אמזון יצאה עם ECS, ומייקרוסופט עם ACS (שתי החברות שאולי היה להן הכי הרבה להפסיד) – פתרון הרצת ו Orchestrations של Containers שתפור לענן שלהן. והן הטילו את כובד משקלן בכדי לשכנע שזו אלטרנטיבה ראויה (וטובה יותר) לקוברנטיס – אלטרנטיבה שרק צריכה עוד זמן להבשיל.
  • השוק לא הגיב יפה להצעות הללו – ופנה ל manual installation של קוברנטיס על גבי EC2.
  • מייקורוסופט הוציאה את AKS (קרי Azure Kubernetes Service) ואמזון את EKS (קרי Elastic Kubernetes Service) כ Kubernetes מנוהל. בהתחלה נראה היה ש EKS ו AKS הולכים להיות offering משני על מנת לשמר לקוחות ולא להישאר יותר מדי מאחור, אבל מהר מאוד התברר שהשירותים הללו תופסים את תשומת-לב הלקוחות, ומשם השתנו הדברים והם התחילו לקבל גם את מירב ההשקעה מצד ספקי הענן. ECS/ACS הופכים להיות פתרונות נישה, בעיקר עבור לקוחות שכבר נרתמו לחזון שהוצג ב 2015, והשקיעו בו, או אולי יצליחו לשלב אותם גם בעולם של קוברנטיס (עבור ה worker nodes – על זה בהמשך).
  • גם פתרונות ענן כגון Pivotal Container Service ו Cloud Foundary – הגיעו למסקנה דומה, ופנו לכיוון קוברנטיס.
  • אפילו החברה מאחורי Docker שניסתה להציג חזון משלה בדמות Swarm (לאחר שהבינה שב COFs יש פוטנציאל עסקי גדול יותר מתמיכה ב Docker בלבד) הכריזה לפני כשנה על תמיכה ״גם״ בקוברנטיס – מה שבפועל ״הוציא את העוקץ״ מ Swarm שמותג כ"פתרון הרשמי של דוקר".
  • Mesosphere DC/OS – הגרסה המסחרית של Mesos, מציעה גם היא תמיכה בקוברנטיס.
בקיצור: מלחמת ה COFs הסתיימה. Long live Kubernetes!
חשוב לציין שלמרות שקוברנטיס הוא ה COF שהפך לברירת-המחדל, יש עדיין תהליכים שלא ברור כיצד יסתיימו:
  • מייקרוסופט שחררה את ACI (קרי Azure Container Instances) ואמזון את AWS Fargate כשירותים בהם ניתן להריץ container on-demand ולשלם ע״פ השימוש הנקודתי. השירותים מזכירים מאוד את AWS Lambda או Azure functions כאשר הרזולוציה היא Container ולא פונקציה, על אף שמנסים לשווק אותם כ"תחליף ל EC2".
    • השימוש העיקרי הוא בהפעלה של containers המבצעים batch של עבודה – ולא בהרכבה של containers מסוגים שונים המתקשרים זה עם זה. האלמנט של Orchestration לא ממש קיים בשירותים הללו.
    • גם קוברנטיס תומכת בהפעלת containers באופן on-demand, מה שעשוי לרמז שהלקוחות העיקריים של ACI ו Fargate הם לקוחות שלא מריצים קובנרטיס או כאלו שיש להם Workload מאסיבי שאינם רוצים להפעיל על ה Kubernetes cluster הרגיל שלהם (למשל: משימות AI כבדות).
  • קוברנטיס עצמה נפתחה לספקי הענן, למשל: החל מגרסה 1.9 קוברנטיס תומכת באופן טבעי ב AWS NLB (קרי Network Load Balancer). מיותר לציין שיש הרבה Kubernetes-plugins שמסייעים לבצע את החיבורים לעננים ספציפיים בצורה טבעית יותר.
  • ישנה השקעה בהרצה של Kubernetes בכדי להפעיל VMs ולא רק Containers. המוטיבציה היא לשלב גם שרתים שאינם לינוקס (למשל Windows או אפליקציות Unikernel) ו/או בידוד גבוה יותר – בעיקר משיקולי אבטחה.
  • קוברנטיס מאפשרת יכולות ניהול משאבים טובות ברמת התשתית, אבל ניהול של מאות מיקרו-שירותים הוא עדיין דבר קשה מאוד בקוברנטיס. יש פרויקטים (למשל: lstio הנתמך ע״י Lyft, IBM וגוגל) המנסים לספק שכבת ניהול ברמה יותר ״אפליקטיבית״ שתקל על ניהול שכזה. פרויקטים כאלו יכולים להשתלב עם קוברנטיס כ plug-in ו/או להחליף כמה מיכולות הליבה שלה (למשל: Service Discovery או Load Balancing).
Amazon EKS

Managed Kubernetes

כמה שאנו אוהבים לשמוע כמה אוטומטית ו״חכמה״ היא קוברנטיס בניהול ה Containers Cluster, הפעלה של קוברנטיס היא עדיין משימה מורכבת:

  • יש שורה של בחירות שיש לבצע בהתקנה של קוברנטיס. האם אתם מעדיפים א או ב, ג או ד? דוגמה טובה היא תקשורת בין Containers: האם Kubenet (ברירת-המחדל) מספיק לכם? אולי אתם צריכים scale, ביצועים, ו/או יכולות שליטה מתקדמות יותר ברמת הרשת וכדאי להשתמש ב Weave? אולי בעצם ב Calico? ואם אתם בוחרים ב Calico – אתם מעדיפים אותו עם Flannel (כלומר: Canal) או בלעדיו?
  • קוברנטיס מטפל בשורה של מקרי-קצה מורכבים להבנה, שללא ניסיון ניכר – קשה להבין אותם ולוודא שהם עובדים בצורה תקינה.
    • High Availability – איך להבטיח ש pods ימשיכו לתפקד ככל האפשר. לדוגמה: האם ה masters הם באמת highly available? האם אתם יכולים ליצור master חדש עם קונפיגורציה שלמה בצורה אוטומטית?
    • אבטחה – האם אתם מודעים כיצד להקשיח התקנה של קוברנטיס, ויש לכם את המנגנון להתקין עדכונים בכל השכבות?
  • חיבור של קוברנטיס לספק הענן הספציפי היא עוד משוכה שיש לעבור. למשל: על EC2 לא תוכלו ליצור cluster גדול מ 50 nodes ללא החלפה של ה CNI Network plugin. כנראה שתרצו כמה מה nodes שירוצו על spots. כדאי מאוד להתחבר ל IAM בצורה נכונה, לקנפג את Route53 כך שה master ימצא את כל הרכיבים שלו ואם יש כמה clusters – הם יהיו זקוקים ל subdomains, וכו׳.
הפתרון הטבעי, בדומה מאוד לבחירה בספק ענן (ולא הפעלה של Data Center של החברה) – הוא שימוש ב Managed Kubernetes. זו הבחירה הטבעית עבור הרוב הגדול של משתמשי Kubernetes, במיוחד אלו שלא הולכים להריץ אלפי שרתים ב Cluster של קוברנטיס (ולכן סביר יותר שיהיה לכם את האינטרנס והמשאבים לעבוד עם קונפיגורציה מאוד ספציפית ("לא מקובלת") לצרכים שלכם).

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

  • גוגל – בתור היוצרים של Kubernetes והמאמצים הראשונים שלו כשירות Managed בענן, אין הפתעה בכך ש GKE (להלן Google Kubernetes Engine) הוא המוצר הבשל ביותר.
    • האינטגרציה הטובה ביותר, UI מלוטש לניהול, והפעלה מהירה של Cluster מאשר של המתחרות.
    • הבעיה היחידה: רוב החברות (בכלל, אך בישראל בפרט) לא פועלות על הענן של גוגל, והם לא יעבירו אותן לשם בשביל "Managed Kubernetes טוב יותר". זה עשוי להשתנות בעתיד – אבל כרגע לא נראה שזה המצב.
    • בניגוד ל BigQuery שהוא שירות שלקוח של AWS עשוי להשתמש בו על אף שהוא שוכן בענן אחר – קוברנטיס מריצה את לב המערכת שלנו. להשתמש בקוברנטיס בענן אחר, משמע להתחיל לעבור לענן אחר.
  • אמזון מציעה את EKS, שהיה עד לא מזמן plan B – שהתממש.
    • EKS תנהל עבורכם את ה Control plane מבחינת scalability ו high-availability על גבי AZs שונים. לא תהיה לכם גישה ברמת Admin אליו (בדומה ל RDS), אבל אתם ממשיכים לעבוד עם kubectl כרגיל.
    • לEKS יש אינטגרציה עם שירותי אמזון בראשם IAM, אבל גם ELB, VPC ו CloudTrail.
    • את ה Worker nodes עדיין עליכם לנהל לבד – ויש עוד קונפיגורציה ועבודה שעליכם לעשות על גבי EKS. יש תוכניות לבצע אינטגרציה בין EKS ל ECS כך ש ECS ינהל את ה worker nodes. מכיוון שהוא לא נבנה מלכתחילה לצורת העבודה הזו – צריך לראות כמה טוב זה יעבוד. רעיון דומה מתוכנן לאינטגרציה בין EKS ל Fargate להרצה של Containers שהם task-oriented.
    • לאחרונה שוחררה גרסאת eks.2 של EKS – ויהיו עוד רבות. כרגע ל EKS יש סיכוי טוב להיות פתרון ה Managed Kubernetes הפופולארי ביותר (גם אם לא בהכרח יהיה המתקדם ביותר).
  • מייקורוספט מציעה על גבי Azure את שירות AKS המנוהל, שדומה ביכולות ל EKS.
    • אולי בגלל קהילה קטנה יותר ופעילה פחות, AKS נראה כשירות קצת פחות פופולארי. אפשר לנחש שהלקוחות הקלאסיים של מייקרוסופט הם ברובם לא early adapters של טכנולוגיות חדשות.
  • גם OpenShift ויבמ BlueMix – מציעות פתרונות Kubernetes שנחשבים מפותחים, אך הם מתאימים בפועל בעיקר למי שכבר פעיל על תשתיות הענן הללו.
  • Stackpoint מציעה פתרון של managed control plane על ידה, כאשר את ה worker nodes תתקינו על ענן לבחירתכם. הפתרון אטרקטיבי בעיקר למי שמריץ את ה workload בענן שבו אין offering סביר של managed kubernetes.
השוואה שנעשתה בין שלושת פתרונות ה Managed Kubernetes הנפוצים. מקור

גם מי שמתקין קוברנטיס לבד (על הענן או On-Premises) לרוב לא עושה זה בדרך הארוכה והקשה.
kubernetes-the-hard-way הוא שם של מדריך פופולארי ונחשב להתקנת קוברנטיס מ scratch, שבעיקר משמש בכדי ללמוד את הפרטים השונים שקיימים בהתקנת קוברנטיס, ואת השיקולים שנלקחים בכזו התקנה.

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

  • Kops הוא המקובל ביותר. הוא חלק מפרויקט קוברנטיס והיה במשך תקופה כלי להתקנת קוברנטיס על גבי AWS בלבד. הוא יודע לייצר קבצי Terraform, יודע לבנות תצורות high-availability (כמובן), ותומך ב-7 אפשרויות שונות ל CNI (כלומר: plugin לתקשורת פנימית. אחת הבחירות עם הווריאציות המגוונות יותר בהתקנת קוברנטיס).
  • Kubespray (לשעבר Kargo), תת-פרויקט של קוברנטיס הוא כלי להתקנת קוברנטיס בהתבסס על Ansible, כלי ה configuration management שנחשב לפופולארי ולמתקדם – עד להופעת ה COFs שהולכים ומייתרים אותו. Kubespray נחשב יותר גמיש מ Kops ומציע מגוון רחב יותר של אפשרויות התקנה. יש לו גם תמיכה ספציפית ב AWS, Azure, Google Cloud, Digital Ocean ו Open Stack.
  • TK8 הוא עוד כלי פופולרי, שהדגש שלו הוא עבודה צמודה עם Terraform והוא זוכה לכמה נקודות פופולריות בזכות זה שהוא כתוב ב Go (אזהרת באזז). TK8 כולל גם אפשרות התקנה של כמה אפליקציות פופולריות בהמשך ל Kubernetes Cluster כמו Zipkin+Jagger, Prometheus ועוד.
  • עוד שמות שאפשר לציין הם RKE (להתקנה של ה Cluster בלבד, לאחר שהכנתם את המכונות באופן מסוים), מודול שקיים ומתוחזק ב Puppet להתקנה של Kubernetes, או Kubeinformation שהוא כלי online שעוזר לייצר templates של קונפיגורציות ע"פ סט בחירות שתתנו לו. Kubeinformation עשוי להיות מקור מעניין להתרשם בזריזות מתהליך ההתקנה, אבל בעת כתיבת פוסט זה הוא עדיין לא תומך ב EKS (זו הפריט הבא ב roadmap שלו).

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

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

סיכום

העולם הטכנולוגי מתקדם בחזית רחבה לכיוון קוברנטיס (גם בצורה עיוורת לפעמים – סממן חזק להפיכתו ל"באזז"). קוברנטיס היא תשתית מרשימה, אבל גם לא-פשוטה לניהול. התסריט הסביר ביותר הוא שפתרונות של Managed Kubernetes ישמשו את רוב השחקנים בשוק, בעיקר הקטנים והבינוניים (תמיד יכולה להיות שחקנית שתדמה ל Netflix ע"ג AWS, כלומר: שתחליט להריץ גם מאות-אלפי worker nodes על גבי פתרון Managed).

Enterprises שרצים On-Premises, או מריצים ענן משלהם, או סתם חברות שמריצות Workload גדול במיוחד – כנראה ימשיכו לנהל את ה Kubernetes Cluster מא' עד ת' – אבל זו התמחות שהן יכולות להרשות לעצמן לפתח ולתחזק.

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

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

בגדול, עובדות בפניכם כרגע האפשרויות הבאות:

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

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

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

השוואה בין GKE, EKS וAKS (מקור: Kubdex)

לקבל מושג ירוק על nginx

nginx הוא רכיב בסיסי ונפוץ בחלק נכבד ממערכות הווב כיום. לאלו שאין nginx, בד"כ יש Apache Httpd – כלי מקביל שנחשב קצת יותר מיושן.את nginx מתקינים ב-3 תצורות עיקריות:

  • שרת Web המגיש תוכן HTML/JS/CSS למשתמשים. שימוש נפוץ – אבל נראה שזה לא השימוש הנפוץ של קוראי הבלוג הזה.
  • Reverse Proxy – מותקן מאחורי ה Load Balancer (למשל: ELB) ולפני המערכת שלנו / המיקרו-שירות. זה כנראה השימוש הנפוץ בקרב קוראי הבלוג.
    • וריאציה של התצורה האחרונה היא API Gateway – מונח שנטען בבאזז מעולם המיקרו-שירותים. בוריאציה הזו ה nginx גם מבצע את ה Authentication.
  • Load Balancer – בתצורה הזו nginx משמש לרוב גם כ Reverse Proxy וגם כ Load Balancer.

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

בפלטפורמות שאינן בנויות ל concurrency (למשל: PHP, Ruby, או פייטון) nginx הוא רכיב קריטי לטפל ב traffic גבוה. ה nginx יכול "לספוג" מאות, אלפי, ועשרות אלפי concurrent connections ש backends מהסוגים הללו לא מתמודדים איתם יפה, ולהקל על ה backend במקרים בעייתיים כמו בעיית ה Slow Client.

ב Backends הבנויים למקביליות (כמו Java או Go) – הצורך ב nginx הוא פחות מובן-מאליו, ויש מקרים שבהם הוא לא באמת נדרש, אך אנו ממשיכים להתקין אותו כי "זה Best Practice" או מתוך הרגל.

בכל מקרה, נראה שכל הנושא של nginx נמצא בידע חסר. מי שניגש אליו הוא אנשי ה Operations ו/או מפתחים ובעיקר כאשר יש "בעיות". למשל: nginx החליט (בחוצפתו) לחתוך URLs ארוכים במיוחד של בקשות GET.

המפגש עם nginx עשוי לפעמים להתאפיין בסריקה של StackOverflow והדבקה של כל מיני Settings לקונפיגורציה עד אשר נראה שהבעיה חדלה מלהציק.

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

ירוק כבר יש לנו. עכשיו חסר רק מושג.

להכיר את nginx

כיצד כדאי לגשת ולהכיר את nginx (מבוטא כ "engine X")?
אולי מדריך התקנה והרצה? אולי התעמקות במבנה הארכיטקטורה? אולי השוואה ל Apache httpd (שגם אותו – רובנו לא ממש מכירים)?

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

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

קובץ ההגדרות הראשי של nginx נקרא nginx.conf ולרוב נמצא בתיקיה etc/nginx/.

אתם בוודאי שואלים את עצמכם "איזה פורמט זה?". ובכן, זהו לא פורמט "סטנדרטי" – זהו פורמט ספציפי שבו משתמשים ב nginx – המושפע מ C-Syntax. בואו נתחיל:

  1. השורה הראשונה היא דוגמה טיפוסית למה שנקרא בקונפיגורציה directive (= פקודה/הוראה). לאחר ה directive יש רווח ואז מספר משתנה של פרמטרים. את ה directive מסיים הסימן ; – שהוא חשוב.
    1. הדיירקטיב של user מגדיר באיזה משתמש (מערכת) התהליכים (OS processes) של nginx ירוצו. לא משהו שנשנה, בד"כ.
  2. כמה Worker Processes להפעיל.
    בניגוד ל Apache Httpd המייצר worker process לכל connection, ב nginx ה worker processes עובדים בסוג של Event Loop ומטפלים כ"א במאות ואלפי connections. הרי, nginx נכתב בכדי להתמודד עם האתגר של C10K – טיפול של יותר מ 10,000 connections בו זמנית מתוך שרת אחד.

    1. כלל האצבע המומלץ הוא להגדיר מספר workers כמספר ה CPU cores הזמינים לנו. את זה ניתן להשיג בעזרת הערך auto. אני לא יודע לומר למה קבעו אותו פה ל 1.
    2. אם הפעולות חסומות ב I/O (למשל: nginx משמש בעיקר ב proxy) – אזי כלל האצבע אומר לקבוע את הערך ל 1.5-2 ממספר ה cores הזמינים למכונה.
  3.  מה הרמה המינימלית של Log level, שאותה נרצה לשמור לתוך לוג ייעודי לבעיות? אפשר להגדיר כמה קבצים כאלו, ברמות (levels) שונות.
    שימו לב ששמות ה log levels ב nginx הוא קצת לא-סטנדרטי.
    בכדי לקבל logs ברמת debug יש להשתמש ב executable של nginx שקומפל עם פרמטר של with-debug–. זהו שיקול של אופטימיזציית ביצועים (לקמפל בלי משמע לדלג על הרבה בדיקות של רמת ה log level).
  4. שם קובץ שבו יישמר מספר התהליך (ברמת מערכת ההפעלה) שאותו קיבל ה master process של nginx.
  5. כאן אנו נתקלים בכמה דברים חדשים:
    1. יש לנו תחביר מעט שונה: תחביר של context בקונפיגורציה.
      1. context מגדיר scope, וכל ה directives שהוגדו בתוכו זמינים רק לו, או ל contexts שנמצאים בתוכו.
      2. יש בקונפיגורציה קונספט של הורשה, כך ש context מקבל את כל ה directives של ה context מעליו – אך לא ליהפך. directive שהוגדר בתוך context ידרוס את ההגדרות של אותו directive שהוגדרו ב context חיצוני יותר (יש גם יוצאי-דופן). החלק הזה שימושי בעיקר ב contexts של server ו location – שלא הגענו אליהם עדיין. אבל שם מוגדרת רוב הקונפיגורציה הספציפית למערכת שלנו.
      3. הרמה הגבוהה ביותר נקראת ה main context, והיא לא מפורשת – אך מתנהגת כ context לכל דבר. סעיפים 1-4 בעצם הוגדרו בתוך ה main context.
    2. ספציפית ה event context מנהל את מה שקשור לניהול connections. היה כנראה יותר נכון לקרוא לו connections.
      1. אנו מגדירים כאן שכל worker process יוכל לפתוח עד 1024 connections. בקשת ה connection ה connection ה 1025 יצטרך להמתין עד ש connection קיים ייסגר – על מנת שיטופל.
      2. אם nginx משמש להגיש קבצים סטטיים – אזי נוכל להגיש קבצים ל 1024 connections. צריך לזכור שדפדפנים עדיין פותחים 2-3 simultaneous connections ל host אם הם צריכים כמה קבצים. אם nginx משמש כ proxy אז המספר הרלוונטי הוא חצי – כי חצי מה connections יפתחו ל application server שמאחורי ה nginx – מה שנקרא במינוח של nginx ה upstream.
      3. הארכיטקטורה של nginx מאפשרת ל nginx לצרוך רק כמה MB של זכרון לכל 1000 connections פתוחים. התוכן שמועבר ב connections (ה buffers אם יש הרבה מידע, ויש פער בקצב ההעברה של ה client וה applications server) הוא גורם שעלול להגדיל את צריכת הזיכרון בצורה מורגשת.
      4. אם צריכת הזיכרון קטנה כ"כ, למה לא להגדיר ערך של 10,000 ב directive הזה?
        אליה וקוץ בה: ללינוקס יש מגבלה 1024 file descriptors ל process, ויש לשנות את המגבלה הזו – בכדי שנוכל להפעיל באמת יותר מ 1000 connections ל worker. ב distros מסוימים של לינוקס יש מגבלה קשיחה ל 4000 open files descriptors ל process.
      5. מסקנת ביניים חשובה: אנו יכולים להגדיר כל מיני דברים בקונפיגורציה של nginx, אבל לא פעם ההגדרות הללו הן לא מה שיקרה בגלל מגבלות / התנהגויות של פרוטוקול ה HTTP, של מערכת ההפעלה, או של ה Application Server שלנו. העבודה ב nginx היא הרבה פעמים עבודה ב infrastructure רחב יותר מסביב ל nginx.
  6. כאן אנו נתקלים ב directive מיוחד של הקונפיגורציה של nginx: ה include.
    1. בעצם מה שקורה הוא שכל תוכן הקובץ המתואר "מוכנס" (inlined) בקונפיגורציה במקום שורת ה include. ה include מאפשר ניהול נוח יותר של קונפיגורציה בקבצים קטנים וממוקדים יותר.
    2. במקרה הזה מדובר על קובץ די משעמם הכולל את ה types context ובו שורה ארוכה של מיפויים בין MIME-types לסיומות של שמות קבצים.
    3. ה directive הבא מתאר fallback: איזה MIME-type להצהיר אם לא מוגדר לנו MIME-type לסיומת של קובץ. גם משעמם.
    4. השימוש בקובץ שהוא included יכולה לגרום לבלבול בהתחלה: בקובץ לא מוגדר ה context בו אנו פועלים ואנו יכולים בטעות להגדיר directives לא רלוונטיים. nginx עשוי לזרוק warnings בעליה (שלא נראה אותם) או פשוט להתעלם – ואז לא נבין מדוע הוא לא מתנהג כפי שציפינו. אולי צריך לשנות עוד קונפיגורציה?
      ארחיב על העניין בזה בהמשך.
  7. כאן אנחנו מגדירים פורמט מסוים ל access log ושומרים את הפורמט בשם "main".
    ה Access log של nginx הוא כלי שימושי למדי, אשר קיומו הוא לעתים אחת הסיבות מדוע אנו מציבים nginx כ reverse-proxy לפני שרת האפליקציה שלנו. הוא שומר שורה בקובץ הלוג לכל בקשה שעברה דרך שרת ה nginx – מה שיכול לסייע לניתוח ה traffic שאנו מקבלים.

    1. לעתים, כאשר ה nginx מטפל בכמות גדולה של תעבורה (נניח: אלפי בקשות בשנייה) – יש היגיון לסגור את ה access log כהתנהגות ברירת מחדל, או לכתוב סלקטיבית רק חלק מהרשומות.
  8. אנו כותבים access log במיקום מסוים, וע"פ פורמט ה main שהגדרנו שורה קודם.
    1. את הפורמט של ה error_log לא ניתן לשנות.
  9. sendfile היא קריאת מערכת של לינוקס שמעתיקה נתונים בין 2 file descriptions כפעולת kernel, בלי להעתיק נתונים ל user spaces. כאשר nginx משמש להגיש קבצים סטטיים (קובץ ל http connection) – היא יכולה לייעל מאוד את עבודתו.
    1. מצד שני, sendfile לא עובדת עבור file descriptors של UNIX sockets, וסתם מציבה מעט overhead מיותר. שרתי רובי (Unicorn למשל) מתקשרים מול nginx על גבי Unix socket ולא קריאות Http (מה שחוסך ב latency לתרגם את המידע לפורמט HTTP, ובחזרה). עבורם – לא נרצה להשתמש ב sendfiile.
  10. tcp_nopush היא אופטימיזציה של לינוקס / FreeBSD שאומרת ל TCP למלא packets לפני שהוא שולח אותם. למשל: לשלוח את ה headers של תשובת ה HTTP באותן tcp packets עם ה body. זו אופטימיזציה ל throughput. מהצד השני קיימת התנהגות הנקראת tcp_nodelay שהיא אופטימיזציה לצמצום ה latency. "כשיש משהו – תשלח".
    1. אם nginx מגיש הרבה תוכן, ה tcp_nopush עשויה להיות אופטימיזציה יעילה למדי: שימוש יעיל יותר ב bandwidth של הרשת, כי ip+tcp headers הם overhead על כל pakcet,  וגם שימוש יותר יעיל בזיכרון של nginx (בעקבות ניצולת גבוהה של ה buffers).
    2. במקרה שלנו האופציה הזו כבויה. לא רוצים להפעיל אותה בתצורת ברירת-המחדל, אבל רוצים להזכיר לנו על קיומה.
    3. tcp_nopush היא קונפיגורציה שתפעל רק עם sendfile מופעלת. התלות הזו מוזכרת בתיעוד הרשמי – אבל היא מסוג הדברים שאפשר לפספס ולבזבז שעות בכדי להבין מה חסר. כמו כן תיאור הפקודה מסתמך על הבנה עמוקה של לינוקס / http. התיאור בתיעוד הרשמי הוא דיי לקוני למי שלא מכיר את המנגנון:

      1. נ.ב. – נאמר לי שאם נראה לי ש nginx מתועד בצורה לקונית, ומאפשר למשתמשים שלו מרחב הגון של טעויות – עלי לנסות לקנפג Apache Httpd. לא זכיתי.
  11. הנה סופסוף קונפיגורציה שמאוד קל להבין: אנו רוצים לעשות שימוש חוזר ב tcp connections שנוצרו (three-way-handshake, או שבע ב https, וכד') על מנת לשרת בקשות HTTP מאותם מקורות. אני מניח שאנחנו זוכרים ש keep_alive ארוך הוא מצוין לביצועים של ה clients, אבל מפחית את ה utilization של ה connections בצד השרת – וזה מחיר בזיכרון / מקביליות שהשרת משלם עליו.
    1. בכדי לאפשר keepalive connections גם מול שרת האפליקציה, עלינו להגדיר את ערך ב keepalive directive שב upstream context. פרמטר זה אומר כמה connections אנו מרשים לשמור "בחיים" במסגרת keepalive.
    2. עוד דוגמה לקונפיגורציה תלויה, ולא בהכרח צפויה היא ה directive הבא בתוך context ה location או ה server:
      ;"" proxy_set_header Connection
      המשמעות כאן היא שאנו דורסים את ה header בשם Connection. בכדי למנוע מצב בו ה Client שלח ל nginx את ה header עם ערך close (הוא מבקש מאיתנו לסגור לאחר הבקשה את ה keep_alive connection), ואנו נעביר את ה header הלאה ל Application Server – והוא יבין ש nginx ביקש ממנו לסגור את ה connection. קומדיה של טעויות.
    3. כאשר אנו קובעים keepalive_timeout מומלץ גם להגדיר את proxy_http_version (גרסת ה http שאנו עובדים עם שרת האפליקציה שאנו עושים לו proxy) ל 1.1. אם ה nginx והשרת החליטו לעבוד ב http 1.0 (ברירת המחדל מצד nginx) – אז לא יהיה שימוש ב keepalive.
  12. אני מניח שאתם מכירים את דחיסת ה gzip: לא הדחיסה הכי אגרסיבית, אבל יעילה מבחינת ההשקעה של ה CPU לצמצום ה bandwidth ובעלת תמיכה רחבה בקרב דפדפנים ורכיבי-רשת שונים. לכאורה בחירה ב gzip היא no_brainer, אך דבר שעלול לגרום למשהו לא לעבוד – לא צריך להיות ב default ולכן אני מניח שהיא נמצאת ב comment.
  13. זוהי שורה חשובה מאוד: בעצם כאן אנו עושים include לכל קבצי הקונפיגורציה בתיקיה בשם conf.d. בתיקיה הזו נהוג להחזיק קובץ לכל Virtual Host ועוד קובץ בשם default.conf המכיל הגדרות ברירת-מחדל של ה server context. ה server context חייב להיות מוגדר בתוך ה http context (אחרת הוא לא תקף).
    1. Virtual hosting – הוא הרעיון שעל שרת פיסי אחד אנחנו מארחים כמה שרתים "לוגים", למשל: אפליקציות שונות. כאשר nginx (או בזמנו: Apache httpd) אירח כמה אתרי-אינטרנט לוגיים – זה היה ממש virtual hosting. בעזרת patterns על הבקשה (למשל: רושמים כל אתר ב DNS כ path מעט שונה) – ה nginx יודע לאיזה שרת לוגי להפנות את הבקשה.
      1. התצורות המקובלות קצת השתנו, בעוד הטרמינולוגיה נשארה. גם כאשר ה nginx משרת כ reverse proxy אך הוא מתנהג מעט שונה לכמה urls שונים – אנו עדיין קוראים לזה virtual hosting.
        ה context בשם server מגדיר "שרת וירטואלי" שבו אנו מטפלים, כלומר: port ותחילית של URL, בעוד קיימת רמה נוספת בשם location המתארת urls ספציפיים יותר בתוך השרת.
      2. בתוך ה location אנו יכולים לאכוף / לעדכן headers מסוימים, לנהל רמה מסוימת של אבטחה (למשל: Authentication או סינון בקשות העונות ל patterns מסוימים), לפצל traffic עבור A/B Testing ועוד. בפוסט הזה לא אכסה אף אחד מהנושאים הללו.
    2. ההפרדה לקבצים היא מודולריזציה חשובה של הקונפיגורציה של nginx, בעיקר כאשר הקונפיגורציה גדלה.
  14. אם אתם עדיין מסוגלים לקלוט עוד מידע – אז משהו בוודאי נראה לכם משונה בשורה הזו. היא הופיעה כבר קודם לכן!
    1. זה נכון. שורה זו איננה חלק מהקונפיגורציה ברירת-המחדל, אך רציתי להשתמש בה להזכיר עקרון חשוב: ה nginx הוא (לרוב) רכיב קריטי ב infrastructure, ולא כ"כ קשה לעשות בו טעויות בהגדרות.
    2. המלצה ראשונה היא לערוך את הקונפיגורציה של nginx ב Editor או IDE שמכיר את מבנה הקובץ ויודע להתריע בפני שגיאות אפשריות. יש plugins כאלו ל IntelliJ ול VS Code, ובוודאי לעוד עורכים.
    3. כאשר אנו טוענים קונפיגורציה שגויה (= עם טעות) ל nginx, ייתכנו אחת מ-3 תגובות:
      1. nginx יעלה כשורה, ויום אחד (ע"פ חוק מרפי – השיא יגיע ביום שישי בערב) – תתגלה בעיית production.
      2. nginx יעלה כשורה, אך יזרוק warnings בעליה – אבל מי בודק אותם בכלל לפני שיש בעיה כללית? הבעיה עוד תגיע…
      3. nginx יסרב לעלות – ואז יש לנו פאדיחת deployment קטנה.
    4. אף אחת מהאופציות היא לא נהדרת.
      1. על מנת לצמצם את הבעיות, ניתן להפעיל בשלב מוקדם ב deployment את הפקודה nginx -t. הפקודה הזו תבצע בדיקות על הקונפיגורציה ותזרוק error אם נמצאה בעיה. וריאציה אחרת: static analysis בזמן ה build.
        למשל: ההגדרה הכפולה שביצעתי למעלה לא נתפסה ע"י ה IDE (בעיות רבות אחרות – נתפסות), אך היא נתפסת כ warning ע"י nginx -t.
      2. דפוס שימוש מקובל הוא לבצע בדיקת קונפיגורציה לפני reload, ולהכשיל את ה reload על כל
        בעיה אפשרית: nginx -t && nginx -s reload. נראה שזו התנהגות ברירת-המחדל ב AWS Beanstalk, למשל.
      3. חברות עתירות משאבי כ"א עשויות להשקיע אף יותר בבדיקות התקינות של קונפיגורציות ה nginx שלהן. כפי שאמרנו, בעיות קונפיגורציה רבות נובעות מתוך פרוטוקולי התקשורת או מערכת ההפעלה – לא משהו ש nginx יידע בהכרח לבדוק עבורנו.
תצורה טיפוסית של התקנת nginx כ reverse proxy בענן.
האם nginx באמת נדרש?!

אז למה בעצם צריך nginx עבור ג'אווה (JVM) או Go?

לכאורה כאשר יש לנו פלטפורמה המסוגלת בקלות לטפל במספיק connections במקביל, וכאשר יש לנו Load Balancer – אנחנו "מסודרים". מדוע רבים עדיין מתקינים ומתפעלים nginx בין ה LB ל Application Server/Process?
האם זה רק כוחו של הרגל?

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

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

  • Access Log – רישום כל הקריאות שנעשו לשירות, עם overhead מינימלי.
  • תפקיד קלאסי של Reverse Proxy – "להחביא" כתובות פנימיות של שרתים, משיקולי אבטחה.
  • Easy SSL termination – כאשר יש תקשורת https.
  • Caching והגשה סופר-יעילה של static resources.
  • GZIP
  • אפשרות קלה להוסיף headers מסוימים על הבקשות המתקבלות.
  • אפשרות לשכתב URLs או להפנות URLs ל ports או URLs פנימיים אחרים.
    • אופציה פופולרית לדוגמה: הפניית (redirect) קריאות http ל https.

היכולות האלו שימושיות בעיקר עבור שרתים שהם Customer Facing, כלומר – אלו שאליהם פונים ישירות המשתמשים (דפדפנים / אפליקציות מובייל).
אם אנחנו עובדים עם AWS אזי Cloudfront מספק פתרון עדיף להגשת static resources, וה ELB (בעיקר הווריאציה שנקראת ALB) מספקת יכולות של Reverse Proxy, SSL Termination ועוד. למשל: לאחרונה הוסיפו את היכולת להפנות קריאות http ל https בסימון של checkbox. שרתי ג'אווה ו Go מספקים Access Log יעיל גם כן.

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

אבטחה

סט יכולות חשוב נוסף ש nginx ממלא הוא יכולות אבטחה:

  • אימות זהות (Authentication) – מה שהופך אותו מ "סתם Reverse Proxy" ל "API Gateway מ-ט-ו-ר-ף"
    • המונח API Gateway הוא באזז רשמי לשנים 2017-2018. אולי גם יגלוש ל 2019.
    • ל nginx יש מגוון יכולות Authenticatoin ואפילו SSO, רבות מבוססות modules (כלומר: plugins).
    • רבים ממשמשים פתרונות אימות לבד, או בעזרת צד-שלישי כמו OKTA או Auth0. גם שירותי הענן השונים נוגסים בנישה הזו של nginx.
  • הגבלת מספר ה connections ע"פ לקוח (נניח: טווח כתובות IP)
  • אפשור / חיוב הצפנה קרי TLS/SSL – הרבה יותר קל ליישם על גבי AWS ELB.
  • היכולת לחסום תעבורה מאזורים גאוגרפים שונים. למשל: אני פועל בארה"ב וארצה לחסוך תעבורה מסין (שיותר סביר שהיא לא-לגיטימית)
    • על בסיס module
    • יכולת בסיסית ומקובלת היום של WAF
  • חשוב לציין שעצם כך שהשרת הראשון שה Traffic רואה הוא שרת פשוט (לא מסובך, פחות באגים סבירים) המאמת את השימוש בפרוטוקולי הרשת השונים (קרי IP/TCP/HTTP, וכו') – זה כבר יתרון אבטחה חשוב שעשוי למנוע לנו בעיות.
    • היתרון הזה נכון גם ל ELB.

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

מצד האבטחה עולה שאלה נוספת: האם כדאי לנהל nginx לכל שירות – או אחד לכל המערכת?

תפקיד חשוב ש nginx יכול למלא הוא לאכוף כללי אבטחה על כלל המערכת. למשל: מדיניות אבטחה המחייבת headers של HSTS ו X-XSS-Protection ומצד שני – הסרה של כמה headers פנימיים מה requests (למשל session token). קל יותר, ונכון יותר לאכוף כללים כאלו פעם אחת ברמת ה nginx מאשר בקוד מספר רב של פעמים. ההתנהגות מ nginx ברמת ה HTTP היא מובנת וצפויה יותר מאשר התנהגויות של frameworks ומנועים שונים שאנו עובדים איתם.

  • nginx לכל שירות יכול להופיע כהתקנה אחת לכל cluster של השירות (s1 ו s2 בתרשים למטה) או nginx על כל server node (בתרשים למטה – s3. התצורה הזו יותר מקובלת).
  • nginx יכול להיות גם מותקן כ cluster יחיד על כלל המערכת. ה cluster של ה nginx חשוב מאוד עבור high-availability..

למרות ש cluster יחיד של nginx לכלל המערכת נשמע הגיוני מבחינת ריכוז השליטה על האבטחה, בעידן הענן קל יותר לנהל nginx על כל מכונה (או pod, אם אנחנו עובדים עם קוברנטיס). את אחידות כללי האבטחה בין כל מופעי ה nginx, עלינו לנהל בעזרת configuration management. נניח: קובץ אחיד conf. שיהיה Included בכל מופעי ה nginx במערכת.

גמישויות נוספות

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

לעתים מדובר גם בצרכים שהם לא מידיים כמו רצון להגדיר throttling, סתם לדבר תעבורה לשירות מסוים, ביצוע a/b testing ועוד. אלו דברים שהכלי שנקרא nginx יכול לספק בצורה טובה ומהירה – למי שיודע להשתמש בו.

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

סיכום

ראינו פרטי קונפיגורציה של nginx בכדי לספק תחושה והתמצאות בסיסית בכלי, וקצת דנו בשיקולי ארכיטקטורה ומקומו ב System Landscape.

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

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

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

נושאים מתקדמים ב MySQL: חלק ג׳ – מנועי אחסון, ומבנה האינדקסים

פוסטים בסדרה:
"תסביר לי" – גרסת ה SQL
לקחת את הביצועים ברצינות, בעזרת MySQL Performance Schema
נושאים מתקדמים ב MySQL: חלק א' – עבודה עם JSON
נושאים מתקדמים ב MySQL: חלק ב' – json_each  ו Generated Columns

נושאים מתקדמים ב MySQL: חלק ג׳ – מנועי אחסון, ומבנה האינדקסים

—-

אלמנט חשוב של הארכיטקטורה של MySql היא ההפרדה של ה Storage Engines כרכיב מודולרי בבסיס הנתונים.
זהו בעצם יישום של דפוס Adapter בו ניהול האחסון לדיסק (או מקור אחר) מבוצע ע"י מודול שניתן להחליף.

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

כדי לסבר את האוזן, הנה רשימה של כמה מנועי-אחסון בהם נעשה שימוש ב MySQL:

  • InnoDB – ברירת המחדל של MySQL מאז גרסה 5.5, וכיום גם ברירת המחדל של MariaDB. נדבר עליו בהמשך.
  • MyISAM – ברירת המחדל של MySQL לפני גרסה 5.5. נדבר עליו בהמשך.
  • Memory (או Heap) – אחסון של הנתונים בזיכרון.  הגישה מהירה, אך באתחול בסיס הנתונים – הסכמה נשמרת, בעוד המידע בטבלאות מתאפס.
  • CSV – אכסון וניהול המידע בקבצי CSV.
  • BlackHole – כמו dev/null/ – המנוע מקבל שאילתות עדכון – אך לא מאחסן מידע בכלל. השימוש הנפוץ במנוע הזה הוא בתצורה מבוזרת בה כל המידע שנשמר ל node משוכפל ל replica מרוחקת, ואין צורך לשמור אותה מקומית.
  • Archive – מנוע ש optimized לגישות נדירות לכמות גדולה של נתונים בכל פעם. למשל: Audit.
  • XtraDB – מנוע בסיס נתונים משופר שנבנה ע"י חברת Percona (חברת ייעוץ / מומחים ל MySQL). תקופה מסוימת נחשב עדיף על InnoDB בביצועים והיה מנוע ברירת המחדל של MariaDB (החליף את Aria), אך לאחרונה הפערים נסגרו – ומנוע ברירת המחדל של MariaDB כיום גם הוא InnoDB.
  • MyRocks – מנוע שפותח ע"י פייסבוק המאפשר להשתמש בנתונים של RocksDB (שהוא בעצם Fork של LevelDB שמתוחזק ע"י פייסבוק). המנוע נכלל בהתקנה הגרסאות החדשות של MariaDB, וגם בהתקנה של Percona Server (ה distro של חברת Percona ל MySQL).
  • TukoDB – עוד מנוע שנוצר ע"י חברת Percona וזמין כברירת מחדל ב MariaDB וב Percona Server, המכוון לטיפול במידע שהוא Steaming או שיש לטפל בו ב Near-Realtime. המנוע משתמש באינדקסים המבוססים על מבנה-נתונים בשם Fractal Tree במקום ה B-Tree המסורתי.

מנועי האחסון הם Pluggable וניתן להתקין אותם על גבי התקנה קיימת של MySQL.
הבחירה הארכיטקטונית של MySQL במנוע אחסון שהוא Pluggble פותחת אופציה להוסיף יכולות, בקלות יחסית, לבסיס הנתונים וגם לבצע שינויים הדרגתיים בארכיטקטורה (נבנה את InnoDB לאורך שנים – עד שיהיה בשל להיות ה Default). מצד שני – הגישה הזו מקשה על אופטימיזציות קצה-אל-קצה ברמת בסיס-הנתונים כולו, כי כל מנוע אחסון מתנהג קצת אחרת.
כמו כל שיקול ארכיטקטוני – יש פה Trade-off.

מקור. הבהרה: Keys Cache היא יכולת ש MyISAM משתמש בה – ולא יכולת של השרת המתבססת על MyISAM.

המנועים המרכזיים: InnoDB מול MyISAM

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

כיום, InnoDB עולה בכמעט כל פרמטר על MyISAM.
בעבר עוד היו ל MyISAM יתרונות יחסיים, כגון דחיסה, Full-Text Index, או אינדקס Geospatial.
הפערים הללו נסגרו, ו MyISAM נראה היום מיושן למדי (אין Transactions! הנעילה היא ברמת הטבלה!).
עד שנת 2009, בערך, MySQL פיגר ביכולות בסיסיות אחרי שאר התעשייה. הוא היה חינמי ופשוט – וכך הצליח לחדור ולתפוס נתח שוק משמעותי.

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

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

InnoDB

המבנה המדובר ביותר לאינדקסים של בסיס נתונים הוא מבנה הנתונים בשם B-Tree. אני מניח שהמבנה הזה מוכר לכם מהתואר האקדמי או מקור אחר. זהו מבנה של עץ שבו כל node הוא בגודל של Page בדיסק, כך שמצמצמים את מספר הקריאות לדיסק. הרעיון תקף גם לבלוקים של זיכרון (הרי הזיכרון הוא לא באמת "Random Access". גם שם ניגשים לבלוקים).

מבנה הנתונים ש InnoDB משתמש בו הוא גרסה מעט "משופרת" הנקראת B+Tree (בחירה נפוצה בקרב בבסיסי-נתונים):

  • כל Leaf node מכיל מצביע לזה שאחריו (על מנת ליעל סריקות של טווחים).
  • כל הערכים נשמרים רק ב Leaf Nodes מה שאומר שב nodes הביניים יש רק מפתחות ולא ערכים. זה טוב כי אז ניתן לשמור יותר מצביעים בכל node ביניים. מצד שני, מאבדים את היכולת לשים ב node ביניים ערכים (key+value) שבשימוש נפוץ, ואז להגיע אליהם בפחות גישות. כרגיל: a trade-off.
מקור: Stack Overflow
MySQL מנהל לכל טבלה שני סוגי אינדקסים:
  • Clustered Index, או Primary Index – בו מאחסנים גם מפתחות (keys) וגם את שאר ערכי הטבלה (row) ביחד, כאשר הערכים בעלי ערך אינדקס דומה/עוקב – מאוחסנים זה ליד זה פיסית על הדיסק.
    • לכל טבלה יש רק Primary Index אחד.
    • אם לא תגדירו Primary Index לטבלה, אזי InnoDB ייצור אחד לבד, על בסיס auto-increment, אבל שלא חשוף לכם. יש בזה כמה בעיות – וההמלצה הגורפת היא תמיד להגדיר Primary Index בעצמכם.
  • Non-Clustered Index או Secondary Index – בו יושבים keys, אך הערכים הם לא ה Rows עצמם, אלא מצביעים ל primary Index.
    • האינדקס ממוין ע"פ ה keys, ולא ע"פ ה primary Index.
    • אפשר להגדיר כמה secondary Indexes שרוצים לכל טבלה.

בואו נראה תרשים שיסביר זאת בצורה יותר ברורה:

מקור: סיני כלשהו. אהבתי את התרשים.
  • ה Primary Index הוא B+Tree, כאשר בכל Leaf Node מאוחסנים <Pair<Key, Row. הרשומה שלנו במקרה הזה היא מס' חברה ושם (אם מתעלמים מה key).
  • על הדיסק נשמרים הערכים בצורה ממוינת. InnoDB ינסה לשמור את ה Pages של ה LeafNodes העוקבים קרובים זה לזה על הדיסק (כדי שיהיה ניתן לקרוא אותם בגישה רציפה אחת).
  • המחיר של Clustered Index הוא בהכנסת רשומות (או בטווח הפחות מיידי – מחיקות). הפעולות הללו יהיו יקרות יחסית ל non-clustered index.
    • פעולות של פיצול / איחוד דפים על הדיסק – הן יקרות.
    • כש InnoDB יוצר Page חדש, הוא מותיר בו 7% שטח פנוי, עבור עדכונים של רשומות (נניח ערך varchar שגדל) או הכנסה של רשומות חדשות.

עכשיו נתבונן על ה secondary index:

  • ה Secondary Index הוא גם B+Tree, כאשר בכל Lead Node מאוחסנים <Pair<Key, Primary Key.

מה שחשוב להבין מזה:

  • הרשומות בטבלה אשכרה נשמרות בדיסק ממוינות ע"פ ה Primary Index. יש לזה מחיר – אבל גישות ע"פ ה Primary Index יהיו יעילות ביותר.
    • לדייק: הרשומות בטבלה נשמרות על ה Primary Index.
    • זה לא בהכרח המצב בבסיסי נתונים רבים אחרים. אין clustered index ב Postgres או MyISAM Engine, ובאורקל זהו פיצ'ר אופציונלי (Index-Organized Tables).
  • אינדקסים משניים הם רק הצבעות ל Primary Index.
    • ככל שה Primary Key הוא ארוך יותר (בבתים) – אזי כל ה secondary index המצביעים אליו יהיו קטנים יותר, ויוכלו להכיל פחות רשומות בכל Leaf Node. הסבר: גודל ה Leaf Node הוא קבוע. למשל 16KB או 64KB.

עוד אינדקסים שכדאי להכיר:

  • Full Text Index – סוג של אינדקס הפוך המכיל את כל ההצבעות לרשומות המכילות מילות מפתח מסוימות. זהו אינדקס גדול מאוד – אך יכול לשפר מאוד חיפושים ע"פ מילות מפתח.
    • בכדי להשתמש בו, יש להשתמש בפקודת MATCH AGAINST במקום ב WHERE.
  • Geospatial Index – לחיפוש בשטחים (למשל: פוליגונים) על גבי מרחב דו-מימדי (מרחב גאוגרפי). המימוש של InnoDB הוא של R-Tree, אם כי גם KD-Tree הוא מבנה מקובל לאינדקסים מסוג זה.
    • שווה לציין שהמימוש של InnoDB ל Geospatial Index הוא מוגבל יחסית למימושים של Oracle, PostgreSQL או MongoDB.
האינדקסים הטובים ביותר ע"פ גוגל
שניה! …. מה?!?!

איך להגדיר את ה Primary Index האולטימטיבי?

סיכמנו שלתת ל InnoDB לקבוע לבד את ה Primary Index הוא פרקטיקה לא טובה.
בואו נראה מהן האופציות המקובלות / ה Best-Practices:
  • אפשר להשתמש ב Auto-Increment (גודל ברירת מחדל = 4 בתים).
    • יתרון: Primary Index מספרי הוא קטן (= מעט בתים בזיכרון) ויאפשר להכניס:
      • יותר מצביעים ב Primary Index intermediate nodes = פחות גישות לדיסק.
      • יותר רשומות ב Secondary Index Leaf Nodes = אינדקסים משניים יותר קטנים ויעילים.
      • יתפוס פחות מקום כ Foreign Key המצביע על הטבלה.
    • יתרון נוסף: ניתן למיין בזריזות את הטבלה ע"פ סדר עולה / יורד של הכנסת הרשומות. זה נחמד בעיקר בעבודה עם כלי Queries נוסח SQL Pro.
    • יתרון נוסף: מפתח קטן יקל על פעולות Join (החלק שמתבצע בזיכרון).
  • אפשר להשתמש ב GUID (כלומר = 128 ביט).
    • למרות ש 128, באופן תאורטי, הם 16 בתים, לרוב נייצג את ה GUID בייצוג הקסדצימלי (0-9A-F) מה שאומר מחרוזת באורך 32 בתים, ואז נגדיר אותו כ (varchar(32 – מה שבמצבים מסוימים עשוי להתפרש ע"י MySQL כ 3 תווים לכל אות + תו delimiter (בגלל ה Varchar) = אורך של 97 בתים.
      • מתוך רצון להטיב, אפשר להגדיר את השדה כ (char(32 – מה שיגרום לבסיס הנתונים להקצות לו 96 תווים (utf8mb3) או 128 בתים (utf8mb4) – כמעט תמיד.
      • יש ב MySQL 8 אופטימיזציות חדשות שיכולות להטיב את המצב. בלתי אפשרי באמת להעריך מה תהיה התוצאה המדויקת של כלל האופטימיזציות שעובדות בשילוב.
    • ההבדל בין 4 בתים ל 97 בתים, או אפילו "רק" 32 בתים – הוא כבר משמעותי למדי!
    • יתרון: ה ID הוא ייחודי בכל המערכת (או כל מערכת). באגים בהם משתמשים ב key הלא נכון – ייחשפו במהרה. ב auto-increment, אם השתמשנו במפתח לא נכון – יש סיכוי טוב שנקבל רשומה לא נכונה ויהיה קשה יותר לגלות זאת.
      • הייחודיות הזו מאפשרת לאחד נתונים מגרסאות שונות של בסיס הנתונים. למשל: תסריט של recovery, תסריט של Multi-region או כמה עותקים של בסיס הנתונים.
      • Id ייחודי ובעל פיזור אחיד יחסית, מאפשר Sharding (תסריט פחות נפוץ).
    • יתרון: אבטחה. מישהו שנחשף למפתח אחד – לא יכול להסיק ממנו ולנחש מפתחות אחרים. ב Auto-increment אפשר בקלות להבין שיש מפתחות דומים במספרים עוקבים.
    • חיסרון: האקראיות של המספרים הופכת את המיון של ה Clustered index לחסר משמעות.
      למשל: ב MS-SQL יש פונקציה בשם ()newsequentialid, המייצרת GUID בעל אלמנטים סדרתיים – כך שעדיין ה clustered index בא לידי ביטוי.
    • שווה לציין גם ש GUID עצמו לא כולו אקראי. חלק ממנו מבוסס על הפרטים של המכונה המקומית (למשל IP address), כך שאם כל ה GUID נוצרים על אותה המכונה (בסיס הנתונים) – יש כאן חוסר יעילות מסוים. זה עדיין משני לכל הנ"ל.
  • אפשר להשתמש במפתח עסקי טהור. למשל: כתובת אימייל. שם חברה + קוד מדינה (אם ייחודי), וכו'
    • המפתח הזה עלול להיות הגדול ביותר = ההשפעה החמורה ביותר על הביצועים מהיבט גודל האינדקסים.
    • מצד שני, אם יש לנו intensive read workload שניתן לאפיין בצורה ברורה (נניח: קריאת כל הרשומות של אותו ה email בצורה תדירה) – אזי מפתח שירכז את כל הרשומות הללו במספר קטן של דפים בדיסק עשוי לשפר מאוד את הביצועים.
    • קריאה של 10 דפים בכדי לטעון 1000 רשומות תהיה מהירה בסדרי גודל מקריאה של 300 דפים בכדי לטעון את אותן 1000 רשומות. זה כבר לא עניין של אינדקס – אלא גישה לנתונים בדיסק.
טוב. לא נפתור כאן את הדילמה הזו, דילמה רבת שנים. בכל זאת, כמה תובנות מצדי:
  • GUID הוא בחירה טובה, כאשר מדובר בטבלאות לא גדולות במיוחד ו/או אינן מעורבות בעבודה אינטנסיבית (הרבה חיפושים מורכבים, הרבה joins, וכו').
  • כאשר הביצועים מתחילים לשחק תפקיד – קרנו של ה Auto-Increment עולה.
    • פשרה אפשרית היא להחזיק שני מפתחות לרשומה:
      גם Id כ auto-Increment (שזה יהיה ה Primary Key) וגם GUID כשדה נוסף בטבלה (שיוחזק כ Secondary Key).

      • כל חשיפה לעולם החיצון (למשל: Clients או בסיס נתונים אחר) – תתבצע על בסיס ה GUID.
      • כל העבודה הפנימית – תתבצע על בסיס Auto-increment.
    • בכל מקרה, דאגו להחזיק את ה GUID כ 16 בתים, ולא כ 97. ההבדל בביצועים עשוי להיות דרמטי.
      • הקצרנים, יוכלו לקצץ חלקים לא אקראיים, ולקצר את המפתח ל 12 או 14 בתים. זה כבר לא כ"כ משמעותי. דווקא להוספת כמה בתים בתחילת ה GUID המאפשרים סדרתיות (למשל – מספר build או מספר טעינה של השרת) – תהיה השפעה טובה יותר.
  • מפתח עסקי טהור, הוא לא בהכרח רע.
    • יש פוטנציאל טוב לשיפור אמיתי, אם יודעים מה עושים ומהם דפוסי השאילתות.
    • דיי נפוץ שדפוסי השימוש משתנים עם הזמן, ובצוות יש אנשים בעלי הבנה פחות עמוקה של ה tradeoffs וההשלכה שלהם, כך שסטטיסטית מפתחות טהורים עסקים – נוטים להיות פחות טובים.
      • לא הזכרנו את המקרה שבו מטעמים עסקיים, אנחנו נדרשים לעקוף את האילוץ של המפתח (למשל: התמיכה רוצה להזין למערכת משתמש שאין להם כתובת אימייל שלו עדיין).
    • ולכן, ככלל, הייתי ממליץ להימנע ממפתחות עסקיים טהורים, מלבד מקרים חריגים. הם בעייתיים ברמת ה Scalability של הצוות.

הערה על Prefix Indexes:

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

CREATE INDEX part_of_name ON customer (name(10));

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

אליה וקוץ בה: prefix Index הוא אינו Covering, כלומר – באינדקס לא מאוחסן הערך השלם של העמודה.
בד"כ במפתח משני ה key הוא ערך השדה. בשאילות מסוימות – מספיק לקרוא את האינדקס המשני מבלי לקרוא בכלל את ה primary index (או "הטבלה").
ב Prefix Index – תמיד MySQL ילך לקרוא את ה Primary index. גם אם ה significant part נראה נכון. היה ניתן לבצע אופטימיזציות ובמקרים מסוימים לא ללכת. למשל: שאילתה המבוססת על התנאי '%name LIKE 'Lio לא צריכה באמת ללכת לאינדקס הראשי, כי יש לה את כל המידע הנדרש ב Prefix Index.
כיום – אין כזו אופטימיזציה ו MySQL תמיד יקרא גם את ה Primary Index.

לסיכום: Prefix Index נשמעים רעיון טוב, אבל הרבה פעמים הם עובדים פחות טוב מהמצופה מכיוון שהם לא Covering.

הארכיטקטורה של InnoDB

תחזוקה של אינדקסים: Analyze Table

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

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

תפקיד ה Query Optimizer של MySQL הוא לבנות את תוכנית הפעולה (להלן Query Plan) היעילה ביותר על מנת לספק שאילתה נתונה. נקודת מפתח בתוכנית הזו היא האם להשתמש באינדקסים – ואלו אינדקסים.

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

  • כמה רשומות יש בטבלה, ומה הפיזור שלהן (להלן Cardinality).
  • כמה רשומות צפויות להיות בטווח מסוים של מפתחות (להלן: ()records_in_range)

ספציפית, InnoDB שומר את הסטטיסטיקות הנ"ל בטבלה שלו המבוססת על ה Memory Storage Engine.
פעולת איסוף הסטטיסטיקות מתבצע ע"י דגימה של מספר קטן של דפים (ברירת מחדל = 8 דפים, המספר שיבחר בסוף מושפע גם מגודל הטבלה) שנבחרו באופן אקראי מתוך הטבלה, ומתוך הנחה שהדגימה הזו מייצגת.

הדגימה הזו תתבצע במצבים הבאים:

  • גישה ראשונה לטבלה.
  • מספר הרשומות בטבלה גדל ב 10% מאז הדגימה האחרונה או 2 מיליארד רשומות נוספו מאז הדגימה האחרונה (לטבלאות גדולות במיוחד).
תובנה חשובה היא שהסטטיסטיקות אינן מדויקות, ואינן בהכרח עקביות. כל ניתוח מחדש של הטבלה עשוי להציג תמונה מעט שונה.
ניתן להורות ל DB לבצע את הדגימה מחדש ע"י הפקודה:
ANALYZE TABLE table_name;
הפעולה הזו קצרה מאוד [א], מכיוון שהיא ניגשת למדגם קטן למדי של נתונים בטבלה.
ניתן לקבוע את גודל המדגם ע"י הפרמטר בשם innodb_stats_transient_sample_pages. ערך גבוה יותר יספק סטטיסטיקות מדויקות יותר, במחיר פעולת ניתוח יקרה יותר. נראה, למשל, שב AWS RDS הערך נקבע ל 20.
מתי חשוב לבצע Analyze Table בצורה יזומה?
אחר שינוי אינדקס, או הוספה של כמות גדולה מאוד של נתונים לטבלה – כדאי לבצע פעולת Analyze Table יזומה. כבר נתקלתי במצבים בהם ביצענו שינויים שאמורים היו להיות משמעותיים לביצועים – אך לא ראינו את ההשפעה שלהם עד הפעלה של פעולת ה Analyze Table
החל מגרסה 5.6.2 ברירת המחדל היא לשמור את הסטטיסטיקות לדיסק בעת אתחול (1 = innodb_stats_persistent). המוטיבציה המוצהרת לשינוי היא שמירה על עקביות של זמני הריצה של השאילתות לאחר אתחול של השרת. ייתכן וזה היה בשל משתמשים שפתחו באגים בנוסח "שאילתה X יותר אטית לאחר עדכון ו/או Restart" ולא אופטימיזציה נכונה לכל מצב.
החל מגרסה 8.0.3 נוסף הפרמטר sysvar_information_schema_stats_expiry המוחק את הסטטיסטיקות כל זמן נתון. ערך ברירת המחדל הוא 24 שעות. זו גישה הפוכה, והרבה יותר הגיונית, לדעתי.
אני לא מכיר דרך פשוטה לבצע Analyze Table תקופתי בגרסה 5.7 (ועדיף: בזמן ה off hours של המערכת). זו פעולה הגיונית מאוד, ובד"כ נעשית ע"י custom scripts.
ניתן לבדוק מתי התעדכנו הסטטיסטיקות לאחרונה ע"י בדיקת עמודת ה last_update בטבלת mysql.innodb_table_stats ובפירוט של אינדקס בטבלת ה mysql.innodb_index_stats.

תחזוקת אינדקסים: Optimize Table

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

הפקודה Optimize Table שקולה ל:

  • איחוי ה primary index.
    • כלומר: דחיסת הדפים ל 93% תפוסה, וכתיבה סדרתית שלהם על הדיסק.
  • איחוי ה secondary indexes
    • כנ"ל.
  • ביצוע ANALYZE TABLE על הטבלה

ב InnoDB, הפקודה OPTIMIZE TABLE ממופה ל ALTER TABLE … FORCE – המבצעת פעולה דומה.
מתחת לקלעים InnoDB ייצור עותק נוסף של הטבלה אשר יכתב לדיסק בצורה רציפה (זהו האיחוי), יעדכן בו שינויים שנעשים בטבלה תוך כדי העבודה, ואז יחליף את העותק הישן בחדש.

  • הפעולה תנעל את הטבלה לזמן קצר (שלב ה preparation) ואז עלולה לארוך זמן לא-מבוטל (מספר שעות הוא לא זמן נדיר לטבלה גדולה / פעילה. במקרים קיצוניים זה גם יכול לארוך ימים).
  • אין אינדיקציה על התקדמות, וביטול הפעולה גם יכול לארוך זמן.
  • בזמן הזה, כל העבודה עם הטבלה תהיה אטית יותר. אם זו טבלה מרכזית – ההשפעה על כלל בסיס הנתונים עשויה להיות ניכרת. חשוב מאוד לבצע פעולות OPTIMIZE TABLE ב Off Hours של המערכת שלכם.
  • כשאפשר – InnoDB ישתמש ביכולת בשם online DDL על מנת לבצע את העדכונים הללו במקביל, ובצורה שתעמיס פחות על הטבלה המקורית.

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

בזמנו היו הצעות לבצע Drop Index לפני ה OPTIMIZE TABLE ואז Create Index לאחריו, בכדי להמהיר את זמני הביצוע. יש כאן סיכון ברור לרגרסיה משמעותית בביצועים בזמן ה Optimize, ואני לא יודע לומר עד כמה העצה הזו רלוונטית גם היום.

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

  • טבלה שבה יש הרבה Inserts אבל ה Primary Key שלה תלוי בהוספה (למשל: מפתח ראשי מסוג Auto-increment) – לא צפויה ליהנות הרבה מ OPTIMIZE TABLE.
  • טבלה שבה יש עדכונים אינטנסיביים (נניח: מאות updates בשנייה) עשויה ליהנות מ OPTIMIZE TABLE – אבל חשוב מאוד לתכנן את הפעולה בזמן של מינימום עבודה על הטבלה. הסכנה ל downtime היא ממשית.
אתם יכולים לבדוק את הצורך ב OPTIMIZE TABLE בעזרת כמה שאילתות:

SELECT *
FROM   sys.schema_index_statistics
WHERE  table_name = 'tbl_name';
לבדוק כמה הכנסות / מחיקות / עדכונים היו בטבלה. הטבלה הזו מבוססת ככל הנראה על ה performance_schema.

השאילתה הבאה היא שאילתת מערכת:

SELECT table_name,
index_length,
data_length,
data_free,
data_length + index_length                           AS total_ו,
( data_free * 100 ) / ( data_length + index_length ) AS free_ratio
FROM   information_schema.tables
WHERE  table_schema = 'schema_name';

המציגה את ה free_ratio – היחס בין השטח שמוקצה ואינו בשימוש – לשטח שבשימוש.
זכרו ש InnoDB מכוון ל 7%. הכל תלוי במקרה, אבל אחוזים גבוהים (נאמר 40-50% ומעלה) הם מועמדים ל OPTIMIZE.

BLOBs

בכל הדיון עכשיו, לא דיברנו על שדות BLOB/CLOB.
ב InnoDB, עמודות באורך משתנה (כמו BLOB, JSON, TEXT, Varchar, וכו׳) עשויות להישמר בתוך ה Pages של ה Primary Index או עשויות להישמר בקובץ אחר נלווה.

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

  • אם גודל השדה הוא 40 בתים או פחות (משתנה בשם BTR_EXTERN_FIELD_REF_SIZE, כפול 2) – אזי השדה יאוחסן בתוך המפתח הראשי.
    • בתצורות שונות (למשל: Table's ROW_FORMAT = COMPACT) המספר הזה עשוי לעלות ל 768 או 1024 בתים.
  • מעבר לכך InnoDB ישתדל להשאיר את השדות באורך משתנה בתוך האינדקס הראשי. אם הוא אינו מצליח להכניס ב Page (להזכיר: ברירת המחדל היא 16KB) לפחות 2 רשומות (rows) – אזי הוא יתחיל להוציא את השדות באורך משתנה מתוך ה Primary Index.
    • הוא יתחיל להוציא את השדות מהגדול – לקטן.
בקיצור: שדות בגודל המתקרב ל 8KB, בתצורת ברירת-המחדל של MySQL – הם המועמדים העיקריים לצאת מתוך ה Primary Index לקובץ נפרד.
לקחים:
  • מכיוון שהשימוש בקובץ BLOB חיצוני איננו אחיד לאורך הרשומות בטבלה, הרעיון לבצע SELECT ללא עמודות מסוימות ולהימנע כך מהמחיר של שדות מסוג BLOB על הטבלה – אינו ממש נכון.
  • תאורטית, ב InnoDB אין ממש הבדל בין (Varchar(65,536 ל Text.
    • בפועל, למרות ש InnoDB תומך בערכי Varchar גדולים, MySQL עצמו לא יאפשר להגדיר סכמה בה סך גודל השדות ברשומה (row) גדול מ 65K – ולכן לא ניתן יהיה להגדיר (varchar(65K
  • כדאי להיזהר מביצוע OPTIMIZE TABLE של טבלה המכילה BLOBs גדולים (למשל: קבצי PDF). חלק מהמדדים (כמו free_ratio) עשויים להצביע על צורך ב defrag לטבלה – בעוד שבפועל ה Primary Index (מה שחשוב) הוא במצב מצוין. השאילתה שבודקת מספר עדכונים ומחיקות – מתאימה יותר למשימה במקרים של BLOBs.

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

——

[א] פעם, פעולת Analyze Table הייתה נועלת את הטבלה – בעיה ממשית. ההתנהגות הזו תוקנה בגרסה 5.6.38 – אך עדיין ניתן למצוא אזהרות באינטרנט לא לבצע פעולת Analyze Table בצורה תכופה. #לא_אקטואלי.

—–

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