Tecnología

¿Operar un MMORPG sobre la JVM??

AAnonymous
4 min de lectura

Introducción

Últimamente me he relajado demasiado. Necesito volver a ponerme serio. :( Ha pasado bastante tiempo desde la última vez que escribí una entrada del blog.

En Cómo hacemos juegos escribí que el MMORPG sería el desafío final. Desde entonces, la reacción más común probablemente ha sido esta: ¿Java? ¿De verdad vas a construir y operar un MMORPG en tiempo real sobre la JVM? ¿Eso no funciona solo a una escala muy pequeña?

Esta entrada tiene una sola idea central. La razón más importante por la que los ticks de servidor de alta frecuencia se han vuelto una opción realista en la JVM es el avance del GC. Netty importa. Kotlin importa. También importan técnicas de MMO como la partición por zonas y el AOI. Pero antes de todo eso, lo primero que hay que mirar es el pause time.

En este género, la pregunta real no es el rendimiento promedio sino el GC

Si recuerdas la Java de antes, la primera preocupación en un juego de red en tiempo real suele ser el GC. Más exactamente, no es el throughput sino la tail latency. Aunque el promedio se vea bien, basta con un stop-the-world largo para que el tick se atrase y se rompan de inmediato la respuesta de entrada y la sensación del combate.

Esto es especialmente crítico en un servidor de juego.

  • Un servidor a 20Hz tiene un presupuesto de tick de 50ms.
  • Un servidor a 60Hz tiene un presupuesto de tick de 16.6ms.
  • Un servidor a 128Hz tiene un presupuesto de tick de 7.8ms.

Si los GC pauses saltan a decenas de milisegundos, el TPS promedio puede verse sano y aun así los jugadores perciben de inmediato que algo va mal. Rubber banding, retraso en la entrada, desincronización de combate y demora en el packet flush suelen empezar exactamente en este tipo de problema de cola. Por eso, la pregunta de si la JVM sirve para juegos en tiempo real se parece mucho más a esta: ¿qué tan corta y predecible se ha vuelto la pausa del GC?

El GC moderno de la JVM ya no es el GC antiguo de la JVM

Aquí es donde la situación cambió mucho. Incluso la ruta de OpenJDK deja el cambio bastante claro.

  • ZGC se convirtió en una feature de producción en JDK 15.
  • Generational ZGC llegó en JDK 21.
  • En JDK 23, Generational ZGC pasó a ser el modo por defecto.
  • En JDK 24, se eliminó ZGC no generacional.
  • Eso significa que Java 25 puede verse como el primer LTS de la era de ZGC exclusivamente generacional.
  • Shenandoah ya era un GC de producción y, en Java 25, su modo generacional fue promovido a product feature.

Esto importa por algo más que una opción nueva de la JVM. Según OpenJDK, Generational ZGC fue diseñado para recolectar objetos jóvenes con mayor frecuencia mientras reduce el riesgo de allocation stalls, el heap overhead requerido y el GC CPU overhead. Y lo más importante es que conserva el perfil de pausas. JEP 439 explica que las pausas de la aplicación suelen mantenerse por debajo de 1ms.

Eso significa que la vieja idea que hacía que mucha gente descartara un servidor de juegos en Java, es decir, que el GC a veces detiene el mundo de una manera destructiva para la latencia, dejó de ser la premisa por defecto. El GC no desapareció. Pero el GC por sí mismo ya no es la razón decisiva para sacar a la JVM de la conversación desde el inicio.

Los casos de producción también apuntan en la misma dirección

Uno de los ejemplos más interesantes es Netflix. Netflix movió su postura por defecto hacia Generational ZGC en JDK 21 y afirma que más de la mitad de sus servicios críticos de video en streaming ya funcionan sobre JDK 21 con Generational ZGC.

Más importante aún fueron los resultados. Netflix esperaba al principio que ZGC fuera una compensación: comprar menor latencia a costa de un poco de throughput. En la práctica, la latencia promedio y la latencia P99 mejoraron, mientras que el uso de CPU fue igual o mejor. En uno de sus peores casos, ZGC no generacional consumía 36% más CPU que G1 para la misma carga, pero con Generational ZGC eso terminó convertido en una mejora de CPU cercana al 10%.

Por supuesto, Netflix no es una empresa de videojuegos. Pero aquí lo relevante no es el tipo de producto. Lo importante es qué tan bien un sistema controla las pausas y la tail latency. Un servidor de juego en tiempo real resuelve el mismo problema, solo que con un presupuesto todavía más apretado.

Entonces, ¿se pueden hacer juegos en tiempo real sobre la JVM?

Llegados a este punto, ya no tiene mucho sentido hacer una pregunta vaga como ¿la JVM sirve para juegos en tiempo real?. Hay que separar el problema por dominios.

El territorio realista ya es bastante amplio.

  • MMORPG basados en ticks
  • Servidores de mundo tipo sandbox
  • Backends de lobby, matchmaking y sincronización de estado
  • Juegos multijugador que necesitan algo como 20~60Hz de tick de servidor

Un ejemplo masivo es el servidor de Minecraft Java Edition. Ese hecho, por sí solo, ya hace difícil seguir afirmando que Java no puede mover un mundo multijugador en tiempo real.

Al mismo tiempo, todavía hay áreas donde hace falta más cuidado.

  • FPS competitivos a 128Hz, donde el presupuesto de tick es extremadamente estrecho
  • Cargas que exigen una latencia determinista extrema
  • Entornos con muy poco margen de memoria
  • Sistemas donde los hilos del GC y los hilos de la aplicación compiten sobre una CPU ya saturada

Así que la idea no es que la JVM pueda con todo. La idea es que el problema de las pausas del GC, que antes era la razón principal para descartar la JVM en la mayoría de escenarios de servidores de juego, se ha reducido de forma drástica.

Solo después de eso las técnicas específicas de MMO realmente importan

El orden importa. Llevo tiempo diciendo que son importantes el zone tick, el cadence por sistema y la sincronización guiada por AOI. El mmorpg-service que estoy construyendo sigue esa estructura en la práctica.

  • El loop base corre a 10Hz
  • La configuración de mundo y zona puede ajustarse entre 1~15Hz
  • La IA de monstruos usa distintos cadence según el estado
  • Las verificaciones de despawn de drops y los reintentos de auto-recuperación corren más lentamente
  • Los hilos I/O de Netty solo reciben paquetes, mientras los zone loops aplican las reglas reales del juego
  • AOIManager cambia la frecuencia de sincronización según distancia y eventos

Pero todo eso sigue siendo la segunda capa. Si los pause times todavía saltan a decenas de milisegundos, dividir zone ticks y afinar AOI encima de eso no arregla la base. La razón principal por la que una frecuencia alta se volvió práctica es el avance del GC. El diseño MMO es la capa que construyes encima de esa base.

Al final, esta entrada se resume en una sola frase

La razón más importante por la que el procesamiento de alta frecuencia se volvió práctico en la JVM es el avance del GC.

En el pasado, la asociación entre Java y los peligrosos stop-the-world era demasiado fuerte. Hoy los low-pause collectors son mucho más maduros, y con Generational ZGC junto con la evolución reciente de Shenandoah, la JVM ya no es una plataforma que quede excluida de forma automática cuando se habla de servidores en tiempo real.

Cuando encima de eso sumas Kotlin y Netty, y después agregas zone loops, cadence y AOI como herramientas de diseño propias de un MMO, la historia cambia por completo. La pregunta ya no debería ser ¿la JVM hace esto imposible?, sino ¿cómo diseñamos GC y ticks dentro de nuestro latency budget?

Justamente por eso creo que operar un MMORPG sobre la JVM se convirtió en una opción muy realista.