Microservice-Architektur auf einem modernen Stapel von Java-Technologien

Wir hatten JDK 11, Kotlin, Spring 5 und Spring Boot 2, Gradle 5 mit produktionsbereitem Kotlin DSL, JUnit 5 sowie ein halbes Dutzend Spring Cloud-Stack-Bibliotheken für die Serviceerkennung, das Erstellen von Gateway-APIs, das Client-Balancing und das Implementieren von Leistungsschaltermustern Schreiben deklarativer HTTP-Clients, verteiltes Tracing und all das. Nicht, dass all dies benötigt worden wäre, um eine Microservice-Architektur zu erstellen - nur zum Spaß ...

Eintrag


In diesem Artikel sehen Sie ein Beispiel für eine Microservice-Architektur unter Verwendung relevanter Technologien in der Java-Welt, von denen die wichtigsten unten angegeben sind (diese Versionen werden zum Zeitpunkt der Veröffentlichung im Projekt verwendet):
Art der TechnologieTitelVersion
PlattformJdk11.0.1
ProgrammierspracheKotlin1.3.10
AnwendungsrahmenFrühlingsrahmen5.0.9
Frühlingsstiefel2.0.5
System erstellenGradle5.0
Gradle Kotlin DSL1.0.4
Unit Testing FrameworkJunit5.1.1
Frühlingswolke
Single Access Point (API-Gateway)Frühlingswolken-GatewayIm Release-Zug Finchley SR2-Projekt Spring Cloud enthalten
Zentralisierte KonfigurationSpring Cloud Konfiguration
Anforderungsverfolgung (verteilte Verfolgung)Frühlingswolkenwahrheit
Deklarativer HTTP-ClientFrühlingswolke OpenFeign
ServiceerkennungFrühlingswolke Netflix Eureka
LeistungsschalterSpring Cloud Netflix Hystrix
Clientseitiger LastausgleichSpring Cloud Netflix-Multifunktionsleiste

Das Projekt besteht aus 5 Microservices: 3 Infrastrukturen (Konfigurationsserver, Service Discovery Server, UI-Gateway) und Beispielen für Front-End (Items UI) und Back-End (Items Service):


Alle von ihnen werden nachfolgend nacheinander betrachtet. In einem „Kampf“ -Projekt wird es offensichtlich deutlich mehr Microservices geben, die alle Geschäftsfunktionen implementieren. Das Hinzufügen zu einer ähnlichen Architektur erfolgt technisch auf die gleiche Weise wie die Benutzeroberfläche für Elemente und der Dienst "Elemente".

Haftungsausschluss


Der Artikel berücksichtigt keine Instrumente für die Containerisierung und Orchestrierung, da sie derzeit nicht im Projekt verwendet werden.

Server konfigurieren


Spring Cloud Config wurde verwendet, um ein zentrales Repository mit Anwendungskonfigurationen zu erstellen. Konfigurationen können aus verschiedenen Quellen gelesen werden, z. B. aus einem separaten Git-Repository. In diesem Projekt befinden sie sich der Einfachheit und Klarheit halber in den Anwendungsressourcen:


In diesem Fall sieht die Konfiguration des Konfigurationsservers ( application.yml ) selbst folgendermaßen aus:

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

Durch die Verwendung von Port 8888 können Config-Server-Clients ihren Port in ihrer bootstrap.yml nicht explizit angeben. Beim Start laden sie ihre Konfiguration hoch, indem sie eine GET-Anforderung an den HTTP-API-Konfigurationsserver ausführen.

Der Programmcode für diesen Microservice besteht aus nur einer Datei, die die Deklaration der Anwendungsklasse und die Hauptmethode enthält, die im Gegensatz zum entsprechenden Java-Code eine Funktion der obersten Ebene ist:

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

Anwendungsklassen und Hauptmethoden in anderen Mikrodiensten sehen ähnlich aus.

Service Discovery Server


Die Serviceerkennung ist ein Microservice-Architekturmuster, mit dem Sie die Interaktion zwischen Anwendungen angesichts einer möglichen Änderung der Anzahl ihrer Instanzen und des Netzwerkstandorts vereinfachen können. Eine Schlüsselkomponente dieses Ansatzes ist die Dienstregistrierung - eine Datenbank mit Mikrodiensten, deren Instanzen und Netzwerkstandorten (weitere Details hier ).

In diesem Projekt wird die Serviceerkennung auf der Grundlage von Netflix Eureka implementiert, einer clientseitigen Serviceerkennung : Der Eureka-Server führt die Funktion der Serviceregistrierung aus , und der Eureka-Client kontaktiert den Eureka-Server, um eine Liste der Instanzen der aufgerufenen Anwendung zu erhalten, und führt unabhängig einen Ausgleich durch, bevor er eine Anforderung an einen Mikrodienst ausführt Laden (mit Netflix Ribbon). Netflix Eureka lässt sich wie einige andere Netflix OSS-Stack-Komponenten (wie Hystrix und Ribbon) mithilfe von Spring Cloud Netflix in Spring Boot-Anwendungen integrieren.

In der Service Discovery Server-Konfiguration in den Ressourcen ( bootstrap.yml ) werden nur der Name der Anwendung und der Parameter angegeben, der angibt, dass der Start des Microservice unterbrochen wird, wenn keine Verbindung zum Config-Server hergestellt werden kann:

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

Der Rest der Anwendungskonfiguration befindet sich in der eureka-server.yml in den eureka-server.yml :

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

Der Eureka-Server verwendet Port 8761, sodass alle Eureka-Clients ihn nicht mit dem Standardwert angeben können. Der Wert des register-with-eureka (der Übersichtlichkeit register-with-eureka angegeben, register-with-eureka er auch standardmäßig verwendet wird) bedeutet, dass die Anwendung selbst wie andere Microservices auf dem Eureka-Server registriert wird. Der Parameter fetch-registry bestimmt, ob der Eureka-Client Daten von der Service-Registrierung empfängt.

Eine Liste der registrierten Anwendungen und anderer Informationen finden Sie unter http://localhost:8761/ :


Alternativen zum Implementieren der Serviceerkennung sind Consul, Zookeeper und andere.

Artikel Service


Diese Anwendung ist ein Beispiel für ein Back-End mit einer REST-API, die mithilfe des in Spring 5 veröffentlichten WebFlux-Frameworks implementiert wurde (die Dokumentation finden Sie hier ), oder besser Kotlin DSL dafür:

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

Die Verarbeitung empfangener HTTP-Anforderungen wird an die ItemHandler Klassen- ItemHandler delegiert. Eine Methode zum Abrufen einer Liste von Objekten einer Entität sieht beispielsweise folgendermaßen aus:

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

Die Anwendung wird zu einem Eureka-Server-Client, d. H. Sie registriert und empfängt Daten von der Dienstregistrierung aufgrund des Vorhandenseins der spring-cloud-starter-netflix-eureka-client . Nach der Registrierung sendet die Anwendung Hartbits mit einer bestimmten Häufigkeit an den Eureka-Server. Wenn der Prozentsatz der vom Eureka-Server empfangenen Hartbits im Verhältnis zum maximal möglichen Wert für einen bestimmten Zeitraum einen bestimmten Schwellenwert unterschreitet, wird die Anwendung aus der Dienstregistrierung gelöscht.

Betrachten Sie eine der Möglichkeiten, um zusätzliche Metadaten an den Eureka-Server zu senden:

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

http://localhost:8761/eureka/apps/items-service Sie sicher, dass diese Daten vom Eureka-Server empfangen werden, indem Sie über Postman zu http://localhost:8761/eureka/apps/items-service :



Elemente Benutzeroberfläche


Dieser Microservice demonstriert nicht nur die Interaktion mit dem UI-Gateway (wird im nächsten Abschnitt gezeigt), sondern führt auch die Front-End-Funktion für den Items-Service aus, der auf verschiedene Arten mit der REST-API interagieren kann:

  1. Mit OpenFeign geschriebene Client-REST-API:

     @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
    In der Java-Konfiguration wird ein Bin erstellt:

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

    Und so verwendet:

     fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result" 
  3. WebClient Klassen- WebClient (für das WebFlux-Framework spezifische Methode)
    In der Java-Konfiguration wird ein Bin erstellt:

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

    Und so verwendet:

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

Die Tatsache, dass alle drei Methoden dasselbe Ergebnis zurückgeben, kann unter http://localhost:8081/example : überprüft werden.


Ich bevorzuge die Option mit OpenFeign, da es möglich ist, einen Vertrag für die Interaktion mit dem genannten Microservice zu entwickeln, dessen Implementierung von Spring durchgeführt wird. Ein Objekt, das diesen Vertrag implementiert, wird injiziert und wie eine normale Bohne verwendet:

 itemsServiceFeignClient.getItem(1) 

Wenn die Anforderung aus irgendeinem Grund fehlschlägt, wird die entsprechende Methode der Klasse aufgerufen, die die FallbackFactory Schnittstelle implementiert. Dabei müssen Sie den Fehler verarbeiten und die Standardantwort zurückgeben (oder eine Ausnahme weiter auslösen). Für den Fall, dass eine bestimmte Anzahl aufeinanderfolgender Anrufe fehlschlägt, öffnet die Sicherung den Stromkreis (mehr über den Leistungsschalter hier und hier ) und gibt Zeit, um den ausgefallenen Mikrodienst wiederherzustellen.

Um den Feign-Client zu verwenden, müssen Sie die Anwendungsklasse @EnableFeignClients mit Anmerkungen versehen:

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

Damit Hystrix Fallback im Feign-Client funktioniert, müssen Sie der Anwendungskonfiguration Folgendes hinzufügen:

 feign: hystrix: enabled: true 

Um die Funktionsweise des Hystrix-Fallbacks im Feign-Client zu testen, rufen Sie einfach http://localhost:8081/hystrix-fallback . Der Feign-Client versucht, die Anforderung auf einem Pfad auszuführen, der im Items-Dienst nicht vorhanden ist. Dies führt zur Rückgabe der Antwort:

 {"error" : "Some error"} 

UI-Gateway


Mit dem API-Gateway-Muster können Sie einen einzelnen Einstiegspunkt für die API erstellen, die von anderen Microservices bereitgestellt wird (weitere Details hier ). Eine Anwendung, die dieses Muster implementiert, führt das Routing (Routing) von Anforderungen an Microservices durch und kann auch zusätzliche Funktionen ausführen, z. B. die Authentifizierung.

In diesem Projekt wird zur besseren Übersichtlichkeit ein UI-Gateway implementiert, dh ein einzelner Einstiegspunkt für verschiedene UIs. Offensichtlich ist die Gateway-API ähnlich implementiert. Der Microservice wird auf Basis des Spring Cloud Gateway Frameworks implementiert. Eine Alternative ist Netflix Zuul, Teil von Netflix OSS und mit Spring Cloud Netflix in Spring Boot integriert.
Das UI-Gateway wird auf Port 443 unter Verwendung des generierten SSL-Zertifikats (im Projekt) ausgeführt. SSL und HTTPS sind wie folgt konfiguriert:

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

Benutzeranmeldungen und Kennwörter werden in einer Map-basierten Implementierung der WebFlux-spezifischen ReactiveUserDetailsService Schnittstelle gespeichert:

 @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) } 

Die Sicherheitseinstellungen sind wie folgt konfiguriert:

 @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() 

Die angegebene Konfiguration bestimmt, dass ein Teil der Webressourcen (z. B. Statik) allen Benutzern zur Verfügung steht, einschließlich denen, die sich nicht authentifiziert haben, und alles andere ( .anyExchange() ) wird nur authentifiziert. Wenn Sie versuchen, eine URL einzugeben, für die eine Authentifizierung erforderlich ist, wird diese auf die Anmeldeseite umgeleitet ( https://localhost/login ):


Diese Seite verwendet die Tools des Bootstrap-Frameworks, das über Webjars mit dem Projekt verbunden ist. Dadurch können clientseitige Bibliotheken als reguläre Abhängigkeiten verwaltet werden. Thymeleaf wird verwendet, um HTML-Seiten zu bilden. Der Zugriff auf die Anmeldeseite wird mit WebFlux konfiguriert:

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

Das Spring Cloud Gateway-Routing kann in einer YAML- oder Java-Konfiguration konfiguriert werden. Routen zu Microservices werden entweder manuell zugewiesen oder automatisch basierend auf den von der Serviceregistrierung empfangenen Daten erstellt. Bei einer ausreichend großen Anzahl von Benutzeroberflächen, für die ein Routing erforderlich ist, ist die Integration in die Dienstregistrierung bequemer:

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

Der Wert des Parameters include-expression gibt an, dass die Routen nur für Microservices erstellt werden, deren Namen mit -UI enden . Der Wert des Parameters url-expression besteht darin, dass auf sie über das HTTP-Protokoll zugegriffen werden kann, im Gegensatz zum UI-Gateway, das über HTTPS funktioniert, und beim Zugriff Sie verwenden den Client-Lastausgleich (implementiert mit Netflix Ribbon).

Betrachten Sie das Beispiel zum manuellen Erstellen von Routen in der Java-Konfiguration (ohne Integration in die Dienstregistrierung):

 @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") } } 

Die erste Route führt zur zuvor angezeigten Eureka-Server-Homepage ( http://localhost:8761 ), die zweite zum Laden von Ressourcen auf dieser Seite.

Alle von der Anwendung erstellten Routen sind unter https://localhost/actuator/gateway/routes verfügbar.

In den zugrunde liegenden Microservices kann es erforderlich sein, auf die Anmeldung und / oder Rollen des im UI-Gateway authentifizierten Benutzers zuzugreifen. Zu diesem Zweck habe ich einen Filter erstellt, der der Anforderung die entsprechenden Header hinzufügt:

 @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()) } } 

Wenden wir uns nun über das UI-Gateway - https://localhost/items-ui/greeting - der Benutzeroberfläche für https://localhost/items-ui/greeting wird zu Recht davon ausgegangen, dass die Verarbeitung dieser Header bereits in der Benutzeroberfläche für https://localhost/items-ui/greeting implementiert wurde:


Spring Cloud Sleuth ist eine Lösung für die Abfrageverfolgung in einem verteilten System. Trace-ID (Pass-Through-ID) und Span-ID (Unit-of-Work-ID) werden zu den Headern der Anforderung hinzugefügt, die mehrere Microservices durchlaufen (zum leichteren Verständnis habe ich das Schema vereinfacht; hier eine ausführlichere Erklärung):


Diese Funktionalität wird durch einfaches Hinzufügen der spring-cloud-starter-sleuth .

Wenn Sie die entsprechenden Protokollierungseinstellungen angeben, wird in der Konsole der entsprechenden Microservices Folgendes angezeigt (Trace-ID und Span-ID werden nach dem Namen des Microservices angezeigt):

 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" 

Für eine grafische Darstellung der verteilten Ablaufverfolgung können Sie beispielsweise Zipkin verwenden, das als Server fungiert, der Informationen zu HTTP-Anforderungen von anderen Microservices zusammenfasst (weitere Details hier ).

Montage


Je nach Betriebssystem wird gradlew clean build oder ./gradlew clean build .

Angesichts der Möglichkeit, Gradle-Wrapper zu verwenden , ist kein lokal installierter Gradle erforderlich.

Build und anschließender Start geben JDK 11.0.1 erfolgreich weiter. Zuvor arbeitete das Projekt mit JDK 10, daher gehe ich davon aus, dass es bei dieser Version keine Probleme mit der Montage und dem Start geben wird. Ich habe keine Daten zu früheren Versionen des JDK. Beachten Sie auch, dass für den verwendeten Gradle 5 mindestens JDK 8 erforderlich ist.

Starten


Ich empfehle, Anwendungen in der Reihenfolge zu starten, in der sie in diesem Artikel beschrieben sind. Wenn Sie Intellij IDEA mit aktiviertem Dashboard ausführen verwenden, sollten Sie Folgendes erhalten:


Fazit


Der Artikel untersuchte ein Beispiel für eine Microservice-Architektur auf dem aktuellen Technologie-Stack in der Java-Welt, seinen Hauptkomponenten und einigen Funktionen. Ich hoffe für jemanden wird das Material nützlich sein. Danke für die Aufmerksamkeit!

Referenzen


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


All Articles