איך מעצבים מתזמן משימות - מקרה בוחן¶
Cron בלינוקס עושה עבודה נהדרת על שרת יחיד - הוא מריץ פקודה בזמן קבוע, וזהו. אבל ברגע שהמערכת שלכם רצה על עשרות שרתים, ואתם צריכים להריץ מיליוני משימות מתוזמנות שונות - חלקן חד פעמיות, חלקן חוזרות - Cron פשוט לא בנוי לזה. מתזמן משימות מבוזר הוא בדיוק הפתרון לבעיה הזאת, והוא מקרה בוחן טוב להבנת אמינות במערכות מבוזרות.
שלב 1 - מה בעצם צריך המתזמן לתמוך בו¶
מתזמן משימות אמיתי צריך לתמוך בכמה סוגי משימות - משימה חד פעמית שצריכה לרוץ בזמן מסוים בעתיד, כמו שליחת תזכורת שעה לפני פגישה, ומשימה חוזרת שרצה כל פרק זמן קבוע, כמו חישוב דוח יומי בחצות. לכל משימה יש גם פרטי ביצוע - איזה קוד להריץ, עם אילו פרמטרים, ומה לעשות אם הביצוע נכשל.
שלב 2 - האחסון - לב המערכת¶
הליבה של המתזמן היא מסד נתונים ששומר את כל המשימות המתוכננות, עם שדה שמסמן מתי בדיוק צריך להריץ כל אחת. שרתי הביצוע סורקים כל פרק זמן קצר - נניח כל דקה - ומחפשים משימות שהגיע זמנן. כדי שזה יעבוד בקנה מידה גדול בלי לסרוק טבלה ענקית בכל דקה, בדרך כלל בונים אינדקס לפי זמן הביצוע המתוכנן, כדי שהשאילתה "איזה משימות צריכות לרוץ עכשיו" תהיה מהירה גם עם מיליוני רשומות.
שלב 3 - הבעיה המרכזית - הרצה כפולה¶
זו הבעיה שהופכת מתזמן מבוזר למעניין באמת. אם יש כמה שרתי ביצוע במקביל, וכולם סורקים את אותה טבלת משימות, מה מונע משני שרתים לתפוס את אותה משימה בו זמנית ולהריץ אותה פעמיים? הפתרון הנפוץ הוא נעילה - כאשר שרת "תופס" משימה להרצה, הוא מסמן אותה כתפוסה עם timestamp, בפעולה אטומית שמונעת משרת אחר לתפוס את אותה משימה בו זמנית. מסדי נתונים רבים תומכים בפעולת "עדכן רק אם עדיין לא תפוס" בצורה אטומית בדיוק בשביל המצב הזה.
שלב 4 - מה קורה כששרת קורס באמצע ביצוע¶
נניח ששרת תפס משימה, ואז קרס באמצע ההרצה - איך המערכת יודעת שהמשימה תקועה ולא הושלמה? הפתרון הוא timeout על הנעילה - אם שרת תפס משימה אבל לא סימן אותה כהושלמה תוך פרק זמן סביר, המערכת מניחה שהוא נכשל, משחררת את הנעילה, ומאפשרת לשרת אחר לתפוס את המשימה ולנסות שוב. זה אומר שכל משימה צריכה להיות בנויה כך שאפשר להריץ אותה שוב בבטחה אם הניסיון הקודם נכשל באמצע - בדיוק אותו עיקרון של אידמפוטנטיות שחשוב גם בעיצוב API.
שלב 5 - סדר עדיפויות ועומס לא אחיד¶
לא כל המשימות שוות. מערכת אמיתית צריכה לתמוך בעדיפויות - משימה קריטית כמו עיבוד תשלום צריכה לרוץ לפני משימת ניקוי לוגים שיכולה לחכות. וכשיש הרבה משימות שמתוזמנות לאותו רגע בדיוק - למשל אלף משתמשים שקבעו תזכורת לאותה שעה עגולה - צריך לפזר את העומס במקום לנסות להריץ את כולן באותה שנייה בדיוק, לרוב על ידי הוספת דיליי אקראי קטן.
שלב 6 - משימות חוזרות ותזמון הבא¶
עבור משימה חוזרת, ברגע שהיא מסתיימת בהצלחה, המתזמן צריך לחשב מיד מתי הריצה הבאה שלה תהיה, ולעדכן את השדה בהתאם - בין אם זה "כל שעה" הפשוט, או ביטוי מורכב יותר בסגנון cron. חשוב לוודא שהחישוב הזה קורה בצורה עקבית, כדי שמשימה חוזרת לא "תיעלם" בטעות אם משהו נכשל בדיוק ברגע חישוב התזמון הבא.
מתזמן משימות ממחיש נקודה חשובה בעיצוב מערכות מבוזרות - שהחלק הקשה כמעט אף פעם הוא לא הלוגיקה העסקית עצמה, אלא ההתמודדות עם כשלים חלקיים. בקורס ארכיטקטורת תוכנה שלנו עוברים על מקרי בוחן כאלה ומתרגלים בדיוק את סוג החשיבה הזה.
בניתם מתזמן משימות ונתקלתם בבעיה של הרצה כפולה? הצטרפו לקהילה בדיסקורד ותספרו איך פתרתם.
לסיכום¶
מתזמן משימות מבוזר צריך אחסון מהיר לחיפוש משימות שהגיע זמנן, מנגנון נעילה שמונע הרצה כפולה על ידי כמה שרתים, וטיפול נכון במקרה של קריסה באמצע ביצוע. הדרישה שכל משימה תהיה בטוחה להרצה חוזרת היא לא פרט טכני שולי - היא התנאי הבסיסי שמאפשר למערכת כזאת בכלל להיות אמינה.