Decisiones de ciclo de vida

El proceso del servicio arranca y termina dentro de un orquestador — Kubernetes en el escenario primario, pero las decisiones aplican igual a Docker Compose o standalone JVM. El arquetipo deja configuradas dos: graceful shutdown con un timeout calibrado contra Shedlock, y JMX deshabilitado por defecto. Los valores concretos viven en Ciclo de vida del proceso.

Por que graceful shutdown es critico en K8s

Cuando K8s decide terminar un pod, la secuencia es:

  1. Marca el pod como Terminating.

  2. Quita el pod del Endpoints del Serviceen paralelo envia SIGTERM al contenedor.

  3. Espera terminationGracePeriodSeconds (default 30s).

  4. Si el proceso aun corre, manda SIGKILL.

El paso 2 es el problema: el SIGTERM puede llegar antes de que el cambio en Endpoints se propague a todos los kube-proxy del cluster. Durante esa ventana — tipicamente 1-5 segundos — el pod sigue recibiendo trafico nuevo aunque ya empezo a terminar. Sin graceful shutdown, el contenedor cierra el listener al instante y esas requests fallan con connection refused o con respuestas truncadas.

server.shutdown: graceful cambia ese comportamiento: el contenedor servlet sigue aceptando conexiones existentes pero deja de aceptar nuevas, y espera a que las en vuelo terminen antes de cerrar. Combinado con ReadinessState cambiando a REFUSING_TRAFFIC durante el shutdown, el pod se vuelve invisible para el kube-proxy mientras drena las requests vivas — sin truncar respuestas ni rechazar conexiones aceptadas.

La pieza que el arquetipo no controla: el preStop hook del descriptor de pod. La practica recomendada es agregar un preStop con sleep 5-10s que demora el SIGTERM lo suficiente para que la quita del Endpoints se propague, antes de que Spring empiece a drenar. Esa configuracion vive en el chart de Helm / manifest del despliegue, no en el arquetipo.

Por que el timeout es 30s y no menor

spring.lifecycle.timeout-per-shutdown-phase: 30s define cuanto puede durar cada fase del cierre antes de forzarse. El valor no es arbitrario — esta calibrado contra el lock distribuido de Shedlock.

app.events.outbox-processor.lock-at-most-for: PT20S y sus equivalentes en las otras bandejas le dicen a Shedlock que un lock se libera automaticamente despues de 20 segundos, asumiendo que el proceso que lo tomo murio. Si el shutdown es mas rapido que eso:

  1. El proceso esta procesando un batch de outbox cuando recibe SIGTERM.

  2. Spring fuerza el cierre antes de que el batch termine (timeout < 20s).

  3. El proceso muere sin liberar el lock — pero el lock todavia esta vivo en la base.

  4. La proxima replica que arranca encuentra el lock activo y espera hasta 20s antes de poder ejecutar — ventana en la que la bandeja no se procesa.

Con 30s > 20s, el batch tiene tiempo de terminar y liberar el lock limpiamente antes del cierre forzado.

Si subes lock-at-most-for de Shedlock por una razon valida (procesos batch largos, por ejemplo), sube tambien timeout-per-shutdown-phase para mantener timeout > lock-at-most-for. La invariante es la relacion, no el valor absoluto.

Por que JMX esta deshabilitado por defecto

Spring Boot inicia un MBeanServer y registra MBeans para el contexto cuando spring.jmx.enabled: true (default historico de Spring). En un servicio que corre en contenedor, JMX agrega:

  • Overhead de arranque y memoria: MBeanServer es eager, registra hooks de management para muchos beans, y deja un set de MBeans residentes durante la vida del proceso.

  • Superficie de exposicion: si JMX queda accidentalmente bindeado a una interfaz alcanzable, expone una API completa de control sobre el JVM — invocacion de metodos, lectura de propiedades, deploy de codigo en algunos casos.

  • Valor cero en runtime: las herramientas que tradicionalmente justificaron JMX (JConsole, VisualVM remoto) son raras en operacion de servicios containerizados. Los datos que aportaban (memoria, threads, GC) ya estan disponibles via /actuator/metrics y exportados a Prometheus.

spring.jmx.enabled: false apaga el MBeanServer y los MBean exporters de Spring. El arranque es marginalmente mas rapido, el footprint de memoria un poco menor, y la superficie de ataque se reduce.

Si necesitas JMX

Hay casos validos para re-habilitarlo puntualmente — debugging local con JConsole / VisualVM, o un Java Flight Recorder remoto sobre un pod especifico para investigar un problema de performance.

Activacion temporal via variable de entorno (no commitear este cambio):

SPRING_JMX_ENABLED=true
JAVA_TOOL_OPTIONS="-Dcom.sun.management.jmxremote \
  -Dcom.sun.management.jmxremote.port=9010 \
  -Dcom.sun.management.jmxremote.rmi.port=9010 \
  -Dcom.sun.management.jmxremote.local.only=false \
  -Dcom.sun.management.jmxremote.authenticate=false \
  -Dcom.sun.management.jmxremote.ssl=false"

La configuracion de arriba deshabilita autenticacion y SSL en JMX. Es aceptable solo cuando la conexion JMX es local al pod — por ejemplo via kubectl port-forward. Bindear estas opciones a una interfaz alcanzable desde fuera expone control completo del JVM sin auth. Si necesitas JMX accesible remotamente de forma sostenida, configurar credenciales y SSL es obligatorio — y a esa altura conviene revisar si la senal que se busca no esta ya en metricas Micrometer.

Referencias