2016-03-31

למה scheduler בצד-שרת - מפחיד אותי? (כתב אורח)

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

איך מתחילה שרשרת פרוצדורלית שכזו? ישנן כמה אפשרויות:
  1. פונקציית ה main והדומות לה - כלומר: קוד שיתחיל להתבצע מיד בעליית האפליקציה.
    אם מדובר בשגרת אתחול של תהליכים אחרים, או עצם ה business logic ש"יחיה" לאורך כל חיי האפליקציה.
  2. טיפול ב HTTP request (או פרוטוקול אחר כלשהו) - קריאה מבחוץ, ביצוע משימה כל שהיא, עם או בלי ערך מוחזר.
  3. האזנה לשירות כלשהו ותגובה למידע שמגיע ממנו - לדוגמה: אפליקציה שמאזינה על עסקאות מט"ח ומבצעת לוגיקה מוגדרת מראש על כל קבלת עסקה שכזו.
  4. שימוש ב scheduler, פנימי או חיצוני לאפליקציה - למשל: cron job שבשעה מסוימת, או ביום מסוים בשבוע או כל פרק זמן מוגדר מראש יתחיל תהליך כלשהו.

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

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

לפני שנסקור את הבעיות האפשריות, בואו נזכר לאילו צרכים אנו לרוב משתמשים ב Scheduler.
הנה תיאור של כמה משימות שהוכנסו ל scheduler שנתקלתי בהן בשנים האחרונות (בחברות שונות ובצוותים שונים):
  1. תחזוקת ה DB (מחיקה של רשומות שפג תוקפן וכד').
  2. שליחת דוחות יומיים.
  3. פנייה לשירות חיצוני כדי לעדכן מידע סטטי של המערכת פעם ביום.
  4. ביצוע סיכום של מסגרת זמן מסוימת מתוך דאגה לביצועים.
    לדוגמה: להתעורר כל דקה ועל סמך מינימום ומקסימום של שער היורו-דולר לבצע פעולה מסוימת (במקום להגיב לכל שינוי ברמת השנייה ואף פחות מזה).
  5. הפעלת לוגיקה עסקית שקשורה לשעה מסוימת - לדוגמה (מומצאת לחלוטין) כל יום ראשון בשעה 9:00 פג תוקפן של אופציות מסוימות ויש צורך לעדכן זאת במערכת.


אז מה רע כל כך בשימוש ב- scheduler? לכאורה מדובר בכלי חשוב והכרחי שמספק פתרון קל ונוח לבעיות מסוימות.

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

  1. בעיית איזור הזמן - במידה והגדרתם שעה מסוימת ביממה להתחלת התהליך, לאיזה איזור זמן התכוונתם? מן הסתם לזמן המכונה. אבל אולי ה Production שלכם רץ באמזון ב EST לעומת שרתי ה release candidate שנבדקים לוקאלית ב GMT + 2? מה קורה במעבר משעון קיץ לחורף? בקיצור - לא פשוט.

    GMT או BST? - זו השאלה! מקור: fotografiaslubnakielce.wordpress.com

  2. בעיית הגמישות - מישהו חכם פעם אמר "כשיש לך פטיש כל בעיה נראית לך כמו מסמר", ולפעמים נדמה שבגלל קלות השימוש ב-scheduler אנו נוטים להשתמש בו למגוון רחב מדי של משימות. בשונה מבקשה מהרשת לקבל webpage, בה מדובר על תהליך מוגדר היטב וסגור שבדרך כלל מסתיים בזמן קצר - אנחנו נוטים להשתמש ב scheduler לבצע משימות שיכולות להיות גם ארוכות ומורכבות.
  3. בעיית concurrency (או לחילופין:latency) - איך המערכת תתנהג כאשר הגיע כבר הזמן לביצוע הבא של ה scheduler והביצוע הקודם עוד לא סיים את עבודתו? במקרה כזה ישנן שתי גישות (המגובות בפ'יצרים מתאימים בכל מימוש של scheduler שתבחרו):
    1. האחת: לא מתחילים את הפעולה המתוזמנת הבאה עד שהסתיימה הפעולה הקודמת.
      לדוגמה: אין טעם להריץ שוב את תהליך הניקוי של ה DB כאשר התהליך המתוזמן הקודם עוד לא הסתיים.
      גישה זו היא בטוחה יותר, אך לא תמיד היא אפשרית: לפעמים צריך לבצע פעולות מסוימות מיד בתחילת כל דקה-שעה-יום. בנוסף במקרה שהמערכת נכנסת למצב של latency יהיה קשה לפעמים להבין עד כמה המערכת בפיגור וכמה פספסנו.
      אנו עלולים להניח שמכיון שתהליך מסויים מתחיל בשבע ואחר בשמונה - אזי הראשון מן הסתם יסיים קודם. במקרה של מערכת שנכנסת לפיגורים - כל ההנחות הללו קורסות.
    2. הגישה השניה: הפעלת הפעולה המתוזמנת בלי קשר למה קרה לפעולה (או פעולות) מתוזמנת קודמת.
      כאן הסכנה ברורה יותר: במקרה קיצון המערכת תתנפח ותקרוס (בגלל בעיית זיכרון או מיצוי כמות ה - threads הזמינים), אבל גם לא במקרה הקיצוני ביותר, אנחנו נכנסים לבעיית concurrency קלאסית ועכשיו כל התהליך צריך להיות מותאם לביצוע מקבילי (concurrent).
      זהו לא אתגר יוצא דופן, אבל מניסיוני לא תמיד זוכרים זאת בשלב הדיזיין.
  4. בעיית ה"פספוס" - תזמונים שעשויים להתפספס
    למשל: "כל יום בשבע בערב נשלח דוח יומי ללקוחות", רק שיום אחד הסרבר נכנס למצב של out of memory בשעה 18:58. למרות שזיהינו זאת בזמן והריסטרט עבד חלק - פספסנו את חלון הזמנים של הדו"ח! מה קורה עכשיו? מי אחראי להשלים את החור הזה? לא טרוויאלי בכלל!
    מה קורה כאשר התהליך המתוזמן נקטע באמצע? מן הסתם עדיף כבר לממש מערכת של משימות שיש לבצע והיא persistent.
  5. בעיית "איבוד השליטה" - כמה schedulers אפשר להגדיר למערכת אחת? כמה שרוצים!
    האם יש מקום אחד בו הכל מנוהל? אם לא אז כנראה שויהיה לכם קשה מאד להחליט האם תהליך אחד עלול להשפיע על תהליך אחר במידה וירוצו במקרה במקביל.
    כמה threads הוספנו למערכת בעקבות כל ה schedulers שהגדרנו? שוב יהיה קשה מאד לדעת.
    היבט נוסף שקשור לשליטה על המערכת הוא שתכנון של Scheduler שמתעורר כל שעה לבצע משימה מסוימת, עלול להתבסס על ההנחה שה Scheduler ביצע את אותה המשימה בעבר ויבצע אותה בעתיד. מניסיוני, ההנחות הללו אינן תמיד תקפות (תרגיל חשיבה: איך ניתן לדעת ש scheduler לא הופעל בזמן? איך מנטרים דבר כזה?) ועלולות לגרום לכשלים במקרי קצה שקשה לצפותם מראש.
  6. בעיית ה - Testing - מכיוון שהתזמון מתבסס על "זמן מכונה אמיתי", בין אם זו נקודה ספציפית על לוח השנה ובין אם מדובר על טיימר כלשהו - קשה לסמלץ בסביבת הבדיקות תזמון מדויק כמו זה שיהיה בפועל ב production. התוצאה: או ש"מלכלכים" את סביבת הבדיקות בכדי לשחזר את ההתנהגות של סביבת הproduction- או שמוותרים על סימלוץ מדויק של סביבת ה-production.
    אני מודה שבעיה זו היא פחות חמורה, ויש לה פתרונות יפים - אך עדיין דרושה כאן קצת אקסטרה מחשבה.

טוב אז מה אני מציע? לא להשתמש ב Schedulers בכלל?

האינסטינקט שלי הוא קודם כל לנסות ולהימנע מ scheduling במידת שאפשר: גם בגלל הבעיות שמניתי לעיל וגם בגלל התחושה הכללית כלפי הכלי הזה - משהו ב-scheduler מרגיש לי לא טבעי לסגנון ה java server side שאני מכיר.

הניסיון להימנע משימוש ב scheduler בדרך כלל הוליד פתרונות טובים יותר: כך שהגעתי למסקנה שהרבה פעמים ה scheduler הוא רק כלי להימנעות מדיזיין נכון של המערכת. לדוגמה:
  1. במקום להשתמש ב-scheduler כדי לשפר ביצועים גילינו שאין בכלל בעיית ביצועים. למה ניסינו מראש ללכת לכיון של שימוש ב-scheduler? אולי כדי להימנע מהצורך להתחייב על תוצאות ניתוח הביצועים של המערכת. בפועל הקדשנו לנושא עוד מעט מחשבה והגענו לפתרון נקי ובטוח הרבה יותר.
  2. במקום לנקות את ה DB פעם בדקה\שעה\יום - לבצע את הניקיון Lazily, רק כשניגשים ל DB. לפעמים זה מספיק טוב וגם חוסך עבודה מיותרת.
לפעמים אין ברירה וצריך להשתמש ב-scheduler. במקרה כזה אני תמיד מעדיף שה scheduler רק יכניס את ה"משימה" לתור. רצוי שתור זה יהיה Persistent, ותהליך אחר (יחיד או thread pool) ישלוף משם משימה אחר משימה. כך גם יש תיעוד, וגם חמקנו מרוב הבעיות שצוינו לעיל.
במידה וגם זה לא מספיק טוב וחייבים להשתמש ב scheduler ויהי מה - אז פוסט זה יכול להוות תזכורת לרשימה של דברים שיש לקחת בחשבון ושכדאי להיזהר מהם.


יניב חדד
Software Engineer at Citi TLV Innovation Lab


4 תגובות:

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

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

      מחק
  2. אנונימי7/4/16 10:41

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

    השבמחק
  3. למי שעובד בעולם ה. net יש ספריה מעולה שנטתנת מענה לרוב (אם לא לכל) הבעיות שהצגת.
    הספריה נקראת Hangfire - מומלצת בחום!

    השבמחק