Aprendendo a implementar microserviços. Parte 1. Spring Boot e Docker

Aprendendo a implementar microserviços. Parte 1. Spring Boot e Docker

Olá, Habr.

Neste artigo, quero compartilhar minha experiência na criação de um ambiente de aprendizado para experimentos com microserviços. Ao estudar cada nova ferramenta, sempre quis experimentá-la não apenas na máquina local, mas em condições mais realistas. Por isso, decidi criar um aplicativo simplificado de microserviços que, posteriormente, posso "enriquecer" com várias tecnologias interessantes. A principal exigência do projeto é sua máxima funcionalidade próxima a um sistema real.

Inicialmente, dividi a criação do projeto em várias etapas:

  1. Criar dois serviços — 'backend' e 'gateway', empacotá-los em imagens Docker e configurar seu funcionamento conjunto.

    Palavras-chave: Java 11, Spring Boot, Docker, otimização de imagens.

  2. Desenvolver a configuração do Kubernetes e implantar o sistema no Google Kubernetes Engine.

    Palavras-chave: Kubernetes, GKE, gerenciamento de recursos, escalonamento automático, segredos.

  3. Criar um chart com o Helm 3 para gerenciamento mais eficiente do cluster.

    Palavras-chave: Helm 3, implantação de chart.

  4. Configurar o Jenkins e o pipeline para entrega automática de código para o cluster.

    Palavras-chave: configuração do Jenkins, plugins, repositório de configurações separado.

Pretendo dedicar um artigo separado para cada etapa.

A abordagem deste ciclo de artigos não é sobre como escrever microserviços, mas sim como fazê-los funcionar em um único sistema. Embora todas essas coisas geralmente estejam além da responsabilidade do desenvolvedor, acredito que ainda é útil estar familiarizado com elas, mesmo que seja em 20% (que, como se sabe, geram 80% do resultado). Alguns temas indiscutivelmente importantes, como a garantia de segurança, serão deixados de fora deste projeto, pois o autor tem pouco entendimento sobre isso; o sistema é criado exclusivamente para uso pessoal. Fico feliz com qualquer opinião e crítica construtiva.

Criação de microserviços.

Os serviços foram escritos em Java 11 utilizando Spring Boot. A interação entre serviços é organizada usando REST. O projeto incluirá um número mínimo de testes (para que depois haja algo para testar no Jenkins). O código fonte dos serviços está disponível no GitHub: backend e gateway.

Para poder verificar o estado de cada um dos serviços, o Spring Actuator foi adicionado às suas dependências. Ele criará um endpoint /actuator/health e retornará status 200 se o serviço estiver pronto para receber tráfego, ou 504 em caso de problemas. Nesse caso, é uma verificação bastante fictícia, já que os serviços são muito simples e, em caso de alguma emergência, eles provavelmente se tornarão completamente indisponíveis em vez de manter alguma funcionalidade parcial. Mas em sistemas reais, o Actuator pode ajudar a diagnosticar o problema antes que os usuários comecem a enfrentar dificuldades. Por exemplo, se houver problemas com o acesso ao banco de dados, poderemos reagir automaticamente a isso, parando de processar pedidos com uma instância de serviço quebrada.

Serviço Backend

O serviço de backend contará e retornará a quantidade de solicitações recebidas.

Código do controlador:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

    @GetMapping("/requests")
    public Long getRequestsCount() {
        return counter.incrementAndGet();
    }
}

Teste do controlador:

@WebMvcTest(RequestsCounterController.class)
public class RequestsCounterControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void firstRequest_one() throws Exception {
        mockMvc.perform(get("/requests"))
            .andExpect(status().isOk())
            .andExpect(MockMvcResultMatchers.content().string("1"));
    }
}

Serviço Gateway

O gateway irá redirecionar solicitações para o serviço de backend, adicionando as seguintes informações:

  • id do gateway. Isso é necessário para que se possa distinguir uma instância de gateway de outra pela resposta do servidor
  • Um "segredo" que atuará como uma senha muito importante (nº da chave de criptografia de um cookie importante)

Configuração em application.properties:

backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"

Adaptador para comunicação com o backend:

@Service
public class BackendAdapter {

    private static final String REQUESTS_ENDPOINT = "/requests";

    private final RestTemplate restTemplate;

    @Value("${backend.url}")
    private String backendUrl;

    public BackendAdapter(RestTemplateBuilder builder) {
        restTemplate = builder.build();
    }

    public String getRequests() {
        ResponseEntity response = restTemplate.getForEntity(
backendUrl + REQUESTS_ENDPOINT, String.class);
        return response.getBody();
    }
}

Controlador:

@RestController
@RequiredArgsConstructor
public class EndpointController {

    private final BackendAdapter backendAdapter;

    @Value("${instance.id}")
    private int instanceId;

    @Value("${secret}")
    private String secret;

    @GetMapping("/")
    public String getRequestsCount() {
        return String.format("Número de solicitações %s (gateway %d, segredo %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Início:

Iniciando o backend:

./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jar

Iniciando o gateway:

./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jar

Verificamos:

$ curl http://localhost:8080/
Número de solicitações 1 (gateway 38560358, secret "default-secret")

Tudo funcionando. O leitor atento notará que nada impede que chamemos o backend diretamente, contornando o gateway (http://localhost:8081/requests). Para corrigir isso, os serviços devem ser unidos em uma única rede, e o que deve "aparecer" externamente é apenas o gateway.
Além disso, ambos os serviços compartilham um sistema de arquivos, gerando threads e, em um momento, podem começar a interferir uns nos outros. Seria bom isolar nossos microsserviços. Isso pode ser alcançado distribuindo os aplicativos em diferentes máquinas (muito caro, complicado), usando máquinas virtuais (intensivo em recursos, inicialização lenta) ou por meio de containerização. Como esperado, escolhemos a terceira opção e Docker como ferramenta para containerização.

Docker

Resumidamente, o Docker cria contêineres isolados, um para cada aplicativo. Para utilizar o Docker, é necessário escrever um Dockerfile — uma instrução para compilar e executar o aplicativo. Em seguida, será possível criar uma imagem, carregá-la no registro de imagens (nº DockerHub) e com um único comando implantar seu microsserviço em qualquer ambiente dockerizado.

Dockerfile

Uma das mais importantes características de uma imagem é seu tamanho. Uma imagem compacta será baixada mais rapidamente de um repositório remoto, ocupará menos espaço e seu serviço iniciará mais rapidamente. Qualquer imagem é construída com base em uma imagem base, e recomenda-se escolher a opção mais minimalista. Uma boa opção é o Alpine — uma distribuição Linux completa com o mínimo de pacotes.

Para começar, vamos tentar escrever um Dockerfile "direto" (devo avisar que essa é uma má ideia, não faça assim):

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests
EXPOSE 8080
ENTRYPOINT ["java","-jar","target/microservices-gateway-1.0.0.jar"]

Aqui estamos usando uma imagem base baseada no Alpine com o JDK já instalado para compilar nosso projeto. Com o comando ADD, adicionamos o diretório atual src à imagem, o marcamos como o diretório de trabalho (WORKDIR) e iniciamos a compilação. O comando EXPOSE 8080 informa ao Docker que o aplicativo no contêiner usará a porta 8080 (isso não tornará o aplicativo acessível externamente, mas permitirá que ele seja acessível, por exemplo, de outro contêiner na mesma rede do Docker).

Para empacotar os serviços em imagens, é necessário executar os comandos a partir da raiz de cada projeto:

docker image build . -t msvc-backend:1.0.0

Como resultado, obtemos uma imagem com tamanho de 456 MB (dos quais a imagem base JDK 340 ocupou MB). E tudo isso levando em conta que as classes em nosso projeto podem ser contadas com os dedos. Para reduzir o tamanho da nossa imagem:

  • Usamos uma construção em múltiplas etapas. Na primeira etapa, construiremos o projeto, na segunda, instalaremos o JRE e, na terceira etapa, copiaremos tudo isso para uma nova imagem Alpine limpa. No final, a imagem final conterá apenas os componentes necessários.
  • Utilizaremos a modularização do Java. A partir do Java 9, é possível usar a ferramenta jlink para criar um JRE apenas com os módulos necessários.

Para os curiosos, aqui está um bom artigo sobre abordagens para reduzir o tamanho da imagem. https://habr.com/ru/company/ruvds/blog/485650/.

Dockerfile final:

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine as builder
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests

FROM alpine:3.10.3 as packager
RUN apk --no-cache add openjdk11-jdk openjdk11-jmods
ENV JAVA_MINIMAL="/opt/java-minimal"
RUN /usr/lib/jvm/java-11-openjdk/bin/jlink 
    --verbose 
    --add-modules 
        java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument 
    --compress 2 --strip-debug --no-header-files --no-man-pages 
    --release-info="add:IMPLEMENTOR=radistao:IMPLEMENTOR_VERSION=radistao_JRE" 
    --output "$JAVA_MINIMAL"

FROM alpine:3.10.3
LABEL maintainer="Anton Shelenkov anshelen@yandex.ru"
ENV JAVA_HOME=/opt/java-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"
COPY --from=packager "$JAVA_HOME" "$JAVA_HOME"
COPY --from=builder /src/target/microservices-backend-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

Recriaremos a imagem, e ela acabou reduzindo 6 vezes, totalizando 77 MB. Não é ruim. Depois, as imagens prontas podem ser carregadas no registro de imagens, para que suas imagens estejam disponíveis para download pela internet.

Execução conjunta de serviços no Docker

Para começar, nossos serviços devem estar na mesma rede. No Docker, existem vários tipos de redes, e usamos o mais simples — bridge, que permite conectar em rede os contêineres executados em um mesmo host. Vamos criar a rede com o seguinte comando:

docker network create msvc-network

Em seguida, iniciaremos o contêiner do backend com o nome 'backend' usando a imagem microservices-backend:1.0.0:

docker run -dit --name backend --network msvc-net microservices-backend:1.0.0

Vale ressaltar que a rede bridge fornece por padrão descoberta de serviços para contêineres pelos seus nomes. Ou seja, o serviço de backend estará acessível dentro da rede do Docker pelo endereço http://backend:8080.

Iniciando o gateway:

docker run -dit -p 80:8080 --env secret=my-real-secret --env BACKEND_URL=http://backend:8080/ --name gateway --network msvc-net microservices-gateway:1.0.0

Nesta equipe, indicamos que estamos redirecionando a porta 80 do nosso host para a porta 8080 do contêiner. Usamos as opções env para definir variáveis de ambiente que serão automaticamente lidas pelo Spring e substituirão propriedades do application.properties.

Após a inicialização, chamamos http://localhost/ e garantimos que tudo funciona como na vez passada.

Conclusão

Em resumo, criamos dois microsserviços simples, os empacotamos em contêineres Docker e os executamos juntos em uma única máquina. No entanto, o sistema resultante possui uma série de desvantagens:

  • Baixa resistência a falhas — tudo está operando em um único servidor
  • Baixa escalabilidade — ao aumentar a carga, seria ideal implantar automaticamente instâncias adicionais dos serviços e equilibrar a carga entre elas
  • Complexidade de inicialização — precisamos executar pelo menos 3 comandos, cada um com parâmetros específicos (isso é apenas para 2 serviços)

Para resolver os problemas mencionados, existem várias soluções, como Docker Swarm, Nomad, Kubernetes ou OpenShift. Se todo o sistema for escrito em Java, pode-se considerar o Spring Cloud (um bom artigo).

Em da próxima parte eu falarei sobre como configurei o Kubernetes e implantei o projeto no Google Kubernetes Engine.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster