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

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

درستی

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

توابع خودتوان را بنویسید

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

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

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

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

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

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

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

شما می‌توانید با انتخاب یک تابع در لیست توابع موجود در کنسول Google Cloud و انتخاب نمودار میزان استفاده از حافظه، میزان حافظه استفاده شده توسط آن را مشاهده کنید.

اگر به دسترسی به فضای ذخیره‌سازی بلندمدت نیاز دارید، استفاده از نگهدارنده‌های فضای ذخیره‌سازی Cloud Run با Cloud Storage یا فضای ذخیره‌سازی NFS را در نظر بگیرید.

شما می‌توانید هنگام پردازش فایل‌های بزرگتر با استفاده از pipelining، نیازهای حافظه را کاهش دهید. به عنوان مثال، می‌توانید با ایجاد یک جریان خواندن، عبور دادن آن از یک فرآیند مبتنی بر جریان و نوشتن مستقیم جریان خروجی در Cloud Storage، یک فایل را در Cloud Storage پردازش کنید.

چارچوب توابع

برای اطمینان از اینکه وابستگی‌های یکسان به طور مداوم در محیط‌های مختلف نصب می‌شوند، توصیه می‌کنیم کتابخانه Functions Framework را در مدیریت بسته خود قرار دهید و وابستگی را به نسخه خاصی از Functions Framework پین کنید.

برای انجام این کار، نسخه مورد نظر خود را در فایل قفل مربوطه (برای مثال، package-lock.json برای Node.js یا requirements.txt برای پایتون) قرار دهید.

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

ابزارها

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

توسعه محلی

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

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

جلوگیری از وقفه‌های استقرار در طول راه‌اندازی اولیه

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

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

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

برای جلوگیری از اجرای کد مقداردهی اولیه در حین استقرار، از قلاب onInit() استفاده کنید. کد داخل قلاب onInit() فقط زمانی اجرا می‌شود که تابع در توابع Cloud Run مستقر شده باشد، نه در طول فرآیند استقرار.

نود جی اس

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() بازسازی کنید، می‌توانید با استفاده از متغیر محیطی FUNCTIONS_DISCOVERY_TIMEOUT ، زمان استقرار CLI را افزایش دهید:

$ export FUNCTIONS_DISCOVERY_TIMEOUT=30
$ firebase deploy --only functions

استفاده از Sendgrid برای ارسال ایمیل

Cloud Functions اجازه اتصال خروجی روی پورت ۲۵ را نمی‌دهند، بنابراین نمی‌توانید اتصال‌های غیر امن به سرور SMTP برقرار کنید. روش پیشنهادی برای ارسال ایمیل، استفاده از یک سرویس شخص ثالث مانند SendGrid است. می‌توانید گزینه‌های دیگر برای ارسال ایمیل را در آموزش ارسال ایمیل از یک نمونه برای Google Compute Engine پیدا کنید.

عملکرد

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

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

از آنجا که شروع سرد (cold start) پرهزینه است، امکان استفاده مجدد از نمونه‌های اخیراً شروع شده در طول یک دوره اوج (spike) یک بهینه‌سازی عالی برای مدیریت بار است. محدود کردن همزمانی، نحوه استفاده از نمونه‌های موجود را محدود می‌کند، بنابراین باعث شروع سرد بیشتر می‌شود.

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

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

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

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

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

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

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

نود جی اس

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 تبدیل شوند، برمی‌گرداند.

به ویژه مهم است که اتصالات شبکه، ارجاعات کتابخانه و اشیاء کلاینت API را در محدوده سراسری ذخیره (cache) کنید. برای مثال‌ها به بخش بهینه‌سازی شبکه مراجعه کنید.

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

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

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

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

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

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

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

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

مثالی از پیش‌گرم کردن یک کتابخانه‌ی async node.js

Node.js با Firestore نمونه‌ای از کتابخانه async 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"
  }
}

نود جی اس

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.
});

نمونه‌هایی از مقداردهی اولیه سراسری

نود جی اس

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 تبدیل شوند، برمی‌گرداند.

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

منابع اضافی

برای کسب اطلاعات بیشتر در مورد بهینه‌سازی عملکرد، ویدیوی «اطلس عملکرد گوگل کلود» با عنوان «زمان بوت سرد Cloud Functions را ببینید.