System Design 102

Puedes leer este post en inglés aquí.

Dicen que las segundas partes nunca son tan buenas como las primeras. Excepto el Batman de Nolan. Pero lo voy a intentar.

Las personas y las computadoras viven en una especie simbiosis. Y esto no es una coincidencia. En 1960 J. C. R. Licklider escribió sobre esta simbiosis. Después dedicó el resto de su carrera a trabajar en ello y a financiar proyectos relacionados a través de ARPA. Licklider fue originalmente psicólogo y el Internet existe porque animó activamente a las personas de su alrededor a construir una Red de Computadoras Intergaláctica. Era un visionario.

¿Por qué te estoy contando esto? Porque hacer software trata de interactuar con las computadoras y darles instrucciones. Al decirles lo que tienen que hacer, las limitaciones y los problemas surgen principalmente porque somos humanos. Conway lo sabía mejor que nadie.

Las organizaciones dedicadas al diseño de sistemas […] están condenadas a producir diseños que son copias de las estructuras de comunicación de dichas organizaciones. —  Melvin E. Conway

¡Que no te engañen! En algún momento de tu vida dividirás un monolito. Y será principalmente porque eres humano y no tanto porque tenga sentido. Al hacerlo, entras en el mundo de los sistemas distribuidos. Y puede que al principio te de miedo, y con razón. Pero sigue leyendo.

Comparte este post en 🐦 Twitter o suscríbete a mi newsletter With a grain of salt para recibir un email con novedades cada cierto tiempo.

Meiosis

Hay muchas formas de dividir un monolito, pero en cualquiera de ellas eliminarás la complejidad de una base de código y la añadirás al sistema. El código que solías invocar con una función ahora está esparcido en varios servicios. Y esos servicios deben comunicarse entre sí. Ese será tu desafío.

Si este diagrama te parece desordenado y extraño es porque lo es. La aplicación frontend interactúa con varios servicios de backend.

Baldur’s Gate

Nunca he jugado a este juego, pero imagino que tiene algo que ver con encontrar el camino hacia algún tipo de puerta única. Perdón. Lo que quiero decir en realidad es que, en lugar de exponer el diseño interno del sistema, sería muy bueno tener una sola interfaz. Daría la ilusión de interactuar con un solo servicio de backend. Esa “puerta” proporcionaría una experiencia cohesiva, uniforme y unificada.

¡Buenas noticias! Ya hay personas que han tenido este problema antes que tú. Así que puedes usar un proxy inverso llamado API Gateway. Algunos de los más famosos son Kong Gateway y el que ofrece AWS.

Pero un API Gateway sirve para mucho más:

  1. Autenticación. En lugar de dejar que cada servicio de backend maneje la autenticación, puedes dejar que el API Gateway se ocupe de ello.

  2. Regular el trafico. Rate limiting, throttling y restricción de tráfico. Más que nada porque probablemente quieras proteger tus servicios de ataques DDoS.

  3. Analytics. Visualizar, inspeccionar y supervisar el tráfico de los servicios backend. Porque seguramente querrás entender cómo se utilizan tus APIs.

  4. Transformaciones. Puedes transformar tanto peticiones como respuestas sobre la marcha.

  5. Serverless. Podrías invocar funciones lambda como si fueran una API. No miento.

Call me maybe

Una vez hayas resuelto los problemas de comunicación entre backend y frontend, es hora de resolver los problemas entre los propios servicios backend.

Claro que puedes utilizar llamadas HTTP. Pero si estás monitorizando al menos las 4 Señales de Oro, eventualmente vas a observar un aumento en la latencia. Espero que esto no te pille por sorpresa. Hacer una llamada HTTP tarda más tiempo que invocar una función. Entonces no, incluso si tu código está aislado, no puedes ignorar el diseño de todo el sistema.

No me malinterpretes. Este no es un mal diseño y en la mayoría de los casos probablemente funcionará durante algún tiempo. Pero eventualmente necesitarás una solución más decente.

En algún momento, la latencia se convertirá en un problema y alguien con más experiencia que tú en System Design te propondrá utilizar algún tipo de comunicación asincrónica.

En el post anterior ya expliqué qué son las colas de mensajes y más específicamente el patrón cola de tareas, también conocido como cola de trabajo.

Un publisher distribuye tareas a través de una cola de mensajes entre workers que compiten para consumirlos. Sólo un servicio y una instancia consume el mensaje y ejecuta la tarea/trabajo.

Pero, ¿y si no quieres que haya competición?. ¿Qué pasa si quieres enviar mensajes de forma asincrónica a varios servicios a la vez? Resulta que RabbitMQ, Apache Kafka y Google Pub/Sub permiten que un servicio publisher envíe un mensaje a varios servicios consumers. Este patrón se conoce como publish/subscribe.

No necesitas preocuparte demasiado por los detalles, pero es bueno entender cómo funciona el patrón pub/sub a grandes rasgos. El publisher, también llamado producer, nunca envía un mensaje directamente a una cola. De hecho, a menudo el producer ni siquiera sabe si el mensaje se entregará a alguna cola.

En cambio, el producer solo envía mensajes a un exchange. Un exchange es algo muy simple. Por un lado recibe mensajes de los producers y por otro lado los envía a las colas que haya. Cada servicio tiene su propia cola pero sólo una instancia de cada servicio consume el mensaje.

Si alguna vez alguien menciona algo llamado bus de eventos, esto es de lo que están hablando. O esto o algo muy parecido. Juntando todas las piezas puedes tener una visión más completa del sistema. Ahora todo vuelve a tener sentido.

Mitosis

De vuelta al frontend. Una forma de dividir un monolito frontend podría ser muy simple. Puedes dividirlo literalmente en dos y almacenar los assets estáticos en diferentes sitios.

Peeeero no todo es un camino de rosas. Administrar el estado y la autenticación de una sola aplicación es sencillo. Pero, ¿qué pasa cuando la divides? Sería una experiencia horrible si alguien navegara de una aplicación a otra y necesitara autenticarse de nuevo. La navegación entre aplicaciones debería ser imperceptible.

Podrías pensar que almacenar un token JWT en local storage sería una buena idea. Pero no. Stop. Caca. Mal. No hagas eso. El almacenamiento local está diseñado para ser accesible a través de javascript, por lo que no proporciona ninguna protección contra ataques XSS. Es mucho más seguro utilizar una cookie de sesión http only.

Divíde y vencerás. O no.

Seguro que ya has oído hablar de los microfrontends. Se supone que ayudan a escalar el desarrollo para que muchos equipos trabajen simultáneamente en un producto grande y complejo. Puedes incluso combinar microfrontends con serverless. Pero no voy a entrar en detalles para que nadie me tire piedras. Ni tampoco voy a hablar de server side rendering. Lo único que quiero es que entiendas que dividir aplicaciones frontend no afecta solo al código. Siempre hay consecuencias de infraestructura.

Dividir aplicaciones no reducirá la complejidad. Simplemente la distribuirá por todo el sistema.

Saber y ganar

Hoy en día estamos acostumbrados a tratar con sistemas distribuidos. Pero no siempre ha sido así. Permíteme una breve lección de historia. Las computadoras solían funcionar de forma aislada. Eso fue hasta que nació ARPANET en 1969 después de un parto que duró varios años. El primer sistema distribuido. Mas o menos.

Uno de los primeros cuatro nodos fue la Universidad de California, Los Ángeles. Tenían parte de la infraestructura necesaria porque ya habían intentado conectar los campus de la UCLA algunos años antes, pero como no se llevaban bien, fracasaron. Después de ese primer intento, la UCLA redirigió sus fuerzas a otro proyecto que tenía como objetivo medir cómo los bits se movían de un lugar a otro en un sistema informático.

Len Kleinrock de la UCLA, quien estuvo involucrado en el diseño de la ARPANET, literalmente dío un puñetazo en la mesa y exigió medir todo lo que ocurría en la red. Su argumento fue que ARPANET era un experimento. Y como tal, había que medirlo todo: cuántos paquetes viajaban por la red, dónde se estaban formando los cuellos de botella, qué velocidad y capacidad se podían lograr en la práctica y qué condiciones de tráfico romperían la red.

Si te das cuenta, todo eso es muy similar a las 4 Señales de Oro que se atribuyen a Google hoy en día. Nada mas lejos de la verdad pero nos gusta reinventar la rueda cada pocos años.

Las personas de la UCLA estaban interesadas en sacarle provecho a su proyecto, eso es verdad. ¿Pero a qué más da? Gracias a los cimientos de un proyecto fallido y el impulso del proyecto de monitorización, la UCLA se convirtió en el Centro de Medición de Redes de la ARPANET. Esto fue en 1969. Justo cuando los Rolling Stones eran considerados la banda de rock más grande del mundo. Si hace 50 años, sí, hace 50 años, las personas se preocupaban por la monitorización, ¿por qué no lo harías tú hoy?

Mírame

No estoy aquí para decirte qué monitorizar. Ese es un tema para un post futuro. Por ahora solo quiero explicarte cómo suelen funcionar las herramientas de monitorización a nivel muy básico.

Las herramientas más tradicionales, basadas en push, envían métricas al servidor de métricas, también conocido como Aggregator. Por lo general, tienes que instalar un agente junto a cada servicio que quieres monitorizar. A veces en el mismo contendor; otras veces en un contenedor sidecar. Tienes que configurar cuándo, dónde y con qué frecuencia enviar las métricas. Pero enviar métricas no es el objetivo principal de tus servicios, por lo que en lugar de enviarlas una por una, también tienes que decidir si las métricas deben agregarse y cómo se deben agregar antes de enviarlas. Aunque, si usas algo como New Relic, no tienes que preocuparse demasiado por los detalles de implementación.

Pero puedes diseñar el sistema de monitorización de una manera completamente transparente para tus servicios. Las herramientas basadas en pull en vez de push como Prometheus lo hacen posible. Debido a que la configuración no está vinculada a los servicios, ya no tendrás que re-desplegar tus servicios si quieres cambiarla.

Así es como podría funcionar una herramienta de monitorización basada en pull:

  1. Todos tus servicios deben tener un endpoint /metrics. Esto es super común y puedes encontrar muchas librerías y/o frameworks que te lo dan gratis.

  2. El Aggregator necesita saber dónde están los servicios a los que pedir las métricas. Puedes definirlos de forma estática pero ¿qué pasaría si una IP cambia? ¿O si un servicio se cae? Quizá sería mejor utilizar algún tipo de Service Discovery que le diga automáticamente al Aggregator de dónde extraer las métricas. Quizá puedas usar Consul para eso.

  3. El Aggregator envía peticiones HTTP al endpoint /metrics de forma regular. Prometheus llama a eso un scrape. La respuesta se analiza y se guarda en el almacenamiento local.

  4. Algunos servicios, por la razón que sea, son más difíciles de instrumentar. En esos casos podrías desplegar un Exporter al lado de ese servicio que proporcionará el endpoint /metrics.

  5. Imagino que querrás visualizar y analizar las métricas de alguna manera. Hay muchas herramientas que te permiten hacer eso pero Grafana es la más famosa. Te permite consultar, visualizar y comprender tus métricas.

  6. Algunos agregadores de métricas como Prometheus también se pueden configurar para enviar alertas cuando ocurre algo inesperado. Un Alert Manager puede recibir alertas del servidor de métricas y convertirlas en notificaciones.

Léeme

El logging es otro mecanismo útil que te da visibilidad sobre el comportamiento de un servicio. Tradicionalmente, era común escribir los registros en un archivo llamado logfile. Pero hoy en día un servicio no debería preocuparse con guardar logs en disco. Debería escupir los logs a la salida estándar o stdout. Hay muchos beneficios de hacer eso. Durante el desarrollo local, puedes ver los logs en tu terminal.

Mientras que en un entorno de producción, los logs se enviarán a un procesador de logs como Fluend. Allí se filtrarán y luego se almacenarán en una base de datos como Elasticsearch. Al igual que con las métricas, querrás buscar, visualizar y analizar los registros con una herramienta como Kibana.

¿Hemos llegado ya?

Sé que hemos hablado de muchos conceptos y herramientas. Y esta última parte sobre las métricas y el logging no es fácil de comprender. No te preocupes. Lee el post de nuevo e indaga en los recursos que te he dejado entre las líneas.

No hemos terminado. No exactamente. Los sistemas distribuidos manejan datos distribuidos. Y ese será el tema del próximo post de esta serie. Y eso que todavía no hemos hablado de Kubernetes.

Comparte este post en 🐦 Twitter o suscríbete a mi newsletter With a grain of salt para recibir un email con novedades cada cierto tiempo.