Alertas que se disparan una vez, no una vez por servidor
Una de nuestras alertas operativas —"créditos bajos" para la app publicada de un creador— está pensada para llegarnos como máximo una vez al día. Una tarde llegó cinco veces. La causa: cada copia de nuestra API recordaba en su propia memoria qué alertas ya había enviado, así que dos servidores la enviaban dos veces, y cada despliegue borraba esa memoria y permitía volver a enviarla. El mismo patrón hacía que un trabajo en segundo plano hiciera su tarea por duplicado. Ahora ambos se coordinan a través de un almacén compartido, y los dos fallan del lado seguro si ese almacén no está disponible.
El síntoma
Mythex envía alertas internas al equipo cuando algo necesita atención: un servicio caído, un trabajo atascado, o el saldo de créditos de un creador bajando mientras su app está publicada. Cada alerta tiene una ventana de deduplicación: un periodo durante el que la misma alerta solo se envía una vez. Para la alerta de créditos bajos, esa ventana es de 24 horas.
Cinco copias de la misma alerta en una tarde significaban que la ventana no estaba funcionando. Peor aún, llegaban en parejas, a unos 11 minutos de distancia.
Por qué pasaba
El paquete de alertas guardaba la deduplicación en memoria: una tabla de "clave de alerta → cuándo se envió por última vez", dentro del proceso en ejecución. Eso funciona cuando hay un solo proceso que nunca se reinicia. Producción no cumple ninguna de las dos cosas:
- Dos réplicas de la API. Producción ejecuta dos copias de la API. Cada una tenía su propia tabla, así que cada una enviaba la alerta una vez. Esas son las parejas.
- Despliegues. Cada despliegue sustituye las dos réplicas por procesos nuevos con tablas vacías. Tras un despliegue, la alerta salía de nuevo, como si nunca se hubiera enviado.
Los unos 11 minutos entre las dos alertas de cada pareja venían del trabajo que genera esta alerta. Se ejecuta en ambas réplicas y toma un bloqueo, así que las réplicas lo ganan por turnos, y cada una generaba la alerta cuando le tocaba.
Había un segundo error, más silencioso, en el mismo código. Para evitar que la tabla creciera sin límite, se purgaba a sí misma al llegar a 500 claves, borrando cada entrada de más de 15 minutos, sin importar su ventana. Con carga, una deduplicación de 24 horas podía durar un cuarto de hora.
La solución
Un registro compartido por alerta
Antes de que la API envíe una alerta, ahora reclama la clave de deduplicación de esa alerta en Redis, un almacén en memoria compartido al que hablan ambas réplicas y que no se reinicia cuando lo hace la API. La reclamación es una única operación de "guarda esta clave solo si no existe, y caduca tras la ventana". La réplica que la reclama primero envía la alerta; la otra encuentra la clave ocupada y se calla. Un proceso nuevo tras un despliegue encuentra la clave todavía ahí.
La clave y la ventana vienen de la misma regla que usa el paquete de alertas, ahora compartida entre ambas capas, de modo que no puedan discrepar sobre qué cuenta como "la misma alerta".
Fallar abierto, no cerrado
La decisión más importante fue qué hacer cuando no se puede contactar con Redis. Elegimos enviar la alerta de todos modos. Si Redis está caído, o no responde en 1,5 segundos, la alerta sale. Una alerta duplicada es ruido. Una alerta descartada es alguien que no se entera.
Las alertas más graves, P0, nunca se retienen por la deduplicación.
Cada entrada conserva su propia ventana
En la deduplicación en memoria del paquete de alertas, cada entrada ahora guarda cuándo termina su ventana, y la purga elimina solo las entradas cuya ventana ya ha terminado. Una clave de 24 horas se mantiene 24 horas, por muchas otras alertas que lleguen.
El mismo error, en otro sitio
El mismo patrón había duplicado también una pieza de trabajo en segundo plano: un trabajo de reconciliación que mantiene nuestro registro del estado de cada espacio de trabajo alineado con el proveedor de entornos de ejecución. Cada pasada lista todos los entornos de la cuenta —miles en producción, unos cinco segundos— más cada proyecto en ejecución o pausado. Ambas réplicas la ejecutaban cada dos minutos.
Sus escrituras son idempotentes —ejecutarlas dos veces da el mismo resultado que una sola vez—, así que nada se rompió. Simplemente era el doble de trabajo. Ahora cada ventana de dos minutos se reclama en Redis de la misma forma en que nuestros medidores de uso ya reclamaban las suyas, y solo ejecuta la pasada la réplica que la reclama. Si no se puede contactar con Redis, la pasada se ejecuta igualmente: hacer el trabajo dos veces es inofensivo, no hacerlo sí es un problema.
Cómo lo probamos
Las pruebas reproducen la configuración real. Cargan el módulo de alertas dos veces, como dos "réplicas" separadas, apuntan ambas a un único almacén compartido, lanzan la misma alerta desde cada una y luego cargan una tercera copia nueva para simular "después de un despliegue" y la lanzan de nuevo. Se envía exactamente una alerta. Otras pruebas comprueban que la alerta de otro creador sigue saliendo, que una P0 se envía desde ambas réplicas, y que la alerta se sigue enviando cuando el almacén está caído o se queda colgado más allá del tiempo límite. La corrección de la purga tiene su propia prueba: una clave de 24 horas sobrevive a 600 alertas más que llegan 20 minutos después.
Para el trabajo de reconciliación, dos réplicas ejecutando a la vez, más una tercera ejecución dentro de la misma ventana, producen un único listado.
Lo que aprendimos
- El estado en memoria es por proceso. Todo lo que deba ser cierto entre servidores —"enviado una vez", "ejecutado una vez"— tiene que vivir en un lugar que todos compartan.
- Los despliegues son reinicios. Un estado que solo sobrevive hasta el siguiente reinicio no sobrevive a un día laboral normal.
- Decide cómo fallar. En alertas, que un almacén compartido esté caído debe significar más alertas, no menos.
- Busca el patrón, no solo la instancia. La alerta fue el caso escandaloso; el trabajo de reconciliación duplicado era el mismo error, costando trabajo en silencio.
Nada de esto es visible desde dentro de Mythex. Para saber más sobre cómo mantenemos la plataforma estable, consulta cómo bajamos los despliegues a cero errores 502 y el archivo de swap que faltaba. Si te resulta nueva la idea de ejecutar más de una copia de un servicio, nuestra guía sobre qué es un servidor explica lo básico.