יום שבת, 29 ביולי 2017

שלום, AWS Lambda! (חלק ב')

בפוסט הקודם שלום, AWS Lambda! הצגנו את למבדה, ובעיקר כיצד ה main successful flow עובד.
לא הספקנו כמעט ולהיכנס לעומק / להתנהגויות השונות במודל של למבדה.

זה מה שננסה לעשות בפוסט הזה.


The Container Model


כאשר מתבצעת הפעלה (invocation) ראשונה של הפונקציה שלנו, למבדה מפעילה "Container" שבו תרוץ הפונקציה.
מהו ה container הזה? אין תיעוד מדויק, אך מדובר בסופו של דבר בסביבת ההרצה של הפונקציה, ע"פ ה settings שהגדרנו.

הרצת ה container אורכת זמן מסוים, נאמר כ 100-200ms - מה שנקרא cold start.
על הזמן הזה - המשתמש מחויב.
  • אם הפונקציה רצה עשר פעמים בדקה - אזי זו הפעלה אחת לכל גרסה / עותק חדש של פונקציה (הסבר בהמשך). הבדל זניח.
  • אם הפונקציה רצה פעם בעשר דקות, וזמן ה execution שלה קצר - ה cold start עשוי להכפיל את העלויות. 
הערה: העניין הכספי כנראה לא יגרום לכם לעזוב את השימוש בלמבדה, מטרת הדיון היא בעיקר להסביר את אופן העבודה של למבדה.

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

בכל הפעלה עוקבת של הפונקציה, למבדה תנסה לעשות שימוש חוזר בקונטיינר קיים. היא לא תעשה שימוש חוזר במקרים הבאים:
  • אין קונטיינר זמין - למשל "הרגו" אותו לא מזמן...
  • הבקשה הגיעה ל Availability Zone A, אבל הקונטיינר נמצא ב Availability Zone אחר.
  • יש מספר הרצות מקביל של הפונקציה: אם מספר ההרצות בשניה עלה מ 10 ל 100, למבדה תתחיל להריץ עוד ועוד קונטיינרים במקביל - עד שהצורך יסופק.
    כלומר: ייתכן ויש כבר 6 קונטיינרים עובדים, אך במקום לחכות שקונטיינר יתפנה - למבדה החליטה להפעיל קונטיינר שביעי.
    • בעת כתיבת הפוסט, אמזון לא תריץ יותר מ 1000 קונטיינרים במקביל - אלא אם ביקשתם.
    • למבדה תקבע את כמות המקביליות כתלות במקור ה event:
      • אם מדובר בהודעות ב Queue (או "stream-based-event") - המקביליות תהיה ע"פ כמות ה shards של queue.
      • כאשר מדובר בקריאות ישירות (למשל HTTP) - המקביליות תהיה מספר הקריאות בשניה * כמה שניות לוקח לפונקציה לרוץ בממוצע.
    • בכל מקרה, למבדה היא לא קסם. גם אם יש spike "מטורף" - אמזון לא תפתח יותר מ 500 קונטיינרים חדשים בדקה. אפשר לקרוא עוד פרטים על המדיניות כאן.
  • סיבה אחרת לא מתועדת / שאני לא מכיר.

זמני ה Cold Start תלויים ב:
  • כמובן: הקוד שלכם. אם אתם משתמשים ב ORM, או ספריות שדורשות אתחול משמעותי - הזמן הזה ישולם ב Cold Start.
  • כמות הזיכרון שהוקצתה לפונקציה (=> CPU). יותר זיכרון - אתחול מהיר יותר.
  • כמות הקוד שנטען ב ZIP, ספריות וכיוצ"ב: פחות קוד - אתחול מהיר יותר.
  • קוד ב node.js ופייטון יאותחל מהר יותר מקוד ג'אווה או #C.

זמני ההרצה של 4 עותקים זהים של פונקצית למבדה. הפונקציה memInfo0 נקראת בצורה תדירה - ולכן כמעט ואינה מושפעת מ cold start, לעומת הפונקציות האחרות שחוות cold start כמעט כל פעם: כלל שהפונקציה תדירה פחות - הסיכוי ל cold start גדל. מקור: New Relic.

למרות החשש האפשרי מ cold start על הביצועים - ההשפעה של ה cold start על משתמש הקצה היא לרוב קטנה למדי. הנה רכיבים טיפוסיים שגם להם אנו ממתינים בהפעלה של פונקציית למבדה:
  • network latency - עוד כ 100ms או יותר (?), אלא אם אתם קוראים לפונקציה באותו ה region - ואז ה latency הוא מילישניות בודדות.
  • זמני הריצה של ה API Gateway - כ 30 עד 150 מילישניות (ע"פ נתונים של New Relic).
    • הקמת חיבור מאובטח (TLS/SSL) - עשוי לארוך עוד 50-200 מילישניות.

דיי פופולארי היום לדבר על טכניקות ל"שמירת ה container חי" והימנעות מ Cold Start.
בשקלול של כל הרכיבים, נראה ש"הימנעות מ Cold Start" היא התעסקות דיי מיותרת - המשקל של הרכיבים האחרים הוא גדול יותר. לפחות, ברוב הגדול של המקרים.


Container reuse

אם לפונקציה שלכם יש קצב הפעלה בינוני עד גבוה, רבות מההפעלות יתרחשו על container קיים.
זה אומר ש:
  • הגדרות "סטטיות" שנעשו מחוץ ל handler function - עדיין תקפות: חיבור לבסיסי נתונים / רדיס וכו'.
  • Cache ששמרתם בזיכרון - עדיין מלא.
  • דברים שנרשמו לדיסק עדיין שם (יש לכם עד חצי ג'יגה אכסון בתיקיה tmp/)
זה פוטנציאל לשיפור ביצועים משמעותי.
כמובן, שלעולם אי אפשר להסתמך על container reuse: אם הוא יצא - אפשר לנצל אותו, אם לא אז לא. עליכם לכתוב את הקוד בהתאם.

כמו כן, חשוב להבין את התנהגות ה cache בצורה נכונה: אם יש מקביליות גבוהה (concurrent invocation), אזי יש כמה  containers שכל אחד עם cache ב state שונה. חשוב לקחת את זה בחשבון. אם חשוב לכם cache אחיד - פשוט השתמשו ברדיס.



Throttling: a process responsible for regulating the rate at which application processing is conducted
מקור: Robyn


Throttling and retries


כאשר יש הרבה יותר בקשות הרצה להפעלה של פונקציות מאשר containers - למבדה תגדיל את מספר ה containers הרצים.
כאשר יש מעט יותר בקשות הרצה להפעלה של פונקציות מאשר containers - למבדה תבצע throttling. כלומר: תמתין עם הבקשה עוד קצת - ואז תפעיל אותה על גבי ה containers הקיימים.
  • עבור מקורות מסוג Queue (רשמית: "stream-based events") - למבדה תבצע retry לעד. עד שהבקשות מטופלות.
  • עבור מקור קריאה אסינכרונית - למבדה תבצע retry בטווח של עד 6 שעות מרגע הקריאה, עם המתנה בין ניסיון לניסיון, מה שאמור ליצר סיכוי נמוך מאוד למצב של הודעה שלא טופלה.
  • עבור קריאות סינכרוניות (ה event source ממתין לתשובה) - למבדה תחזיר לאפליקציה HTTP status code של 429 - ותצפה שהאפליקציה תבצע retry בעצמה.
Throttling הוא מצב תקין וצפוי בלמבדה - מצב שעליכם לקחת בחשבון! הוא יקרה.

פונקציית הלמבדה שלכם כמובן יכולה להחזיר שגיאות. כאלו שחזרו מלבדה עצמה או מהקוד שלכם.
בלמבדה יש הפרדה בין 2 סוגי Exceptions:
  • Synchronous Exception - כזו שתוצאתה חוזרת למי שקרא לפונקציה.
  • Asynchronous Exception - כזו שתוצאתה מגיעה ל cloudwatch. לקוח הפונקציה לא מודע לדבר.

כשאר הפעלה של פונקציה נתקלה ב Synchronous Exception (שנגרמה ע"י הקוד שלכם או ע"י למבדה) - למבדה תבצע retry ע"פ המדיניות הבאה:
  • עבור מקורות מסוג Queue (רשמית: "stream-based events") - למבדה תבצע retry כל עוד ה data מבחינת ה queue הוא בתוקף.
  • עבור מקור קריאה אסינכרונית - למבדה תבצע retry עוד שתי פעמים, בד"כ בהפרש של כדקה, ואז תשלח את ההודעה ל Dead Letter Queue שהוגדר על ידכם על גבי SNS או SQA.
    • ליתר דיוק בדיקות הראו שאכן ב 99% מהפעמים יהיו 2 retries, אבל לעתים רחוקות יהיה רק retry אחד - ולפעמים גם יבוצעו עד שש retries. מקור.
  • עבור קריאות סינכרוניות (ה event source ממתין לתשובה) - הודעת השגיאה תחזור לאפליקציה, ובאחריותה לבצע retries.


חשוב לציין מגבלה שלא כתובה פה: בעת כתיבת הפוסט, ל API Gateway יש timeout קבוע של 30 שניות. אם יש לכם פונקציה שזמן הריצה שלה ארוך יותר - API Gateway יבצע timeout, בכל זאת.


בשלב הזה אתם אמורים להבין כמעט את כל המדדים שמסופקים ב Monitoring של למבדה:


המדד היחידי שלא הוסבר הוא ה iterator age שרלוונטי רק למקורות מסוג stream-based events.
הוא מציין כמה זמן ארך מרגע שפונקציית הלמבדה שלנו קיבלה batch של הודעות, ועד שסיימה לטפל בהודעה האחרונה. כלומר: מה ה latency המרבי של טיפול בהודעות ב batch, שהגיעו ממקור שכזה.


Lambda@Edge


שווה להזכיר את שירות ה Lambda@Edge ששוחרר לאחרונה, שירות המאפשר להריץ פונקציות למבדה (Nodejs בלבד, כרגע) על ה PoPs (כלומר: Point of Presence) של CloudFront.

לאמזון יש כ 14 regions בעולם עליהם אפשר להריץ את כל שירותי ה AWS, אבל יש לה קרוב ל 100 מיקומים בהם יש לה נוכחות - עבור שירות ה CDN שלה, הרי הוא CloudFront.

עבור פעולות פשוטות למדי, אם תוכלו להריץ את הקוד קרוב מאוד ללקוח שלכם - תוכלו לספק לו תשובה מהירה ביותר: ה latency ל"קוד" שלכם עשוי להיות עשרות מילי-שניות, ולא מאות מילי-שינות.
למבדה_על_הקצה (Lambda@Edge) תנהל עבורכם את שכפול הקוד לרחבי העולם - ותדאג שהלקוחות שלכם, יקבלו זמני תגובה משופרים.

את פונקציית הלמבדה_על_הקצה שלכם, אפשר לחבר לארבע נקודות-חיבור שונות בתהליך של cloudfront (בתרשים: החצים הירוקים והכתומים), ולבצע שינוי על המשאב שהתבקש.

מקור: אמזון


כמובן שבשירות כזה מאוד חשוב לספק זמני תגובה מהירים מאוד - מה שלא תמיד מתרחש בצמד למבדה + API Gateway.

ה API Gateway הוא מחוץ לתמונה, יצירת ה HTTP connection עם TLS/SSL - כבר מתבצע ע"י cloudfront, ולכן הרכיב היחידי שעלול לגרום לעיכובים (ולייתר את היתרון שבריצה על PoP) הוא זמן האתחול של ה container של למבדה. לא סתם התחילו ב nodejs כסביבת-ריצה.

ללמבדה_על_הקצה - יש מגבלות משלה (מקסימום 128MB זיכרון, זמני ריצה קצרים יותר, אין גישה לשירותים אחרים של אמזון, וכו')


שירותים נוסח למבדה_על_הקצה כבר היו קיימים (למשל: PubNub Blocks) - אבל לאמזון יש יתרון מובנה מול מתחרים קטנים יותר.

יהיה מעניין!



סיכום


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

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




----

לינקים רלוונטיים

Lambda@Edge

3 תגובות:

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

    השבמחק
    תשובות
    1. צודק - זה לא ממש טריוויאלי.

      אשתף בכמה דוגמאות שאני מכיר:
      * שירות אחד בנה פונקציה שמסיקה מתוך ה user_agent אם הדפדפן תומך בפורמט WebP - ואז הגיש לו תמונות בפורמט הנכון.
      * דוגמה אחרת היא לבצע A/B testing - ולהגיש תוכן אחר למשתמשים מסויימים.
      * ג'ינרוט של תמונה בגודל מתאים למשתמש - כדי לא לשלוח לו קובץ גדול מדי.
      * שירות API שאם מזהה שחסרים headers של ה authentication מחזיר HTTP 401 מתוך ה edge וחוסך את הגישה לשרתים (גם פחות עומס עליהם, וגם תגובה מהירה יותר).

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

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

      מחק