Skip to content
Nathan Colson edited this page May 19, 2025 · 9 revisions

TP10 : High Scalability on Woodytoys

Noms des auteurs : Nathan Colson

Date de réalisation : 18/05/2025

1. Micro-services

Expliquez comment vous avez divisé votre infrastructure en différents micro-services, en illustrant par un schéma.

Dans le but d'améliorer la scalabilité de notre application, nous avons choisi de passer d’une architecture monolithique à une organisation en micro-services. Pour ce faire, nous avons segmenté notre API initiale en trois services distincts, chacun prenant en charge une responsabilité spécifique :

  • api-products gère les interactions autour des produits.

  • api-orders s’occupe du traitement des commandes.

  • api-misc regroupe les autres fonctionnalités de l’API ne nécessitant pas d’accès à la base de données.

Ce découpage permet une meilleure isolation des responsabilités, facilitant à la fois la maintenance, le déploiement individuel des services, et la montée en charge ciblée.

scalability-basic

  • Qu'avez-vous dû adapter dans vos containers pour implémenter ces changements ?

Pour effectuer cette séparation, nous avons dû repenser notre structure de projet et notre configuration Docker. Chaque micro-service dispose désormais de son propre répertoire, avec un Dockerfile spécifique et un point d'entrée indépendant. Le fichier docker-compose.yml a été mis à jour pour déclarer ces services de façon explicite, tout en définissant les liens réseau nécessaires.

  api-misc:
    image: goatuser17/woody_api-misc:latest
    restart: always
    links:
      - 'db'
    networks:
      - swarm_net
    deploy:
      replicas: 2

  api-products:
    image: goatuser17/woody_api-products:latest
    restart: always
    links:
      - 'db'
    networks:
      - swarm_net
    deploy:
      replicas: 2

  api-orders:
    image: goatuser17/woody_api-orders:latest
    restart: always
    links:
      - 'db'
    networks:
      - swarm_net
    deploy:
      replicas: 2

Les dépendances inutiles ont été supprimées dans chaque conteneur afin de réduire leur taille et d’accélérer leur démarrage. Seuls les services ayant besoin d'accéder à la base de données sont connectés à celle-ci, ce qui renforce la sécurité et améliore l’isolation des composants.

  • Qu'avez-vous changé dans votre configuration nginx?

Suite à cette réorganisation, le reverse proxy nginx a été configuré pour rediriger intelligemment les requêtes entrantes vers les bons services. Chaque route est associée à un micro-service spécifique. Les endpoints sont répartis comme suit :

Les requêtes vers /api/products sont traitées par le service api-products.

Celles vers /api/orders sont envoyées à api-orders.

Toutes les autres requêtes sous /api/, comme /api/ping, sont prises en charge par api-misc, qui joue le rôle de fallback.

server {
  listen 8080;

  location /api/misc {
    proxy_pass http://api-misc:5000;
  }
  location /api/products {
    proxy_pass http://api-products:5000;
  }

  location /api/orders {
    proxy_pass http://api-orders:5000;
  }

  location /api/ping {
    proxy_pass http://api-misc:5000;
  }

Grâce à cette configuration, les différentes parties de l’application peuvent évoluer de manière autonome tout en restant accessibles sous une interface unifiée via le reverse proxy.

2. Communication Asynchrone

Expliquez comment vous avez mis en place votre messagerie RabbitMQ. Qui émets les messages? Que contiennent-ils? Qui les reçoit? N'hésitez pas à illustrer vos explications par un schéma.

Pour fluidifier le traitement des requêtes longues et éviter de bloquer l’API principale, nous avons mis en place un système de communication asynchrone basé sur RabbitMQ. Ce composant joue le rôle de message broker, permettant de découpler l’envoi d’une tâche de son exécution.

Le fonctionnement général repose sur trois éléments :

  • L’émetteur : le microservice api-orders se charge d’envoyer des messages contenant les données nécessaires à la création d’une commande.

  • Le message : chaque message encapsule les informations importantes comme l’identifiant unique de la commande et le nom du produit concerné.

  • Le ou les récepteurs : des workers autonomes, spécialisés dans le traitement de ces tâches, consomment les messages de la file et exécutent les actions nécessaires, comme la sauvegarde en base de données.

microservices

Documentez votre configuration RabbitMQ et indiquez ce que vous avez du changer au niveau du code ou de la configuration des éléments pré-existants de votre infrastructure.

L’intégration de RabbitMQ a nécessité plusieurs modifications techniques dans notre infrastructure :

Ajout du conteneur RabbitMQ : nous avons intégré un service dédié à RabbitMQ dans notre docker-compose.yml, en utilisant une image officielle et en configurant les variables d’environnement nécessaires à l’authentification.

  rabbitmq:
    image: rabbitmq:management
    ports:
      - "5672:5672"
      - "15672:15672"
    networks:
      - swarm_net
    deploy:
      replicas: 2

  rabbit-worker:
    image: goatuser17/woody_rabbit-worker:latest
    depends_on:
      - rabbitmq
    networks:
      - swarm_net
    deploy:
      replicas: 2

Création du service worker : un nouveau conteneur a été mis en place pour exécuter un script Python en boucle, à l’écoute des messages en file. Ce rabbit-worker est chargé de traiter les tâches envoyées par api-orders.

def connect_to_rabbitmq():
    """Keep trying to connect until RabbitMQ is ready."""
    while True:
        try:
            connection = pika.BlockingConnection(
                pika.ConnectionParameters(host='rabbitmq')
            )
            return connection
        except pika.exceptions.AMQPConnectionError:
            print("Waiting for RabbitMQ...")
            time.sleep(2)

# #### 4. internal Services
def process_order(order_id, order):
    # ...
    # ... do many check and stuff
    status = woody.make_heavy_validation(order)

    woody.save_order(order_id, status, order)

def main():
    connection = connect_to_rabbitmq()
    channel = connection.channel()

    channel.queue_declare(queue='order_queue')

    def callback(ch, method, properties, body):
        message = json.loads(body)
        order_id = message['order_id']
        order_product = message['order_product']
        print(f" Received: order id = {order_id}, order product = {order_product}", flush=True)
        process_order(order_id, order_product)

    channel.basic_consume(
        queue='order_queue',
        on_message_callback=callback,
        auto_ack=True
    )

    print('Waiting for messages. To exit press CTRL+C', flush=True)
    channel.start_consuming()

Réorganisation du code : une nouvelle logique de communication a été introduite dans api-orders pour publier un message vers la file lors de la création d’une commande. Du côté du worker, une fonction de callback a été développée pour récupérer le message, extraire les données et exécuter le traitement associé.

# ### 3. Order Service
@app.route('/api/orders/do', methods=['GET'])
def create_order():
    # very slow process because some payment validation is slow (maybe make it asynchronous ?)
    # order = request.get_json()
    product = request.args.get('order')
    order_id = str(uuid.uuid4())

    # TODO TP10: this next line is long, intensive and can be done asynchronously ... maybe use a message broker ?
    while True:
        try:
            connection = pika.BlockingConnection(pika.ConnectionParameters(host='rabbitmq'))
            break
        except pika.exceptions.AMQPConnectionError:
            print("Waiting for RabbitMQ...")
            time.sleep(2)

    channel = connection.channel()
    channel.queue_declare(queue='order_queue')
    channel.basic_publish(exchange='', routing_key='order_queue', body=json.dumps({'order_id': order_id, 'order_product': product}))
    print(" [x] Sent order details")
    connection.close()

    return f"Your process {order_id} has been created with this product : {product} and I also test

Optimisation du retour utilisateur : le principal avantage de cette approche est la réduction immédiate du temps de réponse côté client. La commande est enregistrée dans la file sans attendre le traitement complet, ce qui libère l’API très rapidement, même sous forte charge.

--2025-05-18 19:37:42--  http://www.l1-7.ephec-ti.be:8081/api/orders/do
Resolving www.l1-7.ephec-ti.be (www.l1-7.ephec-ti.be)... 54.36.181.82, 54.36.182.1
Connecting to www.11-7.ephec-ti.be (www.l1-7.ephec-ti.be)|54.36.181.82|:8081... connected.
HTTP request sent, awaiting response... 200 OK
Length: 91 [text/html]
Saving to: ‘do’

do                                                100%[===========================================================================================================>]      91  --.-KB/s    in 0s

2025-05-14 13:52:07 (10.45 MB/s) - ‘do’ saved [91/91]


real	0m0.104s
user	0m0.003s
sys	0m0.006s

3. API Gateway

  • Documentez comment vous avez protégé votre API contre une utilisation abusive.

Pour limiter les risques liés à une utilisation excessive ou malveillante de notre API, nous avons mis en place un mécanisme de contrôle du débit (rate limiting) directement au niveau du reverse proxy nginx. Cette mesure permet de restreindre le nombre de requêtes qu’un même client peut effectuer dans une période donnée.

Une zone de limitation nommée gateway a été définie, associée à chaque adresse IP source. Grâce à cette configuration, nous avons fixé un plafond à 3 requêtes par seconde pour chaque client. En cas de dépassement, la requête excédentaire est automatiquement rejetée avec le code HTTP 429 Too Many Requests.

Ce dispositif vise à :

Réduire la charge sur les services backend.

Protéger l’API contre les attaques par force brute ou les spams.

Garantir une meilleure équité d’accès entre les utilisateurs.

limit_req_zone $binary_remote_addr zone=gateway:1m rate=3r/s;

limit_req zone=gateway;
limit_req_status 429;

  • Expliquez votre procédure de validation de votre API Gateway.

Afin de vérifier que la limitation du nombre de requêtes est bien active, nous avons réalisé une série de tests en utilisant cURL dans un script Bash. Ce test envoie un flux rapide de requêtes consécutives vers un endpoint (/api/ping) tout en enregistrant les codes HTTP retournés.

Lors du test, nous avons observé que :

Les premières requêtes sont acceptées (code 200).

Lorsque la cadence dépasse le seuil défini, certaines requêtes sont bloquées et renvoient le code 429.

Occasionnellement, d'autres codes comme 504 peuvent apparaître si la saturation entraîne des délais d’attente côté serveur.

for i in {1..30}; do curl -o /dev/null -s -w "%{http_code}\n" http://www.l1-7.ephec-ti.be:8081/api/ping; done
200
429
429
429
429
200
504
429
429
429
429
429

Ces résultats confirment le bon fonctionnement du rate limiting mis en place sur notre API Gateway.

Clone this wiki locally