תזמון תוכניות מרובות בקליק אחד — מנצחים את מגבלת ה-15 דקות של AWS Lambda
מפעיל ערוץ FAST מתזמן חודש שלם של תוכניות ולוחץ על Deploy פעם אחת. מאחורי לחיצה יחידה זו, מאות תוכניות הופכות לאלפי פעולות תזמון של MediaLive שחייבות לנחות ב-AWS לפי הסדר, הכל בתוך מגבלת ביצוע של חמש עשרה דקות של Lambda — וכל כשל באמצע משאיר ערוץ חי עם חורים בו. הנה איך הפכנו את הפריסה בלחיצה אחת של רשימות השמעה גדולות לאמינה, ומדוע הצעד הבא הוא סביבת ריצה שונה, ולא Lambda גדולה יותר.
סקירה מהירה
| היבט | פרטים |
|---|---|
| Runtime | AWS Lambda (single invocation), 615s backend timeout |
| Output target | MediaLive BatchUpdateScheduleCommand |
| Batching | עד 200 פעולות MediaLive לבקשה — כ-25 תוכניות בפועל |
| Per-program actions | ~7–8 (החלפת קלט + 4 סימני מים לכל רנדור + 2 SCTE-35), יותר עם הפסקות פרסומות |
| Fallback | ניסיון חוזר לכל תוכנית על כל דחיית אצווה |
| Observed | 195 תוכניות נפרסו בכ-44 שניות (מדידה מקומית) |
| Documented target | 360 תוכניות ב-≤90 שניות |
| Scale-out path | חלוקה לפרוסות באמצעות Step Functions עבור רשימות השמעה של יותר מחודש — מתוכנן, לא הוטמע |
האתגר
פריסת רשימת השמעה אינה פעולת כתיבה — זוהי אורקסטרציה. עבור כל תוכנית שתזמן המפעיל, MediaLive צריכה לדעת בדיוק מתי להחליף קלטים, מתי להפעיל את סימן המים עבור כל רנדור, מתי להכניס נקודות ציון SCTE-35, ומתי לשלב פרסומות. הלחצים המבניים הם:
- ספירת הפעולות היא כפלית, לא חיבורית. כל תוכנית משחררת ~7–8 פעולות לפני הפסקות פרסומות — החלפת קלט אחת, ארבע הפעלות סימני מים (אחת לכל רנדור מכיוון ש-StaticImageOutputActivate משתמש בתיאור קואורדינטות פיקסלים של פלט), ושני סמני SCTE-35. הפסקות פרסומות מוסיפות שלוש פעולות נוספות כל אחת. חודש של 360 תוכניות הוא בערך 2,800 פעולות על ה-wire.
- MediaLive אוכפת סדר לכל ערוץ. פעולות תזמון מעוגנות בזמן ומתייחסות זו לזו; לא ניתן לבצע כתיבה במקביל לאותו ערוץ מבלי שה-API ידחה אותן כסותרות.
- ל-AWS Lambda יש מגבלת 15 דקות קשיחה. לא מגבלה רכה, לא תצורה. האורקסטרטור חייב לסיים בתוך מגבלה זו או שהערוץ יסתיים כפרוס למחצה.
- כשל חלקי אינו מקובל מבחינה תפעולית. אם תוכנית 174 מתוך 360 נכשלת ומפסיקה את ההרצה, למפעיל אין תצוגת diff של מה נחת ומה לא. הערוץ עולה לאוויר עם פערים; הצופים רואים מסך שחור היכן שציפו לתוכן.
- הגרסה הראשונה שלחה BatchUpdateSchedule אחד לכל תוכנית עם השהיה של 200ms בין תוכניות. זה ~2.5 שניות של זמן ריצה לכל תוכנית. עם 360 תוכניות, כבר חרגת ממגבלת Lambda לפני ש-MediaLive ביצעה עבודה ממשית כלשהי.
העבודה, אם כן, אינה לכתוב מהר יותר. היא לכתוב פחות פעמים, לשרוד כשל חלקי, ולהישאר בתוך קריאה אחת — מבלי לאבד את הבטחות הסדר ש-MediaLive דורשת.
מדוע גישות קיימות נכשלות
הפתרונות הברורים כולם נכשלים מסיבות מבניות, לא בעיות כוונון.
- "פשוט הגדל את ה-Lambda timeout." אי אפשר. חמש עשרה דקות היא מגבלה קשיחה שהוטלה על ידי AWS על ביצוע Lambda; זה לא כפתור בקונסולה. גם אם היה, העלות לכל תוכנית גדלה עם הקטלוג — רכישת זמן ריצה נוסף רק דוחה את המגבלה הבאה.
- "עבור ל-ECS Fargate או EC2." שקלנו זאת ודחינו. קונטיינרים ארוכי טווח אומרים שאנו אחראים על סביבת הריצה: בדיקות תקינות, קנה מידה אוטומטי, יתרונות וחסרונות של cold-start לעומת warm-pool, הגדרת IAM, ורוטציה של כוננות עבור שירות שרץ בפרצים. Lambda מספקת לנו בידוד לכל קריאה ועלות אפסית בזמן סרק עבור עומס עבודה פרצי במהותו. לא היינו מוכנים לוותר על זה כדי לתקן צוואר בקבוק אחד.
- "בצע מקבילות לכתיבות ב-MediaLive." MediaLive מבצעת סידור סדרתי של עדכוני לוח זמנים לכל ערוץ. קריאות BatchUpdateSchedule מקבילות לאותו ערוץ מתחרות על ציר הזמן של הפעולות ונדחות. המקביליות הלגיטימית היחידה היא בתוך אצווה, לא על פני אצוות.
- "פשוט תן לזה לקרוס ואפשר למפעיל לנסות שוב." זוהי האפשרות הגרועה ביותר. כאשר פריסה מופסקת בתוכנית N, הערוץ נמצא במצב שאף אחד לא יכול לתאר מממשק המשתמש. מפעילים לא מקבלים diff; הם מקבלים קופסה שחורה וערוץ חי עם חורים בו. המערכת חייבת לסיים הכל או לסיים תוצאות חלקיות עם סטטוס לכל תוכנית שהמפעיל יכול לפעול לפיו.
המנוף שנותר לנו היה צורת העבודה עצמה: כתיבות מעטות יותר וגדולות יותר, שבוצעו באופן סדרתי, עם גיבוי המדרדר לגרנולריות ברמת שורה רק כאשר אצווה נדחית.
הפתרון שלנו
הזז את העלות מ-Lambda לפני שהלולאה מתחילה, קבץ את הכתיבות של MediaLive בתוך הלולאה, ודרדר להגשה לפי תוכנית רק כאשר אצווה נכשלת. שלושה רעיונות, לפי הסדר הזה — והחלטה מכוונת שמגבלת ה-15 דקות מספקת עבור גדלי הקטלוג שאנו פורסים בפועל כיום. כאשר רשימות השמעה יגדלו מעבר לקריאה בודדת, התשובה אינה Lambda גדולה יותר; זוהי סביבת ריצה שונה.

ארכיטקטורה
- NestJS backend (schedule.service.ts) — מבצע חימום מוקדם לפריסה: נסיעה הלוך ושוב אחת ל-Mongo עבור כל הסרטונים הייחודיים, אחת עבור כל סמני הפרסומות, ולאחר מכן ffprobe מקבילי על פני כתובות URL ייחודיות של וידאו עם התוצאות שמורות במטמון עבור לולאת ההעשרה.
- Lambda orchestrator (fastChannel-lambda-fun/index.js) — אחראי על לולאת ה-batching, ה-throttle לכל אצווה, ה-fallback לכל תוכנית, וניקוי היתומים.
- MediaLive BatchUpdateScheduleCommand — משטח הכתיבה היחיד. כל פעולה — החלפת קלט, סימן מים, SCTE-35, שילוב פרסומות — עוברת דרכו.
- MongoDB — מקור האמת עבור תוכניות, סרטונים וסמני פרסומות; מעולם לא נשלף ממנו מידע בתוך הלולאה הפנימית.
- scheduleResults map — מפת סטטוס scheduled / error לכל תוכנית המוחזרת ל-backend כך שהמפעיל יקבל diff, לא stack trace.
החלטות הנדסיות מרכזיות
1. חמם מראש את הדברים האיטיים לפני שהלולאה רצה. הקוד המקורי ביצע בדיקת Mongo לכל לוח זמנים וקריאת ffprobe לכל לוח זמנים בתוך לולאת ההעשרה — N+1 קלאסי ששולם פעמיים. ה-backend הנוכחי מאחזר בצובר כל סרטון ייחודי וסמן פרסומת בנסיעה הלוך ושוב אחת, ולאחר מכן מריץ ffprobe במקביל על פני כתובות URL ייחודיות ושומר את התוצאה במטמון רזולוציה. הלולאה הפנימית הופכת לפגיעת מטמון. השהיית ffprobe משולמת פעם אחת לכל URL ייחודי, לא בסדרה בלולאה.
2. קבץ כתיבות MediaLive לאצוות של כ-25. בתוך האורקסטרטור, פעולות מצטברות לפקודה אחת של BatchUpdateScheduleCommand עד ש-200 פעולות נכנסות לתור או שמגיעים לתוכנית האחרונה. מכיוון שכל תוכנית מייצרת ~7–8 פעולות, אצוות נוחתות באופן טבעי סביב 25 תוכניות כל אחת. מגבלת 200 הפעולות נבחרה באופן שמרני כדי להישאר הרבה מתחת למגבלות העומס לכל בקשה של MediaLive ולהפחית את הסיכוי שאצווה תידחה בגלל גודל — מספיק גדולה כדי למתן את עלות הרשת, מספיק קטנה כדי להפוך את ה-fallback לכל תוכנית (החלטה הבאה) לזול כאשר צריך להריץ אותו. נסיעה הלוך ושוב אחת ברשת מחליפה עשרים וחמש. ה-200ms throttle שבעבר ישב בין כל תוכנית יושב כעת בין כל אצווה.
3. עיבוד אצוות למהירות, גיבוי לנכונות. קיבוץ בטוח רק אם תוכנית אחת גרועה לא מרעילה את עשרים וארבע האחרות באצווה שלה. כאשר submitProgramBatch זורק שגיאה, ה-catch מפעיל את retryBatchAsIndividuals, אשר מגיש מחדש כל תוכנית באצווה שנכשלה כ- BatchUpdateScheduleCommand משלה, מתעד status: 'scheduled' או status: 'error' לכל תוכנית, ממתין 200ms בין ניסיונות, ומעגן מחדש את ציר הזמן של הפעולות לאחר כל הצלחה חלקית. הנתיב המהיר הוא באצווה. נתיב השחזור הוא גרנולרי. המפעיל מקבל בכל מקרה diff לכל תוכנית.
4. נקה יתומים בסוף, אל תמנע אותם במהלך הריצה. sweepIncompleteProgramGroups רץ פעם אחת בסוף הפריסה ומסיר כל קבוצת פעולות שלא הגיעה למצב סיום נקי. אנו במכוון לא מנסים לשמור על עקביות פנימית של הערוץ במהלך הלולאה — זה ידרוש נתיב rollback שבעצמו צריך להתאים לתקציב של 15 דקות. הניקוי הוא סריקה בודדת, לא טרנזקציה.
5. אל תגדיל את ה-Lambda; החלף אותה כשהקטלוג יגדל. עבור כל מה שאנו פורסים כיום, נתיב הקריאה הבודדת מסתיים היטב בתוך התקציב. ההרחבה הכנה היא חלוקה לפרוסות באמצעות Step Functions — חלק את רשימת ההשמעה, הרץ פרוסות כמכונות מצבים מקבילות, הרכב מחדש. זהו הנתיב לרשימות השמעה של יותר מחודש ו-6 חודשים. הוא מתוכנן, לא הוטמע. לקרוא לזה הצעד הבא מועיל יותר מאשר להעמיד פנים שהוא כבר רץ.
תוצאות
- בבדיקות פנימיות, פריסה של 195 תוכניות הושלמה בכ-44 שניות — ירידה מהבסיס של דקות רבות, תוכנית אחת בכל פעם.
- היעד המתועד לרשימת השמעה של 360 תוכניות (כחודש) הוא ≤90 שניות, בנוחות בתוך מגבלת הזמן הקצוב של 615 שניות ב-backend ומגבלת ה-15 דקות של Lambda.
- תוכנית אחת גרועה באצווה כבר לא מפסיקה את 24 האחרות. המפעיל מקבל מפת סטטוס לכל תוכנית בחזרה מכל פריסה.
- החלקים האיטיים של הבקשה — בדיקות Mongo ו-ffprobe — משולמים פעם אחת לכל משאב ייחודי, לא פעם אחת לכל ערך בלוח הזמנים.
- מגבלת ה-15 דקות הפסיקה להיות הגורם המגביל עבור גדלי הקטלוג שאנו שולחים בפועל. כאשר היא תהפוך לכזו שוב, חלוקה לפרוסות באמצעות Step Functions היא התשובה, לא Lambda גדולה יותר.
מערך טכנולוגי: AWS Lambda · AWS MediaLive · NestJS · MongoDB · Node 18 · ffprobe · TypeScript · AWS SDK v3

