Java Cloud

Java cloud-native: qué cambia realmente al llevar Java a la nube

Contenedores, Kubernetes y despliegue continuo son la parte visible. Lo que decide si una plataforma Java funciona bien en la nube son la configuración de la JVM, el arranque, los health checks y la gestión de estado.

El concepto

Cloud-native no es empaquetar la aplicación en un contenedor

Una aplicación cloud-native asume que su entorno es efímero: puede arrancarse, detenerse, replicarse y moverse de máquina en cualquier momento, sin intervención humana.

Si una aplicación guarda estado de sesión en memoria, escribe ficheros en disco local o tarda tres minutos en arrancar, seguirá comportándose como una aplicación tradicional dentro de un contenedor, con lo peor de los dos modelos.

Las propiedades que sí determinan el comportamiento en la nube son cuatro: no guardar estado local, configurarse por entorno sin recompilar, arrancar rápido y exponer su estado de salud de forma que la plataforma pueda actuar sin que nadie mire un log.

Runtime

La JVM en contenedores

El heap se dimensiona como porcentaje de la memoria disponible, no con un valor absoluto fijado a mano.

Las versiones modernas de Java reconocen los cgroups y ajustan la memoria y el número de procesadores automáticamente. El problema aparece con configuraciones heredadas que fijan -Xmx con un valor absoluto: cuando cambia el límite del pod, la JVM sigue usando el valor antiguo y el contenedor muere por OOM sin dejar rastro útil.

MaxRAMPercentage

Usar -XX:MaxRAMPercentage en lugar de -Xmx para que el heap siga al límite real del contenedor.

Margen fuera del heap

Metaspace, hilos y buffers directos no cuentan como heap pero sí consumen la memoria del pod.

Requests igual a limits

Igualar ambos valores de memoria evita desalojos impredecibles bajo presión del nodo.

Recolector acorde al perfil

G1 por defecto; ZGC cuando la latencia máxima importa más que el rendimiento agregado.

Orquestación

Lo que Kubernetes espera de un servicio Java

Espera que la aplicación comunique su estado con precisión y que tolere ser detenida en cualquier momento. Sin eso, la plataforma toma decisiones equivocadas: enruta tráfico a instancias que aún no están listas o reinicia instancias que solo estaban ocupadas.

La distinción entre las dos sondas es donde se concentran los errores. Liveness responde a «¿hay que reiniciar este proceso?» y readiness a «¿puede recibir tráfico ahora?». Apuntar liveness a un endpoint que comprueba la base de datos provoca que una incidencia en la base de datos reinicie en bucle todas las instancias, convirtiendo un problema de dependencia en una caída total.

El apagado ordenado es la otra mitad. Al recibir SIGTERM el servicio debe dejar de aceptar peticiones nuevas, terminar las que tiene en curso y cerrar conexiones y consumidores antes de salir. Sin apagado ordenado, cada despliegue produce errores visibles para el usuario.

Runtimes

Cuándo compensa la compilación nativa

Quarkus y GraalVM compensan cuando el tiempo de arranque y el consumo de memoria son restricciones reales: funciones serverless, escalado a cero, o un número muy alto de instancias donde cada cientos de megabytes se multiplican por el coste de infraestructura.

La compilación nativa reduce el arranque de segundos a milisegundos y la memoria residente de forma sustancial. A cambio impone restricciones sobre reflexión y carga dinámica, alarga los tiempos de compilación de forma notable y complica el diagnóstico con las herramientas habituales de la JVM.

Para un servicio de larga duración que arranca una vez y sirve tráfico durante semanas, la ventaja del arranque nativo es irrelevante y la JVM con JIT ofrece mejor rendimiento sostenido. La elección depende del perfil de ejecución, no de qué opción es más moderna.

Estrategia

Cloud-native y elección de proveedor son decisiones distintas

Cloud-native describe cómo está construida la aplicación; migrar a un proveedor describe dónde se ejecuta. Se puede desplegar en AWS una aplicación que no es cloud-native, y ejecutar una cloud-native en un Kubernetes propio.

La diferencia importa al decidir cuánto acoplarse a servicios gestionados de un proveedor concreto. Usar Kubernetes y contenedores mantiene la portabilidad; usar servicios propietarios reduce el trabajo operativo a cambio de un coste de salida mayor. Ninguna de las dos opciones es incorrecta, pero conviene tomarla de forma consciente.

Los servicios concretos de AWS y cuándo compensa acoplarse a ellos los trato en Java sobre AWS.

Preguntas frecuentes

Dudas habituales sobre java cloud en proyectos empresariales.

¿Qué significa que una aplicación Java sea cloud-native?

Significa que asume que su entorno es efímero: puede arrancarse, detenerse, replicarse y moverse de máquina en cualquier momento sin intervención humana ni pérdida de información. Empaquetar una aplicación existente en un contenedor no la hace cloud-native; las propiedades que importan son no guardar estado local, configurarse por entorno sin recompilar, arrancar rápido y exponer su estado de salud de forma que la plataforma pueda actuar automáticamente.

¿Cómo se configura la JVM en contenedores?

Se configura dejando que la JVM detecte los límites del contenedor y dimensionando el heap como porcentaje de la memoria disponible con -XX:MaxRAMPercentage, en lugar de fijar -Xmx con un valor absoluto. También conviene reservar margen fuera del heap para metaspace, hilos y buffers directos, igualar requests y limits de memoria, y elegir el recolector de basura según si prioriza latencia o rendimiento agregado.

¿Qué exige Kubernetes de una aplicación Java?

Exige que comunique su estado con precisión y que tolere ser detenida en cualquier momento. Las sondas liveness y readiness responden a preguntas distintas: si hay que reiniciar el proceso y si puede recibir tráfico. Apuntar liveness a un endpoint que comprueba la base de datos provoca reinicios en bucle ante cualquier incidencia de esa dependencia. Además, el servicio debe implementar apagado ordenado al recibir SIGTERM.

¿Cuándo conviene Quarkus o GraalVM frente a Spring Boot?

Convienen cuando el tiempo de arranque y el consumo de memoria son restricciones reales: funciones serverless, escalado a cero o un número muy alto de instancias. La compilación nativa reduce el arranque a milisegundos, pero impone restricciones sobre reflexión, alarga la compilación y complica el diagnóstico. Para un servicio de larga duración, la JVM con JIT ofrece mejor rendimiento sostenido.

¿Qué diferencia hay entre cloud-native y migrar a AWS?

Cloud-native describe cómo está construida la aplicación y migrar a un proveedor describe dónde se ejecuta; son decisiones independientes. La diferencia importa al decidir cuánto acoplarse a servicios gestionados: Kubernetes y contenedores mantienen la portabilidad, mientras que los servicios propietarios reducen el trabajo operativo a cambio de un coste de salida mayor.

¿Quieres llevar tu plataforma Java a la nube correctamente?

Diseño la arquitectura de despliegue, ajusto la JVM para contenedores y dejo la plataforma preparada para operar sin sorpresas.