Arquitetura de microsserviço em uma pilha moderna de tecnologias Java

Tínhamos JDK 11, Kotlin, Spring 5 e Spring Boot 2, Gradle 5 com DSL Kotlin, JUnit 5 e uma dúzia de bibliotecas de pilhas Spring Cloud prontas para produção para descoberta de serviços, criação de APIs de gateway, balanceamento de clientes e implementação de disjuntor escrevendo clientes HTTP declarativos, rastreamento distribuído e tudo isso. Não que tudo isso fosse necessário para criar uma arquitetura de microsserviço - apenas por diversão ...

Entrada


Neste artigo, você verá um exemplo de arquitetura de microsserviço usando tecnologias relevantes no mundo Java, as principais das quais são fornecidas abaixo (as versões indicadas são usadas no projeto no momento da publicação):
Tipo de tecnologiaTítuloVersão
PlataformaJdk11.0.1
Linguagem de programaçãoKotlin1.3.10
Estrutura de aplicaçãoQuadro de primavera5.0.9
Bota de mola2.0.5
Sistema de compilaçãoGradle5.0
Gradle Kotlin DSL1.0.4
Estrutura de teste de unidadeJunit5.1.1
Nuvem de primavera
Ponto de acesso único (gateway da API)Gateway na nuvem SpringIncluído no projeto Release Train Finchley SR2 Spring Cloud
Configuração centralizadaConfiguração da nuvem da primavera
Rastreamento de solicitação (rastreamento distribuído)Primavera nuvem detetive
Cliente HTTP declarativoSpring Cloud OpenFeign
Descoberta de serviçoPrimavera nuvem Netflix Eureka
DisjuntorSpring Cloud Netflix Hystrix
Balanceamento de carga no lado do clienteFita Netflix da nuvem da primavera

O projeto consiste em 5 microsserviços: 3 infraestrutura (servidor de configuração, servidor de descoberta de serviço, gateway da interface do usuário) e exemplos de front-end (itens da interface do usuário) e back-end (serviço de itens):


Todos eles serão considerados sequencialmente abaixo. Em um projeto de "combate", obviamente, haverá significativamente mais microsserviços que implementam qualquer funcionalidade de negócios. Adicioná-los a uma arquitetura semelhante é tecnicamente feito da mesma maneira que a UI de itens e o serviço de itens.

Isenção de responsabilidade


O artigo não considera instrumentos de conteinerização e orquestração, uma vez que atualmente não são utilizados no projeto.

Servidor de configuração


O Spring Cloud Config foi usado para criar um repositório centralizado de configurações de aplicativos. As configurações podem ser lidas de várias fontes, por exemplo, um repositório git separado; neste projeto, por simplicidade e clareza, eles estão nos recursos do aplicativo:


Nesse caso, a configuração do servidor Config ( application.yml ) se parece com esta:

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

O uso da porta 8888 permite que os clientes do servidor Config não especifiquem explicitamente sua porta no bootstrap.yml . Na inicialização, eles carregam sua configuração executando uma solicitação GET no servidor de configuração da API HTTP.

O código do programa para este microsserviço consiste em apenas um arquivo, que contém a declaração da classe do aplicativo e o método principal, que, diferentemente do código Java equivalente, é uma função de nível superior:

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

As classes de aplicativos e os principais métodos em outros microsserviços têm uma aparência semelhante.

Servidor de descoberta de serviço


A descoberta de serviço é um padrão de arquitetura de microsserviço que permite simplificar a interação entre aplicativos diante de uma possível alteração no número de instâncias e no local da rede. Um componente essencial dessa abordagem é o registro de serviços - um banco de dados de microsserviços, suas instâncias e locais de rede (mais detalhes aqui ).

Neste projeto, a descoberta de serviço é implementada com base no Netflix Eureka, que é uma descoberta de serviço do lado do cliente : o servidor Eureka executa a função do registro de serviço e o cliente Eureka, antes de executar uma solicitação para qualquer microsserviço, entra em contato com o servidor Eureka para obter uma lista de instâncias do aplicativo chamado e executa o balanceamento de forma independente. carregar (usando a fita da Netflix). O Netflix Eureka, como outros componentes de pilha do Netflix OSS (como Hystrix e Ribbon), se integra aos aplicativos Spring Boot usando o Spring Cloud Netflix .

Na configuração do servidor de descoberta de serviço localizada em seus recursos ( bootstrap.yml ), apenas o nome do aplicativo e o parâmetro indicando que o início do microsserviço será interrompido se for impossível conectar-se ao servidor de configuração:

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

O restante da configuração do aplicativo está localizado no eureka-server.yml nos recursos do servidor Config:

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

O servidor Eureka usa a porta 8761, que permite que todos os clientes Eureka não a especifiquem usando o valor padrão. O valor do register-with-eureka (indicado para maior clareza, register-with-eureka também é usado por padrão) significa que o próprio aplicativo, como outros microsserviços, será registrado no servidor Eureka. O parâmetro fetch-registry determina se o cliente Eureka receberá dados do registro de serviço.

Uma lista de aplicativos registrados e outras informações estão disponíveis em http://localhost:8761/ :


Alternativas para implementar a descoberta de serviços são Consul, Zookeeper e outras.

Serviço de itens


Este aplicativo é um exemplo de back-end com uma API REST implementada usando a estrutura WebFlux que apareceu no Spring 5 (a documentação está aqui ), ou melhor, o DSL Kotlin:

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

O processamento de solicitações HTTP recebidas é delegado ao ItemHandler classe ItemHandler . Por exemplo, um método para obter uma lista de objetos de alguma entidade se parece com isso:

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

O aplicativo se torna um cliente servidor Eureka, ou seja, registra e recebe dados do registro de Serviço, devido à presença da spring-cloud-starter-netflix-eureka-client . Após o registro, o aplicativo envia hartbits ao servidor Eureka com uma certa frequência e, por um certo período de tempo, a porcentagem de hartbits recebidos pelo servidor Eureka em relação ao valor máximo possível cai abaixo de um determinado limite, o aplicativo será excluído do registro de Serviço.

Considere uma das maneiras de enviar metadados adicionais para o servidor Eureka:

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

Certifique-se de que esses dados sejam recebidos pelo servidor Eureka, acessando http://localhost:8761/eureka/apps/items-service via Postman:



UI de itens


Esse microsserviço, além de demonstrar a interação com o gateway da interface do usuário (será mostrado na próxima seção), executa a função front-end do serviço Items, que pode interagir com a API REST de várias maneiras:

  1. Cliente para API REST gravado usando o 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
    Um bin é criado na configuração java:

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

    E usado desta maneira:

     fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result" 
  3. WebClient classe WebClient (método específico para a estrutura WebFlux)
    Um bin é criado na configuração java:

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

    E usado desta maneira:

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

O fato de todos os três métodos retornarem o mesmo resultado pode ser verificado acessando http://localhost:8081/example :


Prefiro a opção de usar o OpenFeign, pois possibilita o desenvolvimento de um contrato de interação com o chamado microsserviço, cuja implementação é realizada pelo Spring. Um objeto que implementa este contrato é injetado e usado como um bean comum:

 itemsServiceFeignClient.getItem(1) 

Se a solicitação falhar por algum motivo, o método correspondente da classe que implementa a interface FallbackFactory será chamado, no qual você precisará processar o erro e retornar a resposta padrão (ou lançar uma exceção ainda mais). No caso de um determinado número de chamadas consecutivas falhar, o fusível abrirá o circuito (mais sobre o disjuntor aqui e aqui ), dando tempo para recuperar o microsserviço que caiu.

Para usar o cliente @EnableFeignClients , você precisa anotar a @EnableFeignClients aplicativo @EnableFeignClients :

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

Para que o fallback Hystrix funcione no cliente Feign, você precisa adicionar o seguinte à configuração do aplicativo:

 feign: hystrix: enabled: true 

Para testar a operação do fallback Hystrix no cliente Feign, basta ir para http://localhost:8081/hystrix-fallback . O cliente Feign tentará executar a solicitação em um caminho que não existe no serviço Itens, o que levará ao retorno da resposta:

 {"error" : "Some error"} 

Gateway da interface do usuário


O padrão de gateway da API permite criar um único ponto de entrada para a API fornecida por outros microsserviços (mais detalhes aqui ). Um aplicativo que implementa esse padrão executa o roteamento (roteamento) de solicitações para microsserviços e também pode executar funções adicionais, por exemplo, autenticação.

Neste projeto, para maior clareza, um gateway de interface do usuário é implementado, ou seja, um único ponto de entrada para diferentes UIs; obviamente, a API do gateway é implementada de maneira semelhante. O microsserviço é implementado com base na estrutura do Spring Cloud Gateway. Uma alternativa é o Netflix Zuul, parte do Netflix OSS e integrado ao Spring Boot usando o Spring Cloud Netflix.
O gateway da interface do usuário é executado na porta 443 usando o certificado SSL gerado (localizado no projeto). SSL e HTTPS estão configurados da seguinte maneira:

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

Os logins e senhas do usuário são armazenados em uma implementação baseada em mapa da interface ReactiveUserDetailsService específica da 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) } 

As configurações de segurança são definidas da seguinte maneira:

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

A configuração fornecida determina que parte dos recursos da web (por exemplo, estática) está disponível para todos os usuários, incluindo aqueles que não foram autenticados, e todo o resto ( .anyExchange() ) é autenticado apenas. Se você tentar inserir um URL que exija autenticação, ele será redirecionado para a página de login ( https://localhost/login ):


Esta página usa as ferramentas da estrutura do Bootstrap, conectada ao projeto usando Webjars, o que possibilita gerenciar as bibliotecas do lado do cliente como dependências regulares. Thymeleaf é usado para formar páginas HTML. O acesso à página de login é configurado usando o WebFlux:

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

O roteamento do Spring Cloud Gateway pode ser configurado em uma configuração YAML ou java. As rotas para microsserviços são atribuídas manualmente ou são criadas automaticamente com base nos dados recebidos do registro do Serviço. Com um número suficientemente grande de UIs para as quais o roteamento é necessário, será mais conveniente usar a integração com o registro de Serviço:

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

O valor do parâmetro include-expression indica que as rotas serão criadas apenas para microsserviços cujos nomes terminam em -UI , e o valor do parâmetro url-expression é que eles são acessíveis pelo protocolo HTTP, ao contrário do gateway da interface do usuário que funciona via HTTPS e quando acessados eles usarão o balanceamento de carga do cliente (implementado usando o Netflix Ribbon).

Considere o exemplo de criação de rotas na configuração java manualmente (sem integração com o registro de serviço):

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

A primeira rota roteia para a página inicial do servidor Eureka mostrada anteriormente ( http://localhost:8761 ), a segunda é necessária para carregar recursos nesta página.

Todas as rotas criadas pelo aplicativo estão disponíveis em https://localhost/actuator/gateway/routes .

Nos microsserviços subjacentes, pode ser necessário acessar o logon e / ou funções do usuário autenticado no gateway da interface do usuário. Para fazer isso, criei um filtro que adiciona os cabeçalhos apropriados à solicitação:

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

Agora, vamos voltar para a interface do usuário de itens usando o gateway da interface do usuário - https://localhost/items-ui/greeting , assumindo corretamente que o processamento desses cabeçalhos já foi implementado na interface do usuário de itens:


O Spring Cloud Sleuth é uma solução para rastreamento de consultas em um sistema distribuído. O ID de rastreamento (identificador de passagem) e o Span Span (identificador de unidade de trabalho) são adicionados aos cabeçalhos da solicitação que passa por vários microsserviços (para facilitar a compreensão, simplifiquei o esquema; aqui está uma explicação mais detalhada):


Essa funcionalidade é conectada simplesmente adicionando a spring-cloud-starter-sleuth .

Depois de especificar as configurações de log apropriadas, no console dos microsserviços correspondentes, você pode ver algo como o seguinte (ID de rastreamento e ID de extensão são exibidos após o nome do microsserviço):

 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 uma representação gráfica do rastreamento distribuído, você pode usar, por exemplo, o Zipkin, que atuará como um servidor que agrega informações sobre solicitações HTTP de outros microsserviços (mais detalhes aqui ).

Assembléia


Dependendo do sistema operacional, a gradlew clean build ./gradlew clean build ou ./gradlew clean build .

Dada a possibilidade de usar o wrapper Gradle , não há necessidade de um Gradle instalado localmente.

A construção e o lançamento subsequente passam com êxito no JDK 11.0.1. Antes disso, o projeto funcionava no JDK 10, portanto, suponho que nesta versão não haverá problemas com a montagem e o lançamento. Não tenho dados sobre versões anteriores do JDK. Além disso, lembre-se de que o Gradle 5 usado requer pelo menos JDK 8.

Lançamento


Eu recomendo iniciar aplicativos na ordem em que são descritos neste artigo. Se você estiver usando o Intellij IDEA com o Run Dashboard ativado, deverá obter algo como o seguinte:


Conclusão


O artigo examinou um exemplo de arquitetura de microsserviço na pilha de tecnologia atual no mundo Java, seus principais componentes e alguns recursos. Espero que alguém o material seja útil. Obrigado pela atenção!

Referências


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


All Articles