Conseils et astuces

Ce document décrit les bonnes pratiques à suivre pour concevoir, implémenter, tester et déployer Cloud Functions.

Exactitude

Cette section décrit les bonnes pratiques générales concernant la conception et la mise en œuvre de Cloud Functions.

Écrire des fonctions idempotentes

Vos fonctions devraient produire le même résultat même si elles sont appelées plusieurs fois. Vous pouvez ainsi relancer un appel si l'appel précédent a échoué au milieu de votre code. Pour en savoir plus, consultez la section Effectuer de nouvelles tentatives d'exécution des fonctions basées sur des événements.

Ne pas démarrer les activités d'arrière-plan

L'activité d'arrière-plan désigne tout ce qui se produit après l'arrêt de votre fonction. Un appel de fonction se termine une fois que la fonction renvoie ou signale la fin, par exemple en appelant l'argument callback dans les fonctions basées sur des événements Node.js. Tout code exécuté après un arrêt concerté ne peut pas accéder au processeur et ne progresse pas.

Par ailleurs, lorsqu'un appel ultérieur est exécuté dans le même environnement, votre activité d'arrière-plan reprend, ce qui interfère avec le nouvel appel et peut entraîner un comportement inattendu et des erreurs difficiles à analyser. L'accès au réseau après l'arrêt d'une fonction entraîne généralement la réinitialisation des connexions (code d'erreur ECONNRESET).

L'activité d'arrière-plan peut souvent être détectée dans les journaux d'appels individuels, en recherchant tout ce qui est enregistré à la suite de la ligne indiquant que l'appel est terminé. Elle peut parfois être enfouie plus profondément dans le code, notamment en présence d'opérations asynchrones telles que des rappels ou des timers. Examinez votre code pour vous assurer que toutes les opérations asynchrones se terminent avant d'arrêter la fonction.

Toujours supprimer les fichiers temporaires

Le stockage sur disque local dans le répertoire temporaire est un système de fichiers en mémoire. Les fichiers que vous écrivez consomment de la mémoire disponible pour votre fonction et persistent parfois entre les appels. Si vous ne supprimez pas explicitement ces fichiers, cela risque d'entraîner une erreur de mémoire insuffisante et un démarrage à froid ultérieur.

Pour voir la mémoire utilisée par une fonction individuelle, sélectionnez cette dernière dans la liste des fonctions de la console Google Cloud et choisissez le tracé d'utilisation de la mémoire.

Si vous avez besoin d'accéder à un stockage à long terme, envisagez d'utiliser des installations de volume Cloud Run avec Cloud Storage ou des volumes NFS.

Vous pouvez réduire les besoins en mémoire lors du traitement de fichiers plus volumineux à l'aide du pipeline. Par exemple, pour traiter un fichier sur Cloud Storage, créez un flux de lecture, transmettez-le via un processus basé sur le flux et écrivez le flux de sortie directement dans Cloud Storage.

Framework des fonctions

Pour vous assurer que les mêmes dépendances sont installées de manière cohérente dans différents environnements, nous vous recommandons d'inclure la bibliothèque du framework des fonctions dans votre gestionnaire de paquets et d'épingler la dépendance à une version spécifique du framework des fonctions.

Pour ce faire, spécifiez la version que vous préférez dans le fichier de verrouillage approprié (par exemple, package-lock.json pour Node.js ou requirements.txt pour Python).

Si le framework des fonctions n'est pas explicitement listé en tant que dépendance, il sera automatiquement ajouté lors du processus de compilation à l'aide de la dernière version disponible.

Outils

Cette section fournit des instructions sur l'utilisation des outils pour la mise en œuvre, le test et l'interaction avec Cloud Functions.

Développement local

Le déploiement des fonctions prend un peu de temps. Il est donc souvent plus rapide de tester le code de votre fonction localement.

Les développeurs Firebase peuvent utiliser l'émulateur Cloud Functions de la CLI Firebase.

Utiliser Sendgrid pour envoyer des e-mails

Cloud Functions n'autorise pas les connexions sortantes sur le port 25. Vous ne pouvez donc pas établir de connexions non sécurisées avec un serveur SMTP. La méthode recommandée pour envoyer des e-mails consiste à utiliser un service tiers tel que SendGrid. Vous pouvez trouver d'autres options d'envoi d'e-mails dans le tutoriel Envoyer des e-mails depuis une instance pour Google Compute Engine.

Performances

Cette section décrit les bonnes pratiques relatives à l'optimisation des performances.

Éviter la faible concurrence

Étant donné que les démarrages à froid sont coûteux, la possibilité de réutiliser des instances lancées récemment lors d'un pic est une excellente optimisation pour gérer la charge. Limiter la simultanéité limite l'utilisation des instances existantes, ce qui entraîne davantage de démarrages à froid.

Augmenter la simultanéité permet de différer plusieurs requêtes par instance, ce qui facilite la gestion des pics de charge.

Utiliser les dépendances à bon escient

Les fonctions étant sans état, l'environnement d'exécution est souvent initialisé à partir de zéro (lors d'un démarrage à froid). En cas de démarrage à froid, le contexte global de la fonction est évalué.

Si vos fonctions importent des modules, le temps de chargement de ces modules peut augmenter la latence d'appel lors d'un démarrage à froid. Pour réduire cette latence, ainsi que le temps nécessaire pour déployer votre fonction, chargez correctement les dépendances et ne chargez pas celles que votre fonction n'utilise pas.

Utiliser des variables globales pour réutiliser des objets lors de futurs appels

Il n'est pas garanti que l'état d'une fonction soit préservé pour les futurs appels. Toutefois, Cloud Functions recycle souvent l'environnement d'exécution d'un appel précédent. Si vous déclarez une variable dans le champ d'application global, sa valeur peut être réutilisée dans les appels suivants sans avoir à être recalculée.

De cette façon, vous pouvez mettre en cache des objets qui peuvent être coûteux à recréer à chaque appel de fonction. Le déplacement de ces objets du corps de la fonction vers le champ d'application global peut entraîner des améliorations significatives des performances. Dans l'exemple suivant, un objet lourd est créé une seule fois par instance de fonction et partagé à travers tous les appels de fonction atteignant l'instance donnée :

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}")
  

Cette fonction HTTP prend un objet de requête (flask.Request) et renvoie le texte de la réponse ou tout ensemble de valeurs pouvant être converti en objet Response à l'aide de make_response.

Il est particulièrement important de mettre en cache les connexions réseau, les références de bibliothèque et les objets client API dans un champ d'application global. Pour obtenir des exemples, consultez la section Optimiser la mise en réseau.

Réduisez les démarrages à froid en définissant un nombre minimal d'instances

Par défaut, Cloud Functions adapte le nombre d'instances en fonction du nombre de requêtes entrantes. Vous pouvez modifier ce comportement par défaut en définissant un nombre minimal d'instances que Cloud Functions doit garder prêtes à diffuser les requêtes. La définition d'un nombre minimal d'instances réduit les démarrages à froid de votre application. Nous vous recommandons de définir un nombre minimal d'instances et d'effectuer l'initialisation au moment du chargement si votre application est sensible à la latence.

Pour en savoir plus sur ces options d'exécution, consultez la page Contrôler le comportement du scaling.

Remarques sur le démarrage à froid et l'initialisation

L'initialisation globale se produit au moment du chargement. Sans cela, la première requête devrait terminer l'initialisation et charger les modules, ce qui entraînerait une latence plus élevée.

Toutefois, l'initialisation globale a également un impact sur les démarrages à froid. Pour minimiser cet impact, n'initialisez que ce qui est nécessaire pour la première requête afin de réduire la latence de la première requête au maximum.

Cela est particulièrement important si vous avez configuré des instances minimales comme décrit ci-dessus pour une fonction sensible à la latence. Dans ce scénario, l'initialisation au moment du chargement et la mise en cache des données utiles garantissent que la première requête n'a pas besoin de le faire et est diffusée avec une faible latence.

Si vous initialisez des variables dans un champ d'application global, selon le langage, de longs temps d'initialisation peuvent entraîner deux comportements : - pour certaines combinaisons de langages et de bibliothèques asynchrones, le framework de fonctions peut s'exécuter de manière asynchrone et renvoyer immédiatement, ce qui entraîne la poursuite de l'exécution du code en arrière-plan, ce qui peut entraîner des problèmes tels que l'impossibilité d'accéder au processeur. Pour éviter cela, vous devez bloquer l'initialisation du module, comme décrit ci-dessous. Cela garantit également que les requêtes ne sont pas traitées tant que l'initialisation n'est pas terminée. - D'autre part, si l'initialisation est synchrone, le long temps d'initialisation entraînera des démarrages à froid plus longs, ce qui peut poser problème, en particulier avec les fonctions à faible concurrence lors des pics de charge.

Exemple de préchauffage d'une bibliothèque Node.js asynchrone

Node.js avec Firestore est un exemple de bibliothèque Node.js asynchrone. Pour profiter de min_instances, le code suivant termine le chargement et l'initialisation au moment du chargement, en bloquant le chargement du module.

TLA est utilisé, ce qui signifie qu'ES6 est obligatoire, à l'aide d'une extension .mjs pour le code node.js ou en ajoutant type: module au fichier 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.
});

Exemples d'initialisation globale

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

Cette fonction HTTP utilise des variables globales initialisées de manière paresseuse. Il prend un objet de requête (flask.Request) et renvoie le texte de la réponse ou tout ensemble de valeurs pouvant être transformé en objet Response à l'aide de make_response.

Cette recommandation est d'autant plus importante si vous définissez plusieurs fonctions dans un seul fichier et que chaque fonction utilise des variables différentes. Si vous n'utilisez pas l'initialisation paresseuse, vous risquez de gaspiller des ressources sur des variables initialisées mais jamais utilisées.

Autres ressources

Pour en savoir plus sur l'optimisation des performances, regardez la vidéo "Google Cloud Performance Atlas" sur le délai de démarrage à froid de Cloud Functions.