نکات و ترفندها

این سند روال‌های مطلوب برای طراحی، پیاده‌سازی، آزمایش، و استقرار Cloud Functions را شرح می‌دهد.

صحت

این بخش روال‌های مطلوب عمومی را برای طراحی و پیاده‌سازی Cloud Functions شرح می‌دهد.

نوشتن توابع خودتوان

توابع شما باید نتیجه یکسانی تولید کنند، حتی اگر چندین بار فراخوانی شوند. این کار به شما امکان می‌دهد اگر فراخوانی قبلی در بخشی از کد شما ناموفق بود، فراخوانی را دوباره امتحان کنید. برای اطلاعات بیشتر، به امتحان مجدد توابع رویدادمحور مراجعه کنید.

فعالیت‌های پس‌زمینه شروع نشود

فعالیت پس‌زمینه هر چیزی است که پس‌از پایان تابع شما اتفاق می‌افتد. فراخوانی تابع زمانی به‌پایان می‌رسد که تابع برگردد یا به‌نحوی دیگر تکمیل را نشان دهد، مثلاً با فراخوانی آرگومان callback در توابع رویدادمحور Node.js. هر کدی که پس‌از خاتمه عادی اجرا شود نمی‌تواند به CPU دسترسی داشته باشد و هیچ پیشرفتی نخواهد داشت.

علاوه‌براین، وقتی فراخوانی بعدی در همان محیط اجرا می‌شود، فعالیت پس‌زمینه‌تان ازسر گرفته می‌شود و با فراخوانی جدید تداخل پیدا می‌کند. این کار ممکن است منجر به رفتار غیرمنتظره و خطاهایی شود که تشخیص آن‌ها دشوار است. دسترسی به شبکه پس‌از پایان یافتن عملکرد معمولاً منجر به بازنشانی اتصالات می‌شود (کد خطای ECONNRESET).

فعالیت پس‌زمینه را اغلب می‌توان در گزارش‌های فراخوانی‌های فردی شناسایی کرد، با پیدا کردن هر چیزی که بعداز خطی که می‌گوید فراخوانی تمام شده است، ثبت شده باشد. فعالیت پس‌زمینه گاهی اوقات می‌تواند در عمق بیشتری از کد پنهان شود، به‌ویژه زمانی که عملیات ناهمگام مانند فراخوانی‌های برگشتی یا زمان‌سنج‌ها وجود داشته باشند. کدتان را بازبینی کنید تا مطمئن شوید همه عملیات‌های ناهمگام پیش‌از پایان دادن به تابع تکمیل می‌شوند.

همیشه فایل‌های موقت حذف شود

فضای ذخیره‌سازی دیسک محلی در فهرست راهنمای موقت، یک سیستم فایل درون‌حافظه‌ای است. فایل‌هایی که می‌نویسید حافظه دردسترس تابع شما را مصرف می‌کنند و گاهی بین فراخوانی‌ها باقی می‌مانند. عدم حذف صریح این فایل‌ها ممکن است درنهایت منجر به خطای کمبود حافظه و به‌دنبال آن راه‌اندازی سرد شود.

می‌توانید حافظه استفاده‌شده توسط یک تابع خاص را با انتخاب آن در فهرست توابع در کنسول 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 به‌طور صریح به‌عنوان وابستگی فهرست نشده باشد، به‌طور خودکار درطول فرایند ساخت بااستفاده از جدیدترین نسخه دردسترس اضافه خواهد شد.

ابزارها

این بخش دستورالعمل‌هایی درباره نحوه استفاده از ابزارها برای پیاده‌سازی، آزمایش، و تعامل با Cloud Functions ارائه می‌دهد.

توسعه محلی

استقرار تابع کمی زمان می‌برد، بنابراین اغلب سریع‌تر است که کد تابع خود را به‌صورت محلی آزمایش کنید.

توسعه‌دهندگان Firebase می‌توانند از شبیه‌ساز Cloud Functions واسط خط فرمان Firebase استفاده کنند.

جلوگیری از اتمام مهلت استقرار درطول مقداردهی اولیه

اگر استقرار تابع شما با خطای اتمام مهلت مواجه شود، احتمالاً به این معنی است که اجرای کد دامنه سراسری تابع شما درطول فرایند استقرار بیش‌ازحد طول می‌کشد.

Firebase CLI دارای یک زمان استراحت پیش‌فرض برای کشف توابع شما در طول استقرار است. اگر منطق مقداردهی اولیه در کد منبع توابع شما (بار کردن مدول‌ها، برقراری تماس‌های شبکه، و غیره) از این زمان استراحت فراتر رود، ممکن است استقرار ناموفق باشد.

برای جلوگیری از اتمام مهلت، از یکی از استراتژی‌های زیر استفاده کنید:

از قلاب onInit() استفاده کنید تا کد مقداردهی اولیه درطول استقرار اجرا نشود. کد داخل قلاب 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}`);
});

پایتون

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 اجازه اتصالات خروجی روی درگاه ۲۵ را نمی‌دهد، بنابراین نمی‌توانید اتصالات غیرامن به سرور SMTP برقرار کنید. روش توصیه‌شده برای ارسال ایمیل استفاده از سرویس شخص ثالثی مانند SendGrid است. می‌توانید گزینه‌های دیگر برای ارسال ایمیل را در آموزش ارسال ایمیل از نمونه برای Google Compute Engine پیدا کنید.

عملکرد

این بخش روال‌های مطلوب برای بهینه‌سازی عملکرد را شرح می‌دهد.

از همزمانی کم اجتناب کنید

چون شروع سرد پرهزینه است، امکان استفاده مجدد از نمونه‌های اخیراً شروع‌شده درطول افزایش ناگهانی ترافیک، بهینه‌سازی عالی‌ای برای مدیریت بار است. محدود کردن همزمانی، نحوه استفاده از نمونه‌های موجود را محدود می‌کند و درنتیجه باعث شروع سرد بیشتر می‌شود.

افزایش هم‌زمانی به تعویق انداختن چندین درخواست در هر نمونه کمک می‌کند و مدیریت جهش‌های بار را آسان‌تر می‌کند.

از وابستگی‌ها عاقلانه استفاده کنید

چون توابع بدون حالت هستند، محیط اجرا اغلب از ابتدا مقداردهی اولیه می‌شود (درطول آنچه به‌عنوان راه‌اندازی سرد شناخته می‌شود). وقتی شروع سرد رخ می‌دهد، زمینه سراسری تابع ارزیابی می‌شود.

اگر توابع شما ماژول‌ها را وارد کنند، زمان بارگیری آن ماژول‌ها می‌تواند به تأخیر فراخوانی درطول راه‌اندازی سرد اضافه شود. با بار کردن صحیح وابستگی‌ها و بار نکردن وابستگی‌هایی که تابع شما استفاده نمی‌کند، می‌توانید این تأخیر و همچنین زمان لازم برای استقرار تابع خود را کاهش دهید.

استفاده از متغیرهای سراسری برای استفاده مجدد از اشیاء در فراخوانی‌های آینده

هیچ تضمینی وجود ندارد که وضعیت تابع برای فراخوانی‌های آینده حفظ شود. بااین‌حال، 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}`);
});

پایتون

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) می‌گیرد و نوشتار پاسخ یا هر مجموعه‌ای از مقادیر را که می‌توانند بااستفاده از make_response به شیء Response تبدیل شوند برمی‌گرداند.

ذخیره کردن اتصال‌های شبکه، ارجاع‌های کتابخانه، و اشیای کارخواه میانای برنامه‌سازی کاربردی در دامنه سراسری در حافظه نهان بسیار مهم است. برای مشاهده مثال‌ها، بهینه‌سازی شبکه‌سازی را ببینید.

با تنظیم حداقل تعداد نمونه‌ها، شروع‌های سرد را کاهش دهید

به‌طور پیش‌فرض، Cloud Functions تعداد نمونه‌ها را براساس تعداد درخواست‌های ورودی مقیاس‌بندی می‌کند. می‌توانید این رفتار پیش‌فرض را با تنظیم حداقل تعداد نمونه‌هایی که Cloud Functions باید برای ارائه درخواست‌ها آماده نگه دارد تغییر دهید. تنظیم حداقل تعداد نمونه‌ها، شروع سرد برنامه‌تان را کاهش می‌دهد. توصیه می‌کنیم حداقل تعداد نمونه‌ها را تنظیم کنید و اگر برنامه‌تان به تأخیر حساس است، مقداردهی اولیه را در زمان بارگیری تکمیل کنید.

برای اطلاعات بیشتر درباره این گزینه‌های زمان اجرا، کنترل رفتار مقیاس‌بندی را ببینید.

نکاتی درباره شروع سرد و مقداردهی اولیه

مقداردهی اولیه سراسری در زمان بار کردن اتفاق می‌افتد. بدون آن، اولین درخواست باید مقداردهی اولیه را تکمیل کند و ماژول‌ها را بار کند، که منجر به تأخیر بیشتر می‌شود.

بااین‌حال، مقداردهی اولیه سراسری بر شروع سرد نیز تأثیر می‌گذارد. برای به‌حداقل رساندن این تأثیر، فقط آنچه را که برای اولین درخواست لازم است مقداردهی اولیه کنید تا تأخیر اولین درخواست تا حد امکان پایین نگه داشته شود.

این موضوع به‌ویژه اگر حداقل نمونه‌ها را همان‌طور که در بالا برای تابع حساس به تأخیر شرح داده شد پیکربندی کرده باشید اهمیت دارد. در این سناریو، تکمیل مقداردهی اولیه در زمان بارگیری و ذخیره داده‌های مفید در حافظه نهان تضمین می‌کند که اولین درخواست نیازی به انجام این کار ندارد و با تأخیر کم ارائه می‌شود.

اگر متغیرها را در دامنه سراسری مقداردهی اولیه کنید، بسته به زبان، زمان‌های طولانی مقداردهی اولیه می‌تواند به دو رفتار منجر شود: - برای برخی‌از ترکیب‌های زبان‌ها و کتابخانه‌های ناهمگام، چارچوب تابع می‌تواند به‌صورت ناهمگام اجرا شود و بلافاصله برگردد، که باعث می‌شود کد به اجرا در پس‌زمینه ادامه دهد، که می‌تواند مشکلاتی مانند عدم امکان دسترسی به CPU را ایجاد کند. برای جلوگیری از این مشکل، باید همان‌طور که در زیر شرح داده شده است، در مقداردهی اولیه ماژول مسدود کنید. این کار همچنین تضمین می‌کند که درخواست‌ها تا زمانی که مقداردهی اولیه تکمیل نشده است ارائه نشوند. - ازسوی دیگر، اگر مقداردهی اولیه همگام باشد، زمان طولانی مقداردهی اولیه باعث راه‌اندازی سرد طولانی‌تر می‌شود که می‌تواند به‌ویژه با توابع همزمانی پایین درطول افزایش بار مشکل‌ساز باشد.

نمونه‌ای از پیش‌گرم کردن کتابخانه async 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');
});

پایتون

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) می‌گیرد و نوشتار پاسخ یا هر مجموعه‌ای از مقادیر را که می‌توان بااستفاده از make_response به شیء Response تبدیل کرد برمی‌گرداند.

این موضوع به‌ویژه زمانی اهمیت دارد که چندین تابع را در یک فایل تعریف کنید، و توابع مختلف از متغیرهای مختلف استفاده کنند. مگر اینکه از مقداردهی اولیه تنبل استفاده کنید، ممکن است منابع را برای متغیرهایی که مقداردهی اولیه شده‌اند اما هرگز استفاده نمی‌شوند هدر دهید.

منابع بیشتر

در ویدیو «Google Cloud Performance Atlas» درباره بهینه‌سازی عملکرد بیشتر بدانید Cloud Functions Cold Boot Time.