Arquitecturas Concurrentes, Episodio 1: El diablo está en los detalles

Implementación de Arquitecturas de Software Concurrente (Arquitecturas Concurrentes para los amigos) es una materia electiva del quinto año de la carrera de Ingeniería en Sistemas de UTN-FRBA, que acabamos de abrir tras dos años de trabajo duro preparándola.

La materia tiene un objetivo doble: por un lado, conocer tecnologías novedosas de aplicación en la industria de desarrollo de software, y por otro lado, transmitir conocimiento durables, ideas que trasciendan la tecnología, sobre la concurrencia, y la arquitectura de software.

Empezando por este blogpost, iremos subiendo nuestros resúmenes de clase.

¿Arquitecturas Concurrentes? ¿Qué es eso?

Hace algunos años con @bossiernest empezamos a idear la materia Arquitecturas Concurrentes. La misma, lejos de surgir de un pasión por las colas de mensajes y los mutex, surgía de la frustración:

  • por un lado, de la poca rigurosidad técnica que encontramos en nuestro día a día laboral, proveniente de algunos "Arquitectos Java" o "Evangelistas NodeJS": ideas correctas pero nunca bajadas a detalle, decisiones arbitrarias tomadas en base a un blogpost o a la última charla comercial de TuCompañiaEnterpriseFavorita, posturas dogmáticas en favor de una tecnología sin analizar los contextos.
  • y por el otro, porque la concurrencia nos tiene de hijo: no importa cuanto nos esforcemos, los malignos hilos de ejecución lograrán destruir nuestro programa en modos insospechados. El cuidado parece ser siempre insuficiente: de alguna forma irreproducibles bugs de sincronización nos harían morder el suelo.

¿Qué vimos en común entre estas dos cuestiones? Que el diablo está en los detalles: muchas ideas de arquitectura y manejo de concurrencia intuitivamente "suenan bien". Pero recién cuando las bajamos a detalle vemos realmente sus consecuencias.

Y así condesamos estas ideas en una: un estudio práctico sobre la concurrencía en estado salvaje, desde un punto de vista arquitectónico.

Sobre la Arquitectura

Primero, algunas palabras sobre la arquitectura de software. He aquí algunas interpretaciones comunes y complementarias del término:

  • Es el diseño lógico de alto nivel: diseñar ya no en términos de componentes como objetos, procedimientos o funciones, sino en términos de módulos, nodos de red, etc
  • Es el diseño de aquellos aspectos que software que son difíciles de cambiar: tecnologías de base, lenguajes, etc
  • Es el diseño físico: la selección de los componentes de hardware y el despliegue del software sobre estos componentes.

En general no encontramos muchas desavenencias en torno a esta ideas. Los problemas surgen cuando pensamos en cómo hacer arquitectura.

Haciendo arquitectura

Para nosotros la construcción de una arquitectura es un proceso:

  • iterativo: si bien la arquitectura trata de lidiar con aquellas cosas que son difíciles de cambiar, aún así hay lugar para iterar. Por ejemplo: probablemente cambiar el lenguaje cada 3 iteraciones no sea una opción viable; sin embargo sí lo es empezar con un almacenamiento en archivos o una base embebida SQLite, luego pasar a un motor relacional, luego extraer una parte a una base de datos Mongo, y luego implementar sharding.
  • constructivo: la arquitectura incluye la construcción. Si bien comunicar la arquitectura es una tarea real, la definición de una arquitectura no se limita a generar diagramas de despliegue y listados de tecnologías: hay que meter las manos en el barro. Medir, desplegar, programar, probar, son tareas imprescindibles.
  • verificable: si la arquitectura no se puede validar de forma rápida, entonces el proceso está fallando. De la misma forma que no deberíamos programar todo el sistema antes de hacer las pruebas, o todas las pruebas antes de poner nuestra primera línea de código productivo, o encarar refactors que duren días, tampoco deberíamos embarcarnos en implementar arquitecturas de las que no podamos tener ningún feedback hasta dentro de varios meses.
  • holístico: en el desarrollo de una arquitectura los aspectos humanos suelen tener mucho más peso que los técnicos o tecnológicos. Así, cuestiones económicas o financieras (debemos reducir el gasto mensual en servidores en X%), políticas (vamos a usar el contenedor de aplicaciones X porque nuestra empresa tiene un convenio con quien lo comercializa), interpersonales (el gerente de sistemas de nuestro sector está peleado con el área de base de datos, por lo que utilizaremos almacenamiento en la nube), entre otras, son aspectos que impactan en el desarrollo. Debemos construir teniendo en cuenta estas cuestiones, que a veces pueden jugarnos en contra, y otras, a favor nuestro.
  • potenciado por la tecnología: el conocimiento de la tecnología existente nos ayudará a ahorrarnos el esfuerzo de pensar e implementar ideas ya probadas. Sin embargo, las decisiones arquitectónicas no deberán estar guiadas por la tecnología. No se trata de ir al supermercado, dirigirnos a la góndola de tecnologías, comprar una marca particular de una base de datos, un ESB y soporte comercial. Se trata de entender la problemática, pensar soluciones, y utilizar algún producto si realmente calza con lo que necesitamos. Cuando alguien se presente como Arquitecto Java/.Net/Node/LoQueSea, salí corriendo.
  • enriquecido por la historia: de forma similar al punto anterior, hay valor en conocer las soluciones que otros sistemas aplicaron, pero eso no significa que debamos hacer algo sólo porque otro lo hizo. Debemos siempre entender y estudiar las particularidades de nuestro problema, y no ser naïve pensando que simplemente podemos copiar el éxito de otro ignorando el proceso de meses o años que lo llevó a donde está. Si Facebook hizo X, capaz tu solución no necesite X… ¡porque no sos Facebook!

Cualidades Arquitectónicas

¿Y cuándo tenemos una buena arquitectura de software? ¡Ésta es fácil! Cuando nos da un marco en el que podemos desarrollar buen software: libre de duplicaciones y que minimice la redundancia, robusto, resiliente, consistente, con buenas abstracciones, fácil de probar, fácil de extender y mantener. Y por sobre todas las cosas: simple.

La madre de todas las cualidades

En este último punto vale la pena detenerse: la simplicidad es la madre de todas las cualidades. El sistema debe tener la complejidad mínima necesaria para solucionar su problemática.

Make things as simple as possible, but not simpler” (*)

Cuando la simpleza guía nuestro desarrollo, sin perder sus abstracciones esenciales y evitando la duplicación lógica, las demás cualidades son fáciles de lograr. Nunca lo olvides: keep it simple (stupid)!

Cuidado con la escala

En particular, aunque la escalabilidad es una propiedad importante en gran variedad de sistemas, no es gratis: en general plantea desafíos que normalmente se resolverán agregando complejidad accidental al sistema.

“Do things that don’t scale(*)
Paul Graham

Por eso, la iteración primera de cualquier arquitectura debería ser aquella más simple que soporte la carga que a ciencia cierta deberemos soportar. ¿Y si no sabemos cual será la carga, quizás porque es una aplicación que estamos lanzando por primera vez al mercado?

Simple: que sea una aplicación monolítica implementada con las tecnologías que más rápido te permitan satisfacer tus requerimientos funcionales. Rails, Laravel, Django, da lo mismo. ¿No soporta miles de transacciones por minuto, no es tolerante a fallos, no puede crecer automáticamente, no puede ser distribuida geográficamente? No te preocupes, porque probablemente no vas a necesitarlo.

YAGNI: You aren't gonna need it

Recién cuando estas necesidades surjan, allí podremos construir en base a requerimientos concretos, medibles. Quizás eso signifique distribuir componentes, introducir redundancias, reescribir parte del código, cambiar la forma en que se despliegan las aplicaciones. No se muere ningún gatito por esto.

Lo mismo vale para otras cualidades duras como la tolerancia a fallos, la carga, la seguridad, etc. Son todas cuestiones que deberemos atacar ante demanda.

Descubriendo la arquitectura

Según este enfoque, las buenas arquitecturas no se anticipan, no se planifican. Mas bien, emergen: son la consecuencia de decisiones justificadas en los momentos indicados. Y eso nos lleva a una última idea: las buenas arquitecturas son mínimas.

“Good design is as little design as possible.”
Dieter Rams

Esto significa que no hay elementos innecesarios. Y que apenas percibimos las restricciones que la arquitectura nos propone: programar dentro de la arquitectura se vuelve natural y fácil.

Tener más microservicios no te hace per se mejor persona ni arquitecto.

Moraleja: desconfiá de todo aquel arquitecto que, tras brindarle una somera descripción del problema, te proponga una compleja aplicación distribuida en 12 Capas, 3 lenguajes (Go, Scala, JS, porque están de moda) , un Redis, un Oracle, un Memcached, 4 microservicios, 3 tareas batch, 3 niveles de replicación, un despliegue con Puppet, 10 servidores, un BPM y una lata de duraznos (para asegurar la buena digestión). O cualquier combinación que seguro ya te contaron.

Recapitulando

En los párrafos anteriores presentamos algunos lineamientos sobre lo que desde Arquitecturas Concurrentes consideramos como una buena arquitectura, y un buen proceso de desarrollo de la misma.

No pretendemos con esto crear un manifiesto, ni una metodología, ni cerrar la discusión. Siempre podemos encontrar casos particulares en los que por ejemplo la escala sea un requerimiento real desde la primera iteración. Pero en nuestra experiencia, esto ocurre menos a menudo de lo que creemos.

¿Y por qué hacemos tanto hincapié en estas cuestiones? Porque lo que veremos en los próximos posts son justamente técnicas y tecnologías que permiten mejorar la carga, escala y tolerancia a fallos, a costa de un poco de complejidad extra, para cuando ya pasamos la iteración cero de nuestra arquitectura. ¡Acompañános!

Lectura recomendada