Aprenda como implantar microsserviços. Parte 1. Spring Boot e Docker

Aprenda como implantar microsserviços. Parte 1. Spring Boot e Docker

Olá, Habr.

Neste artigo, quero falar sobre minha experiência na criação de um ambiente de aprendizagem para experimentar microsserviços. Quando aprendia cada nova ferramenta, sempre quis experimentá-la não apenas na máquina local, mas também em condições mais realistas. Portanto, decidi criar um aplicativo de microsserviço simplificado, que posteriormente poderá ser “coberto” com todos os tipos de tecnologias interessantes. O principal requisito do projeto é a sua máxima proximidade funcional com o sistema real.

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

  1. Crie dois serviços - 'backend' (backend) e 'gateway' (gateway), empacote-os em imagens docker e configure-os para funcionarem juntos

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

  2. Desenvolvimento de configuração de Kubernetes e implantação de sistema no Google Kubernetes Engine

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

  3. Criando um gráfico com Helm 3 para melhor gerenciamento de cluster

    Tags: Helm 3, implantação de gráfico

  4. Configurando Jenkins e pipeline para entrega automática de código ao cluster

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

Pretendo dedicar um artigo separado para cada etapa.

O foco desta série de artigos não é como escrever microsserviços, mas como fazê-los funcionar em um único sistema. Embora todas essas coisas geralmente estejam fora da responsabilidade do desenvolvedor, acho que ainda é útil estar familiarizado com elas pelo menos 20% (o que, como você sabe, dá 80% do resultado). Alguns temas incondicionalmente importantes, como segurança, ficarão de fora deste projeto, já que o autor pouco entende sobre este sistema criado exclusivamente para uso pessoal. Aceito quaisquer opiniões e críticas construtivas.

Criando microsserviços

Os serviços foram escritos em Java 11 usando 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: Processo interno и porta de entrada.

Para poder verificar o status de cada um dos serviços, um Spring Actuator foi adicionado às suas dependências. Ele criará um endpoint /actuator/health e retornará um status 200 se o serviço estiver pronto para aceitar tráfego ou 504 se houver um problema. Neste caso, trata-se de uma verificação bastante fictícia, uma vez que os serviços são muito simples e, em caso de força maior, é mais provável que fiquem completamente indisponíveis do que permaneçam parcialmente operacionais. Mas em sistemas reais, o Actuator pode ajudar a diagnosticar um problema antes que os usuários comecem a brigar por ele. Por exemplo, se houver problemas de acesso ao banco de dados, podemos responder automaticamente interrompendo o processamento de solicitações com uma instância de serviço quebrada.

Serviço de back-end

O serviço de back-end simplesmente contará e retornará o número de solicitações aceitas.

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

Gateway de serviço

O gateway encaminhará a solicitação ao serviço backend, complementando-a com as seguintes informações:

  • identificação do gateway. É necessário para que seja possível distinguir uma instância do gateway de outra pela resposta do servidor
  • Algum “segredo” que desempenhará o papel de uma senha muito importante (número 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 de back-end:

@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<String> 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("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Lançar:

Iniciamos o back-end:

./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/
Number of requests 1 (gateway 38560358, secret "default-secret")

Tudo está funcionando. Um leitor atento notará que nada nos impede de acessar diretamente o backend, contornando o gateway (http://localhost:8081/requests). Para corrigir isso, os serviços devem ser combinados em uma rede, e apenas o gateway deve “sobressair” do lado de fora.
Além disso, ambos os serviços compartilham o mesmo sistema de arquivos, produzem fluxos e em um determinado momento podem começar a interferir um no outro. Seria bom isolar nossos microsserviços. Isso pode ser conseguido distribuindo aplicativos em diferentes máquinas (muito dinheiro, difícil), usando máquinas virtuais (uso intensivo de recursos, inicialização longa) ou usando conteinerização. Como esperado, escolhemos a terceira opção e Estivador como uma ferramenta para conteinerização.

Estivador

Resumindo, o docker cria contêineres isolados, um por aplicação. Para usar o docker, você precisa escrever um Dockerfile – instruções para construir e executar o aplicativo. Em seguida, você pode construir a imagem, carregá-la no registro de imagens (No. Docker Hub) e implante seu microsserviço em qualquer ambiente dockerizado em um comando.

dockerfile

Uma das características mais importantes de uma imagem é o 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 na imagem base, sendo recomendável escolher a opção mais minimalista. Uma boa opção é a Alpine, uma distribuição Linux completa e com pacotes mínimos.

Primeiro, vamos tentar escrever um Dockerfile "na testa" (direi desde já que esse é um jeito ruim, não faça isso):

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 em Alpine com o JDK já instalado para construir nosso projeto. Com o comando ADD, adicionamos o diretório src atual à imagem, marcamos-o como funcional (WORKDIR) e iniciamos a construção. O comando EXPOSE 8080 sinaliza ao docker que o aplicativo no contêiner usará sua porta 8080 (isso não tornará o aplicativo acessível de fora, mas permitirá que o aplicativo seja acessado, por exemplo, de outro contêiner na mesma rede docker ).

Para empacotar serviços em imagens, você precisa executar comandos na raiz de cada projeto:

docker image build . -t msvc-backend:1.0.0

O resultado é uma imagem de 456 MB (da qual a imagem JDK base ocupava 340 MB). E tudo apesar de as aulas do nosso projeto poderem ser contadas nos dedos. Para reduzir o tamanho da nossa imagem:

  • Usamos montagem em várias etapas. Na primeira etapa construiremos o projeto, na segunda etapa instalaremos o JRE e na terceira etapa copiaremos tudo para uma nova imagem Alpine limpa. No total, apenas os componentes necessários estarão na imagem final.
  • Vamos usar a modularização do java. A partir do Java 9, você pode usar a ferramenta jlink para criar um JRE apenas a partir dos módulos necessários

Para os curiosos, aqui está um bom artigo sobre abordagens de redução de 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 [email protected]"
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"]

Recriamos a imagem e, com isso, ela perdeu 6 vezes o peso, totalizando 77 MB. Nada mal. Depois disso, as imagens prontas podem ser carregadas no registro de imagens para que suas imagens fiquem disponíveis para download na Internet.

Co-execução de serviços no Docker

Para começar, nossos serviços devem estar na mesma rede. Existem vários tipos de redes no docker, e usamos o mais primitivo deles - bridge, que permite conectar contêineres em rede rodando no mesmo host. Crie uma rede com o seguinte comando:

docker network create msvc-network

Em seguida, inicie o contêiner de back-end denominado 'backend' com a imagem microservices-backend:1.0.0:

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

É importante notar que a rede bridge fornece descoberta de serviço pronta para uso para contêineres por seus nomes. Ou seja, o serviço backend estará disponível dentro da rede docker em 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

Neste comando indicamos que estamos encaminhando a porta 80 do nosso host para a porta 8080 do container. Usamos as opções env para definir variáveis ​​de ambiente que serão lidas automaticamente pelo spring e substituir propriedades de application.properties.

Depois de começar, chamamos http://localhost/ e certifique-se de que tudo funciona, como no caso anterior.

Conclusão

Como resultado, criamos dois microsserviços simples, empacotamos-os em contêineres docker e os lançamos juntos na mesma máquina. O sistema resultante, no entanto, tem uma série de desvantagens:

  • Baixa tolerância a falhas - tudo funciona para nós em um servidor
  • Baixa escalabilidade - quando a carga aumenta, seria bom implantar automaticamente instâncias de serviço adicionais e equilibrar a carga entre elas
  • A complexidade do lançamento - precisávamos inserir pelo menos 3 comandos, e com determinados parâmetros (isso é apenas para 2 serviços)

Para resolver os problemas acima, existem várias soluções, como Docker Swarm, Nomad, Kubernetes ou OpenShift. Se todo o sistema for escrito em Java, você pode olhar para Spring Cloud (bom artigo).

В próxima parte Falarei sobre como configurei o Kubernetes e implantei o projeto no Google Kubernetes Engine.

Fonte: habr.com

Adicionar um comentário