Exploiter un MMORPG sur la JVM ??
Introduction
Ces derniers temps, je me suis beaucoup relâché. Il faut vraiment que je me ressaisisse. :( Cela faisait longtemps que je n’avais pas écrit de billet de blog.
Dans How We Make Games, j’avais écrit que le MMORPG serait le défi final. Depuis, la réaction la plus fréquente ressemble probablement à ceci : Java ? Vous allez vraiment construire et exploiter un MMORPG en temps réel sur la JVM ? N’est-ce pas quelque chose qui ne fonctionne qu’à toute petite échelle ?
Cette note repose sur une idée centrale. La raison la plus importante pour laquelle des ticks serveur à haute fréquence sont devenus un choix réaliste sur la JVM, c’est le progrès du GC. Netty compte. Kotlin compte. Les techniques propres aux MMO, comme la segmentation par zones ou l’AOI, comptent aussi. Mais avant tout cela, il faut d’abord regarder le pause time.
Dans ce domaine, la vraie question n’est pas la performance moyenne mais le GC
Si vous vous souvenez de l’ancienne Java, la première inquiétude lorsqu’on parle de jeu réseau en temps réel est généralement le GC. Plus précisément, ce n’est pas le throughput mais la tail latency. Même si la moyenne paraît bonne, un seul stop-the-world suffisamment long suffit à faire déraper un tick, puis à dégrader immédiatement la réactivité des entrées et la sensation de combat.
Sur un serveur de jeu, c’est particulièrement critique.
- Un serveur à
20Hzdispose d’un budget de tick de50ms. - Un serveur à
60Hzdispose d’un budget de tick de16.6ms. - Un serveur à
128Hzdispose d’un budget de tick de7.8ms.
Si les pauses GC montent soudainement à plusieurs dizaines de millisecondes, le TPS moyen peut sembler sain alors que les joueurs sentent immédiatement que quelque chose cloche. Rubber banding, retard à l’entrée, désynchronisation des combats, flush des paquets retardé : tout cela commence souvent par ce type de problème de queue. C’est pourquoi la question de savoir si la JVM convient au temps réel revient en réalité moins à demander la performance moyenne de la JVM est-elle suffisante ? qu’à demander à quel point le GC est-il devenu court et prévisible ?
Le GC moderne de la JVM n’est plus celui d’autrefois
C’est ici que la situation a profondément changé. La trajectoire d’OpenJDK elle-même le montre clairement.
- ZGC est devenu une fonctionnalité de production dans JDK 15.
- Generational ZGC est arrivé dans JDK 21.
- Dans JDK 23, Generational ZGC est devenu le mode par défaut.
- Dans JDK 24, le ZGC non générationnel a été supprimé.
- Autrement dit, Java 25 peut être vu comme le premier LTS de l’ère ZGC exclusivement générationnelle.
- Shenandoah était déjà un GC de production et, dans Java 25, son mode générationnel a été promu en fonctionnalité produit.
L’enjeu dépasse largement l’ajout d’une option de plus dans la JVM. D’après OpenJDK, Generational ZGC a été conçu pour collecter plus fréquemment les objets jeunes tout en réduisant le risque d’allocation stalls, le heap overhead nécessaire et le GC CPU overhead. Et surtout, le profil de pause reste le point clé. Le JEP 439 explique que les pauses applicatives restent généralement en dessous de 1ms.
Cela signifie que l’ancienne hypothèse qui poussait tant d’ingénieurs à écarter les serveurs de jeu Java, à savoir le GC finit parfois par arrêter le monde d’une manière catastrophique pour la latence, n’est plus l’hypothèse par défaut. Le GC n’a pas disparu. Mais le GC, à lui seul, n’est plus la raison décisive pour exclure d’emblée la JVM.
Les cas réels de production vont dans le même sens
L’un des exemples les plus intéressants est Netflix. Netflix a fait évoluer sa posture par défaut vers Generational ZGC sur JDK 21 et explique que plus de la moitié de ses services critiques de streaming vidéo fonctionnent désormais sur JDK 21 avec Generational ZGC.
Les résultats sont encore plus importants. Au départ, Netflix pensait que ZGC serait un compromis : acheter de la faible latence au prix d’un peu de throughput. En pratique, la latence moyenne et la latence P99 se sont améliorées, tandis que l’utilisation CPU est restée équivalente ou meilleure. Dans l’un des pires cas observés, le ZGC non générationnel consommait 36 % de CPU de plus que G1 pour la même charge, alors qu’avec Generational ZGC, cela s’est transformé en amélioration CPU proche de 10 %.
Bien sûr, Netflix n’est pas un studio de jeux. Mais ce qui compte ici n’est pas la catégorie du produit. Ce qui compte, c’est la capacité d’un système à maîtriser les pauses et la tail latency. Un serveur de jeu en temps réel résout fondamentalement le même problème, avec un budget encore plus serré.
Alors, peut-on réellement faire du temps réel sur la JVM ?
À ce stade, il n’est plus très utile de demander de manière vague la JVM peut-elle faire du jeu en temps réel ?. Il faut découper le problème par domaines.
Le territoire réaliste est déjà assez vaste.
- Les MMORPG à base de ticks
- Les serveurs de monde de type sandbox
- Les backends de lobby, de matchmaking et de synchronisation d’état
- Les jeux multijoueurs qui nécessitent quelque chose comme
20~60Hzde tick serveur
Un exemple grand public est le serveur Minecraft Java Edition. Ce seul fait rend déjà difficile l’affirmation selon laquelle Java ne peut pas faire tourner un monde multijoueur en temps réel.
Cela dit, il existe toujours des cas où davantage de prudence s’impose.
- Les FPS compétitifs à
128Hz, où le budget de tick est extrêmement serré - Les charges qui exigent une latence déterministe extrême
- Les environnements avec très peu de marge mémoire
- Les systèmes où les threads du GC et ceux de l’application se disputent déjà un CPU saturé
L’idée n’est donc pas de dire que la JVM peut tout faire. L’idée est que le problème des pauses GC, qui constituait autrefois la principale raison de rejeter la JVM dans la plupart des scénarios de serveurs de jeu, a été fortement réduit.
Et ce n’est qu’après cela que les techniques propres aux MMO prennent tout leur sens
L’ordre des choses est important. Je répète depuis un moment que le zone tick, le cadence par système et la synchronisation pilotée par AOI sont essentiels. Le mmorpg-service que je construis suit effectivement cette structure.
- La boucle par défaut tourne à
10Hz - Les réglages du monde et des zones peuvent être ajustés dans la plage
1~15Hz - L’IA des monstres utilise différents cadence selon l’état
- Les vérifications de disparition des drops et les tentatives d’auto-récupération tournent à un rythme plus lent
- Les threads I/O de Netty se contentent de recevoir les paquets, tandis que les zone loops appliquent les règles du jeu
AOIManagermodifie la fréquence de synchronisation selon la distance et les événements
Mais tout cela reste la deuxième couche. Si les pause times continuent à bondir de plusieurs dizaines de millisecondes, découper les zone ticks et affiner l’AOI au-dessus de cela ne répare pas les fondations. La raison principale pour laquelle une fréquence élevée est devenue praticable, c’est le progrès du GC. La conception MMO est la couche que l’on construit par-dessus.
Au final, ce billet se résume à une phrase
La raison la plus importante pour laquelle le traitement à haute fréquence est devenu praticable sur la JVM, c’est le progrès du GC.
Autrefois, l’association Java = stop-the-world dangereux était beaucoup trop forte. Aujourd’hui, les low-pause collectors sont bien plus mûrs, et avec Generational ZGC ainsi que l’évolution récente de Shenandoah, la JVM n’est plus une plateforme automatiquement exclue des discussions sur les serveurs temps réel.
Lorsque l’on ajoute à cela Kotlin et Netty, puis les zone loops, le cadence et l’AOI comme outils de conception propres aux MMO, l’histoire change complètement. La vraie question ne devrait plus être la JVM rend-elle cela impossible ?, mais comment concevoir le GC et le tick dans notre latency budget ?
C’est précisément pour cette raison que je considère aujourd’hui l’exploitation d’un MMORPG sur la JVM comme une option tout à fait réaliste.