BookinglyTech News
Infraestructura

Un JVM que moría a los 6 segundos: la causa estaba en max_connections

Un secundario en un clúster de alta disponibilidad caía a los 6-7 segundos de arrancar, sin traza ni excepción. El culpable no era el código de failover, sino el pool de conexiones contra un PostgreSQL consolidado.

3 min de lecturaDev.to0 vistas

Un servidor secundario en un clúster de alta disponibilidad se moría entre 6 y 7 segundos después de cada intento de arranque, siempre, con un único mensaje en el log que no decía nada: System going to Shutdown --- received process interrupt. Sin traza de pila. Sin excepción. Sin pista. La causa acabó siendo un solo parámetro de PostgreSQL, y para llegar hasta él hubo que descompilar el bytecode del propio fabricante.

El escenario era una prueba de concepto del failover de un producto comercial de monitorización de red, montada sobre tres máquinas virtuales en vez de las cuatro que recomienda el fabricante: primario, secundario (el que no levantaba) y un tercero que hacía a la vez de base de datos PostgreSQL y de sistema de ficheros compartido. La documentación no prohibía explícitamente consolidar esos dos roles, así que se hizo para reducir huella. El primario funcionaba sin problemas.

Cuatro rondas antes de mirar el código

La primera ronda fue de sospechosos obvios. Aparecieron cuatro cosas realmente rotas: un directorio de configuración (pgsql/ext_conf/) que no se replicaba entre servidores y provocaba un error de FileOutputStream en una utilidad de arranque; un superusuario de base de datos sin contraseña mientras el fichero de credenciales cifrado esperaba una; una fila ausente en la tabla de estado del nodo, que nunca se había registrado; y un script de arranque mal ordenado, con la verificación de prerequisitos ejecutándose en lugar del lanzador principal. Se corrigieron los cuatro. El secundario seguía cayéndose en el mismo segundo.

La segunda ronda fue buscar precedentes: foros del fabricante, documentación de HA, documentación de un producto hermano sobre el mismo framework. Solo se confirmó una cosa útil, y no era un error del producto: ese mensaje es genérico del Java Service Wrapper, el supervisor que envuelve la JVM, y salta con cualquier salida, sea un System.exit() limpio o una excepción no capturada. Un encogimiento de hombros en forma de log.

La tercera ronda amplió la inspección de la base de datos a tablas que el arranque lee pero que no salían en ningún error visible: la de registro de nodos, vacía para el secundario, y la de la ruta de la carpeta compartida, con un valor obsoleto de una topología anterior. Corregidas las dos, mismo mensaje, mismo instante.

El código del fabricante

Cuando los logs no dicen nada y la documentación tampoco, queda el código compilado. Con CFR, un descompilador de Java open source, se destriparon los JAR del producto usando la JRE que traía el propio paquete:

java -jar cfr.jar SomeVendorClasses.jar --outputdir /tmp/decompiled

Siguiendo la cadena real de arranque por cinco clases repartidas en tres JAR apareció lo importante: ningún camino de código específico de failover llegaba a ejecutarse en el secundario. No es que fallara, es que nunca se alcanzaba. El JVM moría antes. Eso desplazó la investigación a algo mucho más básico, la creación del pool de conexiones.

La respuesta estaba en el stderr crudo, no en los logs de aplicación que se habían estado revisando: Could not instantiate RelationalAPI in NmsUtil. Server quitting, con NmsStorageException: CreateConnectionException. Un grep sobre el código descompilado llevaba al bloque catch que envuelve la creación del pool: si falla, escribe ese mensaje y llama a System.exit(1).

¿Por qué iba a fallar? max_connections estaba en 100 y pg_stat_activity mostraba 57 conexiones, todas del primario. El pool del secundario pedía 50 al arrancar, igual que el primario. La suma no cabe.

Lo que deja esta historia es un aviso para quien consolida roles en menos máquinas de las que pide el fabricante: el dimensionado de max_connections y de los pools hay que hacerlo por nodo, no por instancia. Y los mensajes genéricos del supervisor de la JVM pueden tapar durante días un error que llevaba todo el tiempo en la salida de error estándar.