Arquitectura de microservicios en una pila moderna de tecnolog铆as Java

Ten铆amos JDK 11, Kotlin, Spring 5 y Spring Boot 2, Gradle 5 con Kotlin DSL, JUnit 5 y una docena de bibliotecas de pila Spring Cloud para el descubrimiento de servicios, creaci贸n de API de puerta de enlace, equilibrio de clientes e implementaci贸n de disyuntor. escribir clientes HTTP declarativos, rastreo distribuido y todo eso. No es que todo esto fuera necesario para crear una arquitectura de microservicio, solo por diversi贸n ...

Entrada


En este art铆culo, ver谩 un ejemplo de arquitectura de microservicios que utiliza tecnolog铆as relevantes en el mundo de Java, las principales a continuaci贸n (estas versiones se utilizan en el proyecto en el momento de la publicaci贸n):
Tipo de tecnolog铆aTituloVersi贸n
PlataformaJdk11.0.1
Lenguaje de programaci贸nKotlin1.3.10
Marco de aplicaci贸nMarco de primavera5.0.9
Bota de primavera2.0.5
Sistema de construcci贸nGradle5.0
Gradle Kotlin DSL1.0.4
Marco de prueba de unidadJunit5.1.1
Nube de primavera
Punto de acceso 煤nico (puerta de enlace API)Spring Cloud GatewayIncluido en el tren de lanzamiento del proyecto Finchley SR2 Spring Cloud
Configuraci贸n centralizadaSpring Cloud config
Solicitud de seguimiento (seguimiento distribuido)Detective de nubes de primavera
Cliente HTTP declarativoSpring Cloud OpenFeign
Descubrimiento de servicioSpring Cloud Netflix Eureka
DisyuntorSpring Cloud Netflix Hystrix
Balanceo de carga del lado del clienteSpring Cloud Netflix Ribbon

El proyecto consta de 5 microservicios: 3 infraestructura (servidor de configuraci贸n, servidor de descubrimiento de servicios, puerta de enlace de UI) y ejemplos de front-end (UI de elementos) y back-end (servicio de elementos):


Todos ellos ser谩n considerados secuencialmente a continuaci贸n. En un proyecto de "combate", obviamente, habr谩 significativamente m谩s microservicios que implementar谩n cualquier funcionalidad comercial. Agregarlos a una arquitectura similar se realiza t茅cnicamente de la misma manera que la interfaz de usuario de 铆tems y el servicio de 铆tems.

Descargo de responsabilidad


El art铆culo no considera instrumentos para contenerizaci贸n y orquestaci贸n, ya que actualmente no se utilizan en el proyecto.

Servidor de configuraci贸n


Spring Cloud Config se utiliz贸 para crear un repositorio centralizado de configuraciones de aplicaciones. Las configuraciones se pueden leer desde varias fuentes, por ejemplo, un repositorio git separado; en este proyecto, por simplicidad y claridad, est谩n en los recursos de la aplicaci贸n:


En este caso, la configuraci贸n del servidor de configuraci贸n ( application.yml ) se ve as铆:

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

El uso del puerto 8888 permite a los clientes del servidor de configuraci贸n no especificar expl铆citamente su puerto en su bootstrap.yml . Al inicio, cargan su configuraci贸n ejecutando una solicitud GET al servidor de configuraci贸n de API HTTP.

El c贸digo del programa para este microservicio consta de un solo archivo, que contiene la declaraci贸n de la clase de aplicaci贸n y el m茅todo principal, que, a diferencia del c贸digo Java equivalente, es una funci贸n de nivel superior:

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

Las clases de aplicaci贸n y los m茅todos principales en otros microservicios tienen una apariencia similar.

Servidor de descubrimiento de servicios


El descubrimiento de servicios es un patr贸n de arquitectura de microservicios que le permite simplificar la interacci贸n entre las aplicaciones ante un posible cambio en el n煤mero de sus instancias y la ubicaci贸n de la red. Un componente clave en este enfoque es el registro del Servicio, una base de datos de microservicios, sus instancias y ubicaciones de red (m谩s detalles aqu铆 ).

En este proyecto, el descubrimiento de servicios se implementa sobre la base de Netflix Eureka, que es un descubrimiento de servicios del lado del cliente : el servidor Eureka realiza la funci贸n del registro del servicio, y el cliente Eureka, antes de ejecutar una solicitud a cualquier microservicio, se pone en contacto con el servidor Eureka para obtener una lista de instancias de la aplicaci贸n llamada y realiza el equilibrio de forma independiente cargar (usando la cinta de Netflix). Netflix Eureka, como algunos otros componentes de la pila OSS de Netflix (como Hystrix y Ribbon) se integra con las aplicaciones Spring Boot que utilizan Spring Cloud Netflix .

En la configuraci贸n del servidor de descubrimiento de servicios ubicada en sus recursos ( bootstrap.yml ), solo se indica el nombre de la aplicaci贸n y el par谩metro que indica que el inicio del microservicio se interrumpir谩 si es imposible conectarse al servidor de configuraci贸n:

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

El resto de la configuraci贸n de la aplicaci贸n se encuentra en el eureka-server.yml en los recursos del servidor de configuraci贸n:

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

El servidor Eureka usa el puerto 8761, que permite a todos los clientes de Eureka no especificarlo usando el valor predeterminado. El valor del register-with-eureka (indicado para mayor claridad, porque tambi茅n se usa de forma predeterminada) significa que la aplicaci贸n misma, como otros microservicios, se registrar谩 en el servidor Eureka. El par谩metro fetch-registry determina si el cliente Eureka recibir谩 datos del registro del Servicio.

Una lista de aplicaciones registradas y otra informaci贸n est谩 disponible en http://localhost:8761/ :


Las alternativas para implementar el descubrimiento de servicios son C贸nsul, Zookeeper y otros.

Servicio de art铆culos


Esta aplicaci贸n es un ejemplo de un back-end con una API REST implementada utilizando el marco WebFlux que apareci贸 en Spring 5 (la documentaci贸n est谩 aqu铆 ), o m谩s bien Kotlin DSL para ello:

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

El procesamiento de las solicitudes HTTP recibidas se delega al ItemHandler clase ItemHandler . Por ejemplo, un m茅todo para obtener una lista de objetos de alguna entidad se ve as铆:

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

La aplicaci贸n se convierte en un cliente del servidor Eureka, es decir, registra y recibe datos del registro del Servicio, debido a la presencia de la spring-cloud-starter-netflix-eureka-client . Despu茅s del registro, la aplicaci贸n env铆a hartbits al servidor Eureka con cierta frecuencia, y si durante un cierto per铆odo de tiempo el porcentaje de hartbits recibidos por el servidor Eureka en relaci贸n con el valor m谩ximo posible cae por debajo de un cierto umbral, la aplicaci贸n se eliminar谩 del registro del Servicio.

Considere una de las formas de enviar metadatos adicionales al servidor Eureka:

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

Aseg煤rese de que el servidor Eureka reciba estos datos en http://localhost:8761/eureka/apps/items-service trav茅s de Postman:



UI de elementos


Este microservicio, adem谩s de demostrar la interacci贸n con la puerta de enlace de la interfaz de usuario (se mostrar谩 en la siguiente secci贸n), realiza la funci贸n front-end para el servicio Items, que puede interactuar con la API REST de varias maneras:

  1. Cliente a REST API escrito usando 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. Frijol RestTemplate
    Se crea un bin en la configuraci贸n de java:

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

    Y usado de esta manera:

     fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result" 
  3. WebClient class WebClient (m茅todo espec铆fico para el marco de WebFlux)
    Se crea un bin en la configuraci贸n de java:

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

    Y usado de esta manera:

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

El hecho de que los tres m茅todos devuelvan el mismo resultado puede verificarse en http://localhost:8081/example :


Prefiero la opci贸n usando OpenFeign, porque permite desarrollar un contrato para la interacci贸n con el microservicio llamado, cuya implementaci贸n es realizada por Spring. Se inyecta un objeto que implementa este contrato y se usa como un bean normal:

 itemsServiceFeignClient.getItem(1) 

Si la solicitud falla por alg煤n motivo, se llamar谩 al m茅todo correspondiente de la clase que implementa la interfaz FallbackFactory , en el que debe procesar el error y devolver la respuesta predeterminada (o lanzar una excepci贸n m谩s). En el caso de que falle un cierto n煤mero de llamadas consecutivas, el Fusible abrir谩 el circuito (m谩s sobre el disyuntor aqu铆 y aqu铆 ), dando tiempo para recuperar el microservicio ca铆do.

Para utilizar el cliente Feign, debe anotar la @EnableFeignClients aplicaci贸n @EnableFeignClients :

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

Para que el respaldo de Hystrix funcione en el cliente de Feign, debe agregar lo siguiente a la configuraci贸n de la aplicaci贸n:

 feign: hystrix: enabled: true 

Para probar el funcionamiento de la recuperaci贸n de Hystrix en el cliente Feign, solo vaya a http://localhost:8081/hystrix-fallback . El cliente de Feign intentar谩 ejecutar la solicitud en una ruta que no existe en el servicio de Elementos, lo que conducir谩 a la devoluci贸n de la respuesta:

 {"error" : "Some error"} 

UI gateway


El patr贸n de puerta de enlace API le permite crear un 煤nico punto de entrada para la API proporcionada por otros microservicios (m谩s detalles aqu铆 ). Una aplicaci贸n que implementa este patr贸n realiza el enrutamiento (enrutamiento) de solicitudes a microservicios y tambi茅n puede realizar funciones adicionales, por ejemplo, autenticaci贸n.

En este proyecto, para mayor claridad, se implementa una puerta de enlace de IU, es decir, un 煤nico punto de entrada para diferentes IU; obviamente, la API de puerta de enlace se implementa de manera similar. El microservicio se implementa sobre la base del marco Spring Cloud Gateway. Una alternativa es Netflix Zuul, parte de Netflix OSS e integrado con Spring Boot usando Spring Cloud Netflix.
La puerta de enlace de la IU se ejecuta en el puerto 443 con el certificado SSL generado (ubicado en el proyecto). SSL y HTTPS se configuran de la siguiente manera:

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

Los inicios de sesi贸n y las contrase帽as de los usuarios se almacenan en una implementaci贸n basada en el mapa de la interfaz ReactiveUserDetailsService basada en 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) } 

La configuraci贸n de seguridad se configura de la siguiente manera:

 @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 configuraci贸n dada determina que parte de los recursos web (por ejemplo, estad铆sticas) est谩 disponible para todos los usuarios, incluidos aquellos que no se han autenticado, y todo lo dem谩s ( .anyExchange() ) solo est谩 autenticado. Si intenta ingresar una URL que requiere autenticaci贸n, ser谩 redirigida a la p谩gina de inicio de sesi贸n ( https://localhost/login ):


Esta p谩gina utiliza las herramientas del marco Bootstrap, que est谩 conectado al proyecto mediante Webjars, lo que permite administrar las bibliotecas del lado del cliente como dependencias regulares. Thymeleaf se usa para formar p谩ginas HTML. El acceso a la p谩gina de inicio de sesi贸n se configura mediante WebFlux:

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

El enrutamiento de Spring Cloud Gateway se puede configurar en una configuraci贸n YAML o Java. Las rutas a los microservicios se asignan manualmente o se crean autom谩ticamente seg煤n los datos recibidos del registro del Servicio. Con un n煤mero suficientemente grande de UI para las cuales se requiere enrutamiento, ser谩 m谩s conveniente usar la integraci贸n con el registro del Servicio:

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

El valor del par谩metro include-expression indica que las rutas se crear谩n solo para microservicios cuyos nombres terminan en -UI , y el valor del par谩metro url-expression es que son accesibles a trav茅s del protocolo HTTP, a diferencia de la puerta de enlace UI que funciona a trav茅s de HTTPS, y cuando se accede utilizar谩n el equilibrio de carga del cliente (implementado con Netflix Ribbon).

Considere el ejemplo de crear rutas en la configuraci贸n de Java manualmente (sin integraci贸n con el registro del Servicio):

 @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 primera ruta enruta a la p谩gina de inicio del servidor Eureka mostrada anteriormente ( http://localhost:8761 ), la segunda es necesaria para cargar recursos en esta p谩gina.

Todas las rutas creadas por la aplicaci贸n est谩n disponibles en https://localhost/actuator/gateway/routes .

En los microservicios subyacentes, puede ser necesario acceder al inicio de sesi贸n y / o funciones del usuario autenticado en la puerta de enlace de la interfaz de usuario. Para hacer esto, cre茅 un filtro que agrega los encabezados apropiados a la solicitud:

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

Ahora pasemos a la interfaz de usuario de los elementos utilizando la puerta de enlace de la interfaz de usuario: https://localhost/items-ui/greeting , suponiendo correctamente que el procesamiento de estos encabezados ya se ha implementado en la interfaz de usuario de los elementos:


Spring Cloud Sleuth es una soluci贸n para el seguimiento de consultas en un sistema distribuido. El Id. De seguimiento (identificador de paso) y el Id de intervalo (identificador de unidad de trabajo) se agregan a los encabezados de la solicitud que pasa por varios microservicios (para facilitar la comprensi贸n, simplifiqu茅 el esquema; aqu铆 hay una explicaci贸n detallada)


Esta funcionalidad se conecta simplemente agregando la spring-cloud-starter-sleuth .

Una vez especificada la configuraci贸n de registro adecuada, en la consola de los microservicios correspondientes puede ver algo como lo siguiente (el Id. De seguimiento y el Id de intervalo se muestran despu茅s del nombre del microservicio):

 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" 

Para una representaci贸n gr谩fica del rastreo distribuido, puede usar, por ejemplo, Zipkin, que actuar谩 como un servidor que agrega informaci贸n sobre las solicitudes HTTP de otros microservicios (m谩s detalles aqu铆 ).

Asamblea


Dependiendo del sistema operativo, se gradlew clean build ./gradlew clean build o ./gradlew clean build .

Dada la posibilidad de usar Gradle wrapper , no hay necesidad de un Gradle instalado localmente.

La compilaci贸n y el lanzamiento posterior pasan con 茅xito JDK 11.0.1. Antes de esto, el proyecto funcionaba en JDK 10, por lo que supongo que en esta versi贸n no habr谩 problemas con el ensamblaje y el lanzamiento. No tengo datos sobre versiones anteriores de JDK. Adem谩s, tenga en cuenta que el Gradle 5 utilizado requiere al menos JDK 8.

Lanzamiento


Recomiendo iniciar las aplicaciones en el orden en que se describen en este art铆culo. Si est谩 utilizando Intellij IDEA con Run Dashboard habilitado, deber铆a obtener algo como lo siguiente:


Conclusi贸n


El art铆culo examin贸 un ejemplo de arquitectura de microservicios en la pila de tecnolog铆a actual en el mundo Java, sus componentes principales y algunas caracter铆sticas. Espero para alguien que el material sea 煤til. Gracias por su atencion!

Referencias


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


All Articles