קופסה שחורה, מטבעה היא קופסה שלא ניתן לראות דרכה. למדה היא אחת מאותן קופסאות שחורות, שירות מדהים שמאפשר לך כמפתח לעלות במהירות לענן, כשהיא מצליחה להסתיר את המורכבות של אפליקציה שרצה בענן בצורה די מרשימה. אבל לפעמים האפשרות להרים את המכסה ולראות כיצד אותה קופסה שחורה מתקתקת יכולה לעזור להבין מדוע דברים מתנהגים כפי שהם מתנהגים ואולי לשפר אותם עבור המקרה הפרטי שלכם.
בפוסט זה נרים את המכסה מעל קומפוננטה פנימית בלמדה שנקראית Runtime.
אנו נבנה Runtime צעד אחר צעד באמצעות כתיבתה בקונסול של הלמדה, כל הקוד שמופיע בבלוג, ניתן גם למצאו בריפו הבא. הבלוג מחולק לשני חלקים מרכזיים - החלק התיאורטי ואז הבניה עצמה.
נתחיל בלימוד על המבנה של סביבת הריצה של הלמדה.
סביבת הריצה (Lambda Execution Environment)
מה קורה כשהלמדה שכתבתם נקראית? מתחיל רצף פעולות, שמתוארות בתרשים הבא.
.png)
התרשים מתחיל בסרוויס שמנהל את ההצגה, זה השירות שמפתחי AWS כתבו שמנהל את השירות שנקרא למדה, הוא חיצוני לפעולת הקוד שרץ בלמדה שנכתבה על ידכם.
הסרוויס הגיע למסקנה שצריך לקרוא ללמדה מסוימת, דבר ראשון הוא מוריד את הקוד של הלמדה, זה הקוד שאתם כתבם, ארוז ב-Zip או כקונטיינר.
לאחר הורדת הקוד, מאותחלת סביבה בתוך קונטיינר, הסביבה הזאת נקראית Execution Environment, והיא מכילה בנוסף לקוד שלכם, שירותים נוספים פנימיים שמאפשרים קוד שלכם לרוץ. הקונטיינר הוא טכנולוגיה ייחודית ל-AWS שנקרא Firecracker, זו טכנולוגית קונטיינרים שאחד היתרונות המרכזיים שלה הוא המהירות שבה הקונטיינר עולה והמשאבים המועטים שהוא דורש מהממכונה שבה הוא רץ.
לאחר שהסביבה אותחלה עם השירותים השונים שלה, הקוד שלכם, שירד בתחילת הדרך, מאותחל, כלומר נטען לזיכרון ודברים שהם חיצוניים ל-handler מאותחלים.
ורק כעת הגענו למנה העיקרית שהיא קריאה לקוד שנמצא בתוך ה-handler.
פעולות 1 עד 3 (אלו שצבועות בכחול) הם חלק מהלמדה cold start.
אני מעוניין לעשות זום לנקודה מספר 2, ולראות כיצד סביבת הריצה נראית, אילו שירותים רצים בה
מה קורה בפנים

סביבת הריצה מכילה מספר תהליכים (process) שרצים בנוסף לקוד שלכם:
xray damon - הזכרנו אותו בפוסט על xray , הוא התהליך שמקבל את הטרייסים שהsdk מייצר ומעביר אותם הלאה לשירות ה-xray.
logs daemon - הלוגים שנכתבים נשלחים ל-cloudwatch logs באמצעות התהליך הזה.
Extensions - בהזדמנות נכתוב פוסט גם על הנושא הזה. זהו מנגנון שמאפשר להוסיף לכם, כותבי הלמדות תהליכים נוספים שירוצו לצד הקוד שלכם, בעצם להרחיב את סביבת הריצה ביכולות נוספות.
Runtime - זה התהליך שלשמו התכנסנו, הוא מריץ את הקוד שלכם, מזין אותו בקלט ומחזיר את הפלט לסרוויס של הלמדה.
בנוסף לתהליכים שרצים ישנן מספר נקודות קצה, אלו ה-API’s שהסרוויס של הלמדה שם בתוך סביבת הריצה כדי שהוא יוכל לתקשר עם התהליכים הפנימיים, לדוגמא, ה-Runtime מקבל את הקלט ומחזיר את הפלט לסרוויס של הלמדה באמצעות אותן נקודות קצה.
כעת, לאחר שאנו מבינים את מיקומו את ה-Runtime בשרשרת המזון, בואו נחקור אותו לעומק.
Lambda Runtime
ה-Runtime הוא תהליך שמריץ את הקוד שלכם והוא איש הקשר בין הקוד שלכם לסרוויס של הלמדה והוא עושה זאת באמצעות ממשק ה-API שהסרוויס מספק.
הוא מתרגם את הקלט שהסרוויס של הלמדה שלח לתוכן שהקוד שלכם מבין, הרי הקוד שלכם כתוב בשפת פיתוח X, צריך לתרגם את הקלט למשהו ששפת הפיתוח מבינה, לדוגמא python dictionary במידה ואתם כותבים בפייתון.
ומצד שני הוא מתרגם את הפלט של הקוד שלכם למשהו שה-API של הסרוויס של הלמדה מבין, לדוגמא תרגום ה-python dictionary ל-json.
במידה והקוד שלכם זורק exception, מישהו צריך לתרגם את השגיאה הזאת לקריאת API עבור הסרוויס של הלמדה כדי שהוא יוכל לדווח אחורה שקרתה תקלה.
המשמעות של איש הקשר הזה הוא שה-Runtime פעמים רבות יהיה כתוב בשפה שבה הקוד שלכם כתוב, כי הוא צריך להבין NodeJS Exception, ו-Python Dictionaries וכדומה, כדי שהוא יוכל לבצע תרגום לשפה האוניברסילית שהוא ה-API של הסרוויס של הלמדה.
ה-API של הסרוויס של הלמדה מכתיב את צורת הפעולה של ה-Runtime, בחלק הבא נעבור על מכונת המצבים שכל Runtime צריך לממש.
מכונת מצבים
ה-Runtime מכיל שלושה מצבים:
אתחול
ריצה
כיבוי

באתחול, ה-Runtime נטען לזיכרון, ממש התהליך עצמו. לדוגמא, במידה וה-Runtime היה Node, אז ה-Executable של Node היה רץ. אבל לאחר טעינת ה-Runtime לזיכרון, מתבצע אתחול נוסף והוא טעינת הקוד של מפתח הלמדה לזיכרון ואתחולו, משמעות התהליך ברמה הפרקטית של הלמדה הוא טעינה של כל מה שהוא מחוץ ל-handler. שימו לב, זה חלק מה-cold start שהזכרתי למעלה.
לאחר האתחול, ה-Runtime מוכן לקבל קריאות מבחוץ, ולהעבירם לתוך ה-handler, זהו שלב הריצה. כאן מגיעה נקודה מאוד מעניינת, ה-Runtime הוא סוג של Event loop, כלומר לולאה אינסופית שבכל פעם שהלמדה מוטרגת, הסרוויס של הלמדה מודיע ל-Runtime שישנה קריאה וה-Runtime קורא ל-handler. ה-Runtime לעולם לא ייצא מהלולאה, זו לולאה אינסופית.
הפעם היחידה שה-Runtime ייצא מהלולואה היא כשמחליטים לכבות את הלמדה ״ולהרוג״ את הקונטיינר שבו היא רצה, בעצם את סביבת הריצה. זהו שלב הכיבוי
כפי שהוזכר בחלק הקודם התקשורת עם הסרוויס של הלמדה מתבצע באמצעות קריאות API, ולמעשה שלב הריצה מנוהל כולו על ידי קריאות ה-API.
API
ישנן שלוש קריאות API:
next - קריאת API זו, מסמנת לסרוויס של הלמדה שאנו כ-Runtime מוכנים לקבל את פרטי הקריאה הבאה. זו קריאת API. שהיא חוסמת (Blocking) כלומר, ה-Runtime יחכה כמה זמן שנדרש לתשובה מה-API הזה ובזמן הזה לא יעשה כלום. דרך אגב, זו גם הנקודה שבה הקונטיינר של הלמדה נכנס למצב שינה, אך זה לפוסט אחר. התשובה לקריאה הזאת, היא ה-Payload שמגיע ל-Handler.
reponse - זו קריאת API שמחזירה את תשובת ה-Handler לסרוויס של הלמדה, כל עוד לא בוצעה קריאה ל-response, הסרוויס של הלמדה יחשוב שה-handler עדין רץ וזה ייחשב כחלק מזמן הריצה של הלמדה.
error - דיווח על שגיאה, במידה והיתה תקלה בריצה של הלמדה, exception לדוגמא, נדווח על התקלה באמצעות קריאת ה-API הזאת.
.png)
שלב קריאות ה-API הוא לולאה אינסופית.
אנחנו מוכנים לכתוב את ה-Runtime שלנו, להזכירכם, השפה של ה-Runtime תיכתב באותה שפה שבה הקוד של ה-Handler ייכתב. אנחנו נכתוב Runtime עבור שפת LLRT, אז קודם נבין זריז את נבכי השפה (היא לא מורכבת) ואז נעבור לכתיבת ה-Runtime שיודע להריץ קוד בשפה הזאת.
LLRT
LLRT היא שפת פיתוח קלת משקל, מבוססת על ג׳אווה סקריפט, שתוכננה לתת מענה לביקוש הגובר ליישומי סרברלס מהירים ויעילים. LLRT מציעה זמן אתחול מהיר עד פי 10 ועלות כוללת נמוכה עד פי 2 בהשוואה לזמני ריצה של NodeJS.
זהו תרגום בעברית של התאור הרשמי.
אחת הבעיות של למדה היא נושא ה-coldstart, זמן האתחול של קריאה ראשונה. זמן אתחול ארוך נובע מכמה גורמים:
אתחול סביבת הריצה, הזמן שלוקח לעלות את הקומפיילר לזיכרון, אם זה NodeJs או פייתון והזמן שלוקח לבצע אתחול ראשוני של הקוד שאתם כתבתם.
הקומפיילר של NodeJS מבצע אופטימיזציות שונות של הקוד כגון JIT, שמאוד מועילות במקרים שהקוד שלך רץ לזמן ממושך, אך פחות יעילות כשהוא רץ בתוך למדה שהיא זמנית (Ephemeral) מטבעה.
LLRT (Low Latency Runtime), נבנתנה במיוחד עבור סביבות כגון למדה ומשפרת את הנקודות הנ״ל. היא קטנה בגודל, מכיוון שהיא לא מכילה את כל מה ש-NodeJS מכילה והיא אינה מבצעת אופטימיזציות שהן פחות מתאימות לסביבות מוגבלות כגון למדה.
ניתן לראות באתר הרשמי של LLRT, השוואת ביצועים ואכן המספרים מאוד מרשימים של פי 10 במהלך אתחול של Cold Start ועד פי 2 במהלך אתחול של Warm Start.
אז איך נראה קוד של LLRT, הוא נראה כמו כל קוד של ג׳אווהסקריפט תקני, לדוגמא
1import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 3const client = new DynamoDBClient({}); 4 5export const handler = async (event) => { 6 await client.send( 7 new PutItemCommand({ 8 TableName: process.env.TABLE_NAME, 9 Item: {10 id: {11 S: Math.random().toString(36).substring(2),12 },13 content: {14 S: JSON.stringify(event),15 },16 },17 })18 );19 return {20 statusCode: 200,21 body: "OK",22 };23};
האם ישנם חסרונות, כן, על רגל אחת, אתם מוזמנים להרחיב על כך באתר הרשמי:
אין תמיכה מלאה בכל הספריות שקיימות ב-NodeJS, לדוגמא אין תמיכה ב-https (כן יש תמיכה ב-fetch).
מכיוון שאין תמיכה ב-JIT, ישנם אפליקציות שיחוו דגרגציה בביצועים, לדוגמא אפליקציות שעושות data processing לאורך זמן.
אז אנו נשתמש ב-LLRT על מנת להריץ את Runtime שאנו נכתוב, שימו לב שה-Runtime שלנו יצפה שהקוד של ה-handler יהיה כתוב גם ב-LLRT.
בניית ה-Runtime
אנו נבנה את הפתרון באמצעות הקונסול, נתחיל ביצירת למדה ריקה.
למדה ריקה
גשו לקונסול של הלמדה ולחצו על
Create function
.עבור ה-Runtime בחרו
Amazon Linux 2023
, בבחירה הזאת, אנו אומרים ל-AWS, שאנחנו בעצם נממש את ה-Runtime, אנחנו לא מקבלים שום דבר מוכן. AWS מספקת Runtimes מוכנים כגון NodeJS או Python. במידה ורוצים לייצר אחת בעצמנו, תמיד נבחר ב-Amazon Linux 2023.
בחרו
arm64
בארכיטקטורה, אין סיבה טובה לבחירה הזאת מעבר לכך שהבינארי שנשתמש בו על מנת לקמפל קוד של LLRT, קומפל במקור לרוץ עלarm64
.

לחצו על Create function. חכו מספר שניות עד יצירת הלמדה.
מחקו את הקבצים שנוצרו לכם, אין לנו צורך בהם. ניתן למחוק אותם באמצעות סימונם ולחיצה על הכפתור השמאלי של העכבר.

Bootstrap
כשסביבת הריצה מעוניינת להריץ את ה-Runtime, היא מחפשת קובץ בודד, שהוא נקודת הכניסה של ה-Runtime, הקובץ נקרא bootstrap
ללא שום סיומת והוא צריך להיות executable.
גשו לרשימת הקבצים, לחצו על הכפתור הימני של העכבר ובחרו לייצר קובץ חדש, קראו לו bootstrap.
הוסיפו לו את הקוד הבא:
1#!/bin/sh2echo Hello Runtime!
מכיוון שאנו מייצרים את הקובץ האמצעות העורך של הלמדה, הקובץ אוטומטית מקבל הרשאות ריצה (Executable).
לחצו על
Deploy
ואזTest
.אתם אמורים לראות פלט דומה לבא:
1Response: 2{ 3 "errorType": "Runtime.ExitError", 4 "errorMessage": "RequestId: c5a00f54-3b3c-4dab-9afb-17972753fc32 Error: Runtime exited without providing a reason" 5} 6 7Function Logs: 8Hello Runtime! 9INIT_REPORT Init Duration: 6.23 ms Phase: init Status: error Error Type: Runtime.ExitError10Hello Runtime!11INIT_REPORT Init Duration: 10.38 ms Phase: invoke Status: error Error Type: Runtime.ExitError12START RequestId: c5a00f54-3b3c-4dab-9afb-17972753fc32 Version: $LATEST13RequestId: c5a00f54-3b3c-4dab-9afb-17972753fc32 Error: Runtime exited without providing a reason14Runtime.ExitError15END RequestId: c5a00f54-3b3c-4dab-9afb-17972753fc3216REPORT RequestId: c5a00f54-3b3c-4dab-9afb-17972753fc32 Duration: 30.77 ms Billed Duration: 31 ms Memory Size: 128 MB Max Memory Used: 3 MB17 18Request ID: c5a00f54-3b3c-4dab-9afb-17972753fc32
בואו ננתח את הפלט:
שורות 8 ו-10: סביבת הריצה ניסתה להריץ את ה-Runtime שלנו פעמיים, ניתן לראות זאת באמצעות ההדפסה הכפולה של Hello. למעשה בוצע כאן retry, ריצה מחודשת שסביבת הריצה מבצעת במקרה של תקלה באתחול ה-Runtime. כיצד יודעים שהבעיה היא בשלב האתחול?
שורה 9: הלוגים מראים שהכשלון קרה בשלב האתחול. מדוע נכשל?
שורה 13: הריצה נכשלה בגלל שה-Runtime סיים, סביבת הריצה של הלמדה, מצפה שה-Runtime ירוץ כל עוד הוא לא התבקש לסיים.
השלב הבא הוא הוספת ה-LLRT והרצה של סקריפט בסיסי.
LLRT Layer
אנחנו בונים Runtime המבוסס על LLRT, ולכן נצטרך לכלול את הקובץ הבינארי של LLRT כחלק מהלמדה שלנו. כרגע אנחנו מבצעים את הכל באופן ידני, אך בסביבת ייצור נהוג לארוז את הקובץ הבינארי ואת קובץ האתחול (bootstrap) בתוך שכבת למדה (Lambda Layer).
שכפלו את הקוד שמלווה את הבלוג הזה.
בתוך תיקיית binaries, מצאו את הקובץ
llrt-linux-arm64-full-sdk.zip
. זהו קובץ ה-LLRT הבינארי, שהוכן עבור ארכיטקטורתarm64
עם תמיכה מלאה ב-AWS SDK.
כעת נייצר שכבת למדה חדשה שמכילה את הבינארי:
עברו אל
Layers
בתפריט השמאלי של הקונסול.לחצו על
Create layer
.הגדירו את השכבה:
תנו לה שם (למשל, LLRT).
העלו את קובץ ה-ZIP משלב 2.
בחרו
arm64
כארכיטקטורה תואמת.בחר
Amazon Linux 2023
עבור ה-Runtime.ההגדרות הללו יסייעו ל-AWS להתריע בפניך על בחירת שכבה שאינה מתאימה להגדרות הלמדה. לדוגמא, השכבה שאנו מייצרים כאן, אינה מתאימה במידה ותבחרו בפייתון.
לחצו על
Create
.

הרצה ראשונית
בוא נבדוק את השכבה והקובץ הבינארי לפני שניישם את סביבת הריצה המלאה:
חזרו לפונקציית הלמדה.
הוסיפו את השכבה שיצרתם זה עתה:
במסך Add layer, בחר Custom layers.
בחר את שכבת LLRT (או כל שם אחר שניתן לה) מהתפריט הנפתח.
הוספת שכבה קבצי השכבה זמינים תחת /opt בלמדה. עדכנו את קובץ
bootstrap
כך שיראה כך:
1#!/bin/sh2 3/opt/llrt hello.js
אנו פשוט מריצים את הקומפיילר על קובץ בשם hello.js
צרו קובץ בשם
hello.js
עם התוכן הבא:
1console.info("Hello Lambda")
לחץ על Deploy ואז על Test.
תתקבלנה שגיאות מוכרות: שגיאות חוזרות המעידות על retry והעובדה שזה קרה בשלב האתחול, אך שימו לב, ישנה גם הודעה חדשה:
1FATAL Error: "handler" is not a function in "hello"
מדוע זה קרה?
לקובץ הבינארי של LLRT יש יכולות מובנות של סביבת הריצה של Lambda (כגון ניהול לולאת האירועים, בדיוק כזאת שאנו נבנה), כפי שמופיע ב-קוד המקור:
1if env::var("AWS_LAMBDA_RUNTIME_API").is_ok() && env::var("_HANDLER").is_ok() { 2 3 // ... 4 5 start_runtime(&vm).await 6 7} else { 8 9 start_cli(&vm).await;10 11}
הקוד עצמו כתוב בראסט והוא בודק האם משתני סביבה מסוימים קיימים כדי לזהות אם הוא פועל בתוך למדה. במידה והוא פועל בתוך למדה הוא מתחיל לרוץ כ-Runtime, הוא מחפש handler וזו בדיוק השגיאה אנו רואים, מכיוון שאנו מעוניינים לכתוב את ה-Runtime, אנחנו צריכים ״לעבוד״ עליו כדי להריץ אותו כ-CLI עצמאי, ולכן נסתיר את אחד מאותם משתני סביבה. זהו טיפול שאנו עושים ספיציפית בגלל הצורה שהבינארי של LLRT מתנהג, זה לא יקרה בכל Runtime אחר שנכתוב.
עדכון קובץ bootstrap:
1LOCAL_HANDLER=$_HANDLER env -u _HANDLER /opt/llrt hello.js
כאן אנחנו שומרים את ערך _HANDLER
ב-LOCAL_HANDLER
לשימוש עתידי, ומסירים את _HANDLER
מהסביבה. _HANDLER הוא משתנה סביבה שקיים בסביבת הריצה של כל למדה והוא מכיל את הערך שהוגדר להיות ה-handler של הלמדה.
6. לחץ על Deploy
ו-Test
שוב.
כעת השגיאה הגיונית יותר, ואנחנו מוכנים ליישם את לולאת האירועים המלאה של סביבת הריצה. אך לפני כן, צריך לעבור דרך שלב האתחול.
שלב האתחול
שלב האתחול מורכב משלושה חלקים:
טעינת הקובץ הבינארי של LLRT לזיכרון (והרצתו)
ייבוא ה-handler שהוגדר במשתנה הסביבה
_HANDLER
ביצוע הקריאה הראשונה לנקודת הקצה
/next
, אשר נחשפת על ידי שירות הלמדה בסביבת ההרצה
טעינת LLRT
צרו קובץ חדש בשם
runtime.mjs
(משתמשים בסיומת .mjs כדי לאפשר top level await)הוסיפו את הפונקציות הבאות לעזר ברישום לוגים:
1/** 2 * 3 * @param {string} message 4 * @param {...any} args 5 */ 6function info(message, ...args) { 7 console.info( 8 `[RUNTIME][${new Date().toISOString()}] INFO: ${message}`, 9 ...args10 );11}12 13/**14 *15 * @param {string} message16 * @param {...any} args17 */18function error(message, ...args) {19 console.error(20 `[RUNTIME][${new Date().toISOString()}] ERROR: ${message}`,21 ...args22 );23}24 25info("Starting Melio's Runtime");
בקובץ
bootstrap
, שנו:
LOCAL_HANDLER=$_HANDLER env -u _HANDLER /opt/llrt hello.js
ל:
LOCAL_HANDLER=$_HANDLER env -u _HANDLER /opt/llrt runtime.mjs
משתנה הסביבה
LOCAL_HANDLER
מחליף עבורנו את המשתנה_HANDLER
כדי שנדע מהו ה-handler שאנחנו צריכים לטעון.לחצו על
Deploy
ו-Test
שוב.
ייבוא ה-handler
כעת נממש את ייבוא ה-handler, שהוא הקוד שרץ בכל הפעלה של הלמדה.
1. הוסיפו את הקוד הבא בקובץ runtime.mjs
לפני info("Starting Melio's Runtime")
:
1/** 2 * 3 * @returns {Promise<Function>} 4 */ 5async function initializeHandler() { 6 info("Initializing runtime..."); 7 // Invoke the handler. Get it dynamically from LOCAL_HANDLER environment variable. Remember that the structure is filename.method 8 if (!process.env.LOCAL_HANDLER) { 9 const errorString =10 "Handler not defined in environment variable LOCAL_HANDLER";11 error(errorString);12 await initializationError("Runtime.MissingHandler", errorString, []);13 process.exit(1);14 }15 const [handlerFile, handlerMethod] = process.env.LOCAL_HANDLER.split(".");16 17 // Try to loads js file and if not found, loads mjs file18 let module = undefined;19 try {20 module = await import(`./${handlerFile}.js`);21 } catch (e) {22 try {23 module = await import(`./${handlerFile}.mjs`);24 } catch (e) {25 const errorString = `Handler file ${handlerFile} not found`;26 error(errorString);27 await initializationError("Runtime.MissingHandlerFile", errorString, []);28 process.exit(1);29 }30 }31 32 if (handlerMethod in module === false) {33 const errorString = `Handler method ${handlerMethod} not found in ${handlerFile}`;34 error(errorString);35 await initializationError("Runtime.MissingHandlerMethod", errorString, []);36 process.exit(1);37 }38 39 info("Initialization complete");40 return module[handlerMethod];41}
שורות 8-14: אנו בודקים אם משתנה הסביבה
LOCAL_HANDLER
קיים.שורה 15: אנו מפענחים את ערך ה-handler בפורמט קובץ.פונקציה. זו נקודה מעניינת, אין שום ספסיפיקציה שמחייבת אותנו שככה יראה המבנה של ה-handler, נהוג לעשות זאת, אך זה לא מחייב. כשאתם תבנו את ה-Runtime שלכם, תוכלו לקבל כל החלטה לגבי המבנה של הקובץ שבו נמצא ה-handler. כמו כן, תראו בהמשך שה-handler לא חייב להיות handler 😉.
שורות 18-30: אנו מנסים לייבא את המודול תחילה כ-.js, ואם זה נכשל, כ-.mjs.
שורות 32-37: אנו מוודאים שהשיטה שהוגדרה קיימת ומיוצאת.
שורה 40: לבסוף, אנו מחזירים את פונקציית המטפל לשימוש עתידי.
בכל שלב, אם נתקלנו בבעיה, אנו מדווחים עליה לשירות הלמדה.
2. אחרי info("Starting Melio's Runtime")
, הוסיפוconst handler = await initializeHandler();
תקשורת עם שירות הלמדה
התקשורת עם שירות הלמדה מתבצעת דרך נקודת הקצה
http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime
משתנה הסביבה AWS_LAMBDA_RUNTIME_API
מכיל את הכתובת המקומית של השירות. המפרט המלא נמצא ב-תיעוד AWS. זה ה-Runtime API.
שגיאות אתחול מדווחות ל-Lambda דרך:
נתיב | /runtime/init/error |
---|---|
שיטה | POST |
Headers | Lambda-Runtime-Function-Error-Type |
תחת ה-header ניתן לדווח את סוג השגיאה שתופיע למי שמשתמש בלמדה במידה וקרתה תקלה. עד כה, כל התקלות נכתבות בצורה גנרית, אנו מעוניינים לשנות זאת.
הוסף את הפונקציה הבאה מעל initializeHandler:
1const baseUrl = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime`; 2 3/** 4 * 5 * @param {string} reason 6 * @param {string} errorMessage 7 * @param {string[]} stackTrace 8 * @returns {Promise<void>} 9 */10async function initializationError(reason, errorMessage, stackTrace) {11 const res = await fetch(`${baseUrl}/init/error`, {12 method: "POST",13 headers: {14 "Lambda-Runtime-Function-Error-Type": reason,15 },16 ErrorRequest: JSON.stringify({17 errorMessage,18 errorType: reason,19 stackTrace: stackTrace,20 }),21 });22 if (!res.ok) {23 error("init/error failed", await res.text());24 }25 info("init/error success", res.status, res.statusText);26 info("headers", res.headers);27 const body = await res.text();28 info("body", body);29}
שורות 9–19 הן המקום שבו הקסם קורה. מכיוון ש-LLRT תומך רק ב-fetch, אנו משתמשים בו כדי לתקשר עם נקודת הקצה שסופקה על ידי שירות הלמדה.
שורה 14 - אנו מספקים את הסיבה לשגיאה ב-header.
לחצו על
Deploy
ו-Test
שוב.כעת תוכל לראות שגיאה בסגנון:
1{2 3 "errorType": "Runtime.MissingHandlerMethod"4 5}
השגיאה נובעת משורה 35 בפונקציית האתחול.
בקובץ
hello.js
, הוסיפו את הפונקציה הבאה:
1export function handler(event, context) {2 3 console.info("inside the handler");4 5}
זה ה-handler של הלמדה, בדיוק מה שחסר.
לחצו על
Deploy
ו-Test
שוב.
אנחנו עדיין בשלב האתחול, כפי שמעידה השגיאה שקיבלנו. כדי לצאת משלב האתחול, עלינו לקרוא לנקודת הקצה /next
.
הקריאה הראשונה ל-/next
נקודת הקצה /next
מסמנת לשירות הלמדה שה-Runtime מוכן לטפל בבקשות.
נתיב | /runtime/invocation/next |
---|---|
שיטה | GET |
הוסף את הפונקציה הבאה מתחת ל-
initializeHandler
:
1/** 2 * 3 * @returns {Promise<{requestId: string, deadline: Date, invokedFunctionArn: string, traceId: string, body: any}>} 4 */ 5async function next() { 6 const res = await fetch(`${baseUrl}/invocation/next`, { 7 method: "GET", 8 }); 9 10 if (!res.ok) {11 error("next failed", await res.text());12 return null;13 }14 info("next success", res.status, res.statusText);15 info("headers", res.headers);16 const body = await res.json();17 info("body", body);18 19 return {20 requestId: res.headers.get("Lambda-Runtime-Aws-Request-Id"),21 deadline: Number(res.headers.get("Lambda-Runtime-Deadline-Ms")),22 invokedFunctionArn: res.headers.get("Lambda-Runtime-Invoked-Function-Arn"),23 traceId: res.headers.get("Lambda-Runtime-Trace-Id"),24 body,25 };26}
שורות 6-8: הקריאה בפועל לנקודת הקצה.
שורות 16-25: זהו הלב של הפונקציה. הקריאה ל-
/next
חוסמת עד להפעלת הלמדה. כאשר היא מופעלת, התגובה מ-/next
מכילה את תוכן הקריאה ואת המטא-נתונים שלה ב-headers. מטא-נתונים אלה, כגון requestId ו-traceId, מועברים כחלק מה-context או משמשים לשיפור הניטור הפנימי (לדוגמא xray id).
אחרי
const handler = await initializeHandler();
הוסף
const event = await next();
לחצו על
Deploy
ו-Test
שוב. ייתכן שתופיע שגיאה כי תהליך bootstrap הסתיים מיד לאחר הביצוע. זה תקין ונפתור זאת בשלב הבא כאשר נממש את ה-event loop.
שימו לב שהשגיאות משלב האתחול כבר לא מופיעות, והמערכת אינה מנסה לבצע פעולות חוזרות. היעדר השגיאות משלב האתחול מעידות על כך שהתקדמנו בהצלחה מעבר לשלב זה.
שלב הריצה (Invoke Phase)
ה־runtime טען את המודול שמכיל את ה־handler ואתחל כל מה שמחוץ לו. הוא ביצע את הקריאה הראשונה לנתיב next
וכעת הוא רשמית מחוץ לשלב האתחול.
בקטע זה, נממש את שלב הריצה (invoke), שהוא למעשה לולאה אינסופית המבצעת את השלבים הבאים:
קריאה לנתיב
/next
העברת תוצאת האירוע ל־handler.
החזרת התגובה חזרה לשירות הלמדה, אשר מפיץ אותה ללקוח הקורא.
לולאה אינסופית
הוסף את הקוד הבא מתחת לשורה
const handler = await
initializeHandler();
:
1while (true) { 2 info("Waiting for next invocation..."); 3 const event = await next(); 4 info("Received next invocation"); 5 6 const context = { 7 getRemainingTimeInMillis: () => event.deadline - Date.now(), 8 functionName: process.env.AWS_LAMBDA_FUNCTION_NAME, 9 functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,10 memoryLimitInMB: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,11 awsRequestId: event.requestId,12 logGroupName: process.env.AWS_LAMBDA_LOG_GROUP_NAME,13 logStreamName: process.env.AWS_LAMBDA_LOG_STREAM_NAME,14 invokedFunctionArn: event.invokedFunctionArn,15 // We pull these values from the next endpoint, omitted for brevity16 // identity: null,17 // clientContext: null,18 };19 20 info("Calling handler");21 const response = await handler(event.body, context);22 info("Handler complete", response ? response : "with no response");23}
אנו מבצעים לולאה אינסופית שבכל שלב של הלולאה -
אנו מבצעים קריאה ל-next.
בונים את אובייקט ה-context. כמו הקריאה ל-handler, זו קונבנציה, אנחנו לא מחויבים לייצר אובייקט context וגם לא מחויבים שיהיה במבנה כפי ש-AWS החליטו.
לחצו על
Deploy
ו-Test
שוב.
אנחנו מתקדמים, אומנם הקריאה נכשלה, אך הפעם על שגיאה שונה, שגיאה של Timeout, מדוע?
השגיאה נובעת מכך שלא דיווחנו את התשובה של ה-handler חזרה. וכמו תקשורת שה-Runtime מבצע, הדיווח יתבצע באמצעות API.
החזרת תגובה
שירות הלמדה שומר מצב פנימי עבור כל קריאה. כאשר מבצעים קריאה ל־/next
לקבלת פרטי הקריאה, יש “לסגור” אותה באמצעות קריאה לנתיב /response
, שמוגדר כך:
נתיב | /runtime/invocation/${AwsRequestId}/response |
---|---|
שיטה | POST |
AwsRequestId הוא path param שמכיל את המזהה הייחודי של הקריאה שקיבלנו כחלק מהתשובה ל-next
.
הוסיפו את הקוד הבא לאחר קריאת ה־handler:
1// ...2const response = await handler(event.body, context);3// ...4await invocationResponse(event.requestId, response);
הוסיפו את הקוד הבא מתחת למתודה
initializationError
:
1/** 2 * @param {string} requestId 3 * @param {any} response 4 * @returns {Promise<void>} 5 */ 6async function invocationResponse(requestId, response) { 7 const res = await fetch(`${baseUrl}/invocation/${requestId}/response`, { 8 method: "POST", 9 body: JSON.stringify(response),10 });11 if (!res.ok) {12 error("invocation response failed", await res.text());13 }14 info("invocation response success", res.status, res.statusText);15 info("headers", res.headers);16 const body = await res.text();17 info("body", body);18}
זו מתודה פשוטה - שימו לב לפרמטר requestId, שמתקבל מתגובת הנתיב next
.
לחצו על
Deploy
ו-Test
שוב. וזה עובד! זהו בניתם את ה-runtime הראשון שלכם. כעת נשפר את הטיפול בשגיאות.
טיפול בשגיאות
הוסף את השורה הבאה ל־handler שלך -
throw new Error("Lambda failed");
ולחצו עלDeploy
ו-Test
שוב.
אתם תראו פלט דומה לפלט הבא:
1{2 "errorType": "Runtime.ExitError",3 "errorMessage": "RequestId: 19914c36-ec5c-4fec-9660-ccbc46e79936 Error: Runtime exited with error: exit status 1"4}
הודעת השגיאה הזו אינה מאוד מפורטת. נשפר אותה על ידי החזרת פרטי השגיאה בפועל. נשתמש בנתיב טיפול בשגיאות של הלמדה, שמוגדר כך:
נתיב | /runtime/invocation/${AwsRequestId}/error |
---|---|
שיטה | POST |
Headers | Lambda-Runtime-Function-Error-Type |
AwsRequestId הוא path param שמכיל את המזהה הייחודי של הקריאה שקיבלנו כחלק מהתשובה ל-next
. וה-header מכיל תאור של השגיאה שיוחזר למשתמש.
הוסיפו את פונקציית טיפול השגיאות הבאה:
1/** 2 * @param {string} requestId 3 * @param {string} reason 4 * @param {string} errorMessage 5 * @param {string[]} stackTrace 6 * @returns {Promise<void>} 7 */ 8async function invocationError(requestId, reason, errorMessage, stackTrace) { 9 const res = await fetch(`${baseUrl}/invocation/${requestId}/error`, {10 method: "POST",11 headers: {12 "Lambda-Runtime-Function-Error-Type": reason,13 },14 body: JSON.stringify({15 errorMessage,16 errorType: reason,17 stackTrace: stackTrace,18 }),19 });20 if (!res.ok) {21 error("invocation error failed", await res.text());22 }23 info("invocation error success", res.status, res.statusText);24 info("headers", res.headers);25 const body = await res.text();26 info("body", body);27}
שימו לב לשורה 14, אני מחזיר אובייקט שמכיל את פרטי השגיאה כגון stack trace, זה בנוסף ל-header.
החליפו את קוד ה־handler בתוך לולאת האירועים בקוד הכולל טיפול בשגיאות.
במקום:
1info("Calling handler");2const response = await handler(event.body, context);3info("Handler complete", response ? response : "with no response");4await invocationResponse(event.requestId, response);
השתמשו בזה:
1try { 2 // Call the handler 3 info("Calling handler"); 4 const response = await handler(event.body, context); 5 info("Handler complete", response ? response : "with no response"); 6 7 await invocationResponse(event.requestId, response); 8} catch (e) { 9 error("Error handling event", e);10 await invocationError(11 event.requestId,12 "Runtime.HandlerError",13 e.message,14 e.stack.split("\n")15 );16 continue;17}
לחצו על
Deploy
ו-Test
שוב. הפעם תוכלו לראות הודעת שגיאה מפורטת.
אנחנו רגע לפני סיום, תרתי משמע, השלב האחרון הוא שלב הסגירה.
שלב הסגירה
שלב הסגירה (shutdown) הוא פשוט: כאשר שירות הלמדה מחליט לסיים את פעילות הקונטיינר, הוא שולח אות SIGTERM ל־runtime.
במימוש שלנו, הקובץ ההרצה (executable) ייסגר בצורה מסודרת ללא צורך בקוד נוסף. עם זאת, אם ה־runtime שלנו מנהל משאבים שדורשים ניקוי תקין, ייתכן ונרצה לממש מנגנון סגירה מפורש.
לסיכום, בנינו Runtime מותאם אישית ללמדה שמריץ קוד LLRT. במהלך הדרך למדנו על המבנה הפנימי של סביבת הריצה של למדה, הבנו את מכונת המצבים של Runtime והתנסינו בעבודה מול ה-API של שירות הלמדה. המימוש שלנו כלל את כל שלבי החיים של Runtime - משלב האתחול, דרך שלב הריצה ועד לשלב הסגירה. היכולת להבין ולממש Runtime מאפשרת לנו לא רק להבין טוב יותר כיצד למדות עובדות מתחת למכסה המנוע, אלא גם לייצר פתרונות מותאמים אישית לצרכים הייחודיים שלנו (ועל כך בפוסטים עתידיים).
תגובות