Steven
Steven6 min de lectura

El día que nuestra app se hizo DDoS a sí misma

Un backlog de subidas pendientes, liberado todo de golpe al arrancar, convirtió cada cliente de GeekBye en un pequeño ataque de denegación de servicio contra nuestros propios servidores. El arreglo — y la escalera de comprobación de vida de la conexión que nos obligó a construir — es una de las cosas más útiles que nos enseñó la v2.

Fiabilidad
Redes
Ingeniería
Lanzamientos de GeekBye
El día que nuestra app se hizo DDoS a sí misma

Todo ingeniero de sistemas distribuidos acaba encontrándose con la estampida (la thundering herd): una masa de clientes haciendo todos lo mismo en el mismo instante, y el recurso compartido detrás de ellos cediendo. Normalmente son los clientes de otro. En GeekBye v2.0.1 eran los nuestros, y el atacante era nuestra propia app.

Cómo un notetaker se ataca a sí mismo

GeekBye puede subir grabaciones a tu Google Drive. Si una grabación no puede subirse de inmediato — estabas sin conexión, la app se cerró, Drive tuvo un tropiezo — pasa a un backlog para reintentarlo más tarde. Sensato.

El fallo estaba en cuándo ocurría ese "más tarde". En el siguiente arranque, la app intentaba vaciar todo el backlog de golpe. Un usuario con una semana de grabaciones pendientes se convertía en una ráfaga de peticiones de subida simultáneas en el instante en que abría la app. Multiplícalo por cada usuario arrancando por la mañana, y a nuestro backend se le pedía autenticar, comprobar límites y procesar un muro de tráfico en los mismos pocos segundos — desde clientes que, técnicamente, se estaban portando bien.

Nuestro backend hizo lo correcto y limitó la avalancha. Y ahí empezó el daño de segundo orden. Una respuesta de límite de tasa es un HTTP 429, y un 429 se parece mucho a otros fallos si no tienes cuidado:

  • La carga del perfil al arrancar recibía un 429 y la app lo trataba como fallo de autenticación — desconectando brevemente a los usuarios de una sesión perfectamente válida.
  • La conexión de transcripción recibía un 429 genérico y mostraba un aviso de mejora de plan por límite de audio alcanzado — diciéndoles a usuarios de pago que habían llegado a una cuota que no habían tocado.

Así que una sola causa raíz — un backlog sin dosificar — producía tres síntomas visibles: presión en el servidor, deslogueos falsos y ventas cruzadas falsas. La forma clásica de un auto-DoS: la carga es mala, pero lo que los usuarios sienten de verdad es la mala interpretación de los síntomas de esa carga.

El arreglo, en dos capas

Detener la estampida. El backlog de subidas ahora se dosifica — las peticiones se reparten con backoff, y hay un enfriamiento tras cualquier 429 para que el cliente se aleje de un servidor sobrecargado en vez de apretar más fuerte. Una estampida se convierte en una cola ordenada.

Detener la mala interpretación. Un 429 o 5xx transitorio durante el arranque ya no te desconecta — el cliente distingue "el servidor está ocupado un momento" de "tu sesión no es válida". Y un 429 genérico de límite de tasa en la ruta de transcripción ya no se hace pasar por un error de cuota de audio; solo una respuesta genuina de cuota muestra el aviso de mejora. La lección que quedó: un código de error no es un significado de error. 429 significa "ve más despacio", no "no estás autorizado" ni "te quedaste sin cuota" — y el cliente tiene que conocer la diferencia.

La escalera de vida que vino después

Dosificar la estampida dejó al descubierto un problema más silencioso: cuando una conexión fallaba bajo carga, ¿con qué rapidez nos dábamos cuenta? Más lento de lo que queríamos. Así que la v2.0.4 construyó una escalera de comprobación de vida de la conexión para la transcripción en tiempo real:

  • Heartbeats — se hace ping al socket de transcripción a intervalos fijos, para que una conexión muerta en silencio se detecte en segundos en vez de esperar a que falle el siguiente fragmento de audio.
  • Un reenganche de reconexión al volver la red — cuando el sistema operativo informa de que la red ha vuelto, la app restablece la conexión de forma proactiva en vez de esperar a tropezarse con el descubrimiento.
  • Vida por ping/pong de control — un ida y vuelta a nivel de aplicación que confirma no solo que "el socket está abierto" sino que "el otro extremo realmente responde".
  • Una compuerta de reconexión gestionada por el servicio — un único lugar decide si reconectar, en vez de varias partes de la app compitiendo por hacerlo y pisándose entre sí.

Nada de esto es glamuroso. Todo esto es la razón por la que una sesión de la v2 se siente como si simplemente... siguiera conectada.

Volvió a pasar esta semana — un nivel más abajo

Aquí está la parte que hace que esto sea más que una batallita. Esta misma clase de fallo resurgió hace días, un nivel por debajo de nosotros. Un solo cliente disparó 21 inicios de sesión de transcripción en menos de 400 milisegundos — y disparó el límite de toda la cuenta de nuestro proveedor de voz de 15 peticiones concurrentes. Una estampida contra una infraestructura compartida, exactamente como el backlog de subidas, solo que apuntada a una dependencia en vez de a nuestro propio backend.

La forma es idéntica, y también lo es el arreglo: el cliente que estampida necesita una protección de single-flight para que no pueda disparar veinte inicios por una sola intención, y el recurso compartido necesita un tope por usuario para que un cliente no pueda consumir todo el pool. Ya hemos construido la versión del lado del cliente de esto antes — el dosificador de subidas es este patrón. Ahora lo aplicamos a los inicios de sesión. La estampida no es un bug que arreglas una vez; es una forma que aprendes a reconocer en todas partes donde los clientes se topan con un límite compartido.

Tres cosas para llevarte

  1. Tus propios clientes son un test de carga que no programaste. Cualquier cosa que agrupe-al-arrancar, reintente-al-lanzar o reconecte-al-despertar es una estampida esperando a tener usuarios suficientes. Dosifícala antes de tenerlos.
  2. Distingue el código del significado. 429 es el estado peor interpretado que existe. "Ve más despacio" no es "desconéctate" ni "págame". Enruta cada fallo hacia lo que realmente significa.
  3. La vida es una escalera, no un flag. "¿Está viva la conexión?" tiene varias respuestas honestas en capas distintas — socket abierto, bytes fluyendo, el otro extremo respondiendo, red presente. Una app robusta comprueba más de una.

Esta es la maquinaria poco glamurosa bajo la calma de la v2 de GeekBye. Para las funciones de fiabilidad que habilitó, mira por qué tu notetaker con IA se detiene con mal Wi-Fi y transcripción en directo cuando el firewall bloquea los WebSockets (v2.0.8). Para los lanzamientos vecinos de esta serie, por qué tu notetaker con IA deja de grabar en mitad de la reunión (v2.0.9).