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

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

תקינות

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

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

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

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

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

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

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

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

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

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

אם אתם צריכים גישה לאחסון לטווח ארוך, מומלץ להשתמש ב-Cloud Run mounting של נפח עם 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.

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

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

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

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

הערות על הפעלה במצב התחלתי (cold start) ועל אתחול

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

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

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

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

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

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

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