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

Copie de admin drawio 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 : image

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 à : image 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 449547234-06f003d7-a5a3-47b4-9ad1-af965f91a759 449547234-06f003d7-a5a3-47b4-9ad1-af965f91a759

Heavy computation après

time curl "http://localhost/api/misc/heavy?name=Speed" 2x image

Get last product avant

time curl "http://localhost/api/products/last" 2x image

Get last product après

time curl "http://localhost/api/products/last" 2x image

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 : image 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 :

image

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) :

image

  • Après CDN (curl -w "Time total: %{time_total}s\n" -o /dev/null -s http://cdn.l1-2.ephec-ti.be/5mo.jpg) :

image

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