Introduction
La découverte de services est une fonctionnalité essentielle pour permettre à un paysage de microservices de fonctionner ensemble en production. Netflix Eureka est le premier serveur de découverte pris en charge par Spring Cloud.
Cependant, utiliser le DNS circulaire pour gérer les instances de microservices n'est pas une solution pratique, car chaque instance enregistrerait son adresse IP sous le même nom dans un serveur DNS, ce qui poserait des problèmes pour gérer des instances volatiles. En effet, un client DNS conserve généralement une adresse IP fonctionnelle et ne prend pas en compte l'ensemble des instances disponibles. Par ailleurs, ni une implémentation de serveur DNS typique ni le protocole DNS ne sont bien adaptés pour gérer des instances de microservices qui vont et viennent tout le temps.
Pour garder une trace des instances de microservices disponibles et prendre en compte leur volatilité, nous avons besoin d'un serveur de découverte plus puissant. Nous devons prendre en compte les points suivants :
- De nouvelles instances peuvent démarrer à tout moment.
- Les instances existantes peuvent cesser de répondre et éventuellement planter à tout moment.
- Certaines des instances défaillantes pourraient être correctes après un certain temps et devraient recommencer à recevoir du trafic, tandis que d'autres ne le seront pas et devraient être supprimées du registre de service.
- Certaines instances de microservice peuvent prendre un certain temps pour démarrer ; c'est-à-dire que ce n'est pas parce qu'ils peuvent recevoir des requêtes HTTP que le trafic doit leur être acheminé.
- Un partitionnement réseau involontaire et d'autres erreurs liées au réseau peuvent survenir à tout moment.
C'est pourquoi la construction d'un serveur de découverte robuste et résilient est une tâche complexe. Netflix Eureka est un serveur de découverte populaire utilisé avec Spring Cloud qui permet de relever ces défis.
Netflix Eureka dans Spring Cloud
Eureka est un service client de découverte qui permet aux clients de communiquer avec le serveur de découverte Netflix Eureka pour obtenir des informations sur les instances de microservice disponibles. Les clients utilisent une bibliothèque cliente pour demander régulièrement des informations sur les services disponibles, qui sont stockées dans la bibliothèque cliente. Lorsqu'un client envoie une requête à un autre microservice, il choisit une instance disponible dans sa bibliothèque cliente.
Pour faciliter l'utilisation de Netflix Eureka en tant que service de découverte, Spring Cloud fournit une abstraction appelée DiscoveryClient, qui permet d'interagir avec un service de découverte pour obtenir des informations sur les services et les instances disponibles. L'interface DiscoveryClient est capable d'enregistrer automatiquement une application Spring Boot auprès du serveur de découverte.
En outre, Spring Cloud fournit une interface appelée LoadBalancerClient pour les clients souhaitant effectuer des requêtes via un équilibreur de charge aux instances enregistrées dans le service de découverte. L'implémentation standard du client HTTP réactif, WebClient, peut être configurée pour utiliser l'implémentation LoadBalancerClient. En ajoutant l'annotation @LoadBalanced à une déclaration @Bean qui renvoie un objet WebClient.Builder, une implémentation LoadBalancerClient sera injectée dans l'instance Builder en tant que fichier ExchangeFilterFunction.
En somme, Spring Cloud facilite l'utilisation de Netflix Eureka en tant que service de découverte, en fournissant des abstractions telles que DiscoveryClient et LoadBalancerClient pour simplifier les interactions avec le service de découverte et permettre aux clients de communiquer avec les instances de microservice disponibles.
Configurer un serveur Netflix Eureka
Configurer un serveur Netflix Eureka avec Spring Cloud est très simple. Il suffit de suivre les étapes suivantes :
- Créez un projet Spring Boot en utilisant Spring Initializr.
- Ajoutez la dépendance spring-cloud-starter-netflix-eureka-server.
- Ajoutez l'annotation @EnableEurekaServer à la classe d'application.
- Ajoutez un Dockerfile similaire à ceux utilisés pour les microservices, à l'exception que le port par défaut pour Eureka, 8761, est exposé plutôt que le port par défaut pour les microservices, 8080.
- Ajoutez le serveur Eureka dans votre fichier docker-compose.
eureka:
build: spring-cloud/eureka-server
mem_limit: 512m
ports:
- "8761:8761"
Maintenant que nous avons configuré un serveur Netflix Eureka pour permettre la découverte de services, nous sommes prêts à apprendre comment connecter des microservices à ce dernier.
Connecter les microservices
Dans cette section, nous allons apprendre à connecter une instance de microservice à un serveur Netflix Eureka. Nous verrons comment les instances de microservices s'enregistrent sur le serveur Eureka lors de leur démarrage et comment les clients peuvent utiliser le serveur Eureka pour trouver les instances de microservice qu'ils souhaitent appeler.
Pour enregistrer une instance de microservice dans le serveur Eureka, voici les étapes à suivre :
- Ajouter la dépendance "spring-cloud-starter-netflix-eureka-client" dans le fichier build.gradle :
Implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
Lors de l'exécution de tests sur un seul microservice, il est préférable de ne pas dépendre de la disponibilité du serveur Eureka. Par conséquent, nous allons désactiver l'utilisation de Netflix Eureka dans tous les tests Spring Boot, c'est-à-dire les tests JUnit annotés avec @SpringBootTest. Pour cela, il suffit d'ajouter la propriété "eureka.client.enabled" et de la définir à false, comme ceci :
@SpringBootTest(webEnvironment=RANDOM_PORT, properties = {"eureka.client.enabled=false"})
Il existe une propriété de configuration très importante appelée "spring.application.name". Cette propriété permet de donner à chaque microservice un nom d'hôte virtuel, qui est utilisé par le service Eureka pour identifier chaque microservice. Les clients Eureka utiliseront ce nom d'hôte virtuel dans les URL utilisées pour effectuer des appels HTTP au microservice.
Pour pouvoir rechercher des instances de microservices disponibles via le serveur Eureka dans le microservice "product-composite", nous devons également effectuer les opérations suivantes :
- Ajouter un bean Spring dans la classe d'application principale qui crée un load balancer-aware WebClient-builder :
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
Le bean WebClient peut être utilisé par la classe d'intégration ProductCompositeIntegration en l'injectant dans le constructeur :
private WebClient webClient;
@Autowired
public ProductCompositeIntegration(
WebClient.Builder webClientBuilder,
...
) {
this.webClient = webClientBuilder.build();
...
}
Le constructeur utilise le générateur injecté pour créer le WebClient.
Une fois construit, le WebClient est immuable. Cela signifie qu'il peut être réutilisé par des requêtes simultanées sans risquer de conflit.
Il est maintenant temps de se débarrasser du code dur dans la configuration des microservices disponibles dans application.yml :
app:
product-service:
host: localhost
port: 7001
recommendation-service:
host: localhost
port: 7002
review-service:
host: localhost
port: 7003
Le code correspondant dans la classe d'intégration "ProductCompositeIntegration", qui gérait la configuration codée en dur, est simplifié et remplacé par une déclaration des URL de base vers les API des microservices principaux. Voici le code correspondant :
private static final String PRODUCT_SERVICE_URL = "http://product";
private static final String RECOMMENDATION_SERVICE_URL = "http://recommendation";
private static final String REVIEW_SERVICE_URL = "http://review";
Maintenant que nous avons vu comment connecter des instances de microservices à un serveur Netflix Eureka, nous pouvons passer à la configuration du serveur Eureka et des instances de microservice qui s'y connectent.
Configuration pour une utilisation locale
Maintenant, nous allons aborder la partie la plus délicate de la configuration de Netflix Eureka en tant que service de découverte : mettre en place une configuration de travail pour le serveur Eureka et ses clients, à savoir nos instances de microservice.
Netflix Eureka est un serveur de découverte hautement configurable qui peut être configuré pour un certain nombre de cas d'utilisation différents, et il fournit des caractéristiques d'exécution robustes, résilientes et tolérantes aux pannes. Toutefois, cela signifie qu'il dispose d'un nombre presque écrasant d'options de configuration. Heureusement, Netflix Eureka est fourni avec de bonnes valeurs par défaut pour la plupart des paramètres configurables, du moins lorsqu'il s'agit de les utiliser dans un environnement de production.
Cependant, lorsqu'il s'agit d'utiliser Netflix Eureka pendant le développement, les valeurs par défaut entraînent de longs temps de démarrage. Par exemple, il peut s'écouler beaucoup de temps avant qu'un client effectue un appel initial réussi à une instance de microservice enregistrée sur le serveur Eureka.
L'utilisation des valeurs de configuration par défaut peut entraîner jusqu'à deux minutes de temps d'attente. Ce temps d'attente s'ajoute au temps de démarrage du service Eureka et des microservices. Les processus impliqués doivent synchroniser les informations d'enregistrement les uns avec les autres. Les instances de microservice doivent s'enregistrer auprès du serveur Eureka et le client doit collecter des informations auprès du serveur Eureka. Cette communication est principalement basée sur des battements de cœur, qui se produisent toutes les 30 secondes par défaut. Quelques caches sont également impliqués, ce qui ralentit la propagation des mises à jour.
Pour minimiser ce temps d'attente, nous utiliserons une configuration spécifique qui est utile lors du développement. Cependant, il est important de rappeler que les valeurs par défaut doivent être utilisées comme point de départ pour une utilisation en production.
Commençons par apprendre quels types de paramètres de configuration nous devons connaître.
Paramètres de configuration d'Eureka
Il existe 3 groupes de paramètres :
- Les paramètres pour le serveur Eureka, préfixés par "eureka.server".
- Les paramètres pour les clients Eureka, préfixés par "eureka.client". Ces paramètres sont destinés aux clients qui souhaitent communiquer avec un serveur Eureka.
- Les paramètres pour les instances Eureka, préfixés par "eureka.instance". Ces paramètres sont destinés aux instances de microservices qui souhaitent s'enregistrer sur le serveur Eureka.
Configuration du serveur Eureka
Pour configurer le serveur Eureka pour une utilisation dans un environnement de développement, la configuration suivante peut être utilisée :
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
waitTimeInMsWhenSyncEmpty: 0
response-cache-update-interval-ms: 5000
La première partie de la configuration est destinée à une instance Eureka et à un client et sert à configurer un serveur Eureka autonome. Pour plus de détails, veuillez consulter la documentation de Spring Cloud. Les deux derniers paramètres utilisés pour le serveur Eureka, "waitTimeInMsWhenSyncEmpty" et "response-cache-update-interval-ms", sont utilisés pour minimiser le temps de démarrage.
Une fois que le serveur Eureka est configuré, nous sommes prêts à voir comment configurer les clients du serveur Eureka, c'est-à-dire les instances de microservices.
Configuration des clients sur le serveur Eureka
Pour pouvoir se connecter au serveur Eureka, les microservices ont la configuration suivante :
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
initialInstanceInfoReplicationIntervalSeconds: 5
registryFetchIntervalSeconds: 5
instance:
leaseRenewalIntervalInSeconds: 5
leaseExpirationDurationInSeconds: 5
---
spring.config.activate.on-profile: docker
eureka.client.serviceUrl.defaultZone: http://eureka:8761/eureka/
Le paramètre "eureka.client.serviceUrl.defaultZone" est utilisé pour trouver le serveur Eureka en utilisant le nom d'hôte "localhost" lors de l'exécution sans Docker et le nom d'hôte "eureka" lors de l'exécution en tant que conteneurs Docker. Les paramètres sont utilisés pour minimiser le temps de démarrage et le temps nécessaire pour désenregistrer une instance de microservice qui est arrêtée.
Maintenant, tout est en place pour tester le service de découverte en utilisant le serveur Netflix Eureka avec nos microservices.
Essayer le service de découverte
Maintenant que tout est configuré, nous sommes prêts à utiliser Netflix Eureka.
Tout d'abord, nous devons créer les images Docker avec les commandes suivantes :
./gradlew build && docker-compose build
Avec le paysage système opérationnel, nous pouvons commencer par tester comment augmenter le nombre d'instances pour l'un des microservices. Exécutez la commande suivante pour essayer de faire évoluer un service :
docker-compose up -d --scale review=3
Une fois que les nouvelles instances sont opérationnelles, accédez à http://localhost:8761/ et attendez-vous à voir quelque chose comme ceci :
Une façon de savoir lorsque les nouvelles instances sont opérationnelles, exécutez cette commande :
docker-compose logs review | grep Started
Attendez-vous à une sortie qui ressemble à ceci :
Nous pouvons également utiliser l'API REST exposée par le service Eureka. Pour obtenir une liste des ID d'instance, nous pouvons émettre une commande curl, comme celle-ci :
curl -H "accept:application/json" localhost:8761/eureka/apps -s | jq -r .applications.application[].instance[].instanceId
Attendez-vous à une réponse semblable à celle-ci :
Maintenant que toutes les instances sont opérationnelles, essayons l'équilibrage de charge côté client en effectuant quelques requêtes et en nous concentrant sur l'adresse du service dans les réponses, comme suit :
curl localhost:8080/product-composite/1 -s | jq -r .serviceAddresses.rev
Attendez-vous à des réponses similaires à celles-ci :
Notez que l'adresse du service change dans chaque réponse ; l'équilibreur de charge utilise la logique round-robin pour appeler les instances disponibles, une à la fois !
Tests disruptifs
Pour simuler un crash du serveur Eureka, suivez les étapes suivantes :
- Tout d'abord, arrêtez le serveur Eureka et maintenez les instances de microservices opérationnelles :
docker-compose up -d --scale review=2 --scale eureka=0
Cela arrêtera le serveur Eureka, mais les instances de microservices continueront de fonctionner car elles ont déjà lu les informations sur les instances disponibles avant l'arrêt du serveur Eureka.
- Essayez de faire une requête vers une instance de microservice :
curl localhost:8080/product-composite/1 -s | jq -r .serviceAddresses.rev
Cela devrait fonctionner car les informations sur les instances de microservices ont été mises en cache localement par le client et ne nécessitent pas de communication avec le serveur Eureka pour fonctionner. Cependant, si une nouvelle instance de microservice est ajoutée ou qu'une instance existante est résiliée, le client ne sera pas informé sans le serveur Eureka et les appels aux instances de microservice qui ne sont plus disponibles échoueront.
Voici comment simuler le plantage d'une instance de microservice et ajouter une instance supplémentaire du service produit :
Arrêt d'une instance
Pour approfondir les effets d'un serveur Eureka en panne, simulons également le plantage d'une instance de microservice restante. Arrêtez l'une des deux instances à l'aide de la commande suivante :
docker-compose up -d --scale review=1 --scale eureka=0
Le client, c'est-à-dire le service product-composite, ne sera pas averti que l'une des instances review a disparu puisqu'aucun serveur Eureka n'est en cours d'exécution. Pour cette raison, il pense toujours qu'il y a deux instances en cours d'exécution. Un appel sur deux au client entraînera l'appel d'une instance review qui n'existe plus, ce qui fait que la réponse du client ne contient aucune information du service review. L'adresse de service du service review sera vide.
curl localhost:8080/product-composite/1 -s | jq -r .serviceAddresses.rev
Cela peut être évité, comme décrit précédemment, en utilisant des mécanismes de résilience tels que les délais d'attente et les tentatives.
Démarrage d'une instance supplémentaire du service produit
En tant que test final des effets d'un serveur Eureka en panne, voyons ce qui se passe si nous démarrons une nouvelle instance du microservice product. Effectuez les étapes suivantes :
docker-compose up -d --scale review=1 --scale eureka=0 --scale product=2
Appelez l'API plusieurs fois et extrayez l'adresse du service product avec la commande suivante :
curl localhost:8080/product-composite/1 -s | jq -r .serviceAddresses.pro
Puisqu'aucun serveur Eureka n'est en cours d'exécution, le client ne sera pas informé de la nouvelle instance product, et donc tous les appels iront à la première instance.
Redémarrer le serveur Eureka
Pour terminer la section sur les tests perturbateurs, redémarrez le serveur Netflix Eureka et voyez comment le paysage système gère l'auto-guérison, c'est-à-dire la résilience. Effectuez les étapes suivantes :
docker-compose up -d --scale review=1 --scale eureka=1 --scale product=2
Effectuez l'appel suivant plusieurs fois pour extraire les adresses du produit et du service review :
curl localhost:8080/product-composite/1 -s | jq -r .serviceAddresses
Vérifiez que les événements suivants se produisent :
- Tous les appels vont à l'instance restante, démontrant que le client a détecté que la deuxième instance a disparu.
- Les appels au service sont répartis sur les deux instances, démontrant que le client a détecté qu'il y a deux instances disponibles.
La réponse doit contenir la même adresse pour l'instance et deux adresses différentes pour les deux instances.
Voici la première réponse :
{
"rev": "",
"pro": "192.168.128.7:8080"
}