Architecture de microservice sur une pile moderne de technologies Java

Nous avions JDK 11, Kotlin, Spring 5 et Spring Boot 2, Gradle 5 avec Kotlin DSL prêt pour la production, JUnit 5 et une douzaine de bibliothèques de pile Spring Cloud pour la découverte de services, la création d'API de passerelle, l'équilibrage client et l'implémentation d'un disjoncteur l'écriture de clients HTTP déclaratifs, le traçage distribué et tout ça. Non pas que tout cela ait été nécessaire pour créer une architecture de microservices - juste pour le plaisir ...

Entrée


Dans cet article, vous verrez un exemple d'architecture de microservices utilisant des technologies pertinentes dans le monde Java, dont les principales sont données ci-dessous (ces versions sont utilisées dans le projet au moment de la publication):
Type de technologieLe titreLa version
PlateformeJdk11.0.1
Langage de programmationKotlin1.3.10
Cadre d'applicationCadre de printemps5.0.9
Botte de printemps2.0.5
Système de constructionGradle5,0
Gradle Kotlin DSL1.0.4
Cadre de tests unitairesJunit5.1.1
Nuage de printemps
Point d'accès unique (passerelle API)Passerelle de nuage de printempsInclus dans la version Release Train du projet Finchley SR2 Spring Cloud
Configuration centraliséeConfiguration de nuage de printemps
Traçage des demandes (traçage distribué)Détective des nuages ​​de printemps
Client HTTP déclaratifSpring Cloud OpenFeign
Découverte de serviceSpring Cloud Netflix Eureka
DisjoncteurSpring Cloud Netflix Hystrix
Équilibrage de charge côté clientRuban Netflix Spring Cloud

Le projet comprend 5 microservices: 3 infrastructures (serveur de configuration, serveur de découverte de services, passerelle d'interface utilisateur) et des exemples de front-end (UI d'articles) et de back-end (service d'articles):


Tous seront examinés séquentiellement ci-dessous. Dans un projet de «combat», évidemment, il y aura beaucoup plus de microservices qui implémenteront toutes les fonctionnalités métier. Les ajouter à une architecture similaire se fait techniquement de la même manière que l'interface utilisateur Items et le service Items.

Clause de non-responsabilité


L'article ne considère pas les instruments de conteneurisation et d'orchestration, car à l'heure actuelle, ils ne sont pas utilisés dans le projet.

Serveur de configuration


Spring Cloud Config a été utilisé pour créer un référentiel centralisé des configurations d'application. Les configurations peuvent être lues à partir de diverses sources, par exemple, un référentiel git séparé; dans ce projet, pour plus de simplicité et de clarté, ils sont dans les ressources applicatives:


Dans ce cas, la configuration du serveur de configuration ( application.yml ) elle-même ressemble à ceci:

 spring: profiles: active: native cloud: config: server: native: search-locations: classpath:/config server: port: 8888 

L'utilisation du port 8888 permet aux clients du serveur de configuration de ne pas spécifier explicitement son port dans leur bootstrap.yml . Au démarrage, ils téléchargent leur configuration en exécutant une demande GET sur le serveur HTTP API Config.

Le code du programme pour ce microservice se compose d'un seul fichier, qui contient la déclaration de la classe d'application et la méthode principale, qui, contrairement au code Java équivalent, est une fonction de niveau supérieur:

 @SpringBootApplication @EnableConfigServer class ConfigServerApplication fun main(args: Array<String>) { runApplication<ConfigServerApplication>(*args) } 

Les classes d'application et les méthodes principales des autres microservices ont une apparence similaire.

Serveur de découverte de services


La découverte de service est un modèle d'architecture de microservice qui vous permet de simplifier l'interaction entre les applications face à un changement possible du nombre de leurs instances et de l'emplacement réseau. Un élément clé de cette approche est le registre des services - une base de données des microservices, de leurs instances et des emplacements réseau (plus de détails ici ).

Dans ce projet, la découverte de service est basée sur Netflix Eureka, qui est une découverte de service côté client : le serveur Eureka exécute la fonction du registre de service et le client Eureka, avant d'exécuter une demande vers un microservice, contacte le serveur Eureka pour obtenir une liste des instances de l'application appelée et effectue indépendamment l'équilibrage charger (en utilisant le ruban Netflix). Netflix Eureka, comme certains autres composants de pile Netflix OSS (tels que Hystrix et Ribbon) s'intègre aux applications Spring Boot à l'aide de Spring Cloud Netflix .

Dans la configuration du serveur de découverte de services située dans ses ressources ( bootstrap.yml ), seuls le nom de l'application et le paramètre indiquant que le démarrage du microservice sera interrompu s'il est impossible de se connecter au serveur de configuration sont indiqués:

 spring: application: name: eureka-server cloud: config: fail-fast: true 

Le reste de la configuration de l'application se trouve dans le eureka-server.yml dans les ressources du serveur de configuration:

 server: port: 8761 eureka: client: register-with-eureka: true fetch-registry: false 

Le serveur Eureka utilise le port 8761, ce qui permet à tous les clients Eureka de ne pas le spécifier en utilisant la valeur par défaut. La valeur du register-with-eureka (indiquée pour plus de clarté, car elle est également utilisée par défaut) signifie que l'application elle-même, comme les autres microservices, sera enregistrée sur le serveur Eureka. Le paramètre fetch-registry détermine si le client Eureka recevra des données du registre de service.

Une liste des demandes enregistrées et d'autres informations est disponible sur http://localhost:8761/ :


Les alternatives pour implémenter la découverte de services sont Consul, Zookeeper et autres.

Service d'articles


Cette application est un exemple de back-end avec une API REST implémentée en utilisant le framework WebFlux qui est apparu dans Spring 5 (la documentation est ici ), ou plutôt Kotlin DSL pour cela:

 @Bean fun itemsRouter(handler: ItemHandler) = router { path("/items").nest { GET("/", handler::getAll) POST("/", handler::add) GET("/{id}", handler::getOne) PUT("/{id}", handler::update) } } 

Le traitement des requêtes HTTP reçues est délégué au ItemHandler classe ItemHandler . Par exemple, une méthode pour obtenir une liste d'objets d'une entité ressemble à ceci:

 fun getAll(request: ServerRequest) = ServerResponse.ok() .contentType(APPLICATION_JSON_UTF8) .body(fromObject(itemRepository.findAll())) 

L'application devient le client du serveur Eureka, c'est-à-dire qu'elle enregistre et reçoit des données du registre de service, en raison de la présence de la spring-cloud-starter-netflix-eureka-client . Après l'enregistrement, l'application envoie des hartbits au serveur Eureka avec une certaine fréquence, et si pendant un certain temps le pourcentage de hartbits reçus par le serveur Eureka par rapport à la valeur maximale possible tombe en dessous d'un certain seuil, l'application sera supprimée du registre du service.

Examinez l'une des façons d'envoyer des métadonnées supplémentaires au serveur Eureka:

 @PostConstruct private fun addMetadata() = aim.registerAppMetadata(mapOf("description" to "Some description")) 

Assurez-vous que ces données sont reçues par le serveur Eureka en allant sur http://localhost:8761/eureka/apps/items-service via Postman:



Interface utilisateur


Ce microservice, en plus de démontrer l'interaction avec la passerelle UI (sera montré dans la section suivante), exécute la fonction front-end pour le service Items, qui peut interagir avec l'API REST de plusieurs manières:

  1. API client vers REST écrite à l'aide d'OpenFeign:

     @FeignClient("items-service", fallbackFactory = ItemsServiceFeignClient.ItemsServiceFeignClientFallbackFactory::class) interface ItemsServiceFeignClient { @GetMapping("/items/{id}") fun getItem(@PathVariable("id") id: Long): String @GetMapping("/not-existing-path") fun testHystrixFallback(): String @Component class ItemsServiceFeignClientFallbackFactory : FallbackFactory<ItemsServiceFeignClient> { private val log = LoggerFactory.getLogger(this::class.java) override fun create(cause: Throwable) = object : ItemsServiceFeignClient { override fun getItem(id: Long): String { log.error("Cannot get item with id=$id") throw ItemsUiException(cause) } override fun testHystrixFallback(): String { log.error("This is expected error") return "{\"error\" : \"Some error\"}" } } } } 
  2. RestTemplate Bean
    Un bac est créé dans la configuration java:

     @Bean @LoadBalanced fun restTemplate() = RestTemplate() 

    Et utilisé de cette façon:

     fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result" 
  3. WebClient classe WebClient (méthode spécifique au framework WebFlux)
    Un bac est créé dans la configuration java:

     @Bean fun webClient(loadBalancerClient: LoadBalancerClient) = WebClient.builder() .filter(LoadBalancerExchangeFilterFunction(loadBalancerClient)) .build() 

    Et utilisé de cette façon:

     fun requestWithWebClient(id: Long): Mono<String> = webClient.get().uri("http://items-service/items/$id").retrieve().bodyToMono(String::class.java) 

Le fait que les trois méthodes renvoient le même résultat peut être vérifié en allant sur http://localhost:8081/example :


Je préfère l'option utilisant OpenFeign, car elle permet de développer un contrat d'interaction avec le microservice appelé, dont la mise en œuvre est entreprise par Spring. Un objet qui implémente ce contrat est injecté et utilisé comme un bean standard:

 itemsServiceFeignClient.getItem(1) 

Si la demande échoue pour une raison quelconque, la méthode correspondante de la classe qui implémente l'interface FallbackFactory sera appelée, dans laquelle vous devez traiter l'erreur et renvoyer la réponse par défaut (ou lever une exception plus loin). En cas d'échec d'un certain nombre d'appels consécutifs, le fusible ouvrira le circuit (en savoir plus sur le disjoncteur ici et ici ), donnant le temps de récupérer le microservice tombé.

Pour utiliser le client Feign, vous devez annoter la @EnableFeignClients application @EnableFeignClients :

 @SpringBootApplication @EnableFeignClients(clients = [ItemsServiceFeignClient::class]) class ItemsUiApplication 

Pour que la solution de secours Hystrix fonctionne dans le client Feign, vous devez ajouter les éléments suivants à la configuration de l'application:

 feign: hystrix: enabled: true 

Pour tester le fonctionnement du repli Hystrix dans le client Feign, rendez-vous simplement sur http://localhost:8081/hystrix-fallback . Le client Feign essaiera d'exécuter la demande sur un chemin qui n'existe pas dans le service Items, ce qui conduira au retour de la réponse:

 {"error" : "Some error"} 

Passerelle UI


Le modèle de passerelle API vous permet de créer un point d'entrée unique pour l'API fourni par d'autres microservices (plus de détails ici ). Une application qui implémente ce modèle effectue le routage (routage) des demandes vers les microservices et peut également effectuer des fonctions supplémentaires, par exemple, l'authentification.

Dans ce projet, pour plus de clarté, une passerelle d'interface utilisateur est implémentée, c'est-à-dire un point d'entrée unique pour différentes interfaces utilisateur; évidemment, l'API de passerelle est implémentée de la même manière. Le microservice est implémenté sur la base du framework Spring Cloud Gateway. Une alternative est Netflix Zuul, qui fait partie de Netflix OSS et est intégré à Spring Boot à l'aide de Spring Cloud Netflix.
La passerelle UI s'exécute sur le port 443 à l'aide du certificat SSL généré (situé dans le projet). SSL et HTTPS sont configurés comme suit:

 server: port: 443 ssl: key-store: classpath:keystore.p12 key-store-password: qwerty key-alias: test_key key-store-type: PKCS12 

Les identifiants et les mots de passe des utilisateurs sont stockés dans une implémentation basée sur une carte de l'interface ReactiveUserDetailsService spécifique à WebFlux:

 @Bean fun reactiveUserDetailsService(): ReactiveUserDetailsService { val user = User.withDefaultPasswordEncoder() .username("john_doe").password("qwerty").roles("USER") .build() val admin = User.withDefaultPasswordEncoder() .username("admin").password("admin").roles("ADMIN") .build() return MapReactiveUserDetailsService(user, admin) } 

Les paramètres de sécurité sont configurés comme suit:

 @Bean fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain = http .formLogin().loginPage("/login") .and() .authorizeExchange() .pathMatchers("/login").permitAll() .pathMatchers("/static/**").permitAll() .pathMatchers("/favicon.ico").permitAll() .pathMatchers("/webjars/**").permitAll() .pathMatchers("/actuator/**").permitAll() .anyExchange().authenticated() .and() .csrf().disable() .build() 

La configuration donnée détermine qu'une partie des ressources Web (par exemple, la statique) est disponible pour tous les utilisateurs, y compris ceux qui ne se sont pas authentifiés, et tout le reste ( .anyExchange() ) est uniquement authentifié. Si vous essayez de saisir une URL nécessitant une authentification, elle sera redirigée vers la page de connexion ( https://localhost/login ):


Cette page utilise les outils du framework Bootstrap, qui est connecté au projet à l'aide de Webjars, ce qui permet de gérer les bibliothèques côté client comme des dépendances régulières. Thymeleaf est utilisé pour former des pages HTML. L'accès à la page de connexion est configuré à l'aide de WebFlux:

 @Bean fun routes() = router { GET("/login") { ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("login") } } 

Le routage Spring Cloud Gateway peut être configuré dans une configuration YAML ou java. Les itinéraires vers les microservices sont attribués manuellement ou sont créés automatiquement en fonction des données reçues du registre du service. Avec un nombre suffisamment important d'interfaces utilisateur vers lesquelles le routage est requis, il sera plus pratique d'utiliser l'intégration avec le registre de service:

 spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true include-expression: serviceId.endsWith('-UI') url-expression: "'lb:http://'+serviceId" 

La valeur du paramètre include-expression indique que les routes seront créées uniquement pour les microservices dont les noms se terminent par -UI , et la valeur du paramètre url-expression est qu'elles sont accessibles via le protocole HTTP, contrairement à la passerelle UI qui fonctionne via HTTPS, et lorsqu'elles sont accédées ils utiliseront l'équilibrage de la charge client (implémenté à l'aide du ruban Netflix).

Prenons l'exemple de la création manuelle de routes dans la configuration java (sans intégration avec le registre de service):

 @Bean fun routeLocator(builder: RouteLocatorBuilder) = builder.routes { route("eureka-gui") { path("/eureka") filters { rewritePath("/eureka", "/") } uri("lb:http://eureka-server") } route("eureka-internals") { path("/eureka/**") uri("lb:http://eureka-server") } } 

La première route route vers la page d'accueil du serveur Eureka précédemment affichée ( http://localhost:8761 ), la seconde est nécessaire pour charger les ressources sur cette page.

Toutes les routes créées par l'application sont disponibles sur https://localhost/actuator/gateway/routes .

Dans les microservices sous-jacents, il peut être nécessaire d'accéder à la connexion et / ou aux rôles de l'utilisateur authentifié dans la passerelle UI. Pour ce faire, j'ai créé un filtre qui ajoute les en-têtes appropriés à la demande:

 @Component class AddCredentialsGlobalFilter : GlobalFilter { private val loggedInUserHeader = "logged-in-user" private val loggedInUserRolesHeader = "logged-in-user-roles" override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain) = exchange.getPrincipal<Principal>() .flatMap { val request = exchange.request.mutate() .header(loggedInUserHeader, it.name) .header(loggedInUserRolesHeader, (it as Authentication).authorities?.joinToString(";") ?: "") .build() chain.filter(exchange.mutate().request(request).build()) } } 

Passons maintenant à l'interface utilisateur des éléments à l'aide de la passerelle d'interface utilisateur - https://localhost/items-ui/greeting , en supposant à juste titre que le traitement de ces en-têtes a déjà été implémenté dans l'interface utilisateur des éléments:


Spring Cloud Sleuth est une solution pour le suivi des requêtes dans un système distribué. Les identifiants Trace Id (identifiant d'intercommunication) et Span Id (identifiant d'unité d'oeuvre) sont ajoutés aux en-têtes de la demande passant par plusieurs microservices (pour une meilleure compréhension, j'ai simplifié le schéma; voici une explication plus détaillée):


Cette fonctionnalité est connectée en ajoutant simplement la spring-cloud-starter-sleuth .

En spécifiant les paramètres de journalisation appropriés, dans la console des microservices correspondants, vous pouvez voir quelque chose comme ceci (l'ID de trace et l'ID de plage sont affichés après le nom du microservice):

 DEBUG [ui-gateway,009b085bfab5d0f2,009b085bfab5d0f2,false] oscghRoutePredicateHandlerMapping : Route matched: CompositeDiscoveryClient_ITEMS-UI DEBUG [items-ui,009b085bfab5d0f2,947bff0ce8d184f4,false] oswrfunction.server.RouterFunctions : Predicate "(GET && /example)" matches against "GET /example" DEBUG [items-service,009b085bfab5d0f2,dd3fa674cd994b01,false] oswrfunction.server.RouterFunctions : Predicate "(GET && /{id})" matches against "GET /1" 

Pour une représentation graphique d'une trace distribuée, vous pouvez utiliser, par exemple, Zipkin, qui agira comme un serveur qui agrège les informations sur les requêtes HTTP provenant d'autres microservices (plus de détails ici ).

Assemblage


En fonction du système d'exploitation, la gradlew clean build ./gradlew clean build ou la ./gradlew clean build .

Étant donné la possibilité d'utiliser le wrapper Gradle , il n'est pas nécessaire d'avoir un Gradle installé localement.

La génération et le lancement ultérieur passent avec succès le JDK 11.0.1. Avant cela, le projet fonctionnait sur JDK 10, donc je suppose que sur cette version, il n'y aura pas de problèmes d'assemblage et de lancement. Je n'ai pas de données sur les versions antérieures du JDK. Gardez également à l'esprit que le Gradle 5 utilisé nécessite au moins JDK 8.

Lancement


Je recommande de démarrer les applications dans l'ordre dans lequel elles sont décrites dans cet article. Si vous utilisez Intellij IDEA avec Run Dashboard activé, vous devriez obtenir quelque chose comme ceci:


Conclusion


L'article a examiné un exemple d'architecture de microservices sur la pile technologique actuelle dans le monde Java, ses principaux composants et certaines fonctionnalités. J'espère que pour quelqu'un le matériel sera utile. Merci de votre attention!

Les références


Source: https://habr.com/ru/post/fr431474/


All Articles