Conseils de développement

Ce guide présente les bonnes pratiques pour la conception, la mise en œuvre, le test et le déploiement d'un service Cloud Run. Pour obtenir plus de conseils, consultez la page Migrer un service existant.

Écrire des services efficaces

Cette section présente les bonnes pratiques générales de conception et de mise en œuvre d'un service Cloud Run.

Éviter les activités d'arrière-plan

Lorsqu'une application exécutée sur Cloud Run termine le traitement d'une requête, l'accès de l'instance de conteneur au processeur est désactivé ou très limité. Par conséquent, vous ne devez pas démarrer de routines ni de threads en arrière-plan qui s'exécutent en dehors du champ d'application des gestionnaires de requêtes.

L'exécution de threads en arrière-plan peut entraîner un comportement inattendu, car toute requête ultérieure adressée à la même instance de conteneur reprend l'activité en arrière-plan suspendue.

L'activité d'arrière-plan désigne tout ce qui se produit après la transmission de votre réponse HTTP. Examinez votre code pour vous assurer que toutes les opérations asynchrones sont terminées avant de transmettre votre réponse.

Si vous pensez qu'une activité d'arrière-plan s'exécute dans votre service sans apparaître de façon évidente, recherchez dans vos journaux tous les événements consignés après l'entrée de la requête HTTP.

Supprimer les fichiers temporaires

Dans l'environnement Cloud Run (entièrement géré), le stockage sur disque est un système de fichiers en mémoire. Les fichiers écrits sur le disque consomment de la mémoire disponible pour votre service et persistent parfois entre les appels. Si vous ne supprimez pas explicitement ces fichiers, une erreur pour mémoire insuffisante et un démarrage à froid peuvent se produire.

Signaler des erreurs

Traitez toutes les exceptions et ne laissez pas votre service planter pour cause d'erreurs. Un plantage entraîne un démarrage à froid alors que le trafic est mis en file d'attente jusqu'au remplacement de l'instance de conteneur.

Consultez le guide Error Reporting pour savoir comment signaler correctement les erreurs.

Optimiser les performances

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

Démarrer les services rapidement

Les instances de conteneur étant mises à l'échelle en fonction des besoins, une méthode classique consiste à initialiser complètement l'environnement d'exécution. Ce type d'initialisation est dénommé "démarrage à froid". Si une requête de client déclenche un démarrage à froid, le démarrage de l'instance de conteneur entraîne une latence supplémentaire.

La routine de démarrage lance les actions suivantes :

  • Démarrage du service
    • Démarrage du conteneur
    • Exécution de la commande entrypoint pour démarrer le serveur
  • Vérification du port de service ouvert

L'optimisation de la vitesse de démarrage du service réduit la latence qui retarde le traitement des requêtes par une instance de conteneur.

Utiliser les dépendances à bon escient

Si vous utilisez un langage dynamique avec des bibliothèques dépendantes, telles que l'importation de modules dans Node.js, le temps de chargement de ces modules augmente la latence lors d'un démarrage à froid. Vous pouvez réduire la latence de la façon suivante :

  • Réduisez le nombre et la taille des dépendances pour créer un service allégé.
  • Chargez tardivement le code rarement utilisé, si votre langage le permet.
  • Utilisez des optimisations de chargement de code telles que l'optimisation du chargeur automatique de l'éditeur PHP.

Utiliser des variables globales

Dans Cloud Run, vous ne pouvez pas supposer que l'état du service est préservé entre les requêtes. Toutefois, Cloud Run réutilise des instances de conteneur individuelles pour diffuser le trafic en cours. Vous pouvez donc déclarer une variable dans un champ d'application global pour permettre la réutilisation de sa valeur lors d'appels ultérieurs. Il n'est pas possible de savoir à l'avance si une requête individuelle reçoit le bénéfice de cette réutilisation.

Vous pouvez également mettre en cache des objets en mémoire s’ils sont coûteux à recréer à chaque demande de service. Changer la logique de requête en appliquant le champ d'application global améliore les performances.

Node.js

// Global (instance-wide) scope
// This computation runs at instance cold-start
const instanceVar = heavyComputation();

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
exports.scopeDemo = (req, res) => {
  // Per-function scope
  // This computation runs every time this function is called
  const functionVar = lightComputation();

  res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`);
};

Python

# Global (instance-wide) scope
# This computation runs at instance cold-start
instance_var = heavy_computation()

def scope_demo(request):
    """
    HTTP Cloud Function that declares a variable.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    function_var = light_computation()
    return 'Per instance: {}, per function: {}'.format(
        instance_var, function_var)

Go


// h is in the global (instance-wide) scope.
var h string

// init runs during package initialization. So, this will only run during an
// an instance's cold start.
func init() {
	h = heavyComputation()
}

// ScopeDemo is an example of using globally and locally
// scoped variables in a function.
func ScopeDemo(w http.ResponseWriter, r *http.Request) {
	l := lightComputation()
	fmt.Fprintf(w, "Global: %q, Local: %q", h, l)
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class Scopes implements HttpFunction {
  // Global (instance-wide) scope
  // This computation runs at instance cold-start.
  // Warning: Class variables used in functions code must be thread-safe.
  private static final int INSTANCE_VAR = heavyComputation();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Per-function scope
    // This computation runs every time this function is called
    int functionVar = lightComputation();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Instance: %s; function: %s", INSTANCE_VAR, functionVar);
  }

  private static int lightComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).sum();
  }

  private static int heavyComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

Effectuer une initialisation tardive des variables globales

L'initialisation des variables globales a toujours lieu au démarrage, ce qui augmente le temps de démarrage à froid. L'initialisation tardive des objets rarement utilisés permet de différer le coût horaire et de réduire les temps de démarrage à froid.

Node.js

// Always initialized (at cold-start)
const nonLazyGlobal = fileWideComputation();

// Declared at cold-start, but only initialized if/when the function executes
let lazyGlobal;

/**
 * HTTP function that uses lazy-initialized globals
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
exports.lazyGlobals = (req, res) => {
  // This value is initialized only if (and when) the function is called
  lazyGlobal = lazyGlobal || functionSpecificComputation();

  res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`);
};

Python

# 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

def lazy_globals(request):
    """
    HTTP Cloud Function that uses lazily-initialized globals.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    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 'Lazy: {}, non-lazy: {}.'.format(lazy_global, non_lazy_global)

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"log"
	"net/http"
	"sync"

	"cloud.google.com/go/storage"
)

// client is lazily initialized by LazyGlobal.
var client *storage.Client
var clientOnce sync.Once

// LazyGlobal is an example of lazily initializing a Google Cloud Storage client.
func LazyGlobal(w http.ResponseWriter, r *http.Request) {
	// You may wish to add different checks to see if the client is needed for
	// this request.
	clientOnce.Do(func() {
		// Pre-declare an err variable to avoid shadowing client.
		var err error
		client, err = storage.NewClient(context.Background())
		if err != nil {
			http.Error(w, "Internal error", http.StatusInternalServerError)
			log.Printf("storage.NewClient: %v", err)
			return
		}
	})
	// Use client.
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class LazyFields implements HttpFunction {
  // Always initialized (at cold-start)
  // Warning: Class variables used in Servlet classes must be thread-safe,
  // or else might introduce race conditions in your code.
  private static final int NON_LAZY_GLOBAL = fileWideComputation();

  // Declared at cold-start, but only initialized if/when the function executes
  // Uses the "initialization-on-demand holder" idiom
  // More information: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
  private static class LazyGlobalHolder {
    // Making the default constructor private prohibits instantiation of this class
    private LazyGlobalHolder() {}

    // This value is initialized only if (and when) the getLazyGlobal() function below is called
    private static final Integer INSTANCE = functionSpecificComputation();

    private static Integer getInstance() {
      return LazyGlobalHolder.INSTANCE;
    }
  }

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    Integer lazyGlobal = LazyGlobalHolder.getInstance();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Lazy global: %s; non-lazy global: %s%n", lazyGlobal, NON_LAZY_GLOBAL);
  }

  private static int functionSpecificComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).sum();
  }

  private static int fileWideComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

Réduire la taille de l'image de conteneur

Une image de conteneur de grande dimension a plusieurs effets :

  • Plus de code signifie une surface d'attaque plus étendue, ce qui augmente le nombre de failles de sécurité potentielles.
  • Le téléchargement de nombreux fichiers augmente le temps de création de l'image de conteneur.
  • Le temps de déploiement du service augmente car l'image de conteneur est préparée pour être utilisée dans une nouvelle révision.
  • Les frais de sortie réseau avec Container Registry augmentent si le bucket de stockage du conteneur est géographiquement distant de votre région de service.

Sur Cloud Run, la taille de votre image de conteneur n'affecte pas le démarrage à froid ni le temps de traitement des requêtes, et n'est pas comptabilisée dans la mémoire disponible de votre conteneur.

Pour en savoir plus sur la sécurité des conteneurs, reportez-vous à la section ci-après.

Pour créer un conteneur minimal, procédez à partir d'une image de base légère, telle que :

L'image de base sous Ubuntu est plus grande, mais elle est couramment utilisée avec un environnement de serveur plus complet et prêt à l'emploi.

Si votre service dispose d'un processus de compilation riche en outils, procédez plutôt par étapes pour préserver la légèreté du conteneur au moment de l'exécution.

Les ressources ci-dessous vous permettront d'en savoir davantage sur la création d'images de conteneur allégées :

Utiliser la simultanéité

Cloud Run est compatible avec la simultanéité configurable, ce qui implique des considérations particulières pour votre service. Ceci diffère de Cloud Functions, qui n'est pas compatible avec la simultanéité.

Régler la simultanéité pour votre service

Vous pouvez permettre aux instances de conteneur d'un service de traiter plusieurs requêtes en même temps, soit "simultanément". Le nombre de requêtes simultanées pouvant être traitées par chaque instance de conteneur est limité par la pile technologique et l'utilisation de ressources partagées, telles que les variables globales et les connexions à une base de données.

Pour optimiser votre service de façon à obtenir une simultanéité stable maximale, procédez comme suit :

  1. Optimisez vos performances de service.
  2. Définissez le degré de simultanéité compatible attendu dans votre configuration de simultanéité au niveau du code. Les piles technologiques n'exigent pas toutes ce paramètre.
  3. Déployez votre service.
  4. Définissez la simultanéité de Cloud Run pour votre service sur une valeur égale ou inférieure à celle de la configuration au niveau du code. S'il n'existe pas de configuration au niveau du code, appliquez le niveau de simultanéité attendu.
  5. Utilisez des outils de tests de charge compatibles avec une simultanéité configurable. Vous devez tester la stabilité de votre service en fonction de la charge et de la simultanéité attendues.
  6. Si le service fonctionne mal, revenez à l'étape 1 pour améliorer les performances ou à l'étape 2 pour réduire la simultanéité. Si le service fonctionne correctement, revenez à l'étape 2 et augmentez la simultanéité.

Poursuivez l'itération jusqu'à trouver la simultanéité stable maximale.

Mettre en correspondance la mémoire et la simultanéité

Chaque requête traitée par votre service nécessite une certaine quantité de mémoire supplémentaire. Ainsi, lorsque vous augmentez ou diminuez la simultanéité, veillez également à ajuster votre limite de mémoire.

Éviter un état global modifiable

Si vous souhaitez tirer parti de l'état global modifiable dans un contexte de simultanéité, prenez des mesures supplémentaires au niveau du code pour garantir la sécurité de l'opération. Réduisez les conflits en limitant les variables globales à une initialisation et une réutilisation uniques, comme décrit ci-dessus dans la section relative aux Performances.

Si vous utilisez des variables globales modifiables dans un service qui traite plusieurs requêtes en même temps, veillez à appliquer des verrous ou des mutex pour éviter les conditions de concurrence.

Sécurité des conteneurs

De nombreuses pratiques générales de sécurité logicielles s’appliquent aux applications conteneurisées. Certaines pratiques sont soit spécifiques aux conteneurs, soit conformes à la philosophie et à l'architecture des conteneurs.

Pour améliorer la sécurité des conteneurs, procédez comme suit :

  • Utilisez des images de base activement gérées et sécurisées, telles que les images de base gérées de Container Registry ou les images officielles de Docker Hub.
  • Appliquez les mises à jour de sécurité en effectuant régulièrement une recompilation des images de conteneur et un redéploiement de vos services.
  • Ne placez dans le conteneur que ce qui est nécessaire pour exécuter le service. Le code, les packages et les outils supplémentaires augmentent les failles de sécurité potentielles. Reportez-vous plus haut pour connaître l'impact sur les performances.
  • Mettez en œuvre un processus de compilation déterministe incluant des versions de logiciels et de bibliothèques spécifiques. Vous éviterez ainsi que du code non vérifié soit inclus dans votre conteneur.
  • Définissez votre conteneur pour qu'il s'exécute en tant qu'utilisateur autre que root à l'aide de l'instruction Dockerfile USER. Un utilisateur spécifique peut être déjà configuré dans certaines images de conteneur.

Analyses de sécurité automatisées

Activez l'analyseur de failles d'images de Container Registry pour détecter les failles de sécurité dans les images de conteneur stockées dans Container Registry.

Si vous utilisez Cloud Run pour Anthos sur Google Cloud, vous pouvez également utiliser l'autorisation binaire pour vous assurer que seules les images de conteneur sécurisées sont déployées.