במאמר הזה נתאר שיטות מומלצות לתכנון, להטמעה, לבדיקה ולפריסה של Cloud Functions.
נכונות
בקטע הזה מתוארות שיטות מומלצות כלליות לתכנון ולהטמעה של Cloud Functions.
כתיבת פונקציות אידמפוטנטיות
הפונקציות צריכות להפיק את אותה תוצאה גם אם קוראים להן כמה פעמים. כך אפשר לנסות שוב להפעיל את הפונקציה אם ההפעלה הקודמת נכשלה באמצע הקוד. מידע נוסף זמין במאמר ניסיון חוזר של פונקציות מבוססות-אירועים.
לא להפעיל פעילויות ברקע
פעילות ברקע היא כל מה שקורה אחרי שהפונקציה מסתיימת.
הפעלת פונקציה מסתיימת כשהפונקציה מחזירה ערך או מסמנת שהיא הסתיימה, למשל על ידי קריאה לארגומנט callback בפונקציות מבוססות-אירועים ב-Node.js. קוד שמופעל אחרי סיום תקין לא יכול לגשת למעבד (CPU) ולא יתקדם.
בנוסף, כשמופעלת קריאה נוספת באותה סביבה, פעילות הרקע ממשיכה ומשבשת את הקריאה החדשה. זה עלול לגרום להתנהגות לא צפויה ולשגיאות שקשה לאבחן. גישה לרשת אחרי שפונקציה מסתיימת בדרך כלל מובילה לאיפוס החיבורים (קוד שגיאה ECONNRESET).
לרוב אפשר לזהות פעילות ברקע ביומנים של קריאות ספציפיות, על ידי חיפוש של כל מה שנרשם ביומן אחרי השורה שבה כתוב שהקריאה הסתיימה. לפעמים פעילות ברקע יכולה להיות מוסתרת עמוק יותר בקוד, במיוחד כשיש פעולות אסינכרוניות כמו קריאות חוזרות (callbacks) או טיימרים. בודקים את הקוד כדי לוודא שכל הפעולות האסינכרוניות מסתיימות לפני סיום הפונקציה.
תמיד למחוק קבצים זמניים
האחסון בדיסק המקומי בספרייה הזמנית הוא מערכת קבצים בזיכרון. קבצים שאתם כותבים צורכים זיכרון שזמין לפונקציה, ולפעמים נשמרים בין הפעלות. אם לא תמחקו את הקבצים האלה באופן מפורש, בסופו של דבר עלולה להופיע שגיאת חוסר בזיכרון ואחריה הפעלה קרה.
כדי לראות את הזיכרון שפונקציה מסוימת משתמשת בו, בוחרים אותה ברשימת הפונקציות במסוף Google Cloud ובוחרים בתרשים שימוש בזיכרון.
אם אתם צריכים גישה לאחסון לטווח ארוך, כדאי להשתמש בCloud Run עם Cloud Storage או בנפחי NFS.
כדי להפחית את דרישות הזיכרון כשמעבדים קבצים גדולים, אפשר להשתמש בצינור עיבוד נתונים. לדוגמה, אפשר לעבד קובץ ב-Cloud Storage על ידי יצירת זרם קריאה, העברה שלו דרך תהליך מבוסס-זרם וכתיבת זרם הפלט ישירות ל-Cloud Storage.
Functions Framework
כדי לוודא שאותן יחסי תלות מותקנות באופן עקבי בסביבות שונות, מומלץ לכלול את ספריית Functions Framework במנהל החבילות ולהצמיד את יחסי התלות לגרסה ספציפית של Functions Framework.
כדי לעשות את זה, צריך לכלול את הגרסה המועדפת בקובץ הנעילה הרלוונטי (לדוגמה, package-lock.json ל-Node.js או requirements.txt ל-Python).
אם Functions Framework לא מופיע במפורש כתלות, הוא יתווסף באופן אוטומטי במהלך תהליך ה-build באמצעות הגרסה העדכנית ביותר שזמינה.
כלים
בקטע הזה מוסבר איך להשתמש בכלים כדי להטמיע את Cloud Functions, לבדוק אותו ולתקשר איתו.
פיתוח מקומי
פריסת הפונקציה לוקחת קצת זמן, ולכן בדרך כלל מהיר יותר לבדוק את קוד הפונקציה באופן מקומי.
מפתחי Firebase יכולים להשתמש באמולטור Cloud Functions של Firebase CLI.איך להימנע מזמן קצוב לתפוגה של פריסה במהלך האתחול
אם פריסת הפונקציה נכשלת עם שגיאת זמן קצוב לתפוגה, כנראה שהקוד ברמת הפונקציה הגלובלית לוקח יותר מדי זמן לביצוע במהלך תהליך הפריסה.
ל-CLI Firebase יש זמן קצוב כברירת מחדל לגילוי הפונקציות במהלך הפריסה. אם לוגיקת האתחול בקוד המקור של הפונקציות (טעינת מודולים, ביצוע קריאות לרשת וכו') חורגת מזמן הקצוב לתפוגה, יכול להיות שהפריסה תיכשל.
כדי למנוע את הזמן הקצוב לתפוגה, אפשר להשתמש באחת מהאסטרטגיות הבאות:
(מומלץ) להשתמש ב-onInit() כדי לדחות את ההפעלה
משתמשים ב-hook onInit() כדי להימנע מהרצת קוד האתחול במהלך הפריסה. הקוד ב-hook של onInit() יפעל רק כשהפונקציה תיפרס בפונקציות Cloud Run, ולא במהלך תהליך הפריסה עצמו.
Node.js
const { onInit } = require('firebase-functions/v2/core'); const { onRequest } = require('firebase-functions/v2/https'); // Example of a slow initialization task function slowInitialization() { // Simulate a long-running operation (e.g., loading a large model, network request). return new Promise(resolve => { setTimeout(() => { console.log("Slow initialization complete"); resolve("Initialized Value"); }, 20000); // Simulate a 20-second delay }); } let initializedValue; onInit(async () => { initializedValue = await slowInitialization(); }); exports.myFunction = onRequest((req, res) => { // Access the initialized value. It will be ready after the first invocation. res.send(`Value: ${initializedValue}`); });
Python
from firebase_functions.core import init from firebase_functions import https_fn import time # Example of a slow initialization task def _slow_initialization(): time.sleep(20) # Simulate a 20-second delay print("Slow initialization complete") return "Initialized Value" _initialized_value = None @init def initialize(): global _initialized_value _initialized_value = _slow_initialization() @https_fn.on_request() def my_function(req: https_fn.Request) -> https_fn.Response: # Access the initialized value. It will be ready after the first invocation. return https_fn.Response(f"Value: {_initialized_value}")
(חלופה) הגדלת הזמן הקצוב לתפוגה של הגילוי
אם אתם לא יכולים לארגן את הקוד מחדש כדי להשתמש ב-onInit(), אתם יכולים להגדיל את זמן קצוב לתפוגה של ה-CLI באמצעות משתנה הסביבה FUNCTIONS_DISCOVERY_TIMEOUT:
$ export FUNCTIONS_DISCOVERY_TIMEOUT=30
$ firebase deploy --only functions
שימוש ב-Sendgrid לשליחת אימיילים
ב-Cloud Functions אין אפשרות ליצור חיבורים יוצאים ביציאה 25, ולכן אי אפשר ליצור חיבורים לא מאובטחים לשרת SMTP. הדרך המומלצת לשלוח אימיילים היא להשתמש בשירות של צד שלישי כמו SendGrid. במדריך שליחת אימייל ממכונה של Google Compute Engine תוכלו למצוא אפשרויות נוספות לשליחת אימייל.
ביצועים
בקטע הזה מתוארות שיטות מומלצות לאופטימיזציה של הביצועים.
הימנעו ממספר נמוך של משתמשים בו-זמנית
הפעלות במצב התחלתי (cold start) הן יקרות, ולכן היכולת להשתמש מחדש במכונות שהופעלו לאחרונה במהלך עלייה חדה היא אופטימיזציה מצוינת לטיפול בעומס. הגבלת מספר הפעולות בו-זמנית מגבילה את האופן שבו אפשר להשתמש במכונות קיימות, ולכן גורמת ליותר הפעלות במצב התחלתי (cold start).
הגדלת מספר הבקשות בו-זמנית עוזרת לדחות מספר בקשות לכל מכונה, וכך קל יותר להתמודד עם עומסים פתאומיים.שימוש חכם ביחסי תלות
מכיוון שהפונקציות הן בלי שמירת מצב, סביבת ההפעלה בדרך כלל מאותחלת מאפס (במהלך מה שנקרא הפעלה במצב התחלתי (cold start)). כשמתרחשת הפעלה קרה, ההקשר הגלובלי של הפונקציה נבדק.
אם הפונקציות מייבאות מודולים, זמן הטעינה של המודולים האלה יכול להוסיף לזמן האחזור של הקריאה במהלך התחלה קרה. כדי לצמצם את זמן האחזור הזה, וגם את הזמן שנדרש לפריסת הפונקציה, צריך לטעון את יחסי התלות בצורה נכונה ולא לטעון יחסי תלות שהפונקציה לא משתמשת בהם.
שימוש במשתנים גלובליים כדי להשתמש שוב באובייקטים בקריאות עתידיות
אין ערובה לכך שהמצב של פונקציה יישמר להפעלות עתידיות. עם זאת, Cloud Functions ממחזרת לעיתים קרובות את סביבת ההפעלה של קריאה קודמת. אם תצהירו על משתנה בהיקף גלובלי, תוכלו להשתמש בערך שלו בקריאות הבאות בלי לחשב אותו מחדש.
כך אפשר לשמור במטמון אובייקטים שעלולים להיות יקרים ליצירה מחדש בכל הפעלה של פונקציה. העברת אובייקטים כאלה מגוף הפונקציה להיקף הגלובלי עשויה לשפר משמעותית את הביצועים. בדוגמה הבאה, אובייקט כבד נוצר רק פעם אחת לכל מכונה של פונקציה, ומשותף בין כל הקריאות לפונקציה שמגיעות למכונה הנתונה:
Node.js
console.log('Global scope'); const perInstance = heavyComputation(); const functions = require('firebase-functions'); exports.function = functions.https.onRequest((req, res) => { console.log('Function invocation'); const perFunction = lightweightComputation(); res.send(`Per instance: ${perInstance}, per function: ${perFunction}`); });
Python
import time from firebase_functions import https_fn # Placeholder def heavy_computation(): return time.time() # Placeholder def light_computation(): return time.time() # Global (instance-wide) scope # This computation runs at instance cold-start instance_var = heavy_computation() @https_fn.on_request() def scope_demo(request): # Per-function scope # This computation runs every time this function is called function_var = light_computation() return https_fn.Response(f"Instance: {instance_var}; function: {function_var}")
פונקציית ה-HTTP הזו מקבלת אובייקט בקשה (flask.Request) ומחזירה את טקסט התגובה, או כל קבוצת ערכים שאפשר להפוך לאובייקט Response באמצעות make_response.
חשוב במיוחד לשמור במטמון חיבורים לרשת, הפניות לספריות ואובייקטים של לקוחות API בהיקף גלובלי. דוגמאות מופיעות במאמר אופטימיזציה של הרשת.
הפחתת ההפעלות במצב התחלתי (cold start) על ידי הגדרת מספר מינימלי של מכונות
כברירת מחדל, Cloud Functions משנה את מספר המכונות בהתאם למספר הבקשות הנכנסות. כדי לשנות את התנהגות ברירת המחדל הזו, אפשר להגדיר מספר מינימלי של מכונות ש-Cloud Functions צריך להשאיר מוכנות לטיפול בבקשות. הגדרת מספר מינימלי של מכונות מפחיתה את ההפעלות במצב התחלתי (cold start) של האפליקציה. אם האפליקציה רגישה לזמן אחזור, מומלץ להגדיר מספר מינימלי של מכונות ולהשלים את האתחול בזמן הטעינה.
למידע נוסף על אפשרויות זמן הריצה האלה, קראו את המאמר שליטה בהתנהגות של התאמה לעומס.הערות לגבי הפעלה במצב התחלתי (cold start) ואיניציאליזציה
האתחול הגלובלי מתרחש בזמן הטעינה. בלעדיה, הבקשה הראשונה תצטרך להשלים את האתחול ולטעון מודולים, וכתוצאה מכך זמן האחזור יהיה ארוך יותר.
עם זאת, לאתחול גלובלי יש גם השפעה על הפעלות במצב התחלתי (cold start). כדי לצמצם את ההשפעה הזו, כדאי להפעיל רק את מה שנדרש לבקשה הראשונה, כדי שהזמן שלוקח לבקשה הראשונה יהיה קצר ככל האפשר.
זה חשוב במיוחד אם הגדרתם מספר מינימלי של מכונות, כמו שמתואר למעלה, לפונקציה שרגישה לזמן אחזור. במקרה כזה, אם משלימים את תהליך האיניציאליזציה בזמן הטעינה ושומרים נתונים שימושיים במטמון, לא צריך לעשות את זה בבקשה הראשונה והיא מטופלת עם זמן אחזור קצר.
אם מאתחלים משתנים בהיקף גלובלי, בהתאם לשפה, זמני אתחול ארוכים יכולים לגרום לשתי התנהגויות: - בשילובים מסוימים של שפות וספריות אסינכרוניות, ה-framework של הפונקציה יכול לפעול באופן אסינכרוני ולחזור מיד, וכך לגרום לקוד להמשיך לפעול ברקע, מה שעלול לגרום לבעיות כמו חוסר יכולת לגשת למעבד (CPU). כדי למנוע את זה, צריך לחסום אתחול של מודולים, כמו שמתואר בהמשך. כך גם אפשר לוודא שהבקשות לא יטופלו עד שההפעלה תושלם. - מצד שני, אם ההפעלה היא סינכרונית, זמן ההפעלה הארוך יגרום להפעלות קרות ארוכות יותר, מה שעלול להיות בעייתי במיוחד עם פונקציות בו-זמניות נמוכות במהלך עומסים.
דוגמה לחימום מראש של ספריית Node.js אסינכרונית
Node.js עם Firestore הוא דוגמה לספריית Node.js אסינכרונית. כדי לנצל את היתרונות של min_instances, הקוד הבא משלים את הטעינה והאתחול בזמן הטעינה, וחוסם את טעינת המודול.
נעשה שימוש ב-TLA, כלומר נדרש ES6, באמצעות סיומת .mjs לקוד node.js או הוספת type: module לקובץ package.json.
{ "main": "main.js", "type": "module", "dependencies": { "@google-cloud/firestore": "^7.10.0", "@google-cloud/functions-framework": "^3.4.5" } }
Node.js
import Firestore from '@google-cloud/firestore'; import * as functions from '@google-cloud/functions-framework'; const firestore = new Firestore({preferRest: true}); // Pre-warm firestore connection pool, and preload our global config // document in cache. In order to ensure no other request comes in, // block the module loading with a synchronous global request: const config = await firestore.collection('collection').doc('config').get(); functions.http('fetch', (req, res) => { // Do something with config and firestore client, which are now preloaded // and will execute at lower latency. });
דוגמאות להפעלה גלובלית
Node.js
const functions = require('firebase-functions'); let myCostlyVariable; exports.function = functions.https.onRequest((req, res) => { doUsualWork(); if(unlikelyCondition()){ myCostlyVariable = myCostlyVariable || buildCostlyVariable(); } res.status(200).send('OK'); });
Python
from firebase_functions import https_fn # Always initialized (at cold-start) non_lazy_global = file_wide_computation() # Declared at cold-start, but only initialized if/when the function executes lazy_global = None @https_fn.on_request() def lazy_globals(request): global lazy_global, non_lazy_global # This value is initialized only if (and when) the function is called if not lazy_global: lazy_global = function_specific_computation() return https_fn.Response(f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}.")
פונקציית ה-HTTP הזו משתמשת במשתנים גלובליים שמאותחלים באופן עצל (lazy initialization). היא מקבלת אובייקט בקשה (flask.Request) ומחזירה את טקסט התגובה, או כל קבוצת ערכים שאפשר להפוך לאובייקט Response באמצעות make_response.
זה חשוב במיוחד אם מגדירים כמה פונקציות בקובץ אחד, ופונקציות שונות משתמשות במשתנים שונים. אם לא משתמשים באתחול עצל, יכול להיות שתבזבזו משאבים על משתנים שמאותחלים אבל לא נעשה בהם שימוש.
מקורות מידע נוספים
מידע נוסף על אופטימיזציה של הביצועים זמין בסרטון Google Cloud Performance Atlas Cloud Functions אתחול קר.