Design By Example III: Abstractions

בשני הפוסטים הקודמים של "תכנון מתוך דוגמה" (א.ק.א Design By Example), התמקדנו במציאת פתרון לבעיה קונקרטית. זו חלק קריטי בכל תכנון שהוא – אבל יש בתהליך התכנון גם מעבר לזה. בפוסט הזה נרצה להתקדם מעט יותר ב Liorson Software Design Maturity Model ולגעת ברמות השנייה (החצנת כוונות) והשלישית (התאמות לעתיד) של תכנון מערכת. הפשטות (Abstractions), הן כלי מרכזי בשתי הרמות הללו.

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

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

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

עבור שלב 2 של המודל (החצנת כוונות) עומדים לפנינו כמה כלים:

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

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

בואו נתחיל בתיאור של בעיה.

הבעיה שברצוננו לפתור

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

  • "שאלון" הוא רמת העל, מקצה לקצה – של כל מה שאנחנו שואלים את בעל העסק.
  • השאלון מורכב מדפים, שבכל דף כמה אלמנטים: כותרות, שאלות, ולעתים תמונות / דיאגרמות (התומכות בשאלות).
  • בשאלון אנו אוספים מידע על "ישויות" השייכות לעסק. למשל, ברשת מסעדות – אנו רוצים אוספים פרטים על הרכבים של העסק, ועל המבנים (Facilities, יכולים להיות גם מבנים ארעיים).
    • עמוד מיוחד, Facilities למשל, מציג את רשמית המבנים ומאפשר להוסיף / להוריד מבנים מהרשימה.
      • כל הוספת מבנה תציג סט של שאלות שיש לענות לכל מבנה. למשל: 10 שאלות לכל מבנה, המתפרסים על פני שני עמודים.
    • לאחר שמסיימים את איסוף הפרטים על המבנים, לוחצים בדף ה Facilities על הכפתור "Continue" וממשיכים ברצף השאלון.
  • אהה… ורק בדף של ה Facilities צריך גם להציג שאלה אחת "Include warehouses?" שמשפיעה על השאלות שישאלו לכל מבנה. זה אמור להיות דיי יוצא דופן, ולא להופיע כמעט אף פעם באובייקטים אחרים (רכבים, שותפויות/מועדוני לקוחות (לרשתות מזון), וכאלו…).

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

החלופות

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

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

חלופה 1:

  • Step הוא צעד בשאלון, שיכול להיות דף (Page) או צומת לניהול ישויות (Entity Hub). זה השלב שבו אנו יכולים להוסיף ולהסיר רכבים, מבנים וכו'. אשתמש בכל החלופות במונח Entity Hub בכדי לא לבלבל.
  • כל דף מכיל סדרה של אלמנטים. ב UML (שפת התרשימים לתיאור מבנה של תוכנה) סימן ה * מתאר יחס של רבים. כלומר: לדף יחיד – יש אלמנטים רבים (1 או יותר), ואולי מסוגים שונים – כפי שתיארנו למעלה.
    • על מנת לוודא שברור: גם דף יש יותר מאחד. השאלון (Questionnaire) מכיל צעדים רבים, ודף הוא Generalization (ראש חץ לבן חלול ב UML) של Step.
  • Entity Hub מחזיק קשר למספר דפים, אלו הדפים שצריכים להישאל לכל ישות שמטפלים בה: נניח 2 דפים של שאלות לכל Facility, כמו שראינו בתרשים למעלה.

חלופה 2:

  • הפעם אלמנט הוא תכונה של QuestionniareStep (האם זה שם עדיף? מה המשמעות של בחירה בשם זה או הקודם?) כך שבעצם יש לנו אלמנטים (שאלות, למשל) גם ב EntityHub. זה עוזר לכסות את מקרה הקצה ב Facility Hub, בו עלינו לשאול שאלה. מי יודע, אולי אם הזמן נגלה שאנו צריכים להציג עוד אלמנטים בתוך ה Entity Hubs?
  • ה Entity Hub לא מחזיק דפים, אלא Questionnaire Steps – מה שמאפשר לו להחזיר שורה של דפים (המקרה הידוע) אבל גם Entity Hubs בנים – מה שיכול לאפשר היררכיה. למשל, איסוף מידע על מחסנים (Warehourses) בכל מבנה (Facility). לא נדרש היום, אבל האם זה לא יהיה חכם לאפשר את זה במודל כבר מעכשיו?

חלופה 3:

  • במקרה הזה, אנחנו מגדירים EntityHub כסוג של אלמנט. כלומר: ייתכן שה Entity Hub הוא דף עם אלמנט יחיד: ה EntityHub.
    • הדבר מאפשר להוסיף את השאלה שאנו נדרשים ב FacilityHub, וגם מאפשר בצורה טבעית לשלב בין Hub לאלמנטים נוספים באותו הדף: כמה שאלות, כותרות, או תמונות. הדבר גם מאפשר לצרף 2 EntityHubs באותו דף. למשל: לאסוף רכבים ומבנים – באותו דף של שאלות (כמובן שכל ישות: רכב או מבנה תפנה לשאלות משלה).
  • ע"י כך שהפכנו את ה EntityHub לסוג של Element – ייתרנו את ההפשטה של Step/QuestionnaireStep – וכך בעצם פישטנו את התכנון!

חלופה 4:

  • המבנה הזה, האמת, דומה מאוד לחלופה הראשונה – אבל מתאר הבדל אחד גדול ומשמעותי: ה EntityHub לא מחזיק דפים, הוא מחזיק Questionnaire.
    • הדבר מאפשר לנו ליצור היררכיה של EntityHubs, דומה למה שתואר בחלופה 2 – אבל בדרך אחרת. (איזו דרך עדיפה? מה היתרונות / חסרונות של כל גישה?)
  • יש כאן משמעות עתידית דיי גדולה, בכך ש EntityHub מצביע לשאלון: במקום להחזיק "רשימה של דפים" עם ולהיות מוגבל ליכולות שלהן, כל Entity (רכב, מבנה, וכל אלו שעוד יגיעו) – יזכה לשאלון עם כל היכולות שיהיו קיימות. אלו עשויות להיות יכולות ברמת מבנה הנתונים (למשל: מוסיפים ל Questionnaire שם או צלמית ייחודית שמופיעה ב UI) או ברמת ההתנהגות (יכולת "לאפס" שאלון, יכולת לשנות באופן אקראי את סדר הדפים בשאלון) – שעכשיו יהיו תקפים לשאלון של רכבים / מבנים מעצם היותו של סט השאלות "שאלון" ולא "רשימה של דפים".
  • (יש איזו מידה של עצמאות/כוח שאנחנו נותנים לשאלות של Entities. האם זה דבר טוב או מזיק? מה המשמעות של זה?)

השתתפות הקהל

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

Google Analyhtics טוען שכל פוסט בבלוג נקרא ע"י בין 500 ל 2,000 קוראים (בד"כ). הייתי שמח לבדוק אם אלו בוטים או אנשים אמיתיים 😉.

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

יסודות: גרסאות בתוכנה

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

גִּרְסָאוּת (Versioning) היא טכניקה שנועדה לצמצם מורכבות של "העולם" הנובעת משינויים, בלתי-תלויים, של אלמנטים שונים המשפיעים על המערכת.

כשאנחנו מתחזקים מערכת-תוכנה לאורך זמן, החלקים השונים שלה עוברים שינויים מגוונים: שינויים בקוד המערכת, בספריות צד/שלישי (היום: בעיקר Open Srouce), או שינויים בחומרה ובסביבת הריצה עליה המערכת רצה (מערכת הפעלה, Virtual Machine, ענן, וכו').

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

החוקים הלא כתובים של הגרסאות

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

יש כמה כללים מקובלים לגבי גרסאות:

  • Immutability – שינוי ברכיב בעל הגרסה (להלן: הרכיב), יוביל לשינוי הגרסה. זה לא תמיד נכון, אבל זו הציפיה המקובלת.
    • Git Hash (סוג של גרסה), למשל, נובע מתוך התוכן, כך שכל שינוי קוד, אפילו לא משמעותי (למשל: הוספת שורת רווח) – יתבטא בשינוי ב Hash.
    • אין דבר שמונע מכותב ספריית צד-שלישי לשנות את הקוד, מבלי לעדכן את הגרסה. זו לא הציפיה – אך זה יכול לקרות (אולי יש היום כלי שמתריעים בפני כזה מצב, אני לא מכיר).
  • Orderability – יש דרך מובחנת להבחין איזו גרסה חדשה יותר מגרסה אחרת. לרוב הגרסה ממוספרת כמספר, אז מספר גבוה יותר – גרסה מאוחרת יותר.
    • פעם היה מקובל שמספר גרסה ביטא את גודל ההתקדמות. למשל: אחרי Windows NT 3.1 הגיעה גרסאת 3.5 בכדי לבטא שזה שינוי משמעותי. כנ"ל ב MacOS.
    • כמה הפצות של לינוקס משתמשות בתאריך ההפצה בפורמט YY-MM כגרסה. אז 16.10 אכן מאוחרת יותר מ 16.04, אבל לפעמים מפתיע שזו גרסה גדולה, ולא עדכון קטנטן, שיכול להשתמע מהמספרים העשרוניים. כמו כן, אין גרסאות אמצע. אין גרסה 16.05.
    • GNU בחרו בגרסאות במספרים מאוד גדולים. למשל: 5001, 5002. לא ברור לי למה, או מדוע אני נטפל דווקא למערכות הפעלה…
    • אנשי שיווק לא תמיד מקפידים על כללי ה Orderability בגרסאות. נסו לסדר את הגרסאות השונות ע"פ הסדר הנכון: Xbox One, Xbox 360, Xbox X, Xbox S, Xbox one S, ו Xbox one X.
נשמע כמו טקסט של ד"ר סוס…

Semantic Versioning

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

  • שינויי Patch – מבטיחים תאימות לפנים (forward-compatibility) ולאחור (backward-compatibility). אתם יכולים לשנות לעבור מגרסה 1.4.1 ל 1.4.3 של ספרייה הלוך ושוב – ולצפות ששוב התנהגות לא "תשבר" לכם.
    • כמובן שגרסה מבטאת שינוי ברכיב. ההתנהגות הצפויה אמורה לא להשתנות, אבל אולי "באג" נוסף בגרסה 1.4.1 ושינה את ההתנהגות הצפויה. התיקון שלו בגרסה 1.4.3 החזיר את ההתנהגות בפועל להתנהגות הצפויה. כלומר: גרסת Patch לא תשנה את ההתנהגות הצפויה, אך היא עלולה לשנות את ההתנהגות בפועל (בעקבות באגים).
  • שינויים Minor מבטיחים תאימות לאחור, אך לא תאימות לפנים. כלומר: אם הקוד שלי עבד עם גרסה 1.2.1, הוא צפוי לעבוד גם עם גרסה 1.3.5 – אך לא להיפך. אני לא יכול לצפות שקוד שעבד עם גרסה 1.3.5 יעבוד עם גרסה 1.2.1.
    • הסיבה העיקרית לזה היא שגרסאת Minor משמשת לתוספות התנהגות, למשל: APIs חדשים.
    • זו גם הסיבה מדוע כלי ניהול-תלויות הנתקלים בקונפליקט בגרסאות minor – בוחרים את הגרסה המאוחרת יותר בצורה אוטומטית. זו הציפיה מ"חוזה" הגרסה. נדבר על זה בהמשך.
  • שינויים Major לא מבטיחים שום סוג של תאימות. במעבר מגרסה 1.3.5 לגרסה 2 כלשהי (למשל 2.0.1) – מצופה ממני לקרוא בקפדנות את ה release notes, ולבצע את כל הבדיקות הנחוצות אם ההתנהגות העדכנית מתאימה לי. גרסאות Major שמורות לשינויי התנהגות, שכל מערכת "חייה" צריכה לעשות מדי-פעם.
    • כמובן שאם אני רוצה לשמור על קהל הלקוחות שלי אנסה לצמצם שינויים כלשהם, גם בגרסאות Major – למינימום. לפעמים השינויים בגרסאת Major הם גדולים מדי, והלקוחות שלי פשוט ייעצרו מלעדכן גרסאות. למשל: שינויים שוברי-התנהגות בפייטון 3 עיכבו את הקהילה כעשור, בממוצע, באימוץ החידושים.

ל Semantic Versioning יש כמה הרחבות, שקצת יותר פתוחות לפשרנות:

  • מקף אחרי מספר הגרסה – הוא מקום לתאר Pre-Release
    • ההגדרה המסורתית היא ש Alpha הוא שלב בתוכנה שעדיין מוסיפים / חסרים פיצ'רים עיקריים, בעוד ב Beta כל הפיצ'רים שם – רק לא עובדים עד הסוף.
      • כיום ההגדרה הרבה פחות מדויקת, ולא פעם נטען שמוצרים משוחררים (GA = General Availability), כשבעצם ברמת האיכות הם מתאימים יותר לשלב בטא. בחודשים אחרי השחרור הרשמי – יתבצעו התיקונים. הגישה הזו מאוד מתיישרת עם רעיונות ה Lean Startup.
    • אין גאמה😃, אבל אחרי שלב הבטא נראה לא פעם RC – קיצור של Release Candidate. המשמעות: המוצר כמעט מוכן, ורוצים לבדוק אותו ממש לפני שחרור. לרוב יהיו כמה RCs שימוספרו: RC1, RC2, וכו'.
    • לפעמים משתמשים במספר 0 (אפס) כ Major version לתאר pre-release. למשל: 0.3.1.
    • "SNAPSHOT" כ pre-release הוא סימן מקובל בעולם ה JVM שאנחנו מעדכנים את הרכיב, בלי לעדכן את מספר הגרסה. כלומר: תמיד הגרסה תישאר 1.0.0-SNAPSHOT, למרות שהקוד השתנה. שימוש זה הוא רק בזמן פיתוח – ולא לגרסאות ששוחררו "לעולם".
  • סימן + אחרי ה pre-release הוא מקום סטנדרטי להוספת Build metadata.
    • זה יכול להיות מספר סידורי של הבילד (עולה בכל פעם שעושים Build למוצר השלם). למשל: 3601. המספר גבוה כי בונים כמה פעמים ביום.
    • זה יכול להיות ערבוב גם של מספרים ואותיות, כל מידע אינפורמטיבי שיעזור למפתחים שמדבגים, בעיקר.
    • לא פעם משתמשים ב build number כב Patch version. למשל: 2.13.3601, כאשר 3601 הוא מספר ה build.

גרסאות של ספריות

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

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

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

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

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

בעיה שהייתה נפוצה בעבר, הוא כאשר ספריות הותקנו במערכת ההפעלה לשימוש כל האפליקציות (aka DLL Hell or Jar Hell). אפליקציה א' השתמשה בספרייה X בגרסה 1.0.0, אבל אז הותקנה אפליקציה ב' שהתקינה את ספרייה X בגרסה 2.0.0 (דרסה את 1.0.0) – וכך גרמה לאפליקציה א' להפסיק לעבוד.

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

בעיה שנותרה רלוונטית, היא בעיית "תלויות היהלום" (Diamond dependency) בין ספריות בהן משתמשת אותה האפליקציה/מערכת:

למשל ב Case 1 המערכת שלנו משתמשת, טרנזיטיבית, בשתי גרסאות שונות של Library C: גרסה 3.1.0 וגרסה 3.2.0.

יש שתי דרכים עיקריות לארוז את הקוד של המערכת שלנו:

  • Shared Libraries – כל המערכת / אפליקציה תשתמש בגרסה יחידה לכל ספרייה (להלן: ספרייה C).
    • יתרון אחד הוא Deployable קטן יותר של המערכת. במערכות גדולות יש לעתים מאות תלויות טרנזיטיביות, ולא נדיר למצוא אותה ספרייה ב 10 גרסאות שונות ויותר. לעתים ההבדל בין Shared Libraries ל Isolated Libraries יכול להגיע ל Deployable גדול פי 2-3 כאשר אנחנו משתמשים ב Isolated Libraries.
    • Deployable קטן יותר – משמע פחות זיכרון (RAM) שנדרש. פחות הכפלה של Singleton classes (למשל: או State שיושב על הגדרות המחלקות, ה Classes). אם ישנו בספרייה Cache – יהיה מאגר אחד לכל האפליקציה, ולא כמה Caches כפולים, ע"פ גרסאת הספרייה המדויקת.
    • בשפות JVM ה Class Loader יסרב לטעון שתי מחלקות עם אותו השם (נניח: מגרסאות שונות של ספרייה C). בשפות Strongly Typed אחרות, ייתכן ויהיה אפשר – אך בזמן ריצה מבני נתונים לא תואמים (עם אותו השם, אבל למשל טיפוסים שונים המגיעים מגרסאות שונות של הספרייה) – יגרמו לשגיאות בזמן ריצה, שגיאות שלעתים מאוד קשה לשחזר ולתקן.
    • גם בשפות שאינן Strongly typed צפויות בעיות, הם יצוצו מאוחר יותר – ויהיו קשות יותר למציאה. כאשר המערכת משתמשת לסירוגין פעם אחת באויבקט של גרסה 3.1.0 ולעיתים של גרסה 3.2.0 (ואין type safety לשים לב להבדל) – יכולים לצוץ באגים קשים ומבלבלים.
  • Isolated Libraries – כל ספרייה נארזת עם ספריות המשנה שלה – בגרסה שהיא ביקשה (ובדקה). אמנם ה Deployable שלנו יהיה גדול יותר, ויצרוך יותר זיכרון – אך לא נצטרף לפתור קונפליקטים של גרסאות, כמו: "באיזו גרסה של Library C עלינו להשתמש". ספרייה B תשתמש בגרסה 3.1.0 וספרייה A תשתמש בגרסה 3.2.0.
    • אמנם חסכנו התמודדות עם קונפליקטים בטווח הקצר, אבל עדיין עם אובייקטים בזיכרון, של ספרייה C בגרסאותיה השונות יעברו לסירוגין בקוד של ספריות A ו B – צפויים באגים מוזרים וקשים לאיתור במערכת.

למרות שאפשר לבחור ב Isolated Libraries, מקובל הרבה יותר לבחור ב Shared Libraries גם בשל החיסכון בזיכרון, אבל בעיקר בכדי להתמודד עם קונפליקטים בין גרסאות של ספריות מוקדם ככל האפשר ("Fail Fast") – בשלב ה Build וכמה שפחות בזמן ריצה בפרודקשיין.

על ה JVM למשל, נראה:

  • Isolated Libraries כ Fat Jar (נקרא גם Uber Jar) – כלומר אריזה של כל הספריות התלויות כ jars מקוננים בתוך jar יחיד (להלן: כל אפליקציה מספקת את כל הספריות שהיא זקוקה להן, ולא מניחה שהן מותקנות כבר במערכת ההפעלה).
  • Shared Libraries כ Shadow Jar – כאשר אנחנו אורזים לתוך jar גדול (ולכן מתבלבלים לעתים כאן עם השם Uber Jar) את כל הספריות שנדרשות – אבל עותק אחד מכל אחת. בתהליך יצירת ה Shadow Jar ייתכן וישונה ה bytecode של הספריות בכדי לתאום ל package name יחיד.

אפשר גם להשתמש בגישה מעורבת בה יש Shadow Jar של ספריות משותפות, שנבחרו ידניות להיות כאלו, הנארז בתוך Uber Jar בו כל שאר הספריות יכולות להיות בגרסאות כפולות. כלי ה Build מוודא שאין ספריות שהן גם Shared וגם כפולות. הגישה הזו פחות נפוצה לדעתי – מעולם לא נתקלתי ארגון שהשתמש בה.

נחזור לתרשים למעלה: איך פותרים את הקונפליקט?

ב Case 1 – לרוב כלי ה build יפתור את הקונפליקט אוטומטית (למשל בעולם ה JVM: מייבן וגריידל יפתרו אוטומטית, Ivy – לפי דעתי יזרוק שגיאה וידרוש התערבות ידנית). ההנחה כאן היא שחוקי הגרסאות הסמנטית נשמרים – ולכן שינוי גרסה שהוא minor, הוא backward compatible – ולכן בטוח לקחת את הגרסה המאוחרת יותר. * ההנחה הזו לרוב עובדת, אבל לא תמיד.

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

שווה לציין שמנהלי תלויות שונים עשויים להתנהג בצורה שונה. למשל:

  • במייבן הכלל המוביל הוא "Nearest first", אז ב Case 2 תבחר גרסה 3.1.0 של ספרייה C כי היא רק "קשת אחת" מהאפליקציה.
  • בגריידל הכלל המוביל הוא "Latest first" ולכן תמיד תיבחר גרסה 3.2.0 של ספרייה C (כלל יותר הגיוני ועקבי).
    • במייבן אגב, אם המרחק שווה (כמו ב Case 1), תבחר הגרסה של התלות שהופיעה ראשונה בקובץ ה POM.XML. כלומר: אם כתבנו את התלות ב Library A קודם – אז תבחר גרסה 3.2.0 של ספרייה C, ואם כתבנו קודם את התלות של Library B קודם – אז תבחר גרסה 3.1.0 של ספרייה C. כלומר: סידור קובץ ה POM.XML (למשל: לפי א"ב) – עלול לשבור לכם את האפליקציה. מאוד מבלבל.

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

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

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

  • ברירה נפוצה אחת היא לעשות downgrade לספרייה B. הרי לפני העדכון האחרון שלה, היא עבדה עם גרסה 3 כלשהי, והמערכת עבדה. כלומר: נדחה את העדכון של B עד שספרייה A תתמוך בגרסה 4 של ספרייה C. לעתים זה יכול לקחת שנה ויותר – וזו בהחלט פשרה.
  • עוד ברירה נפוצה, וזו שלפעמים בה פותחים, הוא לנסות ולהכריח את ספרייה B לעבוד עם גרסה ישנה יותר של Library C. אולי 3.2.0, ואולי גרסה מאוחרת יותר 3.4.3 (האחרונה בגרסה 3, למשל).
    • קיים סיכון שהקומפילציה תצליח, אבל רק לאחר שבועות נגלה בעיות ב Production. אם יש לנו סט בדיקות מקיף, וניטור טוב של פרודקשיין – האופציה הזו הופכת ליותר רלוונטית. ניסוי וטעייה.
דוגמה לנעילת ("force") גרסה ספציפית של תלות ב Gradle. במקרה הזה נעלנו טווח בין 3.9 עד 4.0 (לא כולל).
בגרדייל יש אפילו הגדרת because כתיעוד להסביר מדוע הנעילה נעשתה.
שווה לציין ש Gradle סט יכולות רחב, ויש מספר דרכים שונות לבצע נעילות של גרסה של ספרייה.
  • אפשרות קיצונית יותר אך אפשרית הוא לעבור להשתמש בספרייה אחרת (במקום ספרייה A או ספרייה B). האפשרות הזו סבירה יותר ככל שהספרייה קטנה יותר, והתלות שלנו בה קלה יותר.

סיכום

סקרנו את הבסיס של ניהול גרסאות, בדגש על גרסאות של ספריות שהמערכת שלנו תלויה בהן. גִּרְסָאוּת רלוונטית גם לפרוטוקולים, APIs, נתונים גולמיים, ועוד.

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

לפעמים תיקון באג, יקלקל לנו את המערכת. טעות נפוצה היא לחשוב שהוספת Validations בקוד הם backward compatible changes – אבל קריאות שעד לפני השינוי עברו – יתחילו להיכשל. כלומר: Validation נוסף הוא דוגמה חמקמקה לשינוי שסמנטית אינו Backward compatible בהכרח.

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

שווה להזכיר לרגע את האירוע בו מתכנת JavaScript הסיר את הספרייה הפצפונת left-pad ממנהל החבילות NPM – ושבר את האינטרנט, בתור תזכורת לכמה תלויות קיימות בין ספריות שאנחנו לא מודעים אליהן. כל פעם שאני נכנס בפרויקט לתיקיית ה cache של ה package manager ורואה בכמה ספריות המערכת תלויה בפועל – אני מתפלא.

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

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

Design by Example II

אני רוצה להמשיך ולדבר על בעיות Design לדוגמה, בתקווה שהן מעניינות מספיק. הבעיה שבחרתי הפעם: Rate Limiter.

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

  • היכן יישב המנגנון של ה Rate Limiting? בתוך המערכת – או כרכיב נוסף?
  • איך המנגנון יעבוד בפועל? איך יוודא שה Rate נשמר?
  • האם אנחנו צריכים כלים נוספים (Queue, Database, וכו') – על מנת לספק את המנגנון?

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

מה שאנו נוטים לשכוח…

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

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


הספר ?Are your lights on מספר על מגדל "ברונטוזרוס", מגדל משרדים לתעשיית הפיננסים – שבו הלקוחות מתלוננים על תור ארוך למעליות. הוא מציע מספר פתרונות:

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

מה דעתכם? איזה פתרון אתם מעדיפים? מה נראה לכם הכי הגיוני?

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

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

  • קביעת השכירות כפרמטר של מספר העובדים של כל שוכר – וכך צמצום מספר העובדים בבניין. כלומר: יהיה משתלם יותר לחברות עם פחות עובדים / פחות צפיפות לשכור. זה עשוי להשפיע גם על תחזוקה / חניה / עלויות אחרות.
  • יציאה בקמפיין שמבליט את המגניבות בלעבוד ב"מגדל ברונטוזרוס" – כך שהתורים במעליות יתקבלו ביתר הבנה: "זו זכות לעבוד בכזה מגדל, תראה כמה אנשים רוצים את זה".
  • גיוון בתחומי העיסוק של השוכרים בבניין. אולי כל אנשי הפיננסים מתחילים לעבוד בין 7:00 ל 7:30, אבל ההגעה לחברות הייטק – עשויה להתפזר על טווח רחב יותר של שעות (8:00 עד 10:00) ולהוריד את העומס?
  • למכור את הבניין, ולהתמקד במומחיות של החברה – בנייה ושיווק נדל"ן (ולא ניהול נכסים).

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

נסו לחשוב, מה מוביל אותנו ל"ריצה" לפתרון ראשון? פתרון שהוא אולי לא הפתרון לבעיה הנכונה.

חזרה ל Rate Limiter

אוקי. אנחנו רוצים להבין טוב יותר את הבעיה (או הבעיות) שבגינן אנחנו רוצים Rate Limiter ל API שלנו.

השאלה החשובה היא השאלה העסקית: מי רוצה את ה Rate Limiter (או כל Design שאנחנו עובדים עליו),ולמה? איך אנחנו יכולים לבנות ביטחון שאנחנו פותרים את הבעיה הנכונה?

  • בווריאציה אחת נתכנן Rate Limiter שנועד להגן על המערכת שלנו בפני עומס. צד שלישי קורא למערכת שלנו בקצב גבוה (נאמר: אלפי פעמים בשעה), ו/או כל קריאה של הצד השלישי דורשת מהמערכת שלנו כמות נכבדת של עבודה – כך שמעבר לקצב מסוים, המערכת שלנו תגיב לאט מדי לכלל השימושים ואולי תקרוס.
    • להזכיר, גם כאשר יש auto-scaling – הוא לא מיידי. Peak גדול שמגיע ברגע אחד, יכול לגרום לאי יכולת לספק את הבקשות לזמן מה עד שהמערכת גדלה מספיק.
    • נניח שהצד השלישי הוא חלק משני בביזנס. כלומר: אנו מעדיפים לא לתת לו שירות על מנת להגן על המערכת / הביזנס העיקרי.
  • בווריאציה השנייה, אנו רוצים להגן על הלקוחות שלנו מפני עלויות-יתר. הם משלמים לנו על כל קריאת API אבל מצפים שנעזור להם לנהל את העלויות. אם לקוח לא רוצה להוציא יותר מ $5000 בשבוע – אנו נאפשר לו את זה ע"י דחיית בקשות שחורגות מהתקציב.

איטרציה ראשונה: Rate Limiter שמגן על המערכת

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


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

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

האם אתם מזהים כבר את הכשל הלוגי? הנה כמה:

  • נניח שהמערכת יכולה לעמוד בעומס של 60,000 בשעה. הוספה של לקוח נוסף תאפשר 100,000 בקשות בשעה.
  • כמה מהעומס מגיע מפנים המערכת? אולי בעצם השימוש העיקרי של המערכת כבר משתמש ב 20,000-30,000 בקשות בשעה? ואז – 50,000 בקשות נוספות כבר יביאו לקריסה.
  • כמה המספר הזה יציב לאורך זמן? מי יידע לעדכן אותו כאשר שינוי במערכת משנה את הקיבולת הזמינה (להעלות או להוריד)?
  • הכי גרוע אולי, אם מגיע לקוח שעושה מיליון בקשות בשעה – כמה זמן ייקח עד שייחסם?
    • 60,000 בקשות בשעה הן 1,000 בדקה, ו 16 בשנייה. מיליון בקשות בשעה, בממוצע, הן 270 בשנייה – הרבה יותר ממה שהמערכת יכולה לשאת.
    • מה אם מותר לעשות 50,000 בשעה – אבל לקוח בוחר לעשות את כל הבקשות שלו במשך שלוש דקות? התוצאה היא זהה.

זה אגב מה שקרה לנו אז: שעתיים אחרי ש API נפתח לצד השלישי – כל המערכת קרסה. ה Rate Limiter, מומש ונבדק – אבל לא עשה את העבודה. הצד השלישי ידע שלאחר 50,000 קריאות בשעה הוא ייחסם, אבל לא דאג להגבלות מצדו (הוא היה במוד ניסוי). מכיוון שהפנה בקשות בקצב גבוה מאוד, המערכת קרסה תוך דקות, "רק" אחרי כ 10,000 בקשות.

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


בואו נתחיל לחשוב על התכנון. הנה כמה שאלות שהעלתי בתחילת הפוסט:

  • היכן יישב המנגנון של ה Rate Limiting? בתוך המערכת – או כרכיב נוסף? – בהבנת הבעיה נראה לי שאני רוצה את ה Rate Limiting כרכיב נוסף. הוא אמור להגן על המערכת, ושייפול אם צריך – ויגן על המערכת.
    • יתרון נוסף – אוכל להשתמש ב Rate Limiter גם עבור שרתים (מיקרו-שירותים) אחרים. יש לי כלי לשימוש חוזר.
    • חסרון – עלויות. ישנם מקרים ספציפיים בהם זה שיקול מספיק לחבר את ה rate limiter לקוד השרת – אבל אנחנו לא שם.
  • איך המנגנון יעבוד בפועל? איך יוודא שה Rate נשמר?
    • דבר ראשון שברור שהרזולוציה צריכה להיות קטנה. תלוי מאוד אופי הבקשות, אורכן, תדירותן וכו' – אך כברירת מחדל אני אלך על היחידה הבטוחה והפשוטה: שנייה. הגבלת קריאות בשנייה.
    • מכיוון שמה שחשוב לי הוא להגן על המערכת בפני קריסה – חשוב לי שהיה מונה (counter) מרכזי שידחה בקשות בכלל. בנוסף נניח שאנחנו רוצים גם מונה לכל לקוח – לייצר שוויון מסוים. נניח: 1,000 קריאות בשנייה בכלל, ו 200 לכל לקוח. נניח שיש כ 20-30 לקוחות. המספרים המדויקים פחות חשובים לתכנון, אבל קל יותר לדבר ולהתייחס למספר נתון.
      • המקסימום למונה הכללי צריך לנבוע מתוך יכולות המערכת. מספר שנקבע יהיה כלל אצבע שלא יתחשב בשינויים במערכת. לכן, היה עדיף למשל למדוד את ה CPU של השרת הפנימי ולהחליט לפיו (או בעצם: ממוצע CPU כי כנראה יש כמה instances וגם המספר שלהם עשוי להשתנות דינאמית). בכל מקרה, אנחנו באיטרציה ראשונה – ולא נכון לצלול לזה כרגע. נרשום את המחשבה בצד.
    • "האלגוריתם" עצמו לספירה, נשמע לי דיי פשוט ואתאר אותו מיד בהמשך.
  • האם אנחנו צריכים כלים נוספים (Queue, Database, וכו') – על מנת לספק את המנגנון? האמת שתחושת הבטן שלי אומרת Redis (מנוע מבני-נתונים / אולי בסיס נתונים K/V – מבוסס זיכרון. כלי שכדאי להכיר). אני מניח על קצב גבוה כלשהו ובכל מקרה ברור לי שה Rate Limtier צריך כמה מופעים עבור High Availability (לא נרצה שנפילה שלו תפיל את השירות לצד-שלישי) – ורדיס הוא רכיב מרכזי.
    • חשוב בדזיין לפשט, ואולי אנחנו לא זקוקים לרדיס (בקצבים לא גבוהים, גם בסיס נתונים רגיל יכול לעשות את העבודה). תחושת הבטן שלי אומרת לי שכנראה שנרצה, וקל לי אישית לחשוב על הפתרון כך – ולכן אני מתחיל איתו. בסבבים מאוחרים יותר, אולי אמצא דרך לחתוך אותו החוצה, אבל אני הולך כרגע עם מה שהכי פשוט לי לעבוד איתו – ולהתקדם לנושאים נוספים.

הנה תרשים:

  • בקשה מתקבלת ע"י ה Rate Limiter ואני בודק את המונה המרכזי (global counter). אם חרגתי ממנו – אני יכול ישר לקפוץ ל 4b ולדחות את הבקשה.
  • את המונים אני יכול לתחזק ב Redis כ key/value, כאשר ה key הוא שנייה ב Epoch (עבור המונה הגלובאלי) או שילוב של לקוח (שאני מזהה ע"פ ה auth token) + שנייה ב Epoch.
    • ל Keys אקבע expiration של 10 שניות, למשל – כדי שהזיכרון של רדיס לא יתמלא.
    • הנחת יסוד שלי הוא שהשרתים מסונכרנים בשעונים. זו הנחה סבירה לחלוטין בתשתיות הענן הקיימות, ובאמזון, אם זכור לי נכון – אפשר להניח על סטייה מירבית של 1 ms לשרתים באותו Region – מה שלא בעיה עבורנו.
      • אפשר גם לקבוע cloudWatch alert על חריגה בשעונים של שרתים. לא יודע אם הייתי זוכר את זה בסשן Design ו/או ראיון – בטח לא היה לי זמן לחפש בגוגל ולאמת.
    • מי שמכיר היטב את נושא הזמן בטח חושב שיש כאן חור (קטן) בתכנון בגלל Leap Seconds. אני יכול להרגיע ש Epoch מתעלם מ leap seconds – ובכל מקרה זה גליץ' מספיק קטן ונדיר להתעלם ממנו.

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

  • ההחלטה השרירותית כמעט לחסום לקוח יחיד ב 200 קריאות לשנייה אולי בעייתית. תלוי בביזנס – אולי לקוחות פונים מעט וב bursts – ואז חשוב לתת להם את מלוא הקיבולת האפשרית (1000 קריאות בשנייה?). בכל מקרה, מקום לשיפור האלגוריתם ובכל מקרה לא נשמע לי showstopper.
  • עלויות של חומרה: עוד 2-3 שרתים + Redis זה עלות לא מבוטלת. האם היא נדרשת?
    • צעד מידי ופשוט לצמצום עלויות יהיה לממש את ה Rate Limiter על גבי AWS Lambda (או מקבילה בענן אחר). נשמע כמו צעד מתבקש.
    • הנה שיפרתי משהו משמעותי מתוך ביקורת עצמית מובנית 😀
  • ה Scale דיי טוב. ככלל אצבע Redis יכול לטפל ב 100,000 בקשות בשנייה. אפשר לנהל דיון על scales אדירים – אבל נשאיר את זה בצד, עד שיוכח אחרת. במקרה הכי גרוע ה Rate Limiter "קורס" – אך המערכת עדיין מוגנת.
  • עקרונות תוכנה: SOLID / other practices:
    • No single point of failure – לרדיס אין redundancy, אם הוא ייפול (ויעבור מיד restart) – יהיו 10-30 שניות של downtime (הנחה) ומידע שנעלם מהזיכרון בכל מקרה יהיה כבר לא רלוונטי (מונים של שניות קודמות).
  • ייתכן ובאמת החסימה של תעבורה לכל לקוח היא מעבר למינימום ההתחלתי ויכולנו להתחיל עם מונה מרכזי וזהו. תודה לטל רום על ההערה.

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

איטרציה ראשונה: Rate Limiter לניהול עלויות של לקוחות

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

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

  • היכן יישב המנגנון של ה Rate Limiting? בתוך המערכת – או כרכיב נוסף? – במקרה הזה נשמע שעדיף לשבת בתוך המערכת. מדוע?
    • אולי יש מספר קריאות API שמפעילים את אותה פונקציונליות שאנו רוצים לגבות עליה. מנגנון הגבייה/מעקב לא יצטרך לעקוב אחרי הדרכים השונות להגיע ללב הלוגיקה (כלומר: API1,2,3,4 דורשים חיוב)
    • אם יש תקלה ולא הצלחנו לסיים את הפעולה (Exception) – כנראה שלא נכון לחייב. כלומר – אם החיוב היה ב gateway/proxy כמו במקרה הקודם, היה עליו לפעול רק בעת מתן תשובה תקנית.
    • פחות רכיבים במערכת. ייתכן ויש לנו מערכת גבייה ("commission service" או איך שלא ייקרא) – ואז הגיוני לשים את ה Rate Limiter שם. זה עדיין בתוך המערכת – רק במקום הנכון יותר.
  • איך המנגנון יעבוד בפועל? איך יוודא שה Rate נשמר?
    • רדיס כבר לא טוב לנו. מדובר בגבייה של כסף שזה עניין מדויק שאסורים בו פספוסים, ורדיס ש"נפל" ואיבד את כל המונים שלנו – גם אינו מצב סביר.
      • סביר יותר להתחיל בסיס נתוני (רלציוני, או מה שמהערכת עובדת בה) ומשם להמשיך.
    • המנגנון עשוי להיות דיי דומה, נניח Key/Value כאשר המפתח הוא יום Epoch + זיהוי הלקוח. כל זאת כל עוד איפוס המונה ב UTC הוא הגיוני. ייתכן ולא – ואולי יש צורך לנהל את ה offset של אזור הזמן לכל לקוח / או מדיניות אחרת כלשהי.
    • מה עם לקוח שלא השתמש בחיוב שלו במשך כ 3 ימים – וביום הרביעי יש לו חיוב של 125%? האם אנחנו רוצים לאזן מקרים כאלו איכשהו – או להישאר במסגרת נוקשה של יום?
      • בגלל שאנחנו באיטרציה ראשונה – נתחיל ב"מסגרת" נוקשה של יום ונדון בהרחבות אפשריות רק בהמשך.
  • האם אנחנו צריכים כלים נוספים (Queue, Database, וכו') – על מנת לספק את המנגנון?
    • הזכרנו כבר בסיס נתונים, כי מידע צריך להשתמש persistent. אני לא רואה יותר מזה כרגע.

הנה תרשים שמתאר את המבנה:

  • אני מדמיין מבנה קיים כלשהו (בשחור), התוספות הן בכחול.
  • קודם כל, לפני הפעלת פעולה עסקית יש לבדוק האם ניתן לחייב (שלב 1). הוא יגיע ל Rate Limiter והוא מצדו יבדוק את המונה בבסיס הנתונים. הסכמה יכולה להיות key/value פשוט.
  • רק כאשר הפעולה הסתיימה בהצלחה (אני מניח? – תלוי איך המערכת עובדת), מבצעים פעולת charge שאותה נוסיף לחיוב ה counter.
    • אפשר לשאול: "מה יקרה אם יהיה exception ב increase counter? זו לא פעולה אטומית?" – התשובה היא שזה כנראה באג והתשובה כנראה מאוד תלויה במערכת הספציפית ואיך היא עובדת. נשמע לי לא רציני "לדמיין" כאלו פרטים בלי שניתן לבדוק. באופן כללי מכיוון שזה אותו בסיס נתונים – ניתן לצרף טרנזקציות (אם בסיס הנתונים תומך) ולהיות חלק מאותה פעולה אטומית בשלבים 2.1 ו 2.2.

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

  • יכולים להיות מגוון דרישות כיצד לחשב את ה Limit של כל לקוח. כל דרישה – עם פתרונות אפשריים משלה. יש מגוון אלגוריתמים ל Rate Limiting בניתן למצוא באינטרנט. העניין הוא לספק צרכים ספציפיים – ובאופן הפשוט ביותר שניתן. אין טעם להשתמש באלגוריתם מורכב ממה שנדרש – זו לא הנדסה טובה.
  • מה קורה במידה ויש לנו Scale אדיר, שבסיס נתונים יחיד לא יוכל לעמוד בו? אולי – ה latency של בדיקת המונה (קריאה לבסיס נתונים ע"פ מפתח היא לרוב פעולה של מילי-שניות בודדות) יקרה מדי?
    • בסיס נתונים רלציוני לרוב יכול לטפל בכמה אלפי בקשות פשוטות בשנייה. אם צריך יותר ניתן לנהל אולי 2 רמות של עדכונים: אחת ברדיס מהירה, והשנייה מתוזמנת ומסנכרנת, נניח כל דקה, את הנתונים מרדיס לבסיס הנתונים. כאן נוצר הסיכון שלקוח שחרג מחשבונו ייחסם רק דקה מאוחר יותר (במקרה הגרוע). האם זו פשרה סבירה שניתן לקחת? אולי החברה יכולה לספוג סטיות כאלו – בכדי להשאיר את המערכת יחסית פשוטה?
    • המקרה של latency הוא דיי דומה. אפשר לנהל שכבר מהירה ושכבה איטית. אפשר להפוך את פעולות העדכון לאסינכרוניות ואפשר להחזיק cache מקומי על המונה וכאשר הוא מתקרב לקצה (נניח: 95%) רק אז להחיל בדיקה בכל קריאה. עוד אופציה חלופית לזו היא לנהל "כרטיסיות" של גישות כאשר שרת מבקש מבסיס הנתונים בקשה ל 100 בקשות ואותן הוא מנהל בזיכרון ורק כל 100 קריאות פונה לבסיס הנתונים. יש כאן מספר מקרי קצה שדורשים טיפול – גישה אפשרית אך לא מאוד פשוטה.
      • בכל מקרה נראה שצמצום latency ו/או הרחבת scale יגיעו עם tradeoff לגבי הדיוק של האכיפה. חשוב לזכור שהרבה מורכבות טכנית יכולה להיות מומרת בגמישות עסקית, למשל הסכמה לחריגה של לקוחות בכמה אחוזים מההקצאה היומית שלהם – על חשבון החברה.
  • מכיוון שמדובר בכסף, חשוב לבדוק את מקרי-הקצה השונים ולראות שאין פספוסים משמעותיים. אני לא חושב כרגע על בעייה עקרונית, אך הייתי משאיר את העניין כנקודה לבחינה.

סיכום

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

  • תהליך בניית תוכנה (גם תכנון וגם קידוד) הוא תהליך של גילוי, חשיבה, ולמידה מתמשכים. זו טעות לקפוץ למסקנות מהירות, להחליט מוקדם ולהתקבע על כיוונים ופתרונות. לסגור אופציות. בטח שגם לממש דברים שלא ברור לנו שהם נחוצים.
    • כמעט כל "הפתרונות" שסקרתי בווב (בעיקר עבור כתיבת הפוסט הקודם: URL Shortener) היו כאלו: החלטיים, "יודעים", ומציגים תמונה של פתרון כאילו זה תרגיל בטריגונומטריה, עם התחלה וסוף חד-משמעיים. זה לא עובד כך באמת.
  • אם היה תכנון "מיטבי" (מלה שדיי צורמת לי, אגב, כשאני שומע אותה בהקשרים מקצועיים) לסגנון של בעיות: Cache, URL Shortner, או Rate Limiter – אז היה כבר Open Source אחד לכל בעיה שמשתמשים בו ודי.
    • אני מקווה שהצלחתי להדגים כיצד כל מצב עסקי מעט שונה – יהנה מתכנון שהוא קצת או הרבה שונה. ששתי הבעיות בפוסט – באמת דורשות פתרונות דיי שונים.
    • ברור שלפעמים נכון להתפשר, ולקחת פתרון קצת פחות מתאים – כי הוא זמין, אמין, או קל. אני עדיין בעד שימוש חוזר בשירותים וספריות (קוד פתוח, או בכלל).
  • טעות נפוצה שלישית היא צלילה לפרטים לפני בניית "Walking Skeleton" (קרי "שלד מהלך"). במקום לצלול לפרטים ובעיקר למימוש של סכמת בסיס-הנתונים (לרוב דומיין ידוע, שילמד אותנו מעט על התכנון והצרכים, אבל יקשה על שינויים מהותיים בתכנון) – חשוב יותר להרכיב "Flow" עובד מקצה לקצה (להלן: שלד) וגם לאמת אותו עם העולם – למשל להריץ עליו כמה בדיקות או אפילו תעבורה אמיתית של המערכת (להלן: מהלך).

אני מקווה שהצלחתי במשימה.

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

Design By Example

הרבה זמן אני מתחבט בשאלה: כיצד לומדים (או מלמדים) Software Design בצורה יעילה?

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

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

ישנם סגנונות ארכיטקטוניים שאוהבים ללמד (Layered, Microservices, Event-Driven, וכו') – שזו בטוח נקודת מבט חשובה, ויש Quality Attributes – טוב ונחמד, אך עדיין לא מדריך כיצד לעשות Design נכון.

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

"המרכיב הסודי" הוא כנראה שילוב של:

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

אני חושש שללא הארבעה הללו, או לפחות שלושה מהם – קשה להגיע לתכנונים מוצלחים בעקביות ולא משנה כמה ידע תאורטי / UML / SysML / Patterns / Architecture Stytles – למדתם לעומק.

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

התאוריה של עיצוב תוכנה (Patterns / Quality Attributes / Styles / Principles) בעיקר עוזרת לנו להעריך אפשרויות במהירות, ולהבין טוב יותר מה הנקודות החזקות והחלשות בכל אופציה – כדי ליצור / להמציא אפשרויות נוספות, וטובות יותר.

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

The Classical URL Shortener Question

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

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

הנה השאלה:

"נניח שאנחנו רוצים לבנות URL Shortener בחברה שלנו, שלוקח URL ארוך, למשל https://softwarearchiblog.com/wp-admin/post.php?post=3658&action=edit ומקצר אותו ל URL קצקצר כמו https://short.com/xyz1234. איך היית מתכנן שירות כזה? בוא תתחיל/י לתאר בבקשה"

איך מתחילים להתמודד עם שאלת דזיין?

מה דעתם?

אולי הכי נכון לפתוח בבחירת סגנון ארכיטקטוני (microservices או event-driven, אולי space-based)?
אולי להיזכר בכל עקרונות ה SOLID ולראות איך לממש אותם?
אולי בעצם – פשוט להיות פרגמטי, ולחשוב על המימוש – ולבחור טכנולוגיה מתאימה, למשל Spring Boot או Vert.x?

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

אז מאיפה מתחילים? משתי נקודות המוצא הבאות, ובמקביל:

  • בירור הצרכים הפונקציונליים והאיכותיים מהתוכנה. יש הבדל דרמטי בין URL Shortener שיתחרה בזה של Bit.ly בנפח הבקשות שמטפלים בהן, לזה הפנימי של החברה – שאולי לא צריך לטפל יותר מכמה אלפי בקשות ביום. כנ"ל לגבי אבטחה, עלויות, retention וכו'.
    • אני מדגיש את המונח צרכים על פני דרישות – כי דרישות הן פרשנות של הצרכים, שחשוב לבקר ולאתגר את הדרישות, ולא לקבל אותן כאמת מוחלטת. חלק מטעויות ה Design הכואבות ביותר שראיתי היה קבלת הדרישות בלי שאלות – היכן שכמה שאלות הגיוניות ופשוטות, היו יכולות להוביל לנתיב אחר לגמרי.
  • לסגור לופ ראשון, נאיבי, כמודל ייחוס שאפשר להתחיל ולעבוד ממנו באיטרציות.
    • זה לא רק בסדר, אלא נכון יותר להתחיל במודל פשוט.
      • אם נתחיל במודל בסיסי/פשוט – יהיה לנו קל יותר להתמקד בעיקר ולא להיגרר לאופטימיזציות של דאגות משניות (טעות נפוצה), או להיצמד לטכנולוגיה או מבנה מוכר – גם כאשר הוא אינו רלוונטי.
      • אנו חותרים לפתרון פשוט – ועדיף לנסות ולהימנע מכל שיכלול שאינו נדרש. מערכות פשוטות יותר – כושלות פחות, וקל יותר לבצע בהן שינויים עמוקים.

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

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

התכנון הבסיסי הזה עוזר לי להבין כמה דברים בצורה יותר ברורה:

  • למשל, שמדובר ב 2 endpoints: יצירת shortURL, ופיענוח שלו.
  • שצפוי כנראה שהשירות גם יעשה redirect ל shortURL – כלומר, הוא יקבל traffic ישיר מהאינטרנט ולא יקבל רק בקשות לפענוח. כמובן שאני יכול להפריד את האחריות הזו לרכיב אחר, אבל אני יודע שזו דאגה מתקדמת יותר, שאין טעם לכלול אותה בדיון בשלב הזה… רק התחלנו. לעתים אנחנו מנסים להפגין ב Design שלנו חשיבה על כמה שיותר אפשרויות. נחמד לכתוב אותן בצד – מזיק להטמיע אותם ישר ב Design, עוד לפני שהובן שיש צורך – כי כך אנחנו מסבכים את ה Design וכנראה שלא לצורך.
  • כתבתי ג'אווה ו RDBMS – כי זו הסביבה שהכי טבעי לי לחשוב עליה.
    • תוך כדי שאני מביט בתרשים אני חושב שאם מדובר ב hyperscale אולי עדיף כשפה את Rust (ללא GC) ואולי בסיס נתונים Key-Value שיכול to scale out למספר nodes בצורה אמינה.
    • שוב: נחמד לחשוב את זה, אבל טעות גדולה (ולצערי: נפוצה) היא להתחיל להטמיע את המחשבות הללו ב Design לפני שברורים לי הצרכים. אין Design טוב בצורה אבסולוטית. Design הוא מוצלח רק יחסית לצרכים. לצרכים שונים תכנונים שונים יהיו טובים או גרועים.

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

  • שאלה הכי משמעותית ל Design כנראה היא שאלת ה sclae: בכמה URLs אני צפוי לטפל? בכמה בקשות כל אחד מה endpoint יתמודד בשעה (למשל), כמה shortURLs אצטרך לשמור? מיליונים? מיליארדים? יותר?
  • האם יש הנחות מסוימות לגבי מבנה ה URL? הם מגיעים מ domain מסוים / מאפיין מסוים? אם המערכת היא פנימית לחברה יותר סביר שיהיו הנחות כאלו – שיכולות לתת לי leverage אמיתי ב design. אם אני יוצר מתחרה ישיר ל bit.ly/tinyUrl – אז זה פחות סביר.
  • לכמה זמן אני צריך לשמור את ה shortURL? לזמן נתון (נניח 30 יום – מספק צורך נקודתי), או לעד (שירות כללי)? אני מניח שלעד משפיע ממש על מודל עסקי, כי הנזק מיצירת מיליארדי shortURLs ואז אי תמיכה בהם יום אחד בהיר – יפגע בהרבה מאוד אנשים. כאן הייתי רוצה מבנה עלויות מינימלי שיאפשר להמשיך את האופרציה של תמיכה ב shortURLs שכבר נוצרו, לזמן ארוך.

מראיינים שראיתי (אני מכיר את השאלה הזו כבר כעשור) נהגו להפוך אותו לשאלה של Hyperscale: "עליך לתמוך בעד 1,000 מיליארד URLs, עם 100 מיליון בקשות ביום." זה לא מציאותי (או לפחות תאורטי-מדי), כי גם שמגיעים כאלו scales -מתכננים חכמים לא מתחילים בתכנון ל scale מרבי. עוברים שלב-שלב, מדרגה-מדרגה. ארגונים רציניים ישקיעו בתכנון יותר משעה, ולא יטילו את המשימה (לרוב) על העובד שרק הצטרף לחברה. ניחא.

בואו נבחר דרישות שדורשות לחשוב על Scale, אבל מבלי הצורך להתמודד עם נקודות קיצון (של scalability):

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

איטרציה שניה

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

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

שרתים, מן הסתם נרצה יותר מאחד: 2-3 instances לפחות עבור High Availability, ואפשר לגדול עוד, אם תהיה עוד עבודה. קידוד של מיליון URLs בחודש, זה ממוצע של כ 35-30 אלף ביום או 1500 בשעה, פחות מאחד בשנייה – לא נשמע מאתגר, גם אם נניח שבשעות העומס יש פי 5 traffic מהממוצע.

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

מה הייתי עושה אם לא היה לי את הידע הזה? הייתי מתחיל לעשות load testing למערכת – ומגלה. באיחור של כמה שבועות את סדרי הגודל. עיכוב כזה הוא חיסרון גדול – אבל לא מונע ממני מגיע לשם. הרבה פעמים ידע הוא זרז (משמעותי) – אך חסרונו אינו showstopper.

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

איך התכנון שלי עומד במדדים של Design? קרי SOLID/GRASP או עקרונות מסוימים? המבנה כ"כ פשוט שלא נראה לי שיש עקרונות שהוא ממש יכול לסתור. פוריסטים עשויים לטעות ששירות אחד בג'אווה שמבצע שתי פעולות: קידוד ופענוח של URL זה לא SRP – אבל אנחנו לא פוריסטים. כמטאפורה: עפרון עם מחק בקצה זה שימושי וטוב – ואני לא מרגיש צורך להפריד בין השניים "כי אלו שני כלים שונים, ואנחנו עושים בלאגן – כאוס ממש".

עד כאן לא הרבה השתנה ב Design:

עכשיו אני צריך להעמיק שלב אחד הלאה בפרטים: כיצד יעבדו ה endpoints? מה הם יעשו?
תהליך יעיל של Design הוא כזה שאני עושה איטרציה מהירה לסגור "לוף" (כלומר: end-to-end) מסיק מסקנות / בוחן חלופות, ורק אז נכנס לעוד רמת עומק / פרטים. זהו תהליך איטרנטיבי ביסודו.

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

זה רעיון שמאוד קל לי לחשוב עליו ולתאר אותו – אבל נראה שזה לא ה common sense, ולכן אני חוזר על הנקודה הזו כמה פעמים: צלילה מהירה לפרטים מוקדם מדי, התקבעות על רעיונות לא הכי פשוטים שנשמעים "יותר חכמים" (ניקח NoSQL Database, שפת סקאלה, בחירה ב multithreading model כזה או אחר) – זו הדרך הלא נכונה לעשות את הדברים. סיכוי טוב שאין לאופטימיזציות הללו יתרון ממשי, אבל הם מקבעים אותנו על פרטים מסוימים, שיוצרים מגבלות / סוגרים אפשרויות (למשל: scala דורש JVM, בסיס נתונים K/V מגביל אותנו ביכולות חיפוש או דורש מאתנו עוד רכיבים כדי לאפשר חיפוש יעיל) ומרחיקים אותנו מבחינת האופציות העקרוניות – שהיא החשובה ביותר בשלבי ה Design.

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

איך יעבוד ה endpoint של קידוד long URL? יש פה כמה אלטרנטיבות שעולות מיד:

  • ה Short URL הוא Hash על ה Long URL.
  • ה Short URL הוא GUID (מזהה אקראי / בלתי תלוי).

אני מדבר כמובן רק על ה "id" של ה shortURL, קרי: <https://short.com/<id
לא ברור לי מיד איזו אלטרנטיבה עדיפה, ואני שמח שיש לי יותר מאחת. אני אקדיש את הזמן להשוות ביניהן.

  • hash היא פונקציה "סטטיסטית" וייתכנו שני long URLs שונים שיניבו אותו hash.
    • ההסתברות תלויה באיכות ה hash function, גודל ה hash שנוצר, וכמות האיברים שאקדד – אבל בכל מקרה "התנגשות" היא בלתי נמנעת.
    • איך אפשר לטפל? זה ידרוש ממני בכל קידוד לגשת לבסיס הנתונים, לראות אם קיים ה hash הזה והאם הוא מצביע לאותו long URL, ואם לא – לספק איכשהו id אחר, עם לוגיקה שניתן לשחזר כאשר ה LongURL הזה מופיע שוב. אפשרי – אבל זה אומר קריאה מבסיס הנתונים בכל קידוד, וקצת סיבוכיות בטיפול בהתנגשויות.
  • GUID הוא גם סטטיסטי, אבל מספיק גדול שלא סביר שייווצרו איי פעם שניים כפולים. מצד שני, GUID תקני הוא באורך 32-36 תווים, מה שאומר שה URL שלי כבר לא כ"כ קצר. מזיכרוני כמשתמש נראה ש bit.ly לא מייצרים id ארוך ביותר מ 7-8 תווים.
    • mitigation אפשרי הוא להשתמש ב GUID קצר יותר, אך חלש יותר – עם הסתברות הולכת וגוברת ל"התנגשות".
    • חיסרון נוסף הוא ש Long URLs זהים שיתקבלו לקידוד, יקבלו כל אחד GUID חדש – וכך לא יהיה שימוש חוזר ב shortURL, אלא אם נפנה לבסיס הנתונים ונחפש אם ה URL הזה כבר קיים בכל פעולת קידוד.

שוב, אגב, אני מתבסס על ידע (הבנה כיצד פונקציות hash עובדות, או GUID).
נראה ששתי האופציות שעומדות בפני הן יותר נקודות על רצף מאשר גישות שונות שמובילות ל tradeoffs שונים בעליל. איך מחליטים?
נחזור לדרישות ונבחן את האופציות דרכן: shortURL עם id של 32 תווים לא נשמע לי רצוי אם אנשים אמורים להקליד את ה URLs הללו. בתסריטים מסוימים זה עשוי להיות סביר.
מצד שני: טיפול בהתנגשויות גם נראה לי לא דבר רצוי – סיבוכיות.

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

אני חושב שהדרך ליצור URLs הכי קצרים הוא בבסיס הנתונים לנהל auto-increment ואת המספר לקדד לתווים אלפא-נומריים. נניח יש ערכי ה ASCII שזה 256 תווים אפשריים, אני משתמש ב modulo על מנת לחלץ את המספר על בסיס 256. ה ids הראשונים שיתקבלו יהיו תו אחד (a, b, c) ואם הזמן ילכו ויתארכו ככל שישתמשו במערכת יותר. מאיפה זה בא לי? אינטואיציה / ניסיון, אני מניח.

הנה המצב שהגענו אליו:

endpoint 1 בעצם גורר שתי פעולות: 1.1 ו 1.2.


שינוי קטן לסכמה: הפסקנו לשמור shortUrl כי בעצם id של ה shortURL הוא ה autoinc בבסיס 256. כשאני מקבל id בבסיס 256 אני יכול להמיר אותו למספר בבסיס 10 (autoinc) בפעולה חשבונית פשוטה. חבל לשמור את זה בבסיס הנתונים. שווה לציין ש primary key קטן יותר (בבתים) – גם ישפר את ביצועי בסיס הנתונים.

כמובן שכל זה מתאפשר בעקבות שימוש בבסיס נתונים יחיד ומרכזי. אם היינו נאלצים להשתמש בבסיס נתונים מבוזר (עבור scale) – autoinc מרכזי כבר לא היה עובד והיינו נאלצים להשתמש בגישה אחרת: GUID/Hash שהייתה מניבה URLs ארוכים יותר, או אולי פשוט מקצים לכל שרת "bulk" של מספרים ייחודיים שהוא רץ איתם והוא יכול לקבל bulk נוסף – כאשר נגמרו לו המספרים המוקצים (ואז עדיין ה URL יהיה קצר כמעט ככל האפשר).

נעבור לבחון מעט יותר את ה endpoint השני.

ה endpoint השני: shortUrl => Redirect to longURL

כאן היישום נשמע דיי פשוט:

  • קבל shortUrl בקידוד ASCII והמר אותו לבסיס 10 (autoinc).
  • חפש בסיס הנתונים את ה URL המלא.
  • החזרת תשובת redirect (קרי HTTP 302) עם ה longUrl.

משהו נשמע כאן מוזר? אי אפשר להעביר את רוב תווי ה ASCII על גבי URL – זה לא תקני ודפדפנים לא יקבלו את זה (שוב: ידע). פספסנו את זה.
נחזור ונשנה גם את ה endpoint הקודם לא לקדד על בסיס 256 (ASCII) אלא על בסיס של תווים שמותרים ב URL, למשל a..zA..Z0..9 שזה 62 תווים, כנראה שיש עוד קצת מותרים ששווה להשתמש בהם וככה להגדיל את הבסיס (ולקצר עוד קצת את ה URL). שימו לב ש URL הוא case sensitive ויש הבדל בין אות גדולה לאות קטנה (ידע).

איטרציה שלישית

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

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

נצא ל production עם פתרון סופר-נאיבי, ניפול, נחקור ונבין למה נפלנו (למשל: השתמשנו בתווים שאסורים ב URL – שינוי קטן יחסית) נתקן ונצא שוב, וחוזר חלילה.

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

חזרה ל Design: אמרנו שהנקודה הפוטנציאלית של ה Design נראית טיפול ב scale. איך נשפר את ה Design שלנו להיות מוכן יותר ל high scale?
גישה אחת, פחות רצינית, היא "לעבור להשתמש בכלים של scale": למשל: Cassandra, Scylla, אולי ZooKeeper וכו'.

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

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

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

  • אם ה URL חוזרים על עצמם בצורה ניכרת ("blockbusters") – אם בהפעלה (shortUrls מסוימים תופסים נתח מורגש מהשימוש) או בקידוד (המון משתמשים באים לקדד את ה URL שהוא google.com) – אזי caches בהחלט יכולים לעזור. Cache שיחסוך לנו גישה יקרה לבסיס הנתונים: אם בקריאת ה autoinc => longUrl או בחיפוש אחרי longUrl אם כבר קודד. שני caches שונים.
    • Central cache בנוסח Redis (או כלי אחר שאתם מבינים) יהיה יעיל ככל שמספר ה instances רב יותר.
      • כאשר יש Central cache יש מקום לשקול multi-layered cache כאשר יש minimal cache בזיכרון של כל שרת לגישה מהירה באמת, בלי פעולת רשת.
  • הפרדה בין פעולות שונות על מנת לבצע אופטימיזציה טובה יותר של משאבים לכל פעולה: אין צורך שאת הקידוד של longUrl ואת התרגום יעשו אותם שרתים – אפשר לפצל לשניים.
    • אפשר להוסיף לבסיס הנתונים read-replica רק לצורך התרגום (חיפוש לפי id) – וכך לאפשר לבסיס הנתונים לנהל את ה caches הפנימיים שלו בצורה יותר יעילה.
  • מעבר לבסיס נתונים יעיל יותר לצורך הפעולות הנתונות: בעצם אנחנו משתמשים בבסיס הנתונים רק לצורך key/value ויש בסיסי-נתונים שמתמחים בזה. כמה יותר יעילים הם יהיו? האם יהיה משתלם לעשות מעבר (ביצועים / עלויות תפעול / learning curve)?
  • מעבר לבסיס נתונים מבוזר – אם ורק אם בסיס נתונים יחיד מרכזי לא מצליח לעמוד ב capacity של ה URLs. נשתדל לא לשלם סתם על מה שלא צריך.
  • שיפורי performance: הבדיקה בכל endcoding אם קיים כבר longURL כזה היא התקורה הבולטת ביותר בעיני. אפשר לנסות ליישם טכניקות כגון Bloom Filter שמאפשר לייצג "חתימה מינימלית" של ה longURL בהרבה פחות מקום – מה שייכנס בקלות ל cache מקומי של השרת ואולי גם ל cache של המעבד.
    • פעם התחלתי לכתוב פוסט על מבני-נתונים הסתברותיים, אבל הסקתי שזה נושא נישתי שלא ייגע לרוב הקוראים…

סיכום

האם סיפקתי את ה Design הטוב ביותר לבעיית ה URL Shortner?

ברור שלא – כי URL Shortner הוא בעצם סט של בעיות דומות אך שונות. לכל צורך – משהו אחר ישתנה. למשל: אם הצורך הוא להחזיק URL רק 30 יום – כנראה שמבנה הביצועים ישתנה, ותיפתח לנו אפשרות "למחזר" URLs כדי ולשמור עליהם קצרים יותר לאורך זמן (?!).

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

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



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

האינטרנט מלא בפוסטים על URL Shortener וליוי הפתרון. הנה כמה לדוגמה:
URL Shortener

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

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

Continuous Knowledge Delivery

לכולנו כבר ברור לגמרי מדוע אנחנו רוצים למזג קוד כל הזמן (Continuous Integration), מדוע אנחנו רוצים לבדוק ולהכין קוד כל הזמן, (Continuous Delivery) או מדוע אנחנו רוצים לשחרר קוד כל הזמן (Continuous Deployment).

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

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

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

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

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

פעם עבדתי בארגון בו התלוצצנו שמי שמתקבל לעבוד בארגון, לא יוכל אחרי שנתיים או שלוש להתקבל לארגון מחדש: ראיונות העבודה הטכניים היו בהחלט קשים, אבל פילוסופיית העבודה לא "נשענה" על ידע עמוק של העובדים: נבנו תשתיות פנימיות שמגבילות מאוד את צורת העבודה: למשל תשתיות לעבודה מול בסיס הנתונים – סט abstractions מעל hibernate שלא היה ניתן לראות SQL והיה לאנשים רק דימיון (שגוי לרוב) מה באמת קורה מאחורי הקלעים, או מן שפת XML שתורגמה ל HTML+CSS, שהגבילה את יכולת העבודה ו"חסכה" מהאנשים חשיבה והבנה מה באמת מתרחש בדפדפן.

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

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

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

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

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

מה אפשר לעשות?

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

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

  • כנסים הם חוויה, אבל לא מקור למידה משמעותי בעיני. מאוד נהניתי מכמה כנסים – וכמה מהם היו פותחי-עיניים עבורי, אבל את הלמידה עצמה, עשיתי מאוחר יותר, מתוך ספרים ומאמרים. אם לא הייתי מוסיף את הלמידה העצמית, הייתי יודע לדקלם כמה ססמאות נוספות, עם מעט מאוד הבנה במה באמת מדובר.
  • יש משהו מאוד מלמד לתת לאנשים בתוך הארגון להרצות, הם בוודאי מתפתחים באופן אישי ממתן הרצאה – אבל בשלבי ההתפתחות הראשונים הם עלולים לספק סשנים ברמה לא מספיק טובה. אולי כדאי להתחיל מפורום של צוות/קבוצה, ורק בעקבות הצלחות שם – לזמן לאנשים לבמה רחבה? (נניח: 100 איש). כל סשן בינוני ומטה שמועבר ל 100 איש – הוא פספוס לדעתי, שעלול להפוך לנזק מצטבר (איבוד אמון בפורום, הורדת הסטנדרטים).
  • ספרים עדיין "עובדים", אבל לפלח הולך ופוחת של האוכלוסייה. אני מאמין ש Video Tutorials הם המדיה העדיפה כיום. כמובן שלאנשים שונים, מתאימים דברים שונים – אבל אם לא נתתם למדיה הזו סיכוי מספיק – אני ממליץ (בתור אחד שקרא עשרות רבות של ספרים, אבל גם ראה עשרות רבות של Video Tutorials).
    • למרות מוטיבציה גבוהה של אנשים להיות עם ידע, הלמידה עצמה אינה קלה, ולעתים קרובות לא מתממשת (כמו שכולם רוצים להיות בכושר, אבל קשה ליישם. מסגרת יכולה מאוד לעזור). השלב הקשה ביותר הוא כנראה הקצאת זמן – וארגון שרוצה להצליח כנראה צריך לעזור לעובדים לפנות זמן משמעותי, ולהתחייב ללמידה. למשל: לקבוע ברבעון כמה ימים קבועים מראש (ולא ניתנים להזזה) לצורך הלמידה + לבקש לסכם ולהציג את סיכום הדברים מול הצוות.
  • חונך אישי הוא דיי ברירת מחדל, במיוחד כאשר מגיעים Juniors לארגון – אבל הם יקרים מכפי שנדמה: גם בכמות ההשקעה בפועל, וגם מכך שלא תמיד החונך ידע לחנוך בצורה טובה, או אפילו לספק תשובות מספיק טובות למגוון שאלות שיעלו. החניך לרוב לא יוכל לתת אינדקציה לאיכות החניכה, עד שלא צבר כמה שנים טובות בתעשייה – והפידבק יהיה בעיקר סביב זמינות ונחמדות.

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

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

  • תיעוד טוב יכול להיות מאוד יעיל (כנראה, למשל תיעוד של ספריות OSS) – אך מעולם לא עבדתי בארגון שהצליח לייצר תיעוד בעקביות: בכמות ובאיכות.
  • אני מאמין שתיעוד בקוד הוא מאוד שימושי, כפי שכתבתי באריכות בפוסט קוד ספרותי = סופן של ההערות בקוד. הפוסט מתאר אלו הערות בקוד הן מיותרות, ועדיף שייכחדו, ואלו הן שימושיות (בגדול: דברים שאפשר לפספס מקריאת הקוד, תמונה גדולה, עקרונות, עבר וכוונות עתיד). יותר קל ליצור תרבות של תיעוד בקוד – כי קל יותר לכתוב אותו ולהסביר את הערך שבו.
  • כשהארגון גדל מצטבר בו ידע וכללים, כגון Coding Conventions ו Best Practices. יש כאלו שמתנגדים לכל סוג של סדר / חוקים ורוצים להיות "Rock Stars" – ראיתי שוב ושוב את הנזקים המצטברים של הגישה הזו. לא צריך להגזים, וחשוב להשאיר חופש לסגנון אישי – אבל סדר ואחידות בדברים הבסיסיים (לחווייתי ארוכת השנים) יוצרים הרבה יותר תועלת מנזק. בלי תיעוד של הכללים האלו – קשה להפיץ ותחזק אותם. גדילה מסיבית של הארגון יכולה בקלות לגרום לאיבוד של לקחי-העבר, וגם הטובים שבהם. לקחים צריך לתחזק, לחדש, ולהתאים.
  • כלי שנראה מאוד שימושי ויעיל להפצה ואכיפה של קונבנציות של Coding Convention ו Best Practices פנימיים לארגון הוא כלי Static Analysis שניתן להרחיב ולקסטם לחוקים הספציפיים של הארגון. מאסתי מזמן מכלים שאוסרים על שורה להיות מערבר לאורך מסוים או מחייבים אותי להוציא constant מכל מספר שמופיע בקוד – אני מוקיע את הכללים האלו, וממליץ לבטל אותם! מצד שני, הצגתי אולי עשרים פעמים בארגון את כללי הבסיס של מבנה טבלאות שהוחלט עליון (חייב להיות primary key שהוא GUID או Autoinc, להוסיף 2 עמודות לזמני יצירה + triggers שימלאו אותם, שימוש ב UTF8 – לא משהו חריג), ושוב ושוב היו פספוסים ואנשים שלא שמעו מעולם על הכללים (שגם מתועדים היטב). פעם אחת יצרנו חוק של Static Analysis שלא מאפשר לעשות ל commit אם יש חריגות – ונראה שהידע החל לעבור, ובלי כמעט השקעה נוספת. כל כלל הוא הזדמנות ללמד, ולכן חשוב מאוד לא רק להציב את החוק אלא להסביר אותו, ולהפנות למקורות. למשל: מדוע למשל חשוב ב MySQL שיהיה Primary Key ומה העלות של Primary Key גדול (בבתים). בנקסט אנחנו משתמשים ב Detekt (לשפת קוטלין) ו Danger (לשפת TypeScript) לכתיבה של Custom Static Analysis rules.
    • התאוריה המקובלת היא ש 70% מהלמידה בפועל מתבצעת מתוך או תוך-כדי עשייה בפועל, וזה הרגע המתאים ביותר ללמוד בו – כאשר הנושא 100% רלוונטי אלי, ואני זקוק לידע בו. Code Analysis tools (עם הסברים מפורטים בצד) – מצטיינים בקליעה לרגעים החשובים הללו.
    • בלי קשר, כל סוג של למידה כדאי לקשור לצורך נוכחי, למשל: קורס SQL לקחת בעת עבודה על פיצ'ר מורכב בבסיס הנתונים, ולהתמקד בצרכים הספציפיים – ולא בעת ההצטרפות לחברה, שאולי שנתיים רצופות לאחר-מכן לא יעשה בידע שימוש משמעותי.

אחרית -דבר / סיכום

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

הרמה האסטרטגית בהפיכת ארגון לארגון שבו "זורם ידע, באופן מתמשך" – דורשת עבודה… אסטרטגית. למשל: קביעת יעדים משמעותיים ואי התפשרות עליהם. למידה היא השקעה שפירותיה ניתנים רק לאחר זמן, ויש המון פיתויים לדחות אותה. כיצד הופכים למידה משמעותית ותמידית לערך בארגון שנלחמים עליו, ממש כמו Continous Delivery? הרבה ארגונים מוכנים להקריב הרבה בכדי להגיע ל Continuous Delivery הנכסף, אך האם לכל הארגונים הוא ייתן תרומה זהה או גבוהה יותר מ Continuous Knowledge Delivery?!

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

הגדרת מומחיויות: איש QA, איש אוטומציה, Release Manager, איש Operations (בטעות נקרא: DevOps), DBA ועוד – עוזרות לפשט את הניהול בטווח הקצר, אבל יוצרות Silos של ידע (ואי-זרימת ידע) שיקשו עלינו בעתיד.

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

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

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

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

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

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

בקיצור: הרבה שאלות, קצת פחות תשובות.

אתם יותר ממוזמנים להוסיף את התובנות שלכם בנושא.

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