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

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

תקינות

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

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

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

אני לא רוצה להתחיל פעילויות ברקע

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

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

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

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

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

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

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

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

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

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

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

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