Aprende a implementar microservizos. Parte 1. Spring Boot e Docker

Aprende a implementar microservizos. Parte 1. Spring Boot e Docker

Ola Habr.

Neste artigo, quero falar da miña experiencia na creación dun ambiente de aprendizaxe para experimentar con microservizos. Cando aprendín cada ferramenta nova, sempre quixen probala non só na máquina local, senón tamén en condicións máis realistas. Por iso, decidín crear unha aplicación de microservizos simplificada, que despois poida ser "cuberta" con todo tipo de tecnoloxías interesantes. O principal requisito do proxecto é a súa máxima proximidade funcional ao sistema real.

Inicialmente, dividín a creación do proxecto en varios pasos:

  1. Crea dous servizos: "backend" (backend) e "gateway" (gateway), empaquetaos en imaxes docker e configúraos para que funcionen xuntos

    Palabras clave: Java 11, Spring Boot, Docker, optimización de imaxes

  2. Desenvolvemento da configuración de Kubernetes e implantación do sistema en Google Kubernetes Engine

    Palabras clave: Kubernetes, GKE, xestión de recursos, escalado automático, segredos

  3. Creando un gráfico con Helm 3 para unha mellor xestión do clúster

    Etiquetas: Helm 3, despregamento de gráficos

  4. Configurando Jenkins e pipeline para a entrega automática de código ao clúster

    Palabras clave: configuración de Jenkins, complementos, repositorio de configuración separado

Penso dedicar un artigo separado a cada paso.

O foco desta serie de artigos non é como escribir microservizos, senón como facelos funcionar nun único sistema. Aínda que todas estas cousas adoitan estar fóra da responsabilidade do desarrollador, creo que aínda é útil familiarizarse con elas polo menos un 20% (o que, como sabes, dá o 80% do resultado). Algúns temas de importancia incondicional, como a seguridade, quedarán fóra deste proxecto, xa que o autor entende pouco de que este sistema está creado unicamente para uso persoal. Agradezo calquera opinión e crítica construtiva.

Creación de microservizos

Os servizos foron escritos en Java 11 usando Spring Boot. A interacción entre servizos organízase mediante REST. O proxecto incluirá un número mínimo de probas (para que despois haxa algo que probar en Jenkins). O código fonte dos servizos está dispoñible en GitHub: backend и Pasarela.

Para poder comprobar o estado de cada un dos servizos, engadiuse un Spring Actuator ás súas dependencias. Creará un punto final /actuator/health e devolverá un estado 200 se o servizo está preparado para aceptar tráfico, ou 504 se hai un problema. Neste caso, trátase dunha comprobación bastante ficticia, xa que os servizos son moi sinxelos e, en caso de forza maior, é máis probable que non estean completamente dispoñibles que que sigan parcialmente operativos. Pero nos sistemas reais, Actuator pode axudar a diagnosticar un problema antes de que os usuarios comecen a loitar por iso. Por exemplo, se hai problemas para acceder á base de datos, podemos responder automaticamente a isto deixando de procesar as solicitudes cunha instancia de servizo rota.

Servizo de back-end

O servizo de backend simplemente contará e devolverá o número de solicitudes aceptadas.

Código do controlador:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

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

Pasarela de servizo

A pasarela reenviará a solicitude ao servizo de backend, complementándoa coa seguinte información:

  • ID de pasarela. É necesario para que sexa posible distinguir unha instancia da pasarela doutra pola resposta do servidor
  • Algún "segredo" que desempeñará o papel dun contrasinal moi importante (número da clave de cifrado dunha cookie importante)

Configuración en application.properties:

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

Adaptador de 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<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);
    }
}

Lanzamento:

Comezamos o backend:

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

Iniciando a pasarela:

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

Comprobamos:

$ curl http://localhost:8080/
Number of requests 1 (gateway 38560358, secret "default-secret")

Todo está funcionando. Un lector atento notará que nada nos impide acceder directamente ao backend, evitando a pasarela (http://localhost:8081/requests). Para solucionar isto, os servizos deben combinarse nunha soa rede, e só a pasarela debería "salir" fóra.
Ademais, ambos os servizos comparten un sistema de ficheiros, producen fluxos e nun momento poden comezar a interferir entre si. Sería bo illar os nosos microservizos. Isto pódese conseguir distribuíndo aplicacións en diferentes máquinas (moitos cartos, difíciles), utilizando máquinas virtuais (con uso intensivo de recursos, arranque longo) ou utilizando a contenedorización. Como era de esperar, escollemos a terceira opción e Estivador como ferramenta de contenerización.

Estivador

En resumo, docker crea contedores illados, un por aplicación. Para usar Docker, cómpre escribir un Dockerfile: instrucións para construír e executar a aplicación. A continuación, pode construír a imaxe, cargala no rexistro de imaxes (núm. Dockerhub) e despregue o seu microservizo en calquera ambiente acoplable nun só comando.

dockerfile

Unha das características máis importantes dunha imaxe é o seu tamaño. Unha imaxe compacta descargarase máis rápido desde un repositorio remoto, ocupará menos espazo e o teu servizo comezará máis rápido. Calquera imaxe está construída sobre a base da imaxe base, e recoméndase escoller a opción máis minimalista. Unha boa opción é Alpine, unha distribución Linux completa con paquetes mínimos.

Para comezar, intentemos escribir un Dockerfile "na fronte" (deseguido digo que é unha mala forma, non o fagas):

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"]

Aquí estamos usando unha imaxe base baseada en Alpine co JDK xa instalado para construír o noso proxecto. Co comando ADD, engadimos o directorio src actual á imaxe, marcamos como funcionando (WORKDIR) e iniciamos a compilación. O comando EXPOSE 8080 sinala ao docker que a aplicación do contedor utilizará o seu porto 8080 (isto non fará que a aplicación sexa accesible desde o exterior, pero permitirá acceder á aplicación, por exemplo, desde outro contedor da mesma rede docker). ).

Para empaquetar servizos en imaxes, cómpre executar comandos desde a raíz de cada proxecto:

docker image build . -t msvc-backend:1.0.0

O resultado é unha imaxe de 456 MB (da cal a imaxe JDK base ocupaba 340 MB). E todo a pesar de que as clases do noso proxecto se poden contar cun dedo. Para reducir o tamaño da nosa imaxe:

  • Usamos montaxe de varios pasos. No primeiro paso construímos o proxecto, no segundo instalaremos o JRE e no terceiro copiarémolo todo nunha nova imaxe limpa de Alpine. En total, só estarán os compoñentes necesarios na imaxe final.
  • Usemos a modularización de java. Comezando con Java 9, podes usar a ferramenta jlink para crear un JRE só a partir dos módulos que necesites

Para os curiosos, aquí tes un bo artigo sobre enfoques de redución de imaxes. 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"]

Recreamos a imaxe e, como resultado, perdeu 6 veces o seu peso, ascendendo a 77 MB. Non está mal. Despois diso, pódense cargar imaxes preparadas no rexistro de imaxes para que as súas imaxes estean dispoñibles para descargar desde Internet.

Co-execución de servizos en Docker

Para comezar, os nosos servizos deben estar na mesma rede. Existen varios tipos de redes en Docker, e usamos a máis primitiva delas: bridge, que permite conectar contedores en rede que se executan no mesmo host. Crea unha rede co seguinte comando:

docker network create msvc-network

A continuación, inicie o contenedor do backend chamado "backend" coa imaxe microservices-backend:1.0.0:

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

Paga a pena sinalar que a rede de ponte ofrece un servizo de descubrimento listo para usar para contedores polos seus nomes. É dicir, o servizo de backend estará dispoñible dentro da rede docker en http://backend:8080.

Iniciando a pasarela:

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 a reenviar o porto 80 do noso host ao porto 8080 do contedor. Usamos as opcións env para establecer variables de ambiente que serán lidas automaticamente por spring e anular propiedades de application.properties.

Despois de comezar, chamamos http://localhost/ e asegurarse de que todo funciona, como no caso anterior.

Conclusión

Como resultado, creamos dous microservizos sinxelos, empaquetámolos en contedores docker e lanzámolos xuntos na mesma máquina. Non obstante, o sistema resultante ten unha serie de desvantaxes:

  • Poca tolerancia a fallos: todo funciona para nós nun servidor
  • Escalabilidade deficiente: cando a carga aumenta, sería bo implementar automaticamente instancias de servizo adicionais e equilibrar a carga entre elas
  • A complexidade do lanzamento: necesitabamos introducir polo menos 3 comandos e con certos parámetros (isto é só para 2 servizos)

Para solucionar os problemas anteriores, hai unha serie de solucións como Docker Swarm, Nomad, Kubernetes ou OpenShift. Se todo o sistema está escrito en Java, podes mirar cara Spring Cloud (bo artigo).

В seguinte parte Falarei sobre como configurei Kubernetes e despreguei o proxecto en Google Kubernetes Engine.

Fonte: www.habr.com

Engadir un comentario