Esse tutorial te guiará no processo de configuração de afinidade de zonas usando Spring Cloud Netflix Eureka.

O que você construirá

Você construirá três aplicações:

Todas essas aplicações são necessárias para validar que a configuração de afinidade de zonas está correto. Cada uma delas deverá ser deployada duas vezes, uma por zona.

Pre-req

Afinidade de zonas

Não importa o estilo arquitetural que uma aplicação está usando, um caso de uso comum é deployar a mesma aplicação em diferentes regiões/data centers e usar alguma técnica para manter os requets dentro da mesma zona.

Numa arquitetura de microservices também existe a necessidade de fazer a mesma coisa, mas a técnica usada precisa ser aplicada usando o Service Registry Design Pattern.

Spring Cloud Netflix

Spring Cloud Netflix torna fácil implementar os design patterns necessários para uma arquitetura de microservices.

Criando as aplicações

Nesse guia nós criaremos três aplicações, se você tem familiaridade com spring-cloud isso será muito fácil, todas as aplicações não são nada além de um simples jar executável feito com spring-boot.

A parte principal aqui são os arquivos de configurações que serão mostrados adiante.

Base dependencies

Adicione as seguintes dependencias para todas as aplicações, se houver alguma diferença específica por aplicação elas serão mencionadas em cada tópico específico.

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Edgware.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

API Gateway

A primeira aplicação que criaremos é um API Gateway usando Spring Cloud Netflix Zuul.

Primeiro adicione a seguinte dependência no pom.xml.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
Configuração Java

Crie a main SpringApplication classe adicionando @EnableZuulProxy.

src/main/java/com/marcosbarbero/wd

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {

    public static void main(String... args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

}

Service Registry

A segunda aplicação que criaremos será o Service Registry usando Spring Cloud Netflix Eureka.

Adicione a seguinte dependência ao pom.xml.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
Configuração Java

Crie a main SpringApplication classe adicionando @EnableEurekaServer.

src/main/java/com/marcosbarbero/wd

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class ServiceDiscoveryApplication {

    public static void main(String... args) {
        SpringApplication.run(ServiceDiscoveryApplication.class, args);
    }
}

Serviço REST

A terceira aplicação não contém nada além de um endpoint REST apenas para validar que cada request ficará na sua própria região. Para essa aplicação não há nenhuma dependência adicional além das já adicionadas.

Configuração Java

To make things easier I’m creating the main class with a nested RestController, the following controller returns which zone is this application deployed.

src/main/java/com/marcosbarbero/wd

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE;

@EnableDiscoveryClient
@SpringBootApplication
public class SimpleService {

    public static void main(String... args) {
        SpringApplication.run(SimpleService.class, args);
    }

    @RestController
    class SimpleController {

        @Value("${eureka.instance.metadataMap.zone}")
        private String zone;

        @GetMapping(value = "/zone", produces = APPLICATION_JSON_UTF8_VALUE)
        public String zone() {
            return "{\"zone\"=\"" + zone + "\"}";
        }

    }
}

Configuration properties

Já foi mencionado anteriormente que cada aplicação será executada duas vezes para simular duas zonas distintas, para facilitar esse processo nós criaremos as configurações baseadas em profiles, para cada aplicação crie os seguintes arquivos:

src/main/resources/application.yml
src/main/resources/application-zone1.yml
src/main/resources/application-zone2.yml

Nota: O sufixo no nome do arquivo será usado como nome do profile.

Service Registry

src/main/resources/application.yml

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    region: region-1
    service-url:
      zone1: http://localhost:8761/eureka/
      zone2: http://127.0.0.1:8762/eureka/
    availability-zones:
      region-1: zone1,zone2

spring.profiles.active: zone1

Todas as propriedades estão sob o namespace eureka.client.

Property Descrição
region Uma String que contém o nome da região onde a aplicação será deployada
service-url Um Map que contém uma lista das regiões disponíveis para a região
availability-zones Um Map que contém uma lista de zonas separada por vírgulas, para a região

As propriedades register-with-eureka e fetch-registry estão desligando o Service Registry de ser adicionado na lista de aplicações, mas isso não é muito importante para essa configuração.

src/main/resources/application-zone1.yml

server.port: 8761

eureka:
  instance:
    hostname: localhost
    metadataMap.zone: zone1

src/main/resources/application-zone2.yml

server.port: 8762

eureka:
  instance:
    hostname: 127.0.0.1
    metadataMap.zone: zone2

Para os profiles -zone1 e -zone2 a única diferença é server.port, a zona é configurada em eureka.metadataMap.zone e nesse caso ohostname, cada Eureka Server precisa ser executado em um host diferente, como eu estou executando ambos na mesma máquina eu estou nomando um como 127.0.01 e o outro localhost.

Nota: Não é necessário adicionar o hostname no caso de você estar executando em máquinas diferentes.

Gateway

src/main/resources/application.yml

eureka:
  client:
    prefer-same-zone-eureka: true
    region: region-1
    service-url:
      zone1: http://localhost:8761/eureka/
      zone2: http://127.0.0.1:8762/eureka/
    availability-zones:
      region-1: zone1,zone2

spring:
  profiles.active: zone1
  application.name: gateway

management.security.enabled: false

A maior diferença aqui é a propriedade eureka.client.prefer-same-zone-eureka, ela está dizendo para aplicação que a qualquer momento que seja necessário chamar outra applicação EurekaClient isso será feito usando a mesma zona que a própria aplicação foi deployada. No caso de não existir uma instancia disponível na mesma zona a aplicação chamará de outra zona que tem a aplicação disponível.

src/main/resources/application-zone1.yml

server.port: 8080

eureka:
  instance:
    metadataMap.zone: zone1

src/main/resources/application-zone2.yml

server.port: 8081

eureka:
  instance:
    metadataMap.zone: zone2

Como antes, esse profile somente muda a zona de disponibilidade e a porta da aplicação.

REST Service

A configuração para o serviço contém a mesma configuração que o Gateway.

src/main/resources/application.yml

eureka:
  client:
    prefer-same-zone-eureka: true
    region: region-1
    service-url:
      zone1: http://localhost:8761/eureka/
      zone2: http://127.0.0.1:8762/eureka/
    availability-zones:
      region-1: zone1,zone2

spring:
  profiles.active: zone1
  application.name: simple-service

src/main/resources/application-zone1.yml

server.port: 8181

eureka:
  instance:
    metadataMap.zone: zone1

src/main/resources/application-zone2.yml

server.port: 8182

eureka:
  instance:
    metadataMap.zone: zone2

Build & Run

Agora é hora de fazer o build das aplicações, se você (assim como eu) está usando maven, apenas execute o seguinte commando:

$ mvn clean package

Logo depois execute cada aplicação adicionando o profile específico na linha de comando, ex::

$ java -jar target/*.jar --spring.profiles.active=zone1

Lembre-se que cada aplicação precisará ser executada duas vezes, uma vez por profile: zone1 and zone2.

Validação

Para validar se cada request está respeitando cada zona nós precisamos fazer a chamada para o simple-service através de cada gateway.

$ curl http://localhost:8080/simple-service/zone

{"zone"="zone1"}
$ curl http://localhost:8081/simple-service/zone

{"zone"="zone2"}

A diferença entre cada zona é a porta onde cada gateway está rodando.

Validação de Zone failover

Para validar o failover entre as zonas você só precisa para uma das instancias e fazer um request para a zona oposta, ex:

1st - Pare o simple-service na zone2. 2nd - Faça um request para simple-service através do gateway na zone2.

$ curl http://localhost:8081/simple-service/zone

O resultado esperado é um JSON contendo {"zone"="zone1"}. Uma vez que o simple-service da zone1 estiver rodando e registrado no Eureka Server o mesmo request responderá {"zone"="zone2"} novamente.

Leva um tempo até que o simple-service esteja disponível na zona oposta, seja paciente e divirta-se!

Sumário

Parabéns! Você acabou de criar e configurar um API Gateway, Service Registry e um REST Service que respeita afinidade de zonas tornando tua arquitetura de microservices mais resiliente.

Nota de rodapé

  • O código usado nesse tutorial pode ser encontrado no github