Aller au contenu
English

Paramètres de ressources du conteneur

  • Leçon 11
  • 45 min
  • Module 4 : Choisir l'infrastructure et mesurer

Ce que vous saurez faire

Placez le service d’escalade dans un conteneur et définissez sa taille : quantité de CPU, mémoire et nombre de processus. Voyez ensuite l’effet de chaque paramètre sur le démarrage, les requêtes et la mémoire. Faites-le avant qu’une plateforme infonuagique impose les mêmes limites.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec Python 3.14.6 et Docker Desktop (Docker Engine 29.6.1) sur macOS, avec une puce Apple. Les étapes pour Windows et Linux ne sont pas testées. Les durées dépendent de votre ordinateur.

Un appartement a un fusible. Si l’appartement utilise trop d’électricité à la fois, le fusible coupe le courant. Tout l’appartement devient sombre, pas seulement une lampe. Une salle de réunion partagée a une feuille de réservation. Supposons que vous pouvez l’utiliser pendant 30 minutes par heure. Après vos 30 minutes, vous attendez dehors jusqu’à l’heure suivante, même si la salle est vide.

Un conteneur reçoit les deux mêmes types de limites. Une limite de mémoire est le fusible : si les processus du conteneur en utilisent davantage, le système d’exploitation les arrête. Une limite de CPU est la feuille de réservation. Le conteneur peut utiliser le CPU pendant une partie de chaque courte période, puis il attend.

Amira exécute la même image deux fois en changeant un seul paramètre : --cpus 1, puis --cpus 0.5. Le code et le modèle restent identiques. Elle mesure avec le banc d’essai de la prochaine leçon, une requête à la fois :

Limite de CPU Prêt après (médiane de 8 démarrages) p50, une requête à la fois p95, une requête à la fois
1 2,0 s 4,1 ms 6,1 ms
0,5 3,5 s 4,1 ms 44,1 ms

Avec moitié moins de CPU, le démarrage prend presque deux fois plus de temps. Amira s’y attendait. La surprise est le p95 : la durée sous laquelle restent 95 % des requêtes (la prochaine leçon l’explique). La plupart des requêtes sont aussi rapides qu’avant. Mais une sur vingt prend dix fois plus de temps, sans autre requête en attente. Le budget de Grace est de 50 ms pour le p95, réseau compris. Dans cette mesure, un demi-CPU utilise presque tout ce budget.

Pour comprendre pourquoi, Amira doit d’abord créer le conteneur et placer ses paramètres dans des fichiers.

Le Dockerfile vient du cours sur Docker, dans Sécurité à l’exécution. Il utilise les noms de ce projet et une nouvelle ligne : COPY bundles ./bundles.

Dockerfile
FROM python:3.14.8-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_ROOT_USER_ACTION=ignore
RUN useradd --create-home --uid 10001 app
WORKDIR /app
COPY requirements-run.txt .
RUN pip install -r requirements-run.txt
COPY bundles ./bundles
COPY pyproject.toml .
COPY escalation ./escalation
USER app
EXPOSE 8000
CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "8000"]

COPY bundles ./bundles place l’ensemble du modèle dans l’image. On appelle cela intégrer l’ensemble à l’image. L’autre méthode consiste à conserver l’ensemble dans un espace de stockage, par exemple un compte de stockage infonuagique. On le télécharge ensuite au démarrage du conteneur.

Tomás : « Pourquoi l’intégrer? Un nouveau modèle a alors besoin d’une nouvelle image. » Oui, et c’est justement la raison. L’image escalation-service:1.0.0 contient alors le code et le modèle que l’équipe a testés ensemble. Chaque version de l’image est complète. Pour revenir en arrière, on peut donc exécuter l’ancienne image, avec l’ancien code et l’ancien modèle. Dans le module 5, l’image contient plusieurs ensembles. Retour en arrière et retrait permet aussi de rétablir seulement le modèle, avec un paramètre dans .env. L’image démarre sans appel réseau au stockage, ce qui enlève une étape au démarrage à froid. Le coût : chaque nouveau modèle exige une construction, et l’image grossit avec le modèle. Pour un ensemble de 45 Ko, ce coût est négligeable. Pour un modèle de langage de 5 Go, les équipes chargent le fichier depuis le stockage au démarrage. Elles vérifient quand même son empreinte, comme dans Confiance et intégrité des artefacts.

La ligne vient après pip install et avant le code, à cause du cache des couches. Voir Construire efficacement : le cache des couches dans le cours sur Docker. Les paquets changent le moins, le code change le plus, et l’ensemble se situe entre les deux. MODEL_SHA256 n’est pas dans l’image. Il vient de l’extérieur au démarrage du conteneur, pour que l’image ne puisse pas confirmer son propre ensemble.

Ajoutez cette ligne à La décision dans docs/infrastructure.md :

docs/infrastructure.md
- The bundle is baked into the image: each image holds the code and the model that were tested together.

requirements-run.txt liste chaque paquet de l’image avec sa version exacte. Créez-le dans un conteneur Linux, comme dans L’essentiel du Dockerfile du cours sur Docker, car l’image utilise Linux :

Terminal, in ~/projects/escalation-service
docker run --rm python:3.14.8-slim-trixie sh -c 'pip install -q --root-user-action=ignore --disable-pip-version-check "fastapi[standard-no-fastapi-cloud-cli]==0.142.2" scikit-learn==1.9.1 pandas==3.0.6 numpy==2.5.3 joblib==1.6.0 && pip freeze' > requirements-run.txt

Cela a pris 18 s. wc -l requirements-run.txt donne 60 requirements-run.txt. Les cinq paquets fixent les versions que l’ensemble vérifie. Les 55 autres les accompagnent, par exemple SciPy avec scikit-learn.

Ensuite, créez .dockerignore comme liste d’autorisation, comme dans le cours sur Docker. La nouvelle ligne concerne les ensembles :

.dockerignore
# Send only what the image needs. Data, tests, .env and .venv stay out.
*
!requirements-run.txt
!pyproject.toml
!escalation/
!bundles/
**/__pycache__/
Terminal
docker build -t escalation-service:1.0.0 .

La sortie (abrégée; une construction sans cache) :

#4 [internal] load build context
#4 transferring context: 46.61kB done
...
#9 [5/8] RUN pip install -r requirements-run.txt
#9 DONE 19.1s
#10 [6/8] COPY bundles ./bundles
#10 DONE 0.1s
...
#13 naming to docker.io/library/escalation-service:1.0.0 done

La construction a pris 31 s sur l’ordinateur du cours. Regardez maintenant l’image et ses couches :

Terminal
docker image ls escalation-service
docker history escalation-service:1.0.0 --format "{{.Size}}\t{{.CreatedBy}}"

La sortie (abrégée) :

IMAGE ID DISK USAGE CONTENT SIZE EXTRA
escalation-service:1.0.0 1ca084f354f7 787MB 166MB
...
69.6kB COPY escalation ./escalation # buildkit
12.3kB COPY pyproject.toml . # buildkit
45.1kB COPY bundles ./bundles # buildkit
461MB RUN /bin/sh -c pip install -r requirements-r…
...
110MB # debian.sh --arch 'arm64' out/ 'trixie' '@1…
Les couches de escalation-service:1.0.0 (docker history). L'ensemble du modèle est trop petit pour être visible à côté des bibliothèques qui l'exécutent.
Description textuelle du graphique

Un graphique à colonnes des couches de escalation-service:1.0.0, en Mo. L’image de base (Debian et Python) : 160 Mo. RUN pip install : 461 Mo. COPY bundles, l’ensemble du modèle : 45,1 Ko, trop petit pour apparaître comme colonne. COPY escalation, le code : 69,6 Ko, aussi trop petit pour apparaître.

Le modèle fait 5 Ko, et les bibliothèques qui l’exécutent font 461 Mo : SciPy seul fait environ 149 Mo. L’image qu’une plateforme télécharge correspond à la taille du contenu, soit 166 Mo compressés. Le ticket-api du cours sur Docker faisait 73 Mo. Une image plus grosse prend plus de temps à télécharger à chaque démarrage à froid dans le nuage.

La taille du conteneur va dans compose.yaml, pour que chaque démarrage utilise les mêmes paramètres :

compose.yaml
# The escalation service on this computer, with the resource limits of a
# small cloud container. MODEL_SHA256 comes from .env (copy .env.example).
services:
escalation:
image: escalation-service:1.0.0
build: .
ports:
- "127.0.0.1:8000:8000"
environment:
MODEL_BUNDLE: bundles/escalation-${MODEL_VERSION:-1.0.0}
MODEL_SHA256: ${MODEL_SHA256:?Set MODEL_SHA256 in .env}
SHOW_DOCS: "true"
command: ["fastapi", "run", "--host", "0.0.0.0", "--port", "8000", "--workers", "${WORKERS:-1}"]
cpus: ${CPUS:-1.0}
mem_limit: ${MEMORY:-512m}
memswap_limit: ${MEMORY:-512m} # the same value: no swap, as in most cloud containers
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/ready')"]
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
  • cpus est la limite de CPU : 1.0 correspond à un CPU, 0.5 à un demi-CPU.
  • mem_limit est la limite de mémoire. memswap_limit avec la même valeur signifie aucune mémoire d’échange. La mémoire d’échange est un espace disque que le système utilise comme mémoire supplémentaire, mais lente. Docker l’autorise par défaut, et la plupart des conteneurs infonuagiques n’en ont pas. Le portable doit donc échouer comme dans le nuage.
  • --workers définit le nombre de processus (workers), les processus du serveur qui répondent aux requêtes. Chacun charge sa propre copie de l’ensemble, comme dans Charger et initialiser.
  • Le contrôle de santé appelle /ready, pour que Compose sache quand l’ensemble est prêt.

${WORKERS:-1} lit une variable de .env, avec 1 comme valeur par défaut. ${MODEL_SHA256:?…} n’a pas de valeur par défaut : sans cette variable, Compose s’arrête avec le message après ?. Les valeurs vont dans .env, que Git ignore. .env.example est le modèle que Git conserve :

.env.example
# Copy this file to .env and fill in MODEL_SHA256.
# MODEL_VERSION picks the bundle: bundles/escalation-<MODEL_VERSION>.
MODEL_VERSION=1.0.0
# The digest that make_bundle.py printed for that bundle (64 characters).
MODEL_SHA256=
# The container's limits (C17 Module 4).
WORKERS=1
CPUS=1.0
MEMORY=512m

Amira le copie dans .env et démarre Compose avant d’ajouter l’empreinte :

Terminal
cp .env.example .env
docker compose up -d --wait

La sortie :

error while interpolating services.escalation.environment.MODEL_SHA256: required variable MODEL_SHA256 is missing a value: Set MODEL_SHA256 in .env

Bien : pas d’empreinte, pas de démarrage. Elle colle l’empreinte de son ensemble après MODEL_SHA256= et redémarre. Utilisez shasum -a 256 bundles/escalation-1.0.0/SHA256SUMS, comme dans le module 1; votre empreinte diffère de celle du cours :

Terminal
docker compose up -d --wait
docker stats --no-stream

La sortie (abrégée) :

Container escalation-service-escalation-1 Started
Container escalation-service-escalation-1 Waiting
Container escalation-service-escalation-1 Healthy
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % ...
eeeea9b9ac05 escalation-service-escalation-1 0.41% 148.7MiB / 512MiB 29.05% ...

Un processus utilise environ 149 Mio dans le conteneur, sur une limite de 512 Mio.

Dans Vérifier la parité, vous avez vérifié le service sur votre ordinateur. Maintenant, les mêmes billets de soutien vont au conteneur, avec Linux, les paquets de l’image et 1 CPU. Chaque nouvel environnement peut apporter ses propres différences. Refaites donc la vérification :

Terminal
python parity/check_parity.py http://127.0.0.1:8000 bundles/escalation-1.0.0

La sortie (abrégée) :

case ticket expected served difference
normal T-116940 0.187598 0.187598 2.8e-17
normal T-116941 0.064735 0.064735 0.0e+00
normal T-116942 0.129584 0.129584 1.1e-16
...
missing T-900001 0.037929 0.037929 9.7e-17
...
14 of 14 cases match within 1e-09.

Les différences atteignent 1,1 × 10⁻¹⁶, un peu plus que sur macOS, mais bien en dessous de la tolérance. Le code de sortie est 0. Exécutez cette vérification après chaque nouvelle image et chaque déploiement.

Comment Docker donne-t-il un demi-CPU à un conteneur? Il ne ralentit pas le CPU. Il compte le temps par courtes périodes de 100 ms. Avec cpus: 0.5, le conteneur peut utiliser 50 ms de temps CPU par période. Après ces 50 ms, le système le met en pause jusqu’au début de la période suivante. Cette pause s’appelle le bridage.

Deux lignes du temps de 300 ms, divisées en trois périodes de 100 ms. Avec cpus 1.0, un conteneur occupé s'exécute sans arrêt. Avec cpus 0.5, il s'exécute pendant 50 ms, puis reste en pause pendant 50 ms, à chaque période. En dessous, une requête arrive à 60 ms dans le conteneur à 0,5 CPU, après l'utilisation des 50 ms de cette période. Elle attend 40 ms jusqu'à la période suivante, puis demande 3 ms de travail. Elle prend donc environ 43 ms plutôt que 3 ms.
Bridage avec cpus : 0.5. À chaque période de 100 ms, un conteneur occupé s'exécute pendant 50 ms, puis reste en pause. Une requête qui arrive pendant la pause attend la période suivante.
Description textuelle du diagramme

Deux lignes du temps de 300 ms, avec une ligne pointillée au début de chaque période de 100 ms. Avec cpus: 1.0, le conteneur occupé s’exécute sans arrêt. Avec cpus: 0.5, il s’exécute pendant 50 ms, puis reste en pause pendant 50 ms, à chaque période.

En dessous, une requête arrive à 60 ms dans le conteneur à 0,5 CPU. Les 50 ms de cette période sont déjà utilisées. La requête attend donc 40 ms jusqu’au début de la période suivante. Elle demande ensuite environ 3 ms de travail. Elle prend environ 43 ms plutôt que 3 ms.

Cela explique le p95 de 44,1 ms. Le banc d’essai envoie la requête suivante dès que la dernière réponse arrive. Le service est donc occupé presque tout le temps. Il utilise ses 50 ms tôt dans de nombreuses périodes. La plupart des requêtes se terminent encore avant la pause : le p50 reste à 4,1 ms. Certaines arrivent pendant une pause et doivent attendre : le p95 augmente brusquement. Dans le nuage, la taille choisie (0,25, 0,5 ou 1 vCPU) est une limite de CPU de ce type. Le bridage exige toutefois un conteneur occupé. Un service qui répond occasionnellement à une requête n’épuise jamais ses 50 ms. La prochaine leçon examine si le service de Larkfield est parfois aussi occupé.

La limite de CPU change aussi le démarrage. Le démarrage demande du travail au CPU : importer les bibliothèques et charger l’ensemble, dans Charger et initialiser. Amira a démarré l’image huit fois avec chaque limite, alors que l’image était déjà sur l’ordinateur. Elle a mesuré le temps entre docker run et la réponse de /ready :

Démarrage à froid sur le portable du cours, avec l'image déjà sur l'ordinateur. Moitié moins de CPU, environ deux fois plus de temps au démarrage.
Description textuelle du graphique

Un graphique à colonnes du temps médian entre docker run et la première réponse de /ready, pour huit démarrages chacun. 1 CPU : 2,0 s (de 1,6 à 3,0 s). 0,5 CPU : 3,5 s (de 2,7 à 5,6 s). 0,25 CPU : 6,7 s (de 5,9 à 11,1 s).

Moitié moins de CPU, environ deux fois plus de temps : le démarrage dépend presque entièrement du travail du CPU. La dispersion (de 1,6 à 3,0 s avec 1 CPU) vient des autres programmes sur le même portable. Dans le nuage, la plateforme doit aussi trouver une machine et télécharger l’image de 166 Mo. L’équipe du cours l’a mesuré sur Azure Container Apps, en conditions réelles. La première requête après un passage à zéro réplique a pris 26,4 s avec 0,5 vCPU et 23,9 s avec 1 vCPU. Doubler le CPU n’a économisé que 2,5 s. Dans le nuage, la majeure partie du démarrage à froid dépend de la plateforme, pas du CPU.

La concurrence est le nombre de requêtes en cours au même moment. Un processus exécute Python, et Python dans un seul processus utilise environ un CPU pour ce travail. Un processus répond donc à environ une requête à la fois, et les autres attendent dans une file.

Davantage de processus peuvent répondre à davantage de requêtes en même temps, mais seulement s’ils ont assez de CPU. Chacun consomme aussi de la mémoire. Avec deux processus, docker top montre les processus dans le conteneur :

PID RSS COMMAND
32317 58944 /usr/local/bin/python3.14 /usr/local/bin/fastapi run --host 0.0.0.0 --port 8000 --workers 2
32351 17412 /usr/local/bin/python3.14 -B -c from multiprocessing.resource_tracker import main;main(6)
32352 198664 /usr/local/bin/python3.14 -B -c from multiprocessing.spawn import spawn_main; ...
32353 197448 /usr/local/bin/python3.14 -B -c from multiprocessing.spawn import spawn_main; ...

RSS est la mémoire de chaque processus, en Kio (1 024 octets). Un processus parent d’environ 58 Mio surveille les processus de service, et chacun des deux utilise environ 194 Mio. Chaque processus a son propre Python, ses propres bibliothèques et son propre ensemble. Deux processus ne partagent pas une seule copie.

Pourquoi docker stats montre-t-il 149 Mio pour tout le conteneur avec un processus, alors qu’un processus utilise ici 194 Mio? Les deux outils comptent différemment. RSS compte chaque page mémoire qu’un processus utilise. Cela inclut les pages de fichiers de bibliothèques que le système peut partager ou supprimer. docker stats compte la mémoire du conteneur une seule fois, sans le cache de fichiers que le système peut libérer. La limite de mémoire compte comme docker stats; comparez donc la limite avec docker stats. Pour planifier, prévoyez environ 190 Mio par processus. C’est un peu plus que ce que chaque processus supplémentaire ajoute réellement, ce qui laisse une marge.

Amira a mesuré avec plus de processus que de CPU : une limite de 2 CPU, 16 requêtes simultanées, 2 000 requêtes.

Processus (2 CPU) Requêtes par seconde p50 Mémoire
2 524 28,0 ms 350 Mio
4 317 49,3 ms 631 Mio
6 262 76,4 ms 914 Mio

Quatre processus sur deux CPU sont plus lents que deux, et utilisent presque deux fois plus de mémoire. Les processus supplémentaires n’ajoutent pas de temps CPU. Ils utilisent les deux mêmes CPU à tour de rôle, et ces changements prennent du temps. Pour un service limité par le CPU comme celui-ci, la règle est : processus = CPU. La mémoire doit être ≥ nombre de processus × environ 200 Mio, plus une marge.

  1. Dans ~/projects/escalation-service, écrivez les fichiers Dockerfile, .dockerignore, compose.yaml et .env.example de cette leçon.
  2. Créez requirements-run.txt dans un conteneur. Vérifiez qu’il contient 60 lignes.
  3. Ajoutez la ligne sur l’ensemble à La décision dans docs/infrastructure.md.
  4. Construisez l’image avec docker build -t escalation-service:1.0.0 ..
  5. Exécutez docker history escalation-service:1.0.0. Trouvez la couche COPY bundles et la couche pip install.
  6. Copiez .env.example dans .env, puis collez l’empreinte de votre ensemble après MODEL_SHA256=.
  7. Démarrez avec docker compose up -d --wait. Résultat attendu : Healthy (en bonne santé).
  8. Exécutez docker stats --no-stream. Notez la mémoire.
  9. Appelez /ready : curl -s http://127.0.0.1:8000/ready. Résultat attendu : {"status":"ready","model_version":"1.0.0"}.
  10. Exécutez python parity/check_parity.py http://127.0.0.1:8000 bundles/escalation-1.0.0. Résultat attendu : 14 of 14 cases match within 1e-09. (les 14 cas sur 14 concordent avec une tolérance de 1 × 10⁻⁹).
Afficher la sortie attendue des étapes 5 et 8

Étape 5 : COPY bundles ./bundles fait environ 45 Ko, et RUN pip install environ 461 Mo sur une puce Apple. Les tailles sur un ordinateur Intel ou AMD (amd64) sont un peu différentes.

Étape 8 : environ 150 Mio sur 512 Mio, par exemple 148.7MiB / 512MiB. Si la mémoire est environ deux fois plus élevée, vérifiez WORKERS dans .env : sa valeur doit être 1.

Dans .env, définissez WORKERS=2 et gardez CPUS=1.0. Avant de commencer, faites deux prédictions : la mémoire dans docker stats et le nombre de requêtes par seconde. Deux processus répondront-ils à plus de requêtes qu’un seul? Ensuite, exécutez docker compose up -d --wait et docker stats --no-stream.

Afficher la réponse

La mémoire double, environ : 337.5MiB / 512MiB, car chaque processus charge ses propres bibliothèques et son propre paquet. La vitesse n’augmente pas : deux processus partagent un CPU, donc ils travaillent à tour de rôle. La prochaine leçon le mesure : avec 8 requêtes simultanées, un processus répondait à 216 requêtes par seconde. Deux processus sur un CPU répondaient à 209 requêtes par seconde. Vous utilisez deux fois plus de mémoire pour rien. Rétablissez WORKERS=1.

Erreur courante

En santé, puis arrêté : quatre processus dans 512 Mio

Symptôme : Tomás veut plus de vitesse et définit WORKERS=4, avec CPUS=2.0 et MEMORY=512m. docker compose up -d --wait indique :

Container escalation-service-escalation-1 Healthy

et /ready répond 200. Dix secondes plus tard, le service ne répond plus du tout.

Cause : quatre processus ont besoin de plus de 512 Mio. Chaque processus charge ses bibliothèques et son paquet. Le système arrête des processus par manque de mémoire quand le conteneur dépasse sa limite. Le journal montre les processus qui s’arrêtent, tandis que le processus parent en démarre de nouveaux :

INFO: Waiting for child process [89]
INFO: Child process [89] died
INFO: Waiting for child process [90]
INFO: Child process [90] died
...
INFO escalation: model 1.0.0 loaded (bundle 8f233029aa4c...) in 2501 ms

La vérification de santé a réussi, car un processus a répondu à /ready pendant que les autres s’arrêtaient. Ensuite, le système a aussi arrêté le processus principal. docker inspect donne la raison :

Terminal
docker inspect escalation-service-escalation-1 --format "{{.State.Status}} OOMKilled={{.State.OOMKilled}} exit={{.State.ExitCode}}"
exited OOMKilled=true exit=137

Le code de sortie 137 signifie que quelque chose a arrêté le processus. OOMKilled=true indique pourquoi : manque de mémoire.

Mémoire du conteneur après le démarrage, avec une limite de 512 Mio et sans mémoire d'échange : 1 processus, 148,7 Mio; 2 processus, 340,3 Mio; 3 processus, 481,5 Mio; 4 processus, plus de 512 Mio : OOMKilled, code de sortie 137.
La mémoire augmente avec chaque processus. Trois processus tiennent dans 512 Mio avec peu de marge; quatre ne tiennent pas.
Description textuelle du diagramme

Barres horizontales de la mémoire du conteneur après le démarrage, avec une ligne pointillée à la limite de 512 Mio. Aucune mémoire d’échange. 1 processus : 148,7 Mio. 2 processus : 340,3 Mio. 3 processus : 481,5 Mio, près de la ligne. 4 processus : plus de 512 Mio, donc le noyau a arrêté le conteneur : OOMKilled, code de sortie 137.

Solution : mesurez la mémoire d’un processus, multipliez-la par le nombre de processus et ajoutez une marge. Avec une limite de 2 Gio, quatre processus utilisaient 631 Mio (le tableau dans Processus et concurrence). Donnez-leur donc au moins 1 Gio. Ou utilisez moins de processus : sur 2 CPU, 2 processus sont de toute façon plus rapides. Vérifiez OOMKilled (arrêt par manque de mémoire) chaque fois qu’un conteneur s’arrête sans raison claire.

Pour un vrai service d’IA : avec la mémoire d’échange autorisée (le réglage par défaut de Docker), les quatre mêmes processus restaient running (en cours d’exécution) à 511.9MiB / 512MiB. L’ordinateur portable cachait le problème, et seul le nuage le révélerait. C’est pourquoi compose.yaml définit memswap_limit. Un grand modèle aggrave le problème : un modèle de 2 Go dans chacun des quatre processus représente 8 Go. Des vérifications de santé réussies au démarrage ne prouvent pas que le service survit à sa première minute.

Vérifiez votre compréhension

Avec cpus : 0.5, p50 reste à 4,1 ms, mais p95 passe de 6,1 à 44,1 ms. Expliquez pourquoi seules les requêtes lentes changent.

Afficher la réponse

Docker accorde au conteneur 50 ms de temps CPU dans chaque période de 100 ms. Un service occupé utilise ses 50 ms rapidement, puis le système le suspend jusqu’à la période suivante : c’est le bridage. Les requêtes exécutées avant la pause prennent les 4 ms habituelles, donc la médiane ne change pas. Les requêtes qui arrivent pendant une pause attendent la période suivante, jusqu’à environ 50 ms. Elles représentent les quelques pour cent les plus lentes, donc p95 augmente fortement.

Vérifiez votre compréhension

Faites une prédiction : dans un conteneur avec 1 CPU et 1 Gio, Tomás définit WORKERS=4. Qu'arrive-t-il à la mémoire et aux requêtes par seconde, comparativement à un seul processus?

Afficher la réponse

La mémoire augmente beaucoup. Amira a mesuré 631 Mio pour quatre processus et environ 150 Mio pour un seul. Chaque processus charge ses propres bibliothèques et son propre paquet. Le tout tient encore dans 1 Gio, donc rien n’est arrêté. Le nombre de requêtes par seconde n’augmente pas. Quatre processus travaillent à tour de rôle sur un CPU, et les changements de processus prennent un peu de temps. Le service peut donc ralentir. Les processus supplémentaires aident seulement quand des CPU sont disponibles pour eux.

Vérifiez votre compréhension

Pourquoi le cours intègre-t-il le paquet à l'image? Quand chargeriez-vous plutôt le modèle depuis le stockage au démarrage?

Afficher la réponse

Avec le paquet intégré, l’image contient le code et le modèle que l’équipe a testés ensemble. L’image est complète, et exécuter une ancienne image rétablit les deux. (Le module 5 permet aussi de rétablir seulement le modèle, avec un paramètre.) Le démarrage ne nécessite aucun téléchargement depuis le stockage. Chargez le modèle depuis le stockage lorsqu’il est volumineux (plusieurs gigaoctets). Ainsi, un nouveau modèle ne nécessite pas de nouvelle image, et l’image reste petite. Vérifiez alors l’empreinte du fichier téléchargé au démarrage, comme le fait load_bundle.

Récapitulatif

  • Le Dockerfile intègre le paquet à l’image : chaque image contient le code et le modèle testés ensemble. Le modèle occupe 45,1 ko; les bibliothèques occupent 461 Mo.
  • compose.yaml définit les ressources : cpus, mem_limit, memswap_limit (sans mémoire d’échange) et --workers, avec les valeurs de .env.
  • Une limite de CPU suspend un conteneur occupé dans chaque période de 100 ms (bridage). À 0,5 CPU, p95 était de 44,1 ms, et le démarrage à froid prenait environ deux fois plus de temps.
  • Chaque processus possède sa propre copie de tout. Processus = CPU; mémoire ≥ processus × environ 200 Mio.
  • Trop de processus pour la mémoire : Healthy (en santé), puis OOMKilled=true, code de sortie 137.

Ensuite, vous mesurez le service de la même façon chaque fois : une charge fixe, p50 et p95, débit et concurrence.

Leçon suivante : Mesurer la performance du service