نصائح

يوضّح هذا المستند أفضل الممارسات لتصميم Cloud Functions وتنفيذه واختباره ونشره.

الصحة

يصف هذا القسم أفضل الممارسات العامة لتصميم وتنفيذ Cloud Functions.

كتابة دوال متكررة

يجب أن تنتج الدوال النتيجة نفسها حتى إذا تم استدعاؤها عدة مرات. يتيح لك ذلك إعادة محاولة تنفيذ دالة إذا تعذّر تنفيذها السابق في منتصف الرمز. لمزيد من المعلومات، يُرجى الاطّلاع على إعادة محاولة تنفيذ الدوال المستندة إلى الأحداث.

عدم بدء الأنشطة في الخلفية

يشير النشاط في الخلفية إلى أي شيء يحدث بعد انتهاء وظيفتك. ينتهي استدعاء الدالة عندما تعرض الدالة قيمة أو تشير إلى اكتمالها بطريقة أخرى، مثل استدعاء الوسيطة callback في الدوال المستندة إلى الأحداث في Node.js. لن يتمكّن أي رمز يتم تنفيذه بعد الإنهاء السلس من الوصول إلى وحدة المعالجة المركزية، ولن يحقّق أي تقدّم.

بالإضافة إلى ذلك، عند تنفيذ استدعاء لاحق في البيئة نفسها، سيتم استئناف نشاطك في الخلفية، ما يؤدي إلى حدوث تداخل مع الاستدعاء الجديد. وقد يؤدي ذلك إلى حدوث سلوك غير متوقّع وأخطاء يصعب تشخيصها. يؤدي الوصول إلى الشبكة بعد انتهاء إحدى الدوال عادةً إلى إعادة ضبط الاتصالات (رمز الخطأ ECONNRESET).

يمكن غالبًا رصد النشاط في الخلفية في السجلات من عمليات الاستدعاء الفردية، وذلك من خلال العثور على أي شيء يتم تسجيله بعد السطر الذي يشير إلى انتهاء عملية الاستدعاء. قد يكون النشاط في الخلفية مخفيًا في مكان أعمق في الرمز، خاصةً عند توفّر عمليات غير متزامنة، مثل عمليات معاودة الاتصال أو المؤقتات. راجِع الرمز للتأكّد من انتهاء جميع العمليات غير المتزامنة قبل إنهاء الدالة.

حذف الملفات المؤقتة دائمًا

مساحة التخزين على القرص المحلي في الدليل المؤقت هي نظام ملفات في الذاكرة. تستهلك الملفات التي تكتبها الذاكرة المتاحة للدالة، وفي بعض الأحيان، تظل متاحة بين عمليات الاستدعاء. قد يؤدي عدم حذف هذه الملفات بشكل صريح في النهاية إلى حدوث خطأ في الذاكرة وبدء تشغيل بارد لاحق.

يمكنك الاطّلاع على الذاكرة التي تستخدمها دالة فردية من خلال اختيارها في قائمة الدوال في Google Cloud Console واختيار الرسم البياني استخدام الذاكرة.

إذا كنت بحاجة إلى الوصول إلى مساحة تخزين طويلة الأمد، ننصحك باستخدام Cloud Run عمليات ربط وحدات التخزين مع Cloud Storage أو وحدات تخزين NFS.

يمكنك تقليل متطلبات الذاكرة عند معالجة الملفات الأكبر حجمًا باستخدام تقنية تقسيم المعالجة. على سبيل المثال، يمكنك معالجة ملف على Cloud Storage من خلال إنشاء مصدر بيانات للقراءة، وتمريره عبر عملية مستندة إلى مصدر البيانات، وكتابة مصدر البيانات الناتج مباشرةً إلى Cloud Storage.

إطار عمل الدوال

لضمان تثبيت التبعيات نفسها بشكل متّسق في جميع البيئات، ننصحك بتضمين مكتبة Functions Framework في مدير الحزم وتثبيت التبعية على إصدار معيّن من Functions Framework.

لإجراء ذلك، أدرِج الإصدار المفضّل في ملف القفل ذي الصلة (على سبيل المثال، package-lock.json لـ Node.js أو requirements.txt لـ Python).

إذا لم يتم إدراج Functions Framework بشكل صريح كعنصر تابع، سيتم إضافته تلقائيًا أثناء عملية الإنشاء باستخدام أحدث إصدار متاح.

الأدوات

يقدّم هذا القسم إرشادات حول كيفية استخدام الأدوات لتنفيذ بيانات Cloud Functions واختبارها والتفاعل معها.

التطوير المحلي

يستغرق نشر الدالة بعض الوقت، لذا غالبًا ما يكون اختبار الرمز البرمجي للدالة محليًا أسرع.

يمكن لمطوّري Firebase استخدام أداة المحاكاة Cloud Functions في واجهة سطر الأوامر (CLI) لمنصة Firebase.

تجنُّب انتهاء مهلة النشر أثناء عملية الإعداد

إذا تعذّر نشر الدالة بسبب خطأ انتهاء المهلة، من المحتمل أن يكون ذلك بسبب استغراق رمز النطاق العام للدالة وقتًا طويلاً جدًا في التنفيذ أثناء عملية النشر.

تتضمّن واجهة سطر الأوامر Firebase مهلة تلقائية لاكتشاف الدوال أثناء عملية النشر. إذا تجاوزت منطق التهيئة في الرمز المصدر للدوال (تحميل الوحدات، وإجراء طلبات الشبكة، وما إلى ذلك) هذا المهلة، قد يتعذّر النشر.

لتجنُّب انتهاء المهلة، استخدِم إحدى الاستراتيجيتَين التاليتَين:

استخدِم الخطاف 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}`);
});

Python

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:

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

استخدام Sendgrid لإرسال رسائل إلكترونية

لا يسمح Cloud Functions بالاتصالات الصادرة على المنفذ 25، لذا لا يمكنك إجراء اتصالات غير آمنة بخادم 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}`);
});

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.

من المهم بشكل خاص تخزين بيانات مؤقتة لاتصالات الشبكة ومراجع المكتبات وكائنات عميل واجهة برمجة التطبيقات في النطاق العام. راجِع تحسين الشبكات للاطّلاع على أمثلة.

تقليل عمليات التشغيل على البارد من خلال ضبط الحد الأدنى لعدد المثيلات

تزيد Cloud Functions عدد المثيلات أو تقلّله تلقائيًا استنادًا إلى عدد الطلبات الواردة. يمكنك تغيير هذا السلوك التلقائي من خلال ضبط الحد الأدنى لعدد المثيلات التي يجب أن تحتفظ بها Cloud Functions جاهزة لتلبية الطلبات. يؤدي ضبط الحد الأدنى لعدد المثيلات إلى تقليل عمليات التشغيل على البارد لتطبيقك. ننصحك بتحديد الحد الأدنى لعدد المثيلات وإكمال عملية التهيئة في وقت التحميل إذا كان تطبيقك حساسًا لوقت الاستجابة.

يمكنك الاطّلاع على التحكّم في سلوك تغيير الحجم للحصول على مزيد من المعلومات حول خيارات وقت التشغيل هذه.

ملاحظات حول التشغيل على البارد والتهيئة

يتم إجراء عملية الإعداد العامة في وقت التحميل. وبدونها، سيحتاج الطلب الأول إلى إكمال عملية التهيئة وتحميل الوحدات، ما يؤدي إلى زيادة وقت الاستجابة.

ومع ذلك، يؤثّر الإعداد الأوّلي العام أيضًا في عمليات التشغيل البارد. للحدّ من هذا التأثير، عليك إعداد ما يلزم للطلب الأول فقط، وذلك للحفاظ على أقل وقت استجابة ممكن للطلب الأول.

ويُعدّ ذلك مهمًا بشكل خاص إذا كنت قد ضبطت الحد الأدنى لعدد المثيلات كما هو موضّح أعلاه لدالة حساسة لوقت الاستجابة. في هذه الحالة، يضمن إكمال عملية التهيئة في وقت التحميل وتخزين البيانات المفيدة مؤقتًا عدم الحاجة إلى تنفيذ ذلك في الطلب الأول، كما يضمن تقديم الطلب بزمن استجابة منخفض.

إذا بدأت بتهيئة المتغيرات في النطاق العام، قد تؤدي أوقات التهيئة الطويلة إلى سلوكَين مختلفَين، وذلك حسب اللغة: - بالنسبة إلى بعض مجموعات اللغات والمكتبات غير المتزامنة، يمكن أن تعمل بنية الدالة بشكل غير متزامن وأن تعرض النتيجة على الفور، ما يؤدي إلى استمرار تشغيل الرمز البرمجي في الخلفية، وقد يتسبب ذلك في حدوث مشاكل مثل تعذُّر الوصول إلى وحدة المعالجة المركزية. ولتجنُّب ذلك، عليك الحظر عند بدء وحدة الإعلان كما هو موضّح أدناه. يضمن ذلك أيضًا عدم عرض الطلبات إلى أن تكتمل عملية الإعداد. - من ناحية أخرى، إذا كانت عملية التهيئة متزامنة، سيؤدي وقت التهيئة الطويل إلى عمليات بدء تشغيل بارد أطول، ما قد يمثّل مشكلة، خاصةً مع الدوال التي تتضمّن عددًا منخفضًا من عمليات التنفيذ المتزامنة أثناء ارتفاع معدّل التحميل.

مثال على التسخين المُسبَق لمكتبة غير متزامنة في 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.

ويكون ذلك مهمًا بشكل خاص إذا حدّدت عدة دوال في ملف واحد، وكانت الدوال المختلفة تستخدم متغيرات مختلفة. ما لم تستخدم عملية التهيئة المؤجّلة، قد تستهلك موارد على متغيّرات تمت تهيئتها ولكن لم يتم استخدامها مطلقًا.

مراجع إضافية

يمكنك الاطّلاع على مزيد من المعلومات حول تحسين الأداء في الفيديو "Google Cloud Performance Atlas" Cloud Functions وقت بدء التشغيل البارد.