Optimiser les applications Python pour Cloud Run

Ce guide décrit des optimisations pour les services Cloud Run écrits dans le langage de programmation Python. Il présente également des informations générales pour vous aider à comprendre les compromis requis par certaines des optimisations. Les informations contenues sur cette page viennent en complément des conseils généraux d'optimisation, qui s'appliquent également à Python.

Un grand nombre de bonnes pratiques et d'optimisations d'applications Web Python courantes reposent sur les aspects suivants:

  • La gestion des requêtes simultanées (E/S non bloquantes et basées sur un thread)
  • La réduction de la latence de réponse à l'aide de fonctions non critiques de pooling des connexions et de traitement par lots, par exemple l'envoi de traces et de métriques à des tâches en arrière-plan

Optimiser l'image de conteneur

Optimisez l'image du conteneur pour réduire les temps de chargement et de démarrage en utilisant les méthodes suivantes:

  • Minimisez les fichiers que vous chargez au démarrage
  • Optimiser le serveur WSGI

Minimisez les fichiers que vous chargez au démarrage

Pour optimiser le temps de démarrage, ne chargez que les fichiers requis au démarrage et réduisez leur taille. Pour les fichiers volumineux, envisagez les options suivantes:

  • Stockez des fichiers volumineux, tels que des modèles d'IA, dans votre conteneur pour un accès plus rapide. Envisagez de charger ces fichiers après le démarrage ou au moment de l'exécution.

  • Envisagez de configurer des installations de volume Cloud Storage pour les fichiers volumineux qui ne sont pas essentiels au démarrage, tels que les éléments multimédias.

  • Importez uniquement les sous-modules requis à partir de dépendances lourdes, ou importez des modules lorsque cela est nécessaire dans votre code, au lieu de les charger au démarrage de l'application.

Optimiser le serveur WSGI

Python a standardisé la manière dont les applications peuvent interagir avec les serveurs Web grâce à la mise en œuvre de la norme WSGI PEP-3333. L'un des serveurs WSGI les plus courants est gunicorn. Il est utilisé dans la plupart des exemples de documentation.

Optimiser gunicorn

Ajoutez le CMD suivant au Dockerfile pour optimiser l'appel de gunicorn :

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

Si vous envisagez de modifier ces paramètres, ajustez le nombre de nœuds de calcul et de threads individuellement pour chaque application. Par exemple, essayez d'utiliser un nombre de nœuds de calcul égal aux cœurs disponibles et assurez-vous que les performances s'améliorent, puis ajustez le nombre de threads. La définition d'un trop grand nombre de nœuds de calcul ou de threads peut avoir un impact négatif, par exemple une latence de démarrage à froid plus longue, une consommation de mémoire plus importante, un nombre plus faible de requêtes par seconde, etc…

Par défaut, gunicorn génère des nœuds de calcul et écoute le port spécifié au démarrage, même avant d'évaluer le code de votre application. Dans ce cas, vous devez configurer des vérifications de démarrage personnalisées pour votre service, car la vérification de démarrage par défaut de Cloud Run marque immédiatement une instance de conteneur comme opérationnelle dès qu'elle commence à écouter sur $PORT.

Si vous souhaitez modifier ce comportement, vous pouvez appeler gunicorn avec le paramètre --preload pour évaluer le code de votre application avant l'écoute. Vous pouvez ainsi :

  • Identifier les bugs d'exécution graves au moment du déploiement
  • Économiser la mémoire

Vous devez prendre en compte le préchargement de votre application avant d'envisager l'ajout de ce paramètre.

Autres serveurs WSGI

Vous n'êtes pas restreint à l'utilisation de gunicorn pour exécuter Python dans des conteneurs. Vous pouvez utiliser n'importe quel serveur Web WSGI ou ASGI tant que le conteneur écoute sur le port HTTP $PORT, conformément au contrat d'exécution du conteneur.

Les alternatives courantes incluent uwsgi, uvicorn et waitress.

Par exemple, avec un fichier nommé main.py contenant l'objet app, les appels suivants démarrent un serveur WSGI :

# uwsgi: pip install pyuwsgi
uwsgi --http :$PORT -s /tmp/app.sock --manage-script-name --mount /app=main:app

# uvicorn: pip install uvicorn
uvicorn --port $PORT --host 0.0.0.0 main:app

# waitress: pip install waitress
waitress-serve --port $PORT main:app

Ceux-ci peuvent être ajoutés en tant que ligne CMD exec dans un objet Dockerfile ou en tant qu'entrée web: dans un objet Procfile lorsque vous utilisez les packs de création Google Cloud.

Optimiser les applications

Dans votre code de service Cloud Run, vous pouvez également optimiser le délai de démarrage et l'utilisation de la mémoire.

Réduire le nombre de threads

Vous pouvez optimiser la mémoire en réduisant le nombre de threads, en faisant appel à des stratégies réactives non bloquantes et en évitant les activités d'arrière-plan. Évitez également d'écrire dans le système de fichiers, comme indiqué sur la page Conseils généraux.

Si vous souhaitez prendre en charge les activités d'arrière-plan dans votre service Cloud Run, configurez votre service Cloud Run pour qu'il utilise la facturation basée sur les instances 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.

Limiter les tâches de démarrage

Les applications Web Python peuvent avoir de nombreuses tâches à exécuter au démarrage (préchargement des données, préchauffage du cache et établissement de pools de connexions, par exemple). Lorsqu'elles sont exécutées de manière séquentielle, ces tâches peuvent être lentes. Toutefois, si vous souhaitez qu'elles s'exécutent en parallèle, augmentez le nombre de cœurs de processeur.

Cloud Run envoie une requête utilisateur réelle pour déclencher une instance de démarrage à froid. Les utilisateurs dont la requête est attribuée à une instance tout juste démarrée peuvent être confrontés à des délais importants.

Améliorer la sécurité avec des images de base allégées

Pour améliorer la sécurité de votre application, utilisez une image de base allégée avec moins de packages et de bibliothèques.

Si vous choisissez de ne pas installer Python à partir de la source dans vos conteneurs, utilisez une image de base officielle Python à partir de Docker Hub. Ces images sont basées sur le système d'exploitation Debian.

Si vous utilisez l'image python de Docker Hub, envisagez d'utiliser la version slim. Ces images sont moins volumineuses, car elles n'incluent pas un certain nombre de packages qui serviraient à construire des roues, ce qui n'est pas nécessaire pour votre application. L'image python est fournie avec le compilateur, le préprocesseur et les utilitaires de base GNU C.

Pour identifier les dix packages les plus volumineux d'une image de base, exécutez la commande suivante:

DOCKER_IMAGE=python # or python:slim
docker run --rm ${DOCKER_IMAGE} dpkg-query -Wf '${Installed-Size}\t${Package}\t${Description}\n' | sort -n | tail -n10 | column -t -s $'\t'

Comme il y a moins de ces packages de bas niveau, les images basées sur slim offrent également moins de surface d'attaque pour les failles potentielles. Certaines de ces images peuvent ne pas inclure les éléments requis pour créer des roues à partir de la source.

Vous pouvez rajouter des packages spécifiques en ajoutant une ligne RUN apt install à votre fichier Dockerfile. Pour en savoir plus, consultez Utiliser des packages système dans Cloud Run.

Il existe également des options pour les conteneurs non-Debian. L'option python:alpine peut générer un conteneur beaucoup moins volumineux, mais de nombreux packages Python peuvent ne pas comporter de roues précompilées compatibles avec les systèmes Alpine. La compatibilité s'améliore (voir PEP-656), mais reste variable. Envisagez également d'utiliser distroless base image, qui ne contient aucun gestionnaire de paquets, ni shell, ni autre programme.

Utiliser la variable d'environnement PYTHONUNBUFFERED pour la journalisation

Pour afficher les journaux non mis en mémoire tampon de votre application Python, définissez la variable d'environnement PYTHONUNBUFFERED. Lorsque vous définissez cette variable, les données stdout et stderr sont immédiatement visibles dans les journaux du conteneur, au lieu d'être conservées dans une mémoire tampon jusqu'à ce qu'une certaine quantité de données se soit accumulée ou que le flux soit fermé.

Étapes suivantes

Pour accéder à d'autres conseils, consultez les pages suivantes :