Aprenda a implementar microservicios. Parte 1. Spring Boot y Docker

Aprenda a implementar microservicios. Parte 1. Spring Boot y Docker

Hola Habr.

En este artículo, quiero hablar sobre mi experiencia en la creación de un entorno de aprendizaje para experimentar con microservicios. Cuando aprendí cada herramienta nueva, siempre quise probarla no solo en la máquina local, sino también en condiciones más realistas. Por lo tanto, decidí crear una aplicación de microservicio simplificada, que luego puede "cubrirse" con todo tipo de tecnologías interesantes. El principal requisito del proyecto es su máxima proximidad funcional al sistema real.

Inicialmente, dividí la creación del proyecto en varios pasos:

  1. Cree dos servicios: 'backend' (backend) y 'gateway' (puerta de enlace), empaquetelos en imágenes acoplables y configúrelos para que funcionen juntos

    Palabras clave: Java 11, Spring Boot, Docker, optimización de imágenes

  2. Desarrollo de configuración de Kubernetes y despliegue del sistema en Google Kubernetes Engine

    Palabras clave: Kubernetes, GKE, gestión de recursos, escalado automático, secretos

  3. Creación de un gráfico con Helm 3 para una mejor gestión de clústeres

    Etiquetas: Helm 3, despliegue de gráficos

  4. Configuración de Jenkins y canalización para la entrega automática de código al clúster

    Palabras clave: configuración de Jenkins, complementos, repositorio de configuraciones separadas

Planeo dedicar un artículo separado a cada paso.

El enfoque de esta serie de artículos no es cómo escribir microservicios, sino cómo hacer que funcionen en un solo sistema. Aunque todas estas cosas suelen estar fuera de la responsabilidad del desarrollador, creo que no deja de ser útil familiarizarse con ellas al menos en un 20% (que, como sabéis, dan el 80% del resultado). Algunos temas incondicionalmente importantes, como la seguridad, quedarán fuera de este proyecto, ya que el autor entiende poco acerca de este sistema creado únicamente para uso personal. Acepto cualquier opinión y crítica constructiva.

Creación de microservicios

Los servicios fueron escritos en Java 11 usando Spring Boot. La interacción entre servicios se organiza mediante REST. El proyecto incluirá un número mínimo de pruebas (para que luego haya algo que probar en Jenkins). El código fuente de los servicios está disponible en GitHub: back-end и Puerta.

Para poder consultar el estado de cada uno de los servicios se ha añadido un Spring Actuator a sus dependencias. Creará un punto final /actuador/salud y devolverá un estado 200 si el servicio está listo para aceptar tráfico, o un 504 si hay un problema. En este caso, se trata de una comprobación bastante ficticia, ya que los servicios son muy sencillos y, en caso de alguna fuerza mayor, es más probable que queden completamente indisponibles a que permanezcan parcialmente operativos. Pero en los sistemas reales, Actuator puede ayudar a diagnosticar un problema antes de que los usuarios comiencen a luchar por ello. Por ejemplo, si hay problemas para acceder a la base de datos, podemos responder automáticamente deteniendo el procesamiento de solicitudes con una instancia de servicio rota.

Servicio de back-end

El servicio backend simplemente contará y devolverá la cantidad de solicitudes aceptadas.

Código del controlador:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

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

Puerta de enlace de servicio

La puerta de enlace reenviará la solicitud al servicio backend y la complementará con la siguiente información:

  • identificación de la puerta de enlace Es necesario para que sea posible distinguir una instancia de la puerta de enlace de otra por la respuesta del servidor.
  • Algún "secreto" que desempeñará el papel de una contraseña muy importante (número de la clave de cifrado de una cookie importante)

Configuración en application.properties:

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

Adaptador de fondo:

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

Lanzar:

Iniciamos el backend:

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

Iniciando la puerta de enlace:

./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 al backend, sin pasar por la puerta de enlace (http://localhost:8081/requests). Para solucionar esto, los servicios deben combinarse en una red, y solo la puerta de enlace debe "sobresalir" en el exterior.
Además, ambos servicios comparten un sistema de archivos, producen flujos y en un momento pueden comenzar a interferir entre sí. Sería bueno aislar nuestros microservicios. Esto se puede lograr mediante la distribución de aplicaciones en diferentes máquinas (mucho dinero, difícil), usando máquinas virtuales (recursos intensivos, inicio prolongado) o usando contenedores. Como era de esperar, elegimos la tercera opción y Docker como una herramienta para la contenerización.

Docker

En resumen, docker crea contenedores aislados, uno por aplicación. Para usar la ventana acoplable, debe escribir un Dockerfile: instrucciones para crear y ejecutar la aplicación. A continuación, puede crear la imagen, cargarla en el registro de imágenes (No. Docker Hub) e implemente su microservicio en cualquier entorno dockerizado con un solo comando.

Dockerfile

Una de las características más importantes de una imagen es su tamaño. Una imagen compacta se descargará más rápido desde un repositorio remoto, ocupará menos espacio y su servicio comenzará más rápido. Cualquier imagen se construye sobre la base de la imagen base, y se recomienda elegir la opción más minimalista. Una buena opción es Alpine, una completa distribución de Linux con paquetes mínimos.

Primero, intentemos escribir un Dockerfile "en la frente" (diré de inmediato que esta es una mala manera, no lo hagas):

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 una imagen base basada en Alpine con el JDK ya instalado para construir nuestro proyecto. Con el comando ADD, agregamos el directorio src actual a la imagen, lo marcamos como en funcionamiento (WORKDIR) e iniciamos la compilación. El comando EXPOSE 8080 le indica a Docker que la aplicación en el contenedor usará su puerto 8080 (esto no hará que la aplicación sea accesible desde el exterior, pero permitirá acceder a la aplicación, por ejemplo, desde otro contenedor en la misma red de Docker) .

Para empaquetar servicios en imágenes, debe ejecutar comandos desde la raíz de cada proyecto:

docker image build . -t msvc-backend:1.0.0

El resultado es una imagen de 456 MB (de los cuales la imagen JDK base ocupaba 340 MB). Y todo a pesar de que las clases de nuestro proyecto se pueden contar con los dedos. Para reducir el tamaño de nuestra imagen:

  • Utilizamos ensamblaje de varios pasos. En el primer paso construiremos el proyecto, en el segundo paso instalaremos el JRE y en el tercer paso lo copiaremos todo en una nueva imagen limpia de Alpine. En total, solo los componentes necesarios estarán en la imagen final.
  • Usemos la modularización de java. A partir de Java 9, puede usar la herramienta jlink para crear un JRE solo con los módulos que necesita

Para los curiosos, aquí hay un buen artículo sobre enfoques de reducción de imágenes. 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 la imagen y, como resultado, perdió 6 veces su peso, alcanzando los 77 MB. Nada mal. Después de eso, las imágenes preparadas se pueden cargar en el registro de imágenes para que sus imágenes estén disponibles para su descarga desde Internet.

Servicios de co-ejecución en Docker

Para empezar, nuestros servicios deben estar en la misma red. Hay varios tipos de redes en docker, y usamos la más primitiva de ellas: bridge, que le permite conectar en red contenedores que se ejecutan en el mismo host. Cree una red con el siguiente comando:

docker network create msvc-network

A continuación, inicie el contenedor de backend llamado 'backend' con la imagen microservices-backend:1.0.0:

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

Vale la pena señalar que la red del puente proporciona un descubrimiento de servicio listo para usar para contenedores por sus nombres. Es decir, el servicio de backend estará disponible dentro de la red docker en http://backend:8080.

Iniciando la puerta de enlace:

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

En este comando, indicamos que estamos reenviando el puerto 80 de nuestro host al puerto 8080 del contenedor. Usamos las opciones env para configurar las variables de entorno que Spring leerá automáticamente y anulará las propiedades de application.properties.

Después de comenzar, llamamos http://localhost/ y comprobar que todo funciona, como en el caso anterior.

Conclusión

Como resultado, creamos dos microservicios simples, los empaquetamos en contenedores docker y los lanzamos juntos en la misma máquina. El sistema resultante, sin embargo, tiene una serie de desventajas:

  • Mala tolerancia a fallas: todo funciona para nosotros en un servidor
  • Escalabilidad deficiente: cuando la carga aumenta, sería bueno implementar automáticamente instancias de servicio adicionales y equilibrar la carga entre ellas.
  • La complejidad del lanzamiento: necesitábamos ingresar al menos 3 comandos y con ciertos parámetros (esto es solo para 2 servicios)

Para solucionar los problemas anteriores, existen varias soluciones, como Docker Swarm, Nomad, Kubernetes u OpenShift. Si todo el sistema está escrito en Java, puede mirar hacia Spring Cloud (buen articulo).

В siguiente parte Hablaré sobre cómo configuré Kubernetes e implementé el proyecto en Google Kubernetes Engine.

Fuente: habr.com

Añadir un comentario