Saltar al contenido
megapromotingVamos a hablar

Experiencia · Arquitecturas complejas

Varios sistemas que deben funcionar juntos, incluso cuando uno de ellos falla.

Diseñamos y operamos plataformas de varios servicios, con bus de mensajes, reintentos limitados, claves de idempotencia, interruptores de circuito y publicación con reversión automática. Cada patrón de abajo corre en un sistema propio, no en un diagrama.

Ya construidoOperăm patru platforme proprii cu arhitecturi diferite și le putem deschide pe toate. Cifrele sunt măsurate azi cu comenzi, nu preluate din documentație: aichat are 28 de directoare de serviciu, 235 de modele de date pe patru scheme și o magistrală de mesaje cu 297 de cozi și 17.525 de legături; Kallina are 472 de funcții edge, 559 de migrări și 28 de sarcini programate în bază; MEGA CRM are un backend de 9.516 linii cu 99 de rute și 179 de politici de acces pe rând; Taskin rulează 21 de sarcini programate fără niciun Docker. Rezerva pe care o spunem noi: în două locuri documentația proprie a rămas în urma codului — o afirmație despre numărul de linii era depășită cu 16%, iar o diagramă de infrastructură descria un server pe care nu mai rulăm nimic. Am folosit măsurătoarea, nu documentul.

«Arquitectura compleja» no significa muchas cajas en un dibujo. Significa que sabes qué ocurre cuando una de ellas no responde. Un sistema con tres servicios que se llaman de forma síncrona, sin límite de reintentos y sin idempotencia, es más frágil que un monolito — porque cada nuevo enlace es una nueva forma de fallo, y las formas de fallo se multiplican más rápido que las funcionalidades.

Nuestro trabajo parte de la separación de responsabilidades y termina en lo que se ve cuando algo cede. En una de las plataformas propias, los mensajes no pasan directamente entre servicios, sino por un bus con 297 colas, 9 cambios y 17.525 enlaces, de los cuales 278 colas se generan por cliente, no se escriben a mano. Ocho colas tienen intercambio de cartas muertas y ocho tienen tiempo de vida por mensaje de cinco minutos — es decir, un mensaje que no puede ser procesado llega a un lugar donde puede ser visto, no desaparece.

Los patrones que ponemos son pocos y se repiten: reintentos con límite escrito en el código, claves de idempotencia que hacen inocua la segunda entrega del mismo evento, un disyuntor que detiene las llamadas a un servicio que falla repetidamente, una puerta de consumo antes de las acciones que cuestan dinero, y alertas con umbral de repetición para que una avería intermitente no entierre el resto.

La última parte, la que no se ve: la publicación. Una de las plataformas se publica por sincronización de archivos, sin Docker, con la verificación de que la página servida contiene exactamente el identificador del paquete recién publicado — no solo que el servidor responde 200 — y con reversión automática a la versión anterior si la verificación falla. La regla apareció después de un incidente real en el que una verificación superficial anuló una publicación buena.

Qué incluye

El trabajo, por componentes

Separamos los servicios por lo que se puede romper de forma independiente

En una plataforma propia hay 28 directorios de servicio, de los cuales 16 tienen su propio punto de entrada, y en producción corren cinco procesos bajo un gestor de procesos. Cada canal de comunicación tiene su propio servicio y su propio puerto, justo para que un fallo en uno no detenga el resto. Los datos están modelados en cuatro esquemas separados, con 235 modelos en total — la separación no es solo a nivel de proceso, también lo es a nivel de esquema.

Ponemos un bus de mensajes, no llamadas sincrónicas en cadena

297 colas, 9 cambios, 17.525 enlaces. El nombre de las colas es por cliente y, en algunos casos, por hilo de conversación — es decir, la topología se genera, no se escribe a mano. También existe un cambio con entrega diferida, para las cosas que deben ocurrir más tarde, no ahora. Ocho colas tienen configurado el intercambio de cartas muertas y ocho tienen tiempo de vida por mensaje de cinco minutos.

Reintentos con límite, no hasta el infinito

Tres límites diferentes para tres situaciones diferentes, todos escritos en código: cinco reintentos para los mensajes programados, con el contador persistido, no guardado en memoria; tres reintentos a nivel de cola, contados en un encabezado del mensaje; y tres reintentos con aumento exponencial de la pausa para el envío de correo, por dos vías de proveedor. Un reintento sin límite no es resiliencia, es un bucle.

Claves de idempotencia, para que una segunda entrega no arruine nada

Para los eventos de terceros usamos una inserción que falla si el evento ya fue visto: la violación de unicidad de la base de datos se trata como «duplicado», no como error, junto con una verificación de frescura del momento. Para pagos, la clave es un marcador en la transacción de crédito, así que el reenvío del mismo webhook no acredita dos veces. Para mensajes, la deduplicación se hace por identificador, con tiempo de vida y limpieza.

Un interruptor de circuito en la integración frágil

La integración que depende de la sesión de un sitio de terceros tiene un interruptor real, no solo una nota en la documentación: después de diez fallos consecutivos, las llamadas se detienen dos minutos. Sin él, un sitio que no responde convierte un canal caído en una plataforma lenta, porque todo el mundo espera en la petición.

Gate de consumo antes de las acciones que cuestan

En una de las plataformas, cualquier acción pagada pasa por una función de verificación de cuatro niveles, cada uno con su propio motivo devuelto en claro: cuenta suspendida, límite mensual de conversaciones alcanzado, tope diario de gasto superado, saldo insuficiente. En la excepción devuelve «no permitido», no «permitido» — la puerta se cierra, no se abre, cuando algo falla. Detrás hay un registro de eventos de consumo con coste por evento, desglosado por proveedor, modelo y modalidad.

Limitación de tasa que sobrevive al reinicio

Ventana deslizante mantenida en la base de datos, con un mapa en memoria solo como atajo dentro de una invocación. El motivo está escrito justo al principio del archivo: los procesos que atienden las solicitudes son de corta duración, así que su memoria no puede ser la fuente de verdad. Quince funciones la usan, y la cuota de un proveedor externo tiene su propio limitador, separado.

Observabilidad que distingue «ha respondido» de «ha funcionado»

Una respuesta 200 no significa que la tarea haya tenido éxito. El envoltorio con el que ejecutamos las tareas programadas recorre recursivamente la respuesta tras listas de fallos o errores y trata «200 con fallos» como un resultado distinto. Las alertas tienen umbral de repetición — diez minutos en un sistema, dos horas en el otro — y la marca se borra al primer éxito, para que una avería recurrente pueda alertar de nuevo de inmediato.

Publicación con verificación y rollback automático

La publicación comprueba no solo que la dirección responde 200, sino que la página servida contiene exactamente el identificador del paquete recién publicado; de lo contrario restaura la versión anterior. Para el servicio en segundo plano, la publicación compara el árbol por sumas de control y omite el reinicio si nada ha cambiado — un reinicio innecesario mata un bucle en curso.

Qué aspecto tiene

El recorrido, paso a paso.

01

Dibujamos el mapa de los modos de fallo, no de las cajas

Para cada vínculo entre dos sistemas: qué pasa si el otro está lento, si está caído, si responde dos veces, si responde mal. Entregamos: la lista de vínculos con el comportamiento esperado en cada una de las cuatro situaciones, más la decisión síncrono/asíncrono para cada uno.

02

Ponemos el bus y los contratos de mensaje

Colas, intercambios, correos muertos, tiempo de vida por mensaje y la clave de idempotencia para cada tipo de evento. Entregamos: la topología, los contratos de mensaje y el comportamiento documentado en la reemisión.

03

Añadimos las puertas: tasa, consumo, interruptor

La limitación de tasa por ventana deslizante en la base, la puerta de consumo antes de las acciones que cuestan y el interruptor en las integraciones fuera de nuestro control. Entregamos: los umbrales configurados, los motivos devueltos en claro y las pruebas para cada nivel de rechazo.

04

Construimos la publicación y el rollback

Publicación con verificación de contenido, no solo de código de respuesta, versión anterior conservada al lado y reversión automática. Entregamos: el procedimiento de publicación, el procedimiento de reversión practicado al menos una vez, y las sondas de salud: una superficial para “está vivo”, una profunda para “realmente funciona”.

05

Entregamos con la documentación verificada frente al código

El documento se verifica contra la medición antes de la entrega, porque la documentación que se queda atrás es más peligrosa que su ausencia. Entregamos: el diagrama, los procedimientos y la lista explícita de los lugares donde el documento y el código se encontraron en desacuerdo, con lo que corregimos.

DocumenteConversațiiSurseSintezăAcțiuneEchipăContexteazăSurse autorizate. Acțiuni revizuite.
Sistemele nu se apelează în lanț: între ele stă o magistrală cu cozi per client, schimburi separate pe tip de trafic și scrisori moarte pentru ce nu se poate procesa. În jurul ei, porțile — limitare de rată, poartă de consum, întrerupător de circuit — și sondele de sănătate, una superficială și una adâncă.

Los datos

Qué tocamos, dónde están y cuánto se quedan

Las preguntas que hace cualquiera que tenga un delegado de protección de datos — hechas aquí antes de que las haga él.

Dónde están los datos, por plataforma
No existe una respuesta única y es bueno que no exista: una plataforma usa MySQL en servidor propio, dos usan PostgreSQL a través de Supabase — una alojada, una instalada en nuestra infraestructura — y una guarda el contenido como archivos estáticos, sin base de datos en absoluto. La elección se hace según los requisitos, no por costumbre.
El acceso se aplica en la base, no solo en la aplicación
En un sistema interno nuestro, la seguridad a nivel de fila está activada en 43 tablas, mediante 90 instrucciones, con 179 políticas escritas. La regla que seguimos: si una llamada evita la aplicación y golpea la base directamente, aun así no debe ver las filas de otra persona.
Registro de consumo
Una fila por evento con tipo, proveedor, cantidades separadas por modalidad — segundos de conversación, tokens de entrada, tokens de salida, caracteres — además de coste en moneda extranjera con seis decimales y los créditos descontados. De él salen el límite diario de gasto, el coste por llamada y la proyección de consumo.
Las migraciones son el historial, no la documentación
559 migraciones en una plataforma, 80 en otra. El esquema cambia mediante migraciones versionadas, así que el estado de la base se puede reconstruir y se puede leer cronológicamente. Cuando el documento y la migración no están de acuerdo, la migración tiene razón.
Las tareas programadas se guardan en la base o en cron, no en el proceso
28 tareas programadas se ejecutan dentro de la base de datos en una plataforma; en otra, 21 tareas se ejecutan mediante cron del sistema. El motivo está escrito en el archivo: los temporizadores dentro de un servicio se reinician en cada reinicio, así que un bucle cada tres horas de un servicio que se republica nunca se dispara.

Un caso

Una topología de mensajes que se genera sola para cada cliente

La situación

Una plataforma con varios clientes, cada uno con sus propios canales de comunicación, sus propios hilos de conversación y sus propias notificaciones. La variante ingenua — una cola común y un filtro por identificador de cliente — hace que un cliente con mucho volumen bloquee al resto, y un error en un hilo pare la cola para todos.

Qué construimos

La topología se genera por cliente, no se escribe a mano: de 297 colas, 278 son de tipo «notificaciones para una cuenta determinada», y algunas bajan hasta el nivel de un solo hilo de conversación. Encima de ellas, nueve intercambios separan los tipos de tráfico — notificaciones, tokens, webhooks, canales — y existe un intercambio con entrega diferida para lo que debe ocurrir más tarde. Ocho colas tienen intercambio de mensajes muertos, ocho tienen tiempo de vida por mensaje de cinco minutos. Las reglas de orquestación se pueden recargar en caliente, mediante un canal de publicación, sin reiniciar a los consumidores.

Qué salió

Un cliente con mucho volumen no retrasa a los demás clientes, y un mensaje que no puede procesarse llega a un lugar donde puede verse y reintentarse, en lugar de desaparecer o bloquear la cola. El número de enlaces en la magistral — 17.525 — muestra exactamente por qué la topología debe generarse: nadie mantiene eso a mano.

Qué no dice el caso

El precio es operativo: una magistral de ese tamaño necesita su propia monitorización y un plan para las colas abandonadas, de lo contrario crece sin fin. Y la generación por cliente implica que borrar un cliente borra también su topología — si ese paso falta, se acumulan colas muertas.

Preguntas

Lo que nos pregunta la gente antes de llamar

¿Cómo sé que no me están vendiendo complejidad que no necesito?

Porque la primera recomendación que solemos dar es no separar. Cada nuevo servicio es un nuevo modo de fallo, y los modos de fallo se multiplican más rápido que las funcionalidades. Un ejemplo de nuestros propios sistemas: el backend de uno de ellos es un solo archivo de 9.516 líneas con 99 rutas. No es elegante y lo decimos, pero se publica en un paso y se depura en un solo lugar. La separación se hace cuando existe un motivo medible — un servicio que debe escalarse por separado, un equipo separado, un ritmo de entrega separado.

¿Qué pasa cuando un sistema de la cadena no responde?

Depende de lo que decidimos juntos en la etapa de mapa, y esa es la idea. En nuestros sistemas: el mensaje entra en cola y se reintenta un número limitado de veces, luego llega al intercambio de cartas muertas, donde puede ser visto; la integración inestable tiene un interruptor que, después de diez fallos consecutivos, se detiene dos minutos en lugar de dejar las solicitudes en espera; y las acciones que cuestan dinero son detenidas por una puerta que, por excepción, deniega, no permite.

¿Cómo evitas que el mismo evento se procese dos veces?

Con una clave de idempotencia, no con una verificación de tipo «¿ya vi esto?» que tiene carrera de datos. Concretamente: insertamos una fila con el identificador del evento y, si la base rechaza por violación de unicidad, tratamos ese código de error como señal de duplicado, no como fallo. Para pagos, la marca está en la transacción de crédito, así que el reenvío del mismo webhook no acredita dos veces.

¿Usáis Docker o no?

Ambos, y justificamos la elección cada vez. Un sistema se ejecuta en un contenedor, pero sin volúmenes montados, lo que significa que la publicación es una copia en el contenedor más reinicio — un `docker rm` ahí pierde el estado, y está escrito en el documento de operación exactamente así. Otra plataforma no tiene ningún Docker: la publicación es sincronización de archivos, y los scripts de publicación se instalan manualmente con derechos de administrador y se exponen al flujo automático por exactamente dos vías permitidas. El motivo está escrito en el código: un script que el flujo puede reescribir es un script por el que el flujo puede escalar.

¿Cómo sabes que una tarea programada realmente se ejecutó?

Por mala experiencia. Un informe diario nuestro estuvo muerto once días, entre el 7 y el 17 de agosto, mientras la tarea programada se activaba cada día — el comando usado salía en silencio ante el error y no escribía nada. Desde entonces cada tarea se ejecuta en una envoltura que recorre la respuesta por listas de fallos, trata «200 con fallos» como resultado distinto, tiene tiempo máximo de ejecución y escribe una línea estructurada por ejecución. Las alertas tienen umbral de repetición, y la marca se borra al primer éxito.

¿Cuánto importa la abstracción por proveedor?

Mucho, si el proveedor está en una zona que se mueve rápido. Para voz tenemos tres puentes separados — uno para cada proveedor de voz en tiempo real — que implementan la misma interfaz. El puente hace la conversión de audio entre la central telefónica y el proveedor, en un puerto dedicado, con el servidor de salud vinculado solo a la interfaz local, no expuesta. Cambiar de proveedor se convierte en una decisión, no en una reescritura.

¿Vuestra documentación está al día?

No en todas partes, y preferimos decir dónde. Al preparar esta página medimos dos sistemas y encontramos el documento propio retrasado: una afirmación sobre el tamaño de un archivo era aproximadamente un 16% menor que la realidad, y un diagrama de infraestructura describía un servidor del que ya nos habíamos movido. La regla que aplicamos y que pedimos en los proyectos: cuando el documento y la medición no coinciden, la medición tiene razón, y el documento se corrige en el mismo paso.

¿Qué no hacéis?

No prometemos objetivos de disponibilidad sin medición — un porcentaje del tipo «99,9%» exige datos de campo durante un periodo, y donde no los tenemos no lo afirmamos. No diseñamos sistemas que no podemos operar o entregar: si el resultado es una arquitectura que tu equipo no puede mantener, es una arquitectura incorrecta. Y no hacemos certificaciones de conformidad — podemos construir los controles, no podemos emitir el certificado.

En qué se basan las afirmaciones anteriores (23 fuentes)

23 de ellas son código y archivos de nuestros repositorios. No publicamos su nombre ni la línea: juntos, en una sola página, describirían con demasiada precisión cómo están construidos sistemas que no son solo nuestros. Los repasamos contigo, en el repositorio, a petición — la verificación sigue siendo posible, solo que se hace en una conversación.

¿Qué te gustaría que funcionara mejor?

Cuéntanos tu proceso. Juntos decidimos qué merece la pena construir, qué podemos conectar y cómo verificamos el resultado.

Vamos a hablar