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.
Activité en arrière-plan
L'activité d'arrière-plan désigne tout ce qui se produit après la transmission de votre réponse HTTP. Pour déterminer s'il existe une activité en arrière-plan dans votre service qui n'apparaît pas de manière évidente, recherchez dans vos journaux tous les événements consignés après l'entrée de la requête HTTP.
Configurer le processeur pour qu'il soit toujours alloué en vue d'utiliser des activités d'arrière-plan
Si vous souhaitez prendre en charge les activités d'arrière-plan dans votre service Cloud Run, configurez le processeur de votre service Cloud Run pour qu'il soit toujours alloué afin de pouvoir exécuter des activités d'arrière-plan en dehors des requêtes tout en conservant l'accès au processeur.
Éviter les activités d'arrière-plan si le processeur n'est alloué que pendant le traitement des requêtes
Si vous devez configurer votre service pour allouer un processeur uniquement pendant le traitement des requêtes, lorsque le service Cloud Run termine le traitement d'une requête, l'accès de l'instance au processeur est désactivé ou fortement limitée. 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 si vous utilisez ce type d'allocation de processeur.
Examinez votre code pour vous assurer que toutes les opérations asynchrones sont terminées avant de transmettre votre réponse.
L'exécution de threads en arrière-plan avec ce type d'allocation de processeur peut entraîner un comportement inattendu, car toute requête ultérieure adressée à la même instance de conteneur entraîne la reprise des activités d'arrière-plan suspendues.
Supprimer les fichiers temporaires
Dans l'environnement Cloud Run, 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 les 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.
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 conteneurs rapidement
Les instances étant soumises à un scaling en fonction des besoins, leur temps de démarrage a un impact sur la latence de votre service. Bien que Cloud Run dissocie le démarrage de l'instance et le traitement des requêtes, il peut arriver qu'une requête doive attendre le démarrage d'une nouvelle instance, notamment en cas de scaling à partir de zéro. On parle alors de "démarrage à froid".
La routine de démarrage lance les actions suivantes :
- Télécharger l'image de conteneur (à l'aide de la technologie de streaming d'image de conteneur de Cloud Run)
- Démarrer le conteneur en exécutant la commande entrypoint
- Attendre que le conteneur commence à écouter sur le port configuré
L'optimisation de la vitesse de démarrage du conteneur réduit la latence de traitement des requêtes.
Utiliser l'optimisation du processeur au démarrage pour réduire la latence de démarrage
Vous pouvez activer l'optimisation du processeur au démarrage afin d'augmenter temporairement l'allocation de processeur lors du démarrage de l'instance et ainsi de réduire la latence de démarrage.
Utiliser un nombre minimal d'instances pour réduire les démarrages à froid
Vous pouvez configurer le nombre minimal d'instances et la simultanéité afin de réduire les démarrages à froid. Par exemple, si vous utilisez un nombre minimal d'instances valant 1, cela signifie que votre service est prêt à recevoir au plus ce nombre de requêtes simultanées configurées pour votre service sans que cela ne nécessite le démarrage d'une nouvelle instance.
Notez qu'une requête en attente du démarrage d'une instance est conservée en attente dans une file d'attente comme suit :
- Si les nouvelles instances démarrent, par exemple lors d'une évolutivité horizontale, les requêtes seront mises en attente pendant au moins le temps de démarrage moyen des instances de conteneur de ce service. Cela inclut le moment où la requête lance un scaling horizontal, par exemple lors d'un scaling à partir de zéro.
- Si le temps de démarrage est inférieur à 10 secondes, les requêtes sont mises en attente pendant une durée maximale de 10 secondes.
- Si aucune instance n'est en cours de démarrage et que la requête n'initie pas de scaling horizontal, les requêtes seront mises en attente pendant une durée maximale de 10 secondes.
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 de démarrage.
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 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 peut bénéficier 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
Python
Go
Java
Effectuer une initialisation différée 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
Python
Go
Java
Utiliser un environnement d'exécution différent
Vous pouvez accélérer les temps de démarrage en utilisant un environnement d'exécution différent.
Optimiser la simultanéité
Les instances Cloud Run peuvent diffuser plusieurs requêtes simultanément, jusqu'à une simultanéité maximale configurable.
Ceci diffère de Cloud Functions, qui utilise concurrency = 1
.
Cloud Run ajuste automatiquement la simultanéité jusqu'à la valeur maximale configurée.
La simultanéité maximale par défaut de 80 est adaptée à de nombreuses images de conteneurs. Vous devez toutefois :
- La réduire si votre conteneur n'est pas en mesure de traiter de nombreuses requêtes simultanées.
- L'augmenter si votre conteneur est capable de gérer un volume important de requêtes.
Régler la simultanéité pour votre service
Le nombre de requêtes simultanées pouvant être traitées par chaque instance peut être limité par la pile technologique et l'utilisation de ressources partagées, telles que les variables et les connexions à la base de données.
Pour optimiser votre service de façon à obtenir une simultanéité stable maximale, procédez comme suit :
- Optimisez vos performances de service.
- 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.
- Déployez votre service.
- 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.
- 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.
- 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.
Compromis entre débit, latence et coûts
Ajuster le paramètre du nombre maximal de requêtes simultanées peut vous aider à équilibrer le compromis entre le débit, la latence et les coûts de votre service.
En général, un nombre inférieur de requêtes simultanées entraîne une latence et un débit inférieurs par instance. Avec un nombre maximal de requêtes simultanées inférieur, moins de requêtes se disputent les ressources dans chaque instance, et chacune d'elles offre de meilleures performances. Cependant, comme chaque instance peut diffuser moins de requêtes à la fois, le débit par instance est plus faible et le service nécessite davantage d'instances pour diffuser le même trafic.
À l'inverse, un nombre maximal de requêtes simultanées plus élevé entraîne généralement une latence et un débit plus élevés par instance. Les requêtes peuvent avoir besoin d'attendre l'accès à des ressources telles que le processeur, le GPU et la bande passante mémoire dans l'instance, ce qui augmente la latence. Cependant, chaque instance peut traiter davantage de requêtes à la fois, de sorte que le service nécessite moins d'instances dans son ensemble pour traiter le même volume de trafic.
Considérations liées au coût
La facturation Cloud Run est basée sur le temps d'instance. Si Le processeur est toujours alloué, le temps d'instance correspond à la durée de vie totale de chaque instance. Si le processeur n'est pas alloué en permanence, le temps d'instance correspond à la durée pendant laquelle chaque instance a traité au moins une requête.
L'impact du nombre maximal de requêtes simultanées sur la facturation dépend de votre schéma de trafic. La réduction du nombre maximal de requêtes simultanées peut réduire votre facture si le paramètre le plus bas produit les effets suivants :
- La latence est réduite
- Les instances effectuent le travail plus rapidement
- Les instances s'arrêtent plus rapidement, même si un plus grand nombre d'instances est nécessaire
Mais l'inverse est également possible : la réduction du nombre maximal de requêtes simultanées peut augmenter la facturation si l'augmentation du nombre d'instances n'est pas compensée par la réduction du temps d'exécution de chaque instance grâce à l'amélioration de la latence.
Le meilleur moyen d'optimiser la facturation consiste à effectuer des tests de charge à l'aide de différents paramètres de nombre maximal de requêtes simultanées pour identifier celui qui permet d'obtenir la durée d'instance facturable la plus faible, comme indiqué dans la métrique de surveillance container/billable_instance_time.
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 des images de base fournies par Google ou des 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.
Implémentez 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 via un profil utilisateur autre que
root
à l'aide de l'instruction DockerfileUSER
. Un utilisateur spécifique peut être déjà configuré dans certaines images de conteneur.Empêchez l'utilisation des fonctionnalités Preview à l'aide de règles d'organisation personnalisées.
Automatiser les analyses de sécurité
Activez l'analyse des failles pour détecter les failles de sécurité dans les images de conteneurs stockées dans Artifact Registry.
Créer des images de conteneurs minimales
Les images de conteneurs volumineuses peuvent augmenter les failles de sécurité, car elles contiennent plus que ce dont le code a besoin.
En raison de la technologie de streaming d'images de conteneur de Cloud Run, la taille de votre image de conteneur n'affecte pas le démarrage à froid ni le temps de traitement des requêtes. La taille de l'image de conteneur n'est pas non plus comptabilisée dans la mémoire disponible de votre conteneur.
Pour créer un conteneur minimal, envisagez de 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 :
- Bonnes pratiques Kubernetes : Pourquoi et comment créer des images de conteneurs réduites
- Sept bonnes pratiques en matière de création de conteneurs