INTRODUCCIÓN
Cuando comencé con el diseño de ZU hace unos dos años, la premisa principal era que tenía que ser realmente masivo. Siempre estuve relacionado laboralmente con sistemas que manejaban mucho volumen de datos y es una de las áreas que mas me gustan de la informática, el poder aplicarlo a la creación de un juego, que también es algo que siempre quise hacer, fue el combustible de la idea. El catalizador fue el entusiasmo de alguien con quien me toco laburar bastante tiempo con la idea, lo que me llevo a ponerla en marcha. Si algún día ZU existe, estará en los créditos.
Los dos problemas básicos en un sistema en línea de alto volumen son la capacidad de expansión y el tiempo de respuesta. Un sistema de nace gigante requiere una inversión muy grande al principio y durante mucho tiempo tiene una gran capacidad ociosa, que es lo mismo que quemar dólares por diversión. Por otro lado, si el sistema llega a ser exitoso y la demanda sobre el mismo crece, un diseño monolítico puede llevarte a que la única forma de agrandar la capacidad sea comprar una maquina cada vez mas grande. Esto funciona durante un rato, pero finalmente cae por su propio peso. Por otro lado, en un sistema en línea hay que tener una alta disponibilidad 24/7, mas si es un juego global, donde no hay periodo ociosos para sincronizaciones y procesos batch porque siempre en algún lugar del mundo es el horario pico.
La solución es un diseño donde el sistema divida sus tareas en módulos y estos a su vez puedan hacer algún tipo de balance de carga entre ellos. Si se consigue esto, se puede cubrir la nueva demanda armando un farm de servidores en vez de comprarle una heladera a Sun y tirar la anterior. La desventaja es que el balance de carga no es siempre fácil o posible y que el overhead para lograrlo puede ser importante. Otra cosa a tener en cuenta es el impacto en otros recursos, como la carga de la red, ya de alguna forma hay que comunicar los módulos, o el consumo de memoria, por ejemplo, manteniendo datos redundantes para cada proceso.
Otra decisión que tome al encarar el diseño fue que iba a hacer un engine y un juego, y no solo un juego. La mayoría de los juegos de "empire-building" tienen una arquitectura similar, así que escribiendo un engine genérico, transformar el juego de conquista espacial a lucha por la dominación mundial en el Medioevo debería ser solo una cuestión de implementación y skinning del cliente. Para eso pensé dos escenarios de uso, con el objetivo de trazar un común denominador. Por un lado la idea original de ZU que es un juego ambientado en el espacio, con sistemas solares distribuidos por toda una galaxia, asteroides, bases espaciales y naves interestelares. Por el otro, un escenario de fantasía, ambientado en algo que correspondería mas a una campaña de AD&D que otra cosa, con castillos, minas, granjas, torres, dragones y magos, etc. Si bien no espero poder implementar cualquier tipo de escenario sin ciertas customizaciones de la base de código, el objetivo era mantener un alto porcentaje, por lo menos del core, común a ambos diseños.
ARQUITECTURA GENERAL
La arquitectura general de ZU consta de cinco grandes componentes:
Presentation Layer
La capa de presentación esta implementada como una a través de un servlet java. Esta capa solo tiene la lógica mínima necesaria para filtrar los errores más burdos de datos y nunca tiene acceso de escritura al modelo de datos. Estos servlets leen los datos que tienen que presentarle al usuario directamente desde una réplica de solo lectura de la base de datos. Esto es un compromiso entre la flexibilidad de tener un modelo MVC implementado como corresponde, la velocidad de respuesta y la carga del sistema. Desde el punto de vista de la implementación es más barato codificar una capa de representación no genérica pero optimizada para una alta concurrencia, a tratar de desacoplarla del modelo de datos y absorber el costo, tanto de desarrollo de la capa intermedia como del tiempo de proceso.
Gatekeeper
El Gatekeeper es el modulo que se encarga de toda la autenticación del flujo de datos en el sistema. Mantiene y actualiza la información de las cuentas de usuario y realiza el managment de todo lo relacionado con ellas. Dentro del esquema de comunicación entre la capa de representación y el core, actúa como un autenticador de los interlocutores y hace el escrow de los tokens de autenticación.
Logic Core
El core lógico mantiene todo el universo funcionando en tiempo real. Su trabajo es procesar todas las interacciones con los usuarios y entre los usuarios, actualizar el modelo de datos con los cambios y mantener una cola de eventos temporales para que se vayan disparando en el momento indicado.
El core esta a su vez dividido en tres módulos, adecuado cada uno al tipo de eventos que tiene que procesar:
Statics Processor.El modulo de statics procesa todos los aspectos del juego sobre los cuales el usuario no tiene interacción y por lo tanto no son cancelables. Estos incluyen cosas como el crecimiento de la población, el consumo de recursos, el cambio de humor de los habitantes de un imperio, etc. Al no ser cancelables, este modulo no necesita ningún soporte de indexación para eliminar o modificar los eventos, lo que lo hace realmente liviano. Otra ventaja de este tipo de eventos es que en general son de baja prioridad, con lo que pueden ser ralentizados si la carga del sistema se eleva por encima de cierto límite.
Dynamics Processor.El modulo de dynamics es lo opuesto al primer modulo que nombré. Este tiene como misión llevar el control y ejecutar la lógica de todos los eventos que nacieron de una interacción directa con el usuario y que pueden ser modificados o eliminados antes de que se disparen. Entre los ejemplos de este tipo de eventos encontramos las ordenes de construcción, el movimiento de tropas, los ataques, etc. Este modulo tiene que mantener un índice interno a la cola temporizada para poder cancelar o modificar los eventos, por lo que las demandas de recursos por evento son mucho más altas.
Universe Processor.El modulo universo no está avocado a los eventos de jugadores o mundos individuales, sino que se encarga del mantenimiento macroscópico del universo. Procesa eventos tales como los llamados a elecciones universales, las "modas" de cada época, eventos especiales colaborativos entre varios jugadores, desastres naturales y los eventos relacionados con los artefactos de esas típicas civilizaciones antiguas que uno encuentra en estos juegos.
Database
La base de datos cumple dos roles. Primero, mantiene el estado del sistema en forma persistente. Segundo, actúa como canal de propagación de cambios hacia la capa de presentación a través de una réplica de solo lectura de la base de datos. El juego es agnóstico al RDBMS que se use siempre y cuando tenga drivers JDBC medianamente aceptables. Hoy en día está usando MySQL 5.1 simplemente para poder usar las modificaciones de Google a ese motor para alta concurrencia y alta disponibilidad.
Message Router
La comunicación entre la capa de presentación, el gatekeeper y el core se realiza a través de este modulo. Utilizar un message router tiene muchas ventajas, la más importante es que es transaccional. Esto libera a los otros módulos de toda la lógica necesaria para implementar un protocolo de query/response y asegurarse que las respuestas llegan y sobre todo, llegan a tiempo. Otra ventaja importante es que la entrega de los mensajes está garantizada aun en el evento de una falla del sistema, cuando todo levanta de nuevo, el router mira su cola persistente de mensajes y comienza a repartir los que había llegado antes del cataclismo. Finalmente, no es una capa de comunicación boba, sino que tiene la capacidad de analizar la estructura de cada mensaje y derivarlo al destinatario correspondiente. Esto permite modificar la topología del sistema en tiempo real, sin tener que darlo de baja, prender y apagar servidores según la carga del sistema y hacer balanceo de carga transparente a los servicios balanceados. Usarlo para un juego, es como matar moscas con granadas de fragmentación, pero es un modulo que ya tengo desarrollado de mi laburo "serio" y es hasta mas cómodo que implementar un par de sockets pedorros.
TOPOLOGIA
La topología de un deploy del sistema con balanceo de carga incluido seria esta:
Como verán es la típica estructura de estrella que se tiene cuando la comunicación está centralizada. Esto representaría un punto de falla critico para el sistema, ya que si se cae el router, se apaga todo. En realidad esto está contemplado en el router mismo, que puede tener varios nodos esclavos con un master que al fallar este último, se auto promueven a master.
La principal ventaja de este modelo es que expandir el sistema es conectar un nuevo modulo a la red y listo. El router funciona sobre tcp/ip, así que técnicamente no hay ningún problema en que esta topología este repartida en varios datacenters para otorgar mayor resistencia a las fallas. De todas formas, eso ya seria matar moscas con bombas termonucleares.
Cuando tenga un poco más de tiempo, voy a escribir una segunda parte enfocada en la estructura del motor en sí y los problemas que encontré en la implementación de ciertas cosas.