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 plus d'informations, consultez la page 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.

N'essayez pas d'écrire hors du répertoire temporaire et veillez à utiliser des méthodes indépendantes de la plate-forme/de l'OS pour créer des chemins de fichiers.

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

Lorsque vous déployez une fonction, le framework des fonctions est automatiquement ajouté en tant que dépendance, en utilisant sa dernière version en date. 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'épingler votre fonction à 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).

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 SendGrid. Vous trouverez d'autres options d'envoi d'e-mails dans le tutoriel Envoyer des e-mails depuis une instance pour Google Compute Engine.

Performance

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

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. Consultez les exemples de la section Optimisation des réseaux.

Procéder à l'initialisation différée des variables globales

Si vous initialisez des variables dans un champ d'application global, le code d'initialisation est toujours exécuté via un appel de démarrage à froid, ce qui augmente la latence de votre fonction. Dans certains cas, cela entraîne des délais d'inactivité intermittents pour les services qui sont appelés s'ils ne sont pas gérés correctement dans un bloc try/catch. Si certains objets ne sont pas utilisés dans tous les chemins de code, envisagez de procéder à une initialisation différée à la demande :

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.

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 à répondre aux 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 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.

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.