in eureka ribbon feign netflix spring cloud microservice ~ de lecture.

Microservice Spring Cloud Netflix #1 : Annuaire de services avec Eureka, Ribbon et Feign

Ce billet fait partie d'une suite de billets dédiés à la mise en place d'une architecture microservices à l'aide d'outils opensourcés par Netflix OSS et appropriés par Spring Cloud.

Architecture microservice
Microservice Spring Cloud Netflix #1 : Annuaire de services avec Eureka, Ribbon et Feign

Application d'exemple

Pour bien comprendre le fonctionnement de l’architecture, nous allons créer une application toute simple permettant de démontrer le bon déroulement des opérations, l’intéret de l’architecture microservice et les outils à mettre en place.
Notre application représentera une édition de cartes Magic l'assemblée, pour cela 2 services sont créés (on s'arrêtera à 2 pour l'exercice) :

  • Edition : représentant les informations d'une édition (nom, code, date ...)
  • Carte : représentant les informations d'une carte (nom, coût, texte, image ...)

Trois cas d'usages sont possibles :

  • L'utilisateur souhaite récupérer les informations d'une édition
  • L'utilisateur souhaite récupérer les informations d'une édition ainsi que ses cartes
  • L'utilisateur souhaite récupérer les informations d'une carte

La création de ces deux microservices se fait à l'aide de Spring Boot, chacun de ces services possède donc une adresse d'accès unique, voici à titre d'exemple :

Edition : http://127.0.0.1:3333/editions
Carte : http://127.0.0.1:2222/cards

spring_netflix_oss

Annuaire de services (Eureka)

Afin que Edition puisse récupérer les informations depuis Carte, il faut lui fournir l' adresse IP de ce dernier, ce qui rend le couplage très fort entre les services. Afin que les services puissent communiquer indépendamment les uns des autres sans qu'ils aient besoin de se connaître (dans une architecture microservices il peut y avoir plusieurs instances par service) il faut mettre en place un annuaire de services (service discovery) qui va faire le lien entre tous les services disponibles, Eureka est l'outil utilisé par la suite.

Véritable pièce maîtresse, Eureka se compose d'une partie serveur et d'une partie cliente, chaque service s'enregistre au démarrage auprès d'Eureka Server via un composant Eureka Client (une simple annotation) qui va tenir informer ce dernier de son état par intervalles réguliers.
Chaque service client constitue un cache des informations nécessaires à la communication inter-services, récupérées auprès du serveur, cela permet de résister à une interruption de service d'Eureka Server.

1/ pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>

2/ application.yml
(les valeurs sont valables pour une application de démo ou de test, à ajuster pour un environnement de production)

# Service registers under this name
spring:
  application:
    name: discovery-microservice

# Configure this Discovery Server
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
  server:
    enableSelfPreservation: false

# HTTP (Tomcat) port
server:
  port: ${PORT:8761}

3/ Application.java

@SpringBootApplication
@EnableEurekaServer
public class Application {

	public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
	}
}

L'exécution de ce dernier permet simplement d'obtenir votre premier annuaire de services up (http://localhost:8761), il n'attend plus que les services clients se connectent à lui.

Service et communication inter-services (Feign et Ribbon)

La communication entre le service Edition et le service Carte se fait par appel REST en utilisant Feign qui n'est pas un client HTTP comme les autres, il a la particularité de bien s'intégrer avec les outils de Netflix OSS comme Ribbon qui permet de faire de l'équilibrage de charge côté client (client side load balancer).

Ribbon intervient comme Routeur Dynamique et est couplé avec Eureka dans notre exemple, pour la découverte des services. Son intégration au sein de Feign (module) permet de surcharger la résolution d'URL et de profiter des caractéristiques de Ribbon :

  • Equilibrage de charge selon plusieurs stratégies : Round Robin, Random, Weighted Response Time, Zone Avoidance, Best Available ... (Load balancing)
  • Tolérance aux pannes (Fault tolerance)
  • Modèle asynchrone et réactif (Asynchronous and reactive model)
  • Gestion du cache (Caching)
  • Possibilité de batch (Batching)

Pour résumer, chaque microservice communique avec les autres par le biais d'un client Feign couplé avec Ribbon qui permet de façon transparente d'obtenir l'adresse IP d'un microservice cible disponible grâce aux informations récoltées d'Eureka.

microservice--1

1/ pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

2/ application.yml
(les valeurs sont valables pour une application de démo ou de test, à ajuster pour un environnement de production)

# Service registers under this name
spring:
  application:
    name: editions-microservice

# Discovery Server Access
eureka:
  client:
    serviceUrl:
      defaultZone: ${DISCOVERY_URL:http://localhost:8761}/eureka
  instance:
    leaseRenewalIntervalInSeconds: 1
    leaseExpirationDurationInSeconds: 2

# Ribbon enabled
ribbon:
  eureka:
    enabled: true
  ReadTimeout: 2000 # bounces on the next server available beyond the timeout

# HTTP Server (Tomcat) Port
server:
  port: ${PORT:3333}

## Configuring info endpoint
info:
  app:
    name: ${spring.application.name}
    description: editions microservice server
    version: 1.0.0

3/ Application.java

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(clients = {CardClient.class})
public class Application {

	public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
	}
}

4/ Feign

Ribbon étant activé pour lire les informations récoltées par Eureka, "cards-microservice" est un identifiant de service connu (le service Carte étant lancé avec la propriété spring.application.name = cards-microservice) sans avoir à précisier quoi que ce soit d'autre.

@FeignClient(value = "cards-microservice")
public interface CardClient {

    @GetMapping("/cards/{cardId}")
    Card getById(@PathVariable("cardId") String cardId);

    @GetMapping("/cards/search")
    List<Card> getBySearch(@RequestParam("edition") String editionCode);

}

5/ Utilisation du client Feign

La méthode "getByCodeWithCards()" ci-dessous permet au service Edition d'appeler le service Carte de façon très simple et efficace.

@Repository
public class StubEditionRepository {

    private Logger logger = Logger.getLogger(this.getClass());

    private final CardClient cardClient;
    private final List<Edition> fakeEditions = new ArrayList<>();

    public StubEditionRepositoryB(CardClient cardClient) {
        this.cardClient = cardClient;

        Edition edition = new Edition("Rivals of Ixalan", "RIX", "19/01/2018", 196);
        fakeEditions.add(edition);
        edition = new Edition("Ixalan", "XLN", "29/09/2017", 279);
        fakeEditions.add(edition);

        logger.info("Created StubEditionRepository");
    }

    public Optional<Edition> getByCode(String editionCode) {
        return fakeEditions.stream()
                .filter(c -> StringUtils.equals(c.getCode(), editionCode))
                .findAny();
    }

    public Optional<Edition> getByCodeWithCards(String editionCode) {
        Optional<Edition> editionOptional = getByCode(editionCode);

        if (editionOptional.isPresent()) {
            Edition edition = editionOptional.get();
            edition.setCards(cardClient.getBySearch(editionCode));
        }

        return editionOptional;
    }
}

6/ Exposition de la ressource /editions

@RestController
public class EditionResource {

    protected Logger logger = Logger.getLogger(EditionResource.class.getName());

    @Autowired
    private StubEditionRepository editionRepository;

    @GetMapping("/editions/{editionCode}/cards")
    public Edition getByCodeWithCards(@PathVariable("editionCode") String code) {
        logger.info(String.format("editions-microservice getByCodeWithCards() invoked: %s", code));
        Edition edition = editionRepository.getByCodeWithCards(code).orElse(null);
        logger.info(String.format("editions-microservice getByCodeWithCards() found: %s", edition));
        return edition;
    }

}

Voilà tout est en place, notre annuaire est opérationnel ainsi que ses services et la communication inter-services.

Web UI avec Spring Boot/Freemarker

Voici un exemple de "webapp frontend" qui permet d'utiliser les services connectés à l'annuaire Eureka.

1/ pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

2/ application.yml

Notre webapp passe directement par Eureka (sans connaître aucune IP d'un de ses microservices).

# Service registers under this name
spring:
  application:
    name: web-ui

# Discovery Server Access
eureka:
  client:
    serviceUrl:
      defaultZone: ${DISCOVERY_URL:http://localhost:8761}/eureka

# Ribbon enabled
ribbon:
  eureka:
    enabled: true

# HTTP Server (Tomcat) Port
server:
  port: ${PORT:8080}

## Configuring info endpoint
info:
  app:
    name: ${spring.application.name}
    description: web-ui server
    version: 1.0.0

3/ Application.java

@SpringBootApplication
@EnableDiscoveryClient
public class Application {
  
     public static void main(String[] args) {
          SpringApplication.run(Application.class, args);
     }
 
     @Bean
     @LoadBalanced
     public RestTemplate restTemplate() {
          return new RestTemplate();
     }
}

4/ Appel vers Eureka pour récupérer une édition et ses cartes

@Repository
public class RemoteEditionRepository {
 
    private final RestTemplate restTemplate;
    private final String serviceUrl;
 
    public RemoteAccountRepository(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
        this.serviceUrl = "http://editions-microservice";
    }

    @Override
    public Edition getEditionWithAllItsCards(String editionCode) {
        return restTemplate.getForObject(serviceUrl + "/editions/{editionCode}/cards", Edition.class, editionCode);
    }

}

5/ @Controller et page .ftl

L'affichage d'une page d'édition avec ses cartes est basique, voici un exemple rapide :

@Controller
public class EditionController {

    @Autowired
    private RemoteEditionRepository editionRepository;

    @GetMapping("/editionDetails")
    public String editionDetails(@RequestParam("code") String code, Model model) {
        model.addAttribute("edition", editionRepository.getEditionWithAllItsCards(code));
        return "edition-details";
    }
}
<!DOCTYPE html>

<html>
<head></head>
<body>

    <h1>Edition</h1>

    <section id="details">
        <div>Nom : ${edition.name}</div>
        <div>Code : ${edition.code}</div>
        <div>Date de sortie : ${edition.releaseDate}</div>
    </section>

    <section id="cards">
        <ul>
            <#list edition.cards as card>
                <li>
                    <a href="/cardDetails?id=${card.id}">${card.name}</a>
                </li>
            </#list>
        </ul>
    </section>

</body>
</html>

Le prochain billet autour de l'architecture microservice portera sur la mise en place d'une porte d'entrée (Gateway) nommée Zuul au dessus de notre annuaire de services.