יום שלישי, 25 ביולי 2017

שלום, AWS Lambda!

למבדה, שירות ה FaaS של AWS, הוא שירות המאפשר להריץ "פונקציות" בצורה מנוהלת:
אין צורך לחשוב על שרתים, על תשתיות, High Availability ו Fault tolerance, או על Scaling.

אתם כותבים את הפונקציה, ומוסיפים קצת הגדרות.
שירות למבדה יעשה עבורכם את הניהול: הקצאת CPU, רשת ו I/O, זמינות, טיפול ב scale, וכו'.

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

בעת כתיבת הפוסט, השירות תומך בסביבות הריצה של Java, Node.js, Python, ו #C.
עם מעט "tweaking", ניתן להריץ שפות נוספות כמו Go על גבי סביבות הריצה הקיימות.

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



מקור: אמזון




שלום, למבדה!



הנה פונקציית ה Hello World בלמבדה. היא מאוד פשוטה.

ה event הוא אובייקט ה input שלנו, הוא יכיל את כל המידע שזמין להפעלת פונקציית הלמבדה. פונקציית למבדה היא מעין "טריגר ל event" - ועל כן המינוח.
בג'אווה וב #C יש וריאציה בה ניתן לקבל את ה input כ stream.

ה context הוא אובייקט בעזרתו אנחנו מתקשרים עם סביבת הריצה של למבדה, למשל: לבדוק כמה זמן ריצה נותר לנו (()context.getRemainingTimeInMillis) או כדי לקבוע את תוצאת הרצת הפונקציה (succeed או fail).

ה callback ספציפי למימוש של Node.js - ולו קוראים כאשר סיימנו את הפעולה (אם אנחנו רוצים לשדר פלט).
הפרמטר הראשון הוא error (אם יש), השני הוא אובייקט התוצאה, שיעבור ()JSON.stringify.

מדוע בחרתי ב node.js עבור הדוגמה?
זו הסביבה הכי קלה לשימוש והכי מתועדת של למבדה. ג'אווהסקריפט היא קלילה (זמן טעינה מהיר) ואינה צורכת הרבה זיכרון - מה שטוב עבור פונקציות למבדה. גם פייטון נמצאת במקום טוב, אבל התיעוד על Java ו #C - נמצא מאחור.

מה עושים עם הקוד? "איפה שמים אותו"?
בואו נראה קצת מסכים (מה AWS Console) - ה UI עוזר לשקף דברים שלא נראה ב aws-cli:


הלכתי ב AWS Console (קרי - ה UI) לאזור של למבדה, ויצרתי פונקציה חדשה.
אני אבחר את ה template הריק. באופן כללי אני לא אוהב להסתמך על templates...



טריגרים? - לא מעניינים אותי. אני רוצה רק קוד! נקסט!


  1. שם הפונקציה, כפי שיופיע ב console ובכלי האדמיניסטרציה.
  2. כאן בוחרים סביבת-ריצה. לסביבות השונות יש defaults שונים וגם פרמטרים שונים לקונפיגורציה.
  3. יש אופציה להקליד את קוד הפונקציה בתוך online editor - אבל בסביבה אמיתית הרבה יותר הגיוני לטעון את הקוד ל S3 - ולהשתמש בו משם. ממש path לשם הקובץ.


הנה עוד הגדרות, והן קצת יותר מעניינות:
  1. אמנם כמות הזיכרון המוקצה לפונקציה היא תחת "Advanced setting" - אך יש לה חשיבות גדולה.
    אמזון לא מכירה את הפונקציה שלנו ולא לוקחת "אחריות" על הקצאת הזיכרון. זו אחריות שלנו.
    1. ללמבדה יש monitoring שיראו לנו אם הפונקציה שלנו מתקרבת לקצה הזיכרון שמוקצה לה (או כשלה בשל מחזור בזיכרון) - ואז אפשר להגדיל את נפח הזיכרון. המקסימום הוא 1.5GB.
    2. החיוב בלמבדה נעשה ע"פ GB-second שנצרכו ע"י הפונקציה, כאשר אמזון מעגלת את זמן ריצת הפונקציה לרזולוציה של 100ms כלפי מעלה. פונקציה שרצה במשך 6ms - תחויב בעשירית GB-second על ההרצה הספציפית.
    3. למבדה תקצה CPU לפונקציה ע"פ הזיכרון שהוגדר. לא מפורט כמה CPU זה יהיה, אך יש קשר ישיר בין כמות הזיכרון שהוקצתה - ל CPU שיוקצה: על מכונה עם 4GB זכרון, פונקציה שהוקצה לה 1GB זיכרון - תקבל 25% מה CPU time - יחס ישר. המודל הזה מפשט לאמזון את ה scheduling של invocation של פונקציות - לחומרה הזמינה. הגיוני.
      1. המשמעות הראשונה היא שאם יש לי פונקציה שזקוקה להרבה CPU, ומעט זיכרון - יהיה עלי להקצות עבורה הרבה זיכרון בכדי לקבל חלק גדול יותר מה CPU.
        בחישובי עלות, פעמים רבות יהיה יותר זול להקצות לפונקציה הרעבה ל CPU יותר זיכרון/CPU. היא תרוץ מהר יותר - וכף נחוייב על פחות GB-sec.
      2. למרות שאין תיעוד ברור בנושא, נראה שמעל הקצאה של 1GB - ייתכן ופונקציית הלמבדה תקבל הקצאה של שני cpu cores. אין מניעה להשתמש ב threads בתוך פונקציית למבדה.
    4. מעניין לציין, שה default ב UI של nodeJs הוא 128MB זיכרון, בעוד זה של ג'אווה הוא 512MB זיכרון. לא קשה להגיע בג'אווה לצריכה גבוהה של זיכרון - בשל tradeoff תכנוני שנלקח ע"י מתכנני ג'אווה וה JVM. אמזון לא רוצה להכשיל אתכם - ולכן מקצה ב default גבוה מספיק עבור רוב הפונקציות האפשריות.
  2. עלי גם להגדיר timeout להרצת הפונקציה. ישנן פונקציות ש invoication שלהן עשוי לארוך דקה או שתיים - וזה בסדר. בפונקציה כמו שלי ("שלום עולם") - זמן ריצה של דקה משמעו באג שגרם ל infinite loop, שיגרום לי לשלם הרבה יותר ממה שהתכוונתי. האחריות על ה tradeoff בין סיכון לקטיעת הפונקציה ובין סיכון לתשלום מוגזם - היא על המשתמש (אבל ה default הוא הגיוני).
    1. לא ניתן כיום לקבוע timeout ארוך מ 5 דקות לפונקציה. אם יש לכם עיבוד כבד (נאמר: ETL) - אזי יהיה עליכם לשבור את העבודה ל chunks קטנים יותר. זה בריא גם בשבילכם מהסיבות הנ"ל.
  3. כאן אני מצביע על נקודת ההרצה של הפונקציה (ה "main" שלה).
    במקרה של nodejs, אם שם הקובץ שלי הוא index.js, ועשיתי export לפונצקיה בשם handler אזי עלי להקליד "index.handler". לכל סביבת ריצה יש חוקים מעט שונים.
  4. אלו הן הרשאות הריצה (execution) של הפונקציה - הרשאות לגשת לשירותים אחרים של AWS.
    יצרתי role עם הרשאות ה default (קרי basic execution policy - גישה לכתיבת לוגים וזהו) - וקראתי לו lambda_basic_execution - שאוכל להשתמש בו בפונקציות נוספות.


אני ממשיך הלאה, מאשר את יצירת הפונקציה, ומגיע למסך ניהול הפונקציה:


הוספתי שורת קוד של logging בכדי להדגים את כלי האדמיניסטרציה של למבדה. היכולת לכתוב לוגים היא חשובה מאוד - זו הדרך הכי יעילה לבדוק איך הפונקציה רצה. ב nodeJS פעולת logging נעשית באמצעות console.log (הרי "קל" לדרוס את console). בג'אווה, למשל, מקבלים את ה Logger מתוך ה context.

אני יכול ללחוץ על פתור "Test" - ולראות את הפונקציה שלנו בפעולה!

  1. הנה ה output של ריצת-הבדיקה. יש פלט - זה כבר טוב! אחרת הייתה לי הודעת שגיאה.
  2. הנה הלוג של ריצת-הבדיקה - אני יכול לראות שם את הלוג שכתבתי.
  3. מאחר ולא הרצתי את הפונקציה לאחרונה, אנו רואים ביצועים של cold state. בהרצות הבאות שאריץ בטווח של דקות - הביצוע ייקח פחות מ 1ms (זמן הביצוע הוא רק של הקוד שלי, ולא כולל את החלק של למבדה). ה cold start - הוא נושא לפוסט המשך.
  4. הנה אפשר לראות את צריכת הזכרון של ההרצה. בהרצה עוקבת הזיכרון מעט יותר גבוה: 22-24MB. מדוע? - פוסט המשך. 

כמובן, שברגע שאני רוצה שהפונקציה שלי תהיה reproducible (ליצור אותה קודם על סביבת בדיקות ורק אז ליצור בדיוק אותו דבר בסביבת הפרודקשיין), אני לא אשתמש ב UI אלא בכלי ניהול תצורה כמו Terraform בכדי לייצר אותה.



למבדה Prison Break


איזה יופי! איזו גאווה! כתבנו פונצקיית למבדה שרצה. אשכרה רצה!
אני לוחץ שוב, ושוב על כפתור ה "Test" - ואני מרוצה.

אבל נניח שאני רוצה לקרוא לה ממקור שאינו ה UI של אמזון. כרגע, זה לא אפשרי...

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

זוכרים את הטריגר שדילגנו עליו למעלה, ממש בהתחלה? - הגיע הזמן להשלים אותו.




לפונקציות למבדה יש שני סוגי הרשאות:
  • הרשאות ריצה (Execution) - הרשאות לגשת לשירותים אחרים של AWS. 
    • את ההרשאות האלו מגדירים כ IAM role - כמו "lambda_basic_execution" שהגדרנו למעלה.
    • בתיעוד של למבדה קוראים לחלק הזה גם בשם Pull Model - מה הפונקציה שלכם יכולה "למשוך".
    • לפעמים הטריגר של הפונקציה יהיה רק scheduler ("רוצי כל 5 דקות") ואז נפונקציה תקרא ל SQS או Kinesis - למשוך משם נתונים ולטפל בהם.
  • הרשאות הפעלה (Invocation) - הרשאות הנדרשות ע"י ה trigger / event source - על מנת להפעיל את פונקצית הלמבדה.
    • את ההרשאות מגדירים גם כ IAM role - אבל על מי שרוצה לקרוא ללמבדה, למשל: API ב API Gateway.
    • בתיעוד של למבדה קוראים לחלק הזה גם בשם Push Model - מי מפעיל את הפונקציה שלכם.

נגדיר טריגר מסוג AWS API Gateway.


כמובן שעדיף להגדיר IAM role, אבל כרגע לצורך הדוגמה - אני בוחר ב "Open": כל מי שיש לו את ה URL יכול להפעיל את הפונקציה (ולחייב אותי).



סיימנו. תוכנית הבריחה הושלמה!

אני לוחץ על קישור 1 - שהוא ה URL של ה API שיצרתי:



אופס! ב Prison Break, כמו ב Prison Break - תוכניות משתבשות. צריך לעבוד על עוד משהו.
אני בודק שוב: הפונקציה שלי תקינה.

אז מה הבעיה?

בלמבדה זו שאלה שתשאלו את עצמכם לא מעט. כלי העבודה העיקרי - הוא לוגים.

לכן אני אלחץ על קישור 2 - שלוקח אותי ל view הכללי  של ה API שהגדרתי ב API Gateway. משמעות המונח "ANY" = כל מתודות ה HTTP.


המסך הזה ממש "מצייר" את ה flow שמתרחש: מהלקוח, דרך ה API Gateway (קודם צד ה HTTP, ואז צד ה Integration / Transformation), עד ללמבדה - ובחזרה.

אני יכול להקליד על 1 לראות את מהות האינטגרציה. מכיוון שה Integration type הוא lambda_proxy - אזי כמעט ואין מה להגדיר. כמעט הכל מנוהל ע"י אמזון. כנ"ל לגבי הטרנפורמציה בחזרה.
קישור 2 - מוביל אותי חזרה למסך של ניהול פונקציית הלמבדה שלי.
אני לוחץ על קישור 3 - שמאפשר לי לבדוק, מקצה לקצה, את ה API ביחד עם פונקציית הלמבדה.
שם, בתוך הלוג - אני מוצא את הבעיה:


לפני הטרנספורמציה הכל נראה בסדר.
שניה! איזו טרנספורמציה?

קריאת ה HTTP עצמה לא מתאימה בדיוק לאובייקט ה event שאנחנו מקבלים. מישהו לקח את ה HTTP payload (טקסט רציף), פרסר אותו וסידר אותו יפה במבנה היררכי של פרמטרים. מי שעשה את זה הוא ה lambada_proxy של ה API gateway: הוא המיר את ה HTTP למבנה json שלמבדה יודעת לקבל ולעטוף באובייקט ה event.

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


הנה, שינינו את הקוד למבנה המצופה ע"י ה lamda_proxy והנה התוצאה:



איזה כיף!



סיכום



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

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

מקווה שהצלחנו.

כמובן שיש עוד כמה דברים בסיסיים וחשובים שכדאי לספר על למבדה. בלי נדר, אולי יהיה פוסט המשך.


עדכון: הנה חלק ב' של הפוסט.



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




רפרנסים

מדריך למפתח של אמזון על פונקציות למבדה
AWS Lambda Blog



5 תגובות:

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

    השבמחק
    תשובות
    1. כי אתה מחויב על זמן, כתלות בזיכרון.
      GB-sec, זה זכרון שהוקצה לשניה, כלומר פי 2 זיכרון = פי 2 עלות.
      הנה הקישור: https://aws.amazon.com/lambda/pricing/

      מקווה שעכשיו זה ברור.

      מחק
    2. עכשיו זה ברור, תודה.

      מחק
  2. כרגיל, תענוג לקרוא :)

    השבמחק