ליאור בר-און לפני 3 שנים כ- 18 דקות קריאה
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.co.il/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 בזיכרון של כל שרת לגישה מהירה באמת, בלי פעולת רשת.
- Central cache בנוסח Redis (או כלי אחר שאתם מבינים) יהיה יעיל ככל שמספר ה instances רב יותר.
- הפרדה בין פעולות שונות על מנת לבצע אופטימיזציה טובה יותר של משאבים לכל פעולה: אין צורך שאת הקידוד של 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, קרי המערכת תוכננה כמה שנים קודם לכן, ואולי לכן היא נשמעת מעט מיושנת.