Plan du site  
pixel
pixel

Articles - Étudiants SUPINFO

HTTP Load balancing avec Nginx, Spring Boot et Docker

Par Kwensy Eli BLU Publié le 30/03/2019 à 23:28:58 Noter cet article:
(0 votes)
Avis favorable du comité de lecture

Introduction

Le load balancing est un concept qui concerne non seulement le monde de l'infrastructure, mais aussi ceux du développement et de l'architecture logiciels. Pour tenter de donner une définition générale, le load balancing consiste à répartir la charge d'un composant réseau, web ou de stockage sur plusieurs autres composants de même nature. C'est une technique qui permet de mettre en place une architecture hautement disponible. En fonction du contexte, les composants peuvent être des équipements physiques ou des instances logicielles. Le but de cet article est de mettre en place du HTTP load balancing ; l'objectif étant de rediriger les requêtes HTTP vers un composant qui va se charger de répartir celles-ci sur plusieurs instances d'une application Web. Nous allons pour cela d'abord définir une architecture, ensuite l'implémenter et enfin tester son fonctionnement.

Architecture type

Le fonctionnement de cette architecture est simple. Son élément central est un load balancer. C'est un composant logiciel qui, dans un premier temps, va recevoir les requêtes depuis les clients agissant ainsi comme serveur http. Dans un second temps, en fonction d'une stratégie de load balancing il va se charger de distribuer plus ou moins équitablement ces requêtes vers des serveurs Web. Ces deux fonctions sont généralement assurées par un reverse proxy. Une caractéristique de ces serveurs Web est qu'ils déploient chacun une instance d'un même service. De ce fait peu importe le serveur Web vers lequel une requête est redirigée, le traitement est effectué de la même manière ; ce qui reste absolument transparent pour les clients. On distingue plusieurs avantages pour ce type d’architecture. L’un de ces avantages est que plus il y aura de serveurs Web, plus les temps de latence liés à un nombre élevé de requêtes à traiter seront moindres. Aussi le risque qu'un dysfonctionnement de l'un des serveurs affecte la disponibilité des services délivrés se trouve diminué. Il n’est donc pas osé de dire que, si cette architecture est bien implémentée, la qualité, la performance et la fiabilité desdits services seront améliorés.

Implémentation de l'architecture


Pour implémenter notre architecture, nous avons besoin des environnements suivants :

  • Nginx (Version utilisée au moment de la rédaction de l'article : 1.15)

  • Maven (Version utilisée au moment de la rédaction de l'article : 3.0.5)

Ils nous permettront d'une part de configurer un load balancer et d'autre part de développer une application Web qui va nous servir de test.

Infrastructure système logicielle de l'architecture

Nous allons déployer notre architecture en environnement Docker, l'objectif étant de disposer rapidement d'un environnement virtualisé contenant toutes les dépendances nécessaires à nos composants applicatifs. Pour installer Docker sous Windows, je vous recommande l'article suivant : https://www.supinfo.com/articles/single/6673-docker.

L'application Web

Nous allons développer une application web simple qui renvoie un message en réponse à une requête. Pour nos besoins de test de l'application, nous allons inclure dans ce message des informations concernant l'instance spécifique en cours d'exécution.

  • Les fichiers principaux

Le code source de l'application s'organise en répertoires comme le montre la figure ci-dessous :

La classe Application est le point d'entrée de l'application et définit une ressource REST.

Les configurations permettant de construire l'application se trouvent dans le modèle objet du projet pom.xml

Ce fichier nous permet également de donner un descriptif de l'application ainsi que ses dépendances. Il s'agit d'une application Web basée sur le framework Spring Boot.

Le code source de l'application Web ne se limite qu'à ses deux fichiers et se trouve en annexe A.

A ce stade nous pouvons déjà tester le fonctionnement de l'application Web en exécutant les commandes dans un terminal :

- Pour la compilation et le lancement de l'application

mvn -Dspring.application.name=node1 spring-boot:run

La sortie de la commande devrait être similaire aux captures ci-dessous.

- Pour contacter l'application

curl localhost:8080/

L'application Web est opérationnelle et répond avec succès aux requêtes reçues sur le port 8080 (il s'agit du port par défaut lorsque non renseigné pour le serveur d'application lancé lors du déploiement d'une application Web développée avec Spring Boot). Cette information est utile à savoir pour les étapes suivantes.

  • Préparation pour le déploiement Docker

- Le fichier Dockerfile

Le fichier Dockerfile ci-dessous nous permettra de créer une image Docker de l'application Web.

La ligne 1 du fichier Dockerfile nous montre que notre image est basée sur la distribution alpine de linux qui contient une installation d'un environnement d'exécution java. En construisant l'image, le fichier .jar de l'application Web sera copiée à l'intérieur de celle-ci dans le dossier spécifié (Ligne 2). La ligne 3 permet d'exposer une variable d'environnement SPRING_APPLICATION_NAME pour laquelle une valeur par défaut a été définie et qui pourra être modifiée par les conteneurs créés à partir de notre image. Avec la valeur fournie pour la variable d'environnement et grâce à directive CMD, chaque conteneur créé devra exécuter à son démarrage la commande : java -jar /tmp/docker-spring-boot-simple-1.0-SNAPSHOT.jar --spring.application.name=${SPRING_APPLICATION_NAME}.

L'exécution de cette commande déclenchera à son tour le démarrage d'un processus serveur écoutant sur le port 8080. Il faudra alors notifier cette information à la Docker Engine - EXPOSE 8080 - de manière à ce que celle-ci soit accessible à tous les autres conteneurs.

Nous pouvons maintenant construire l'image en nous positionnant dans le répertoire contenant le fichier Dockerfile et en exécutant la commande :

docker build -t docker-spring-boot-simple:1.0-SNAPSHOT .

Configuration du load balancer avec nginx

Par définition, nginx est un reverse proxy et un serveur http (Wikipédia). De ce fait il se positionne - en tant que composant d'une architecture - de manière frontale à des services applicatifs ou des serveurs Web et est capable de répondre à des requêtes HTTP. Avec les configurations appropriées il peut être utilisé comme load balancer. De manière simplifiée, nginx peut être configuré en écrivant des directives dans un fichier de configuration. Ces directives nous permettront donc de définir éventuellement une stratégie de répartition de charges et les services applicatifs vers lesquels rediriger les requêtes.

- Le fichier de configuration

Ce fichier de configuration est vraiment simple. La section http permet de configurer le http load balancing. Elle indique que le processus serveur de nginx écoute sur le port 80. Toutes les requêtes entrantes sont redirigées vers un hôte avec le nom "backend". Cet hôte est en fait un hôte virtuel défini dans la section upstream ; cette section est l'emplacement où est défini l'algorithme et le groupe de serveurs avec lesquels communiquer. Voyons les détails de cette section ; elle définit deux autres hôtes node1 et node2 écoutant sur le port 8080. Ceux-ci ne sont tout simplement que les serveurs terminaux qui traitent effectivement les requêtes reçues par nginx. L'algorithme de load balancing, n'étant pas défini explicitement, est par défaut le round-robin. Avec cet algorithme chaque serveur est sélectionné à tour de rôle pour répondre aux requêtes l'un après l'autre dans l'ordre dans lequel ils ont été listé dans la configuration. En d'autres termes node1 répondra à la première requête, node2 la deuxième, node1 la troisième et ainsi de suite.

- Préparation pour le déploiement Docker : le fichier Dockerfile

Nous allons créer un fichier Dockerfile qui nous permettra de construire l'image Docker d'une instance de nginx pour laquelle les configurations seront chargées à partir du fichier ci-dessus.

La ligne 1 du fichier Dockerfile nous montre que notre image est basée sur la dernière version de l'image officielle de nginx (https://hub.docker.com/_/nginx?tab=description). Grâce aux directives des lignes suivantes, le fichier de configuration nginx.conf sera copié à l'intérieur de l'image et le port 80 sera exposé pour tout conteneur créé à partir de ce dernier.

Construire l'image en se positionnant dans le répertoire contenant le fichier Dockerfile et en exécutant la commande :

docker build -t nginx-lb .

Test de l'architecture

Avec tous nos composants applicatifs fonctionnels et configurés nous pouvons déployer notre architecture. Puisque l'infrastructure logicielle technique de notre architecture est basée sur Docker, nous devons faire en sorte que les différents composants applicatifs puissent communiquer entre eux. Pour cela nous allons créer un network dans Docker. Dans un terminal taper la commande :

docker network create slb-network

La commande s'exécute avec succès et renvoie l'identifiant network. Un network permet de regrouper des conteneurs dans un même réseau virtuel. Pour ce faire, il existe une option pour la commande de démarrage d'un conteneur - option que nous verrons dans les paragraphes suivants - permettant de spécifier le réseau virtuel dans lequel intégrer ce conteneur.

Pour visualiser les conteneurs se trouvant dans ce réseau virtuel, il suffit de taper la commande :

docker network inspect slb-network

Nous allons créer deux conteneurs dans ce network en exécutant l'image précédemment créée à cet effet. Nous aurons ainsi les deux instances d'applications Web requis pour notre architecture.

docker run -d --net slb-network --name node1 -eSPRING_APPLICATION_NAME=node1 docker-spring-boot-simple:1.0-SNAPSHOT
docker run -d --net slb-network --name node2 -eSPRING_APPLICATION_NAME=node2 docker-spring-boot-simple:1.0-SNAPSHOT

Les conteneurs ont été déployés avec succès dans le réseau slb-network via l'option --net. L'option --name quant à elle permet de définir des noms pour les conteneurs créés. Ces derniers seront alors visibles en tant qu'hôtes pour tout autre conteneur créé dans le même réseau spécifiquement pour le conteneur du load balancer. Nous pouvons voir que les noms (node1 et node2) donnés aux conteneurs correspondent à ceux présents dans le groupe de serveurs de la configuration de load balancing.

Il ne nous reste plus qu'à rajouter le conteneur du load balancer au réseau virtuel grâce à la commande :

docker run -d --net slb-network -p8080:80 --name docker-spring-boot-simple nginx-lb

A la différence des serveurs Web, le load balancer doit pouvoir être accessible depuis l’extérieur ; pour cela son conteneur est exécuté en faisant une publication de ports qui consiste au mapping du port 80 de celui-ci au port 8080 de l'hôte (option -p8080:80). L'hôte n'est autre que la Docker Machine sur laquelle s'exécute le conteneur. Une manière d'obtenir son adresse IP est de taper la commande :

docker-machine ip

Tous les composants applicatifs ont été déployés avec succès dans le même réseau virtuel Docker. Il est aisé de le constater en réexécutant la commande :

docker network inspect slb-network

En analysant le retour de la commande, on peut constater que la section containers nous livre un certain nombre d'informations : les conteneurs node1, node2 (les instances de serveurs Web) et docker-spring-boot-simple (le load balancer) ont bien été déployés au sein du réseau. Des adresses IP virtuelles dans la même plage leur ont été attribués, ce qui ne fait que confirmer le résultat attendu de notre manipulation, c'est-à-dire permettre aux conteneurs de pouvoir communiquer entre eux.

Une fois ses vérifications effectuées nous pouvons tester le fonctionnement de l'architecture en exécutant de manière répétée la commande suivante (pensez à remplacer <DOCKER_MACHINE_IP> par l'adresse IP de la Docker Machine) :

curl <DOCKER_MACHINE_IP>:8080

Comme attendu, les instances node1 et node2 de l'application Web sont sélectionnées l'une après l'autre pour répondre aux requêtes. Le load balancing est parfaitement fonctionnel.

Pour supprimer les conteneurs, il faut exécuter la commande :

docker container rm -f <ID_OU_NOM_CONTENEUR_1> <ID_OU_NOM_CONTENEUR_2> <ID_OU_NOM_CONTENEUR_3>

Conclusion

Dans cet article, nous avons défini tout d'abord ce qu'est le load balancing et les concepts associés ainsi que les avantages qu'elle apporte. Nous avons ensuite analysé les composants minimaux requis d'une architecture générique de load balancing à savoir un load balancer et des instances de serveurs Web. Après nous avons effectué l'implémentation de cette architecture en déployant un reverse proxy nginx et plusieurs instances d'une application Web dans des conteneurs s'exécutant dans une infrastructure technique logicielle basé sur Docker. Enfin lors du test de l'architecture, conformément à la configuration mise en place pour le load balancing, nous avons vu en action le fonctionnement de l'algorithme du round-robin grâce auquel les instances de serveurs Web étaient sélectionnés à tour de rôle pour répondre aux requêtes.

Comme vous l'avez sans doute remarqué, plusieurs étapes de l'implémentation de l'architecture ont été effectuées manuellement, ce qui peut être compréhensible pour notre cas d'étude avec quelques composants à gérer. Pour des architectures plus complexes, il ne serait pas très aisé de procéder de la sorte. Il serait préférable, le cas échéant, d'envisager l'utilisation d'outils comme Kubernetes ou Docker Swarm pour par exemple la gestion automatisée du déploiement des conteneurs ou Docker Compose pour par exemple l'exécution groupée de conteneurs.

Annexes

Annexe A - Code source des fichiers

  • Application Web

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.webapps</groupId>
    <artifactId>docker-spring-boot-simple</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>docker-spring-boot-simple</name>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.coderplus.maven.plugins</groupId>
                <artifactId>copy-rename-maven-plugin</artifactId>
                <version>1.0</version>
                <executions>
                    <execution>
                        <id>copy-file</id>
                        <phase>package</phase>
                        <!-- copie -->
                        <goals>
                            <goal>copy</goal>
                        </goals>
                        <!-- Configuration fichier source, destination -->
                        <configuration>
                            <sourceFile>${project.build.directory}/docker-spring-boot-simple-1.0-SNAPSHOT.jar</sourceFile>
                            <destinationFile>${project.basedir}/docker-spring-boot-simple-1.0-SNAPSHOT.jar</destinationFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Application.java

package com.webapps.dockerspringbootsimple;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;

@SpringBootApplication
@RestController
public class Application{
    
    public static void main(String [] args){
        SpringApplication.run(Application.class, args);
    }
    
    @Value("${spring.application.name}")
    private String instanceName;

    @GetMapping("/")
    public String hello(){
        if(instanceName == null || instanceName.length() == 0)
                throw new RuntimeException("spring.application.name property is empty."+
                 "Please rerun the application with the "+
                 "option --spring.application.name=<applicationName>");
        return "Response from " + instanceName + " at " + System.currentTimeMillis();
    }
}

Dockerfile

FROM java:8-jre-alpine
ADD docker-spring-boot-simple-1.0-SNAPSHOT.jar  /tmp/
ENV SPRING_APPLICATION_NAME node
CMD  java -jar  /tmp/docker-spring-boot-simple-1.0-SNAPSHOT.jar --spring.application.name=${SPRING_APPLICATION_NAME}
EXPOSE 8080
  • Load balancer

nginx.conf

events {}
http {
  upstream backend {
      server node1:8080;
      server node2:8080;
  }
  server {
      listen 80;
      location / {
          proxy_pass http://backend/;
      }
  }
}

Dockerfile

FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

Annexe B - Liens utiles

Image Docker nginx : https://github.com/nginxinc/docker-nginx

Documentation des directives Dockerfile : https://docs.docker.com/engine/reference/builder/

Load balancing avec nginx : http://nginx.org/en/docs/http/load_balancing.html

A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Toeic, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels