TP09 - bpatureau/admin-2-TP GitHub Wiki
TP9 : High Throughput on Woodytoys
Noms des auteurs : Guillaume Ladrière, Bastien Patureau, Maxime Bongartz Date de réalisation : 1/06/2025
1. Un nouveau service Web pour WoodyToys avec Docker Stack
Découverte du service Web
Pourquoi build ne conviens pas au swarm
La directive build permet a Docker de construire une image localement à partir d'un Dockerfile. Dans docker swarm, les noeuds du cluster ne partagent pas automatiquement le contexte de build. Ils ont donc besoin d'images déjà construites et poussées sur un registres comme docker hub pour que toutes les machines puissent y accéder. Il faut donc construire l'image localement puis la push sur docker hub pour ensuite l'utiliser dans docker-compose.yml via la directive image
et non build
.
Explication de notre schéma de flux de données
Sur ce schéma nous pouvons voir que notre server nginx fait 2 choses : il fournis la page html, et il répond aux requêtes en les redirigent vers l'api flask qui elle se charge de traiter les requêtes (pas directement mais via un script python). Pour traiter cela l'api à parfois besoin de stocker ou récupérer des infos dans la base de données.
Déploiement sur Swarm
Adaptation du service pour swarm
Tout d'abord on a push 4 images sur docker hub : woody_api, woody_database, woody_front et woody_rp. Ces 4 images sont ensuite utilisées par notre stack à la place des directives build
voici un exemple pour le reverse proxy :
reverse:
image: maxion78/tp9_woody_rp:latest
ports:
- "80:8080"
deploy:
replicas: 2
restart_policy:
condition: on-failure
depends_on:
- front
- api
Et ensuite on à déployé le tout grâce à cette commande : docker stack deploy -c stack.yml woody
. Et on peut voir que l'app se réplique bien :
Mesures
Nous avons mesuré les performances avec un seul répicas pour chaques services et voici le résultat :
Test | Total Time (s) | Notes |
---|---|---|
1. PING endpoint | 0.006053 | Rapide, OK |
2. TIME endpoint (short computation) | 0.007102 | Très rapide |
3. HEAVY COMPUTATION endpoint | 5.007149 | Temps de calcul élevé |
4. ADD PRODUCT endpoint | 0.117219 | Ajout rapide |
5. GET LAST PRODUCT endpoint | 15.110928 | Très long, potentielle optimisation |
6. CREATE ORDER endpoint | 5.093805 | Long |
7. GET ORDER STATUS endpoint | 0.085630 | Très rapide |
8. FRONT-END (HTML page) | 0.003941 | Très rapide |
9. Load testing (5 concurrent heavy calls) | Variable | 5s → 10s → 15s → 20s → 25s |
Grâce à cette analyse on peut facilement voir quels endpoints sont rapides, lequels sont lents:
Endpoints critiques (très lents) :
- GET LAST PRODUCT : 15.11s
- CREATE ORDER : 5.09s
- HEAVY COMPUTATION : 5.01s
Endpoints rapides :
- PING : 0.006s
- TIME : 0.007s
- FRONT-END : 0.004s
- ADD PRODUCT : 0.117s
- GET ORDER STATUS : 0.086s
On découvre aussi un goulot d'étranglement : Les 5 requêtes concurrentes se sont process les unes à la suite des autres (5s → 10s → 15s → 20s → 25s).
On va maintenant ajuster les réplicas :
tip : pour scaler l'app (changer le nombre de réplicas) sans redémarrer la stack on peu faire par exemple :
docker service scale woody_api=6
-
API 1 -> 6 : l'api est très gourmande, étant donnée que les requêtes se sérialisent, on va augmenter au max les réplicas. Ici on passe de 1 -> 6 ce qui devrais permettre de process 6 chose en parallèle au lieu de 1 à la fois.
-
Reverse Proxy 1 -> 3 : pour pouvoir répartir la charger étant donné que c'est un point d'entrée critique
-
Front 1 -> 3 : pas pour les performances mais pour augmenter pour la disponibilité
-
DB 1 -> 1 : pas de changements car MySQL nécessite une configuration complexe pour la réplication
Ensuite en refait la mesure et voici les résultats :
Test | Total Time (s) | Notes |
---|---|---|
1. PING endpoint | 0.009021 | Rapide, OK |
2. TIME endpoint (short computation) | 0.008883 | Très rapide |
3. HEAVY COMPUTATION endpoint | 5.007783 | Calcul intensif |
4. ADD PRODUCT endpoint | 0.113820 | Ajout rapide |
5. GET LAST PRODUCT endpoint | 15.122086 | Très lent, à optimiser |
6. CREATE ORDER endpoint | 5.100604 | Long |
7. GET ORDER STATUS endpoint | 0.081673 | Très rapide |
8. FRONT-END (HTML page) | 0.005295 | Ultra rapide |
9. Load testing (5 concurrent heavy calls) | Variable | 5s (3 requêtes) → 10s (2 requêtes), linéarité suspecte |
Conclusion, est-ce que l'augmentation du nombre de replicas résout les problèmes
On peut voir qu'on à résolu un bottleneck avec la réplication :
quand on lance 5 requêtes à un endpoint lourd avant ça durait ~25s et maintenant ça dure ~10s. D'ailleurs on peut voir que effectivement les requêtes sont process en parallèle sur des machines diffèrentes car on reçoit les réponses dans le désordre. Et aussi on en reçoit 3 par 3 ce qui correspond au nombre de machines qu'on à :
Par contre les réplicas n'ont pas résolu le temps que prends GET LAST PRODUCT ni CREATE ORDER ce qui est normal car l'action elle-même est longue, plusieurs machines ne peuvent pas travailler sur le même process, donc augmenter le nombre de machines ne réduira pas le temps du process en lui-même.
Ce qu'on peut faire pour améliorer les performances (outre réplicas)
- healthchecks : Configurer un HEALTHCHECK dans la
stack.yml
peut aider car il permet de recréer automatiquement les conteneurs non sains ça évite donc de router les requêtes vers des API « mortes » - placement constraint : ça permet de décider où répliquer un service, c'est utile pour les performances parce que on peut par exemple obliger la DB à se mettre sur le node le plus rapide et fiable et donc éviter qu'elle soit sur un node lents ce qui pourrait créer un bottleneck
2. Mise en place d'une cache
Mise en place
Pour mettre en place redis il faut adapter le stack.yml pour inclure l'image de redis : image: redis:alpine
. Ensuite on va chercher ce qui est pertinent à utiliser le cache. Ce qui prends le plus de temps on l'a vu c'est heavy computation
, get last product
et create order
. Mettre en cache create order n'est pas pertinent donc on va mettre en cache les 2 autres. Pour faire ça on va modifier le main.py de l'api, voici un exemple de mise en cache pour heavy computation
:
[...]
# Création du client Redis
redis_client = redis.Redis(host='redis', port=6379, db=0)
[...]
@app.route('/api/misc/heavy', methods=['GET'])
def get_heavy():
name = request.args.get('name', '')
cache_key = f'heavy:{name}'
cached_result = redis_client.get(cache_key)
if cached_result:
result = cached_result.decode('utf-8')
return f'(from cache) {datetime.now()}: {result}'
else:
result = woody.make_some_heavy_computation(name)
redis_client.setex(cache_key, 300, result)
return f'(computed) {datetime.now()}: {result}'
Mesures
Pour des raisons techniques (petits problèmes qui ont été résolus au tp10) les screens suivants proviennent du tp10 mais ça revient au même.
Le cache fonctionne en ajoutant dans un stockage, les données souvent utilisées. Pour bien le tester il faut alors faire 2x la même requête et voir si la 2ème fois la réponse est plus rapide. Voici les test avant et après redis pour le heavy computation
et le get last product
qui ont été mis en cache :
Heavy computation avant
time curl "http://localhost/api/misc/heavy?name=Speed"
2x
Heavy computation après
time curl "http://localhost/api/misc/heavy?name=Speed"
2x
Get last product avant
time curl "http://localhost/api/products/last"
2x
Get last product après
time curl "http://localhost/api/products/last"
2x
On peut voire que la cache améliore drastiquement les performance si la donnée à déjà été demandé récemment.
3. CDN
Mise en place
On a tout d'abord du créé un compte sur gcore. Dessus on a défini notre vps qui héberge le service web. Ensuite on a du configurer notre service DNS et crée un CNAME pour rediriger vers le CDN, cette ligne à donc été rajoutée dans notre fichier zone :
cdn IN CNAME cl-gld72bed55.gcdn.co.
On vérifie grâce à dig cdn.l1-2.ephec-ti.be
:
Ca fonctionne. Ensuite le but est de déplacer les fichiers statiques (.js, .css et .jpg) vers notre CDN. Pour l'exercice (et étant donné qu'on a accès uniquement à ce fichier statique) nous allons déplacer uniquement l'image
5mo.jpg
. Pour cela on remplace la ligne dans notre index.html qui charge l'image locale par un ligne qui charge l'image qui provient du CDN :
Mesures
Pour tester le CDN, on va calculer le temps que prends le chargement de l'image sans CDN puis on va faire la même chose avec CDN.
- Avant CDN (
curl -w "Time total: %{time_total}s\n" -o /dev/null -s http://localhost/5mo.jpg
) :
- Après CDN (
curl -w "Time total: %{time_total}s\n" -o /dev/null -s http://cdn.l1-2.ephec-ti.be/5mo.jpg
) :
On constate une différence drastique.
4. Solutions pour améliorer les performances de la DB
Solution | Avantages | Inconvénients |
---|---|---|
Réplication (master-slave) | Lecture possible sur plusieurs serveurs | Complexité +, pas efficace pour les écritures |
Sharding | Meilleure scalabilité horizontale | Très complexe |
Indexation | Accélère les requêtes ciblées | Peut ralentir les écritures |
Caching (Redis) | Très rapide pour lecture fréquente | Pas permanent |