טיפים & טריקים

במסמך הזה מתוארות שיטות מומלצות לתכנון, להטמעה, לבדיקה ולפריסה של Cloud Functions.

תקינות

בקטע הזה מתוארות שיטות מומלצות כלליות לתכנון ולהטמעה של Cloud Functions.

כתיבת פונקציות אידמפוטנטיות

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

לא להפעיל פעילויות ברקע

פעילות ברקע היא כל מה שקורה אחרי שהפונקציה מסתיימת. קריאה לפונקציה מסתיימת ברגע שהפונקציה מחזירה ערכים או מסמנת שהיא הושלמה בדרך אחרת, למשל על ידי קריאה לארגומנט callback בפונקציות מבוססות-אירועים של Node.js. כל קוד שרץ אחרי סיום חינני לא יכול לגשת למעבד (CPU) ולא יקרה דבר.

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

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

תמיד למחוק קבצים זמניים

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

כדי לראות את נפח הזיכרון שבו נעשה שימוש בפונקציה מסוימת, בוחרים אותה ברשימת הפונקציות במסוף Google Cloud ובוחרים בתרשים Memory usage.

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

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

Functions Framework

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

כדי לעשות זאת, צריך לכלול את הגרסה המועדפת בקובץ הנעילה הרלוונטי (לדוגמה, package-lock.json עבור Node.js או requirements.txt עבור Python).

כלים

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

פיתוח מקומי

פריסת הפונקציה אורכת קצת זמן, ולכן בדרך כלל מהר יותר לבדוק את הקוד של הפונקציה באופן מקומי.

מפתחי Firebase יכולים להשתמש באמולטור Cloud Functions של Firebase CLI.

שימוש ב-Sendgrid לשליחת אימיילים

Cloud Functions לא מאפשר חיבורים יוצאים ביציאה 25, ולכן אי אפשר ליצור חיבורים לא מאובטחים לשרת SMTP. הדרך המומלצת לשליחת אימיילים היא באמצעות SendGrid. תוכלו למצוא אפשרויות נוספות לשליחת אימייל במדריך שליחת אימייל ממכונה של Google Compute Engine.

ביצועים

בקטע הזה מתוארות שיטות מומלצות לאופטימיזציה של הביצועים.

שימוש חכם ביחסי תלות

מכיוון שהפונקציות ללא שמירת מצב, סביבת הביצוע בדרך כלל מאותחלת מחדש (במהלך מה שנקרא הפעלה במצב התחלתי (cold start)). כשמתרחש התחלה קרה, מתבצעת הערכה של ההקשר הגלובלי של הפונקציה.

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

שימוש במשתנים גלובליים לשימוש חוזר באובייקטים בהפעלות עתידיות

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

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

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 ברמת ה-global. דוגמאות מופיעות בקטע אופטימיזציה של רשתות.

ביצוע אתחול עצל של משתנים גלובליים

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

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 הזו משתמשת במשתנים גלובליים שמאותחלים באיטרציה. היא לוקחת אובייקט בקשה (flask.Request), ומחזירה את טקסט התשובה או כל קבוצת ערכים שאפשר להפוך לאובייקט Response באמצעות make_response.

חשוב במיוחד לעשות זאת אם מגדירים כמה פונקציות בקובץ אחד, ופונקציות שונות משתמשות במשתנים שונים. אם לא משתמשים בהפעלה איטית (lazy initialization), יכול להיות שתתבזבזו משאבים על משתנים שהוגדרו אבל אף פעם לא נעשה בהם שימוש.

צמצום ההפעלות הראשונות על ידי הגדרת מספר מינימלי של מכונות

כברירת מחדל, Cloud Functions משתנה בהתאם למספר הבקשות הנכנסות. אפשר לשנות את התנהגות ברירת המחדל הזו על ידי הגדרת מספר מינימלי של מכונות ש-Cloud Functions צריך לשמור מוכנות כדי לטפל בבקשות. הגדרת מספר מינימלי של מכונות מפחיתה את ההפעלה במצב התחלתי (cold start) של האפליקציה. אם האפליקציה שלכם רגישה לזמן אחזור, מומלץ להגדיר מספר מינימלי של מכונות.

מידע נוסף על האפשרויות האלה בסביבת זמן הריצה זמין במאמר שליטה בהתנהגות ההתאמה לעומס.

משאבים נוספים

מידע נוסף על אופטימיזציה של הביצועים זמין בסרטון Cloud Functions זמן הפעלה מחדש (Cold Boot) של Google Cloud Performance Atlas.