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:
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
Palabras clave: Kubernetes, GKE, gestión de recursos, escalado automático, secretos
Creación de un gráfico con Helm 3 para una mejor gestión de clústeres
Etiquetas: Helm 3, despliegue de gráficos
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)
$ 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
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.
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.