Diseño de infraestructuras backend escalables desde cero

Diseñar una plataforma de back-end lista para el futuro desde cero es muy demandado en estos días, pero no es fácil comprender la abrumadora información disponible en Internet. Por lo tanto, crearemos un backend escalable con todas las funciones paso a paso en esta serie de varias partes.

He creado una serie de youtube a partir de esta publicación de blog desde que recibí tantas solicitudes. Siga el código alfabético de mi canal de YouTube para ver una serie de conferencias sobre arquitectura de microservicios.

Enlace a la primera serie de cursos intensivos: https://www.youtube.com/playlist?list=PLZBNtT95PIW3BPNYF5pYOi4MJjg_boXCG

Al desarrollar la primera versión de una aplicación, a menudo no tiene problemas de escalabilidad. Además, el uso de una arquitectura distribuida ralentiza el desarrollo. Este puede ser un problema importante para las startups cuyo mayor desafío es evolucionar rápidamente el modelo de negocio y reducir el tiempo de mercado. Pero como estás aquí, supongo que ya lo sabes. Saltemos directamente al tema teniendo en cuenta los siguientes objetivos:

  1. Distribuya el desarrollo de la API: el sistema debe diseñarse de tal manera que varios equipos puedan trabajar en él simultáneamente y un solo equipo no debe convertirse en un cuello de botella ni necesita tener experiencia en toda la aplicación para crear puntos finales optimizados.
  2. Admite múltiples idiomas: para aprovechar las tecnologías emergentes, cada parte funcional del sistema debe ser capaz de admitir el idioma preferido para esa funcionalidad.
  3. Minimice la latencia: cualquier arquitectura que propongamos siempre debe tratar de minimizar el tiempo de respuesta del cliente.
  4. Minimice los riesgos de implementación: diferentes componentes funcionales del sistema deberían poder implementarse por separado con una coordinación mínima.
  5. Minimice la huella de hardware: el sistema debería intentar optimizar la cantidad de hardware utilizado y debería ser escalable horizontalmente.

Construyendo Aplicaciones Monolíticas

Imaginemos que estaba comenzando a crear una nueva aplicación de comercio electrónico destinada a competir con Amazon. Comenzaría creando un nuevo proyecto en su plataforma preferida de elección, como Rails, Spring Boot, Play, etc. Normalmente tendría una arquitectura modular similar a esta:

Figura 1

La capa superior generalmente manejará las solicitudes del cliente y después de hacer algunas validaciones reenviará la solicitud a la capa de servicio donde se implementa toda la lógica empresarial. Un servicio utilizará varios adaptadores como componentes de acceso a la base de datos en la capa DAO, componentes de mensajería, API externas u otros servicios en la misma capa para preparar el resultado y devolverlo al controlador que el interno lo devuelve al cliente.

Este tipo de aplicación generalmente se empaqueta y se implementa como un monolito, lo que significa un gran archivo. Por ej. será un frasco en caso de arranque de primavera y un archivo zip en el caso de la aplicación Rails o Node.js. Las aplicaciones como estas son bastante comunes y tienen muchas ventajas, son fáciles de comprender, administrar, desarrollar, probar e implementar. También puede escalarlos ejecutando varias copias detrás de un equilibrador de carga y funciona bastante bien hasta cierto nivel.

Desafortunadamente, este enfoque simple tiene enormes limitaciones como:

  • Bloqueo de idioma / marco: dado que toda la aplicación está escrita en una sola pila tecnológica. No puedo experimentar con tecnologías emergentes.
  • Difícil de digerir: una vez que la aplicación se hace grande, a un desarrollador le resulta difícil comprender una base de código tan grande.
  • Difícil de distribuir el desarrollo de API: se vuelve extremadamente difícil realizar un desarrollo ágil y una gran parte del tiempo del desarrollador se desperdicia en la resolución de conflictos.
  • Implementación como una sola unidad: no se puede implementar de forma independiente un solo cambio en un solo componente. Los cambios son "rehenes" por otros cambios.
  • El desarrollo se ralentiza: he trabajado en una base de código que tenía más de 50,000 clases. El gran tamaño de la base de código fue suficiente para ralentizar el IDE y los tiempos de inicio debido a la productividad que solía sufrir.
  • Los recursos no están optimizados: algunos módulos pueden implementar una lógica de procesamiento de imágenes con uso intensivo de CPU que requiere instancias optimizadas para computación y otro módulo puede ser una base de datos en memoria y más adecuada para instancias optimizadas para memoria. Pero tendremos que comprometer nuestra elección de hardware. También puede suceder que un módulo de aplicación requiera escalado, pero tendremos que ejecutar una instancia completa de la aplicación nuevamente porque no podemos escalar un módulo individualmente.

¿No sería maravilloso si pudiéramos dividir la aplicación en partes más pequeñas y administrarlas de tal manera que se comportara como una sola aplicación cuando la ejecutamos? Sí, lo sería y eso es exactamente lo que haremos a continuación.

Arquitectura de microservicios

Muchas organizaciones, como Amazon, Facebook, Twitter, eBay y Netflix han resuelto este problema adoptando lo que ahora se conoce como el patrón de Arquitectura de Microservicios. Aborda este problema dividiéndolo en subproblemas más pequeños, también conocidos como dividir y conquistar en el mundo de los desarrolladores. Mire la figura 1 detenidamente, cortaremos los cortes verticales y crearemos servicios interconectados más pequeños. Cada segmento implementará una funcionalidad distinta, como la gestión de carritos, la gestión de usuarios y la gestión de pedidos, etc. Cada servicio se puede escribir en cualquier lenguaje / marco y puede tener la persistencia políglota que se adapte al caso de uso. Easy-peasy ¿verdad?

¡Pero espera! También queríamos que se comportara como una aplicación única para el cliente; de ​​lo contrario, el cliente tendrá que lidiar con toda la complejidad que conlleva esta arquitectura, como agregar los datos de varios servicios, mantener tantos puntos finales, aumentar el chat del cliente y el servidor, autenticación separada para cada servicio. La dependencia del cliente en microservicios directamente dificulta también la refactorización de los servicios. Una forma intuitiva de hacer esto es ocultar estos servicios detrás de una nueva capa de servicios y proporcionar API que se adapten a cada cliente. Esta capa de servicio del agregador también se conoce como API Gateway y es una forma común de abordar este problema.

Patrón de arquitectura de microservicios basado en API Gateway

Todas las solicitudes de los clientes pasan primero por la API Gateway. Luego enruta las solicitudes al microservicio apropiado. La API Gateway a menudo maneja una solicitud invocando múltiples microservicios y agregando los resultados. Puede tener otras responsabilidades, como autenticación, supervisión, equilibrio de carga, almacenamiento en caché y manejo de respuesta estática. Dado que esta puerta de enlace proporciona API específicas para el cliente, reduce la cantidad de viajes de ida y vuelta entre el cliente y la aplicación, lo que reduce la latencia de la red y también simplifica el código del cliente.

La descomposición funcional del monolito variará según el caso de uso. Amazon usa más de 100 microservicios para mostrar una sola página de producto, mientras que Netflix tiene más de 600 microservicios que administran su backend. Los microservicios enumerados en el diagrama anterior le dan una idea de cómo se debe descomponer una aplicación de comercio electrónico escalable, pero puede ser necesaria una observación más cuidadosa antes de implementarla para la producción.

No hay tal cosa como un almuerzo gratis. Microservicios trae consigo algunos desafíos complejos, como:

  • Desafíos de la computación distribuida: dado que diferentes microservicios deberán ejecutarse en un entorno distribuido, tendremos que ocuparnos de estas falacias de la computación distribuida. En resumen, debemos suponer que el comportamiento y las ubicaciones de los componentes de nuestro sistema cambiarán constantemente.
  • Las llamadas remotas son caras: los desarrolladores deben elegir e implementar un mecanismo de comunicación eficiente entre procesos.
  • Transacciones distribuidas: las transacciones comerciales que actualizan varias entidades comerciales deben confiar en la consistencia eventual sobre ACID.
  • Manejo de la falta de disponibilidad del servicio: tendremos que diseñar nuestro sistema para manejar la falta de disponibilidad o la lentitud de los servicios. Todo falla todo el tiempo.
  • Implementando características que abarcan múltiples servicios.
  • Las pruebas de integración y la gestión del cambio se vuelven difíciles.

Por supuesto, la gestión manual de las complejidades de los microservicios pronto comenzará a salirse de control. Para construir un sistema distribuido automatizado y de recuperación automática, necesitaremos tener las siguientes características en nuestra arquitectura.

  • Configuración central: un sistema de configuración centralizado y versionado, algo así como Zookeeper, cambios que se aplican dinámicamente a los servicios en ejecución.
  • Descubrimiento de servicios: cada servicio en ejecución debe registrarse con un servidor de descubrimiento de servicios y el servidor informa a todos los que están en línea. Al igual que una aplicación de chat típica. No queremos codificar la dirección del punto final del servicio entre sí.
  • Equilibrio de carga: equilibrio de carga del lado del cliente, para que pueda aplicar estrategias complejas de equilibrio y realizar almacenamiento en caché, procesamiento por lotes, tolerancia a fallas, descubrimiento de servicios y manejar múltiples protocolos.
  • Comunicación entre procesos: necesitaremos implementar una estrategia de comunicación entre procesos eficiente. Puede ser algo como REST o Thrift o mecanismos de comunicación asíncronos basados ​​en mensajes como AMQP o STOMP. También podemos usar formatos de mensajes eficientes como Avro o Protocol Buffers, ya que esto no se utilizará para comunicarse con el mundo exterior.
  • Autenticación y seguridad: necesitamos tener un sistema para identificar los requisitos de autenticación para cada recurso y rechazar las solicitudes que no los satisfacen.
  • IO sin bloqueo: API Gateway maneja las solicitudes invocando múltiples servicios de back-end y agregando los resultados. Con algunas solicitudes, como una solicitud de detalles del producto, las solicitudes de servicios de backend son independientes entre sí. Para minimizar el tiempo de respuesta, API Gateway debe realizar solicitudes independientes al mismo tiempo.
  • Consistencia eventual: necesitamos tener un sistema para manejar las transacciones comerciales que abarcan múltiples servicios. Cuando un servicio actualiza su base de datos, debe publicar un evento y debe haber un intermediario de mensajes que garantice que los eventos se entreguen al menos una vez a los servicios de suscripción.
  • Tolerancia a fallas: debemos evitar la situación por la cual una sola falla cae en cascada en una falla del sistema. API Gateway nunca debe bloquear indefinidamente la espera de un servicio descendente. Debe manejar las fallas con gracia y devolver respuestas parciales siempre que sea posible.
  • Sesiones distribuidas: idealmente no deberíamos tener ningún estado en el servidor. El estado de la aplicación debe guardarse en el lado del cliente. Ese es uno de los principios importantes de un servicio RESTful. Pero si tiene una excepción y no puede evitarla, siempre tenga sesiones distribuidas. Dado que el cliente solo se comunica con API Gateway, necesitaremos ejecutar varias copias de él detrás de un equilibrador de carga porque no queremos que API Gateway se convierta en un cuello de botella. Esto significa que las solicitudes posteriores del cliente pueden aterrizar en cualquiera de las instancias en ejecución de API Gateway. Necesitamos tener una manera de compartir la información de autenticación entre varias instancias de API Gateway. No queremos que el cliente vuelva a autenticarse cada vez que su solicitud se encuentre en una instancia diferente de API Gateway.
  • Almacenamiento en caché distribuido: debemos tener mecanismos de almacenamiento en caché en varios niveles para reducir la latencia del cliente. Múltiples niveles simplemente significa que el cliente, la API Gateway y los microservicios deberían tener un mecanismo de almacenamiento en caché confiable.
  • Monitoreo detallado: Deberíamos poder rastrear datos significativos y estadísticas de cada componente funcional para darnos una visión precisa de la producción. Se deben activar alarmas adecuadas en caso de excepciones o tiempos de respuesta altos.
  • Enrutamiento dinámico: API Gateway debería poder enrutar de manera inteligente las solicitudes a microservicios si no tiene una asignación específica al recurso solicitado. En otras palabras, los cambios no deberían ser necesarios en API Gateway cada vez que un microservicio agrega un nuevo punto final de su lado.
  • Escalado automático: cada componente de nuestra arquitectura, incluida API Gateway, debe ser escalable horizontalmente y debe escalarse automáticamente cuando sea necesario, incluso si se implementa dentro de un contenedor.
  • Compatibilidad con Polyglot: dado que se pueden escribir diferentes microservicios en diferentes idiomas o marcos, el sistema debe proporcionar invocaciones de servicio sin problemas y las características mencionadas anteriormente, independientemente del idioma en el que esté escrito.
  • Implementación sin problemas: la implementación de nuestros microservicios debe ser rápida, independiente y automatizada si es posible.
  • Independiente de la plataforma: para hacer un uso eficiente del hardware y mantener nuestros servicios independientes de la plataforma en la que se implementa, debemos implementar nuestros servicios web dentro de algún contenedor como el docker.
  • Agregación de registros: deberíamos tener un sistema en su lugar que automáticamente agregue registros de todos los microservicios en un sistema de archivos. Estos registros podrían usarse para varios análisis más adelante.

Whoa! Estas son muchas características para implementar solo para cuidar una arquitectura. ¿Realmente vale la pena? Y la respuesta es sí". La arquitectura de microservicios es probada en batalla por compañías como Netflix, que solo consume alrededor del 40% del ancho de banda de Internet del mundo.

En mi próxima publicación, describo cómo podemos comenzar a diseñar nuestros microservicios. Estén atentos y síganme en cualquier lugar de Youtube para recibir actualizaciones. ¡Tranquilízate!