-->

יום שבת, 20 בפברואר 2016

ארכיטקטורה: האם לנסות שוב?!

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

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

מה עושים ב 0.1% (עשירית אחוז) מהמקרים האחרים, כאשר דווקא מודול B מסיים ראשון?

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

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

סוג אחר של מתכנתים, יחפש פתרונות אג׳יליים ופשוטים למימוש. ״בוא נוסיף sleep של 500ms למודול ב׳ - וכך הוא תמיד יסיים אחרי״. הפתרון אמנם פשוט מאוד למימוש אך הוא נדחה על הסף על ידי הצוות: להאריך את התהליך כולו סתם בחצי שניה, עבור 99.9% מהמקרים  - זה לא נשמע סביר...
ננסה שוב: ״בואו נזהה מצב שמודול ב׳ מסיים ראשון, ואם זה קרה - נפעיל את כל התהליך בשנית״.

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

מה אתם אומרים?





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

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

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

באיזה גישה אתה הייתם בוחרים?



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

התבונה המקובלת היא: "בואו נעשה את זה פעם אחת ולתמיד".


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

התבונה המקובלת היא: "בואו לא נסבך את זה".


אלתור. יעיל, או חפלפ?  מקור: redstateeclectic

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

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

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


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

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


לא ספר אמיתי :-)



אז מה אני חושב על פתרונות "מהירים ואופטימיים"?

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

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

"לפתור את הבעיה אחת ולתמיד" זו בעצם הגישה שאומרת להשקיע יותר בפתרון, ו"בוא לא נתעכב" זו בעצם הגישה שאומרת (you ain't gonna need it (YAGNI - להשקיע פחות.

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

צעד שני הוא לשאול את השאלות הבאות:
  • עד כמה חלק הקוד המדובר הוא קריטי להצלחת הביזנס? עד כמה הוא מורכב ואנו סובלים מתחזוקתו? - ככל שהוא יותר מורכב ורגיש, נכון להשקיע יותר משאבים ולצמצם סיכונים.
    • מצד שני: ככל שמדובר באיזור קריטי למערכת - סביר יותר להניח שנחזור ונתקן / נשפר את ה "Retry" אם הוא לא מספיק טוב.
  • עד כמה המערכת צעירה / בוגרת? ככל שהמערכת בוגרת - יש טעם להשקיע יותר בכדי לשמר את האיכות והיציבות שהיא כבר השיגה. במערכת ממש חדשה - תקלה אקראית עשויה להיבלע בזרם בעיות גדולות וחמורות יותר. בעיות גדולות יותר יכולות להיות באגים, אך לא פחות מכך הן יכולות להיות פיצ'רים חשובים שחסרים ללקוחות, או אי-התאמה בסיסית לצרכים העסקיים.
  • עד כמה ההתנהגות שאנו יוצרים היא צפויה? (Principle of least astonishment) - ככל שהפתרון ה"אופטימי" שלנו הוא מפתיע ולא-צפוי (יחסית להתנהגות הסדירה והמקובלת של המערכת) - כך גדלה הסבירות שהוא יישבר עם הזמן, או יגרום לבעיות שיהיה קשה לצפות. 
  • עד כמה קל לדעת אם ה Retry עובד? אם יש לנו שטף של אירועים מבוקרים ומנוטרים - קל יותר להצדיק התנסות ב"פתרון מהיר ואופטימי". סיכוני אבטחה (אירועים נדירים יחסית, אך אולי עם השפעה חמורה), למשל - הם מקום פחות מומלץ לעשות בו ניסויים.
    • הוספת Alerts למצבים בלתי צפויים סביב הפתרון - עשוי להיות מעשה נבון.
  • אל תנהגו בטיפשות. אל תעשו Retry על פעולות שאינן idempotent - כלומר פעולות שהפעלה שלהן מספר פעמים תסתיים בתוצאה שונה מהפעלה בודדת (למשל: חיוב של כרטיס אשראי).

בקיצור: It depends.


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




---

[א] מונח המתאר את ה"חובות הטכנולוגיים במוצר". כמו חובות פיננסיים, נבון לעתים לקחת אותם - אבל אם הם תופחים הריבית היא בלתי-נשלטת ועלולה להוביל לאסון.
במילה Debt, אגב, לא מבטאים את ה b. זה נשמע כמו "Technical Det".




3 תגובות:

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

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

    השבמחק