שדרוג פונקציות של Node.js מדור ראשון לדור שני

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

בדוגמאות שבדף הזה מניחים שאתם משתמשים ב-JavaScript עם מודולים של CommonJS (require ייבוא סגנונות), אבל אותם עקרונות חלים על JavaScript עם ESM (import … from ייבוא סגנונות) ועל TypeScript.

תהליך ההעברה

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

אימות הגרסאות של Firebase CLI ו-firebase-function

מוודאים שמשתמשים לפחות בגרסה 12.00 של Firebase CLI ובגרסה firebase-functions של 4.3.0. כל גרסה חדשה יותר תתמוך בדור השני וגם בדור הראשון.

עדכון ייבוא

ייבוא פונקציות מהדור השני מחבילת המשנה v2 ב-SDK‏ firebase-functions. נתיב הייבוא השונה הזה הוא כל מה ש-Firebase CLI צריך כדי לקבוע אם לפרוס את קוד הפונקציה שלכם כפונקציה מדור ראשון או מדור שני.

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

לפני: דור ראשון

const functions = require("firebase-functions/v1");

אחרי: דור שני

// explicitly import each trigger
const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");

עדכון הגדרות הטריגר

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

הארגומנטים שמועברים לקריאות חוזרות (callback) עבור חלק מהטריגרים השתנו. בדוגמה הזו, שימו לב שהארגומנטים של הקריאה החוזרת onDocumentCreated אוחדו לאובייקט event יחיד. בנוסף, לחלק מהטריגרים יש תכונות חדשות ונוחות להגדרה, כמו האפשרות cors של הטריגר onRequest.

לפני: דור ראשון

const functions = require("firebase-functions/v1");

exports.date = functions.https.onRequest((req, res) => {
  // ...
});

exports.uppercase = functions.firestore
  .document("my-collection/{docId}")
  .onCreate((change, context) => {
    // ...
  });

אחרי: דור שני

const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");

exports.date = onRequest({cors: true}, (req, res) => {
  // ...
});

exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  /* ... */
});

שימוש בהגדרת פרמטרים

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

מעבר לחבילת המשנה params

אם השתמשתם בהגדרת סביבה עם functions.config, אתם יכולים להעביר את ההגדרה הקיימת להגדרה עם פרמטרים.

לפני: דור ראשון

const functions = require("firebase-functions/v1");

exports.date = functions.https.onRequest((req, res) => {
  const date = new Date();
  const formattedDate =
date.toLocaleDateString(functions.config().dateformat);

  // ...
});

אחרי: דור שני

const {onRequest} = require("firebase-functions/v2/https");
const {defineString} = require("firebase-functions/params");

const dateFormat = defineString("DATE_FORMAT");

exports.date = onRequest((req, res) => {
  const date = new Date();
  const formattedDate = date.toLocaleDateString(dateFormat.value());

  // ...
});

הגדרת ערכי פרמטרים

בפעם הראשונה שמבצעים פריסה, Firebase CLI מבקש את כל ערכי הפרמטרים ושומר את הערכים בקובץ dotenv. כדי לייצא את הערכים של functions.config, מריצים את הפקודה firebase functions:config:export.

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

מקרה מיוחד: מפתחות API

מודול params משולב עם Cloud Secret Manager, שמספק בקרת גישה מפורטת לערכים רגישים כמו מפתחות API. מידע נוסף זמין במאמר בנושא פרמטרים סודיים.

לפני: דור ראשון

const functions = require("firebase-functions/v1");

exports.getQuote = functions.https.onRequest(async (req, res) => {
  const quote = await fetchMotivationalQuote(functions.config().apiKey);
  // ...
});

אחרי: דור שני

const {onRequest} = require("firebase-functions/v2/https");
const {defineSecret} = require("firebase-functions/params");

// Define the secret parameter
const apiKey = defineSecret("API_KEY");

exports.getQuote = onRequest(
  // make the secret available to this function
  { secrets: [apiKey] },
  async (req, res) => {
    // retrieve the value of the secret
    const quote = await fetchMotivationalQuote(apiKey.value());
    // ...
  }
);

הגדרת אפשרויות זמן ריצה

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

לפני: דור ראשון

const functions = require("firebase-functions/v1");

exports.date = functions
  .runWith({
    // Keep 5 instances warm for this latency-critical function
    minInstances: 5,
  })
  // locate function closest to users
  .region("asia-northeast1")
  .https.onRequest((req, res) => {
    // ...
  });

exports.uppercase = functions
  // locate function closest to users and database
  .region("asia-northeast1")
  .firestore.document("my-collection/{docId}")
  .onCreate((change, context) => {
    // ...
  });

אחרי: דור שני

const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");
const {setGlobalOptions} = require("firebase-functions/v2");

// locate all functions closest to users
setGlobalOptions({ region: "asia-northeast1" });

exports.date = onRequest({
    // Keep 5 instances warm for this latency-critical function
    minInstances: 5,
  }, (req, res) => {
  // ...
});

exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  /* ... */
});

שימוש במקביליות

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

const {onRequest} = require("firebase-functions/v2/https");

exports.date = onRequest({
    // set concurrency value
    concurrency: 500
  },
  (req, res) => {
    // ...
});

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

ביקורת על השימוש במשתנים גלובליים

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

במהלך השדרוג, אפשר להגדיר את המעבד של הפונקציה ל-gcf_gen1 ולהגדיר את concurrency ל-1 כדי לשחזר את ההתנהגות של דור ראשון:

const {onRequest} = require("firebase-functions/v2/https");

exports.date = onRequest({
    // TEMPORARY FIX: remove concurrency
    cpu: "gcf_gen1",
    concurrency: 1
  },
  (req, res) => {
    // ...
});

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

העברת התנועה לפונקציות החדשות מהדור השני

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

אי אפשר לשדרג פונקציה מדור ראשון לדור שני עם אותו שם ולהריץ את firebase deploy. פעולה כזו תוביל לשגיאה:

Upgrading from GCFv1 to GCFv2 is not yet supported. Please delete your old function or wait for this feature to be ready.

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

  1. משנים את השם של הפונקציה בקוד הפונקציות. לדוגמה, משנים את השם של resizeImage ל-resizeImageSecondGen.
  2. פורסים את הפונקציה כך שגם הפונקציה המקורית מדור ראשון וגם הפונקציה מדור שני יפעלו.
    1. במקרה של טריגרים מסוג HTTP, טריגרים שניתן להפעיל וטריגרים של תור משימות, צריך להתחיל להפנות את כל הלקוחות לפונקציה מהדור השני על ידי עדכון קוד הלקוח בשם או בכתובת ה-URL של הפונקציה מהדור השני.
    2. עם טריגרים ברקע, פונקציות מהדור הראשון ומהדור השני יגיבו לכל אירוע מיד אחרי הפריסה.
  3. אחרי שכל התעבורה מועברת, מוחקים את הפונקציה מהדור הראשון באמצעות הפקודה firebase functions:delete של Firebase CLI.
    1. אופציונלי: משנים את השם של הפונקציה מהדור השני כך שיהיה זהה לשם של הפונקציה מהדור הראשון.