Archives For Pablo Bravo

Continuando con el post anterior en el que hablábamos del servidor de juegos de Ideateca, en éste voy a comentar el diseño de la librería de comunicaciones y qué es un diseño SEDA y por qué es tan bueno para estos sistemas.

El diseño de la librería de comunicaciones que da soporte al servidor de Ideateca tiene como principal objetivo escalar muy bien para cargas muy altas de usuarios.

La primera decisión de diseño es evidente, I/O asíncrona. La implementación utiliza tres descriptores de I/O para gestionar peticiones de conexión, peticiones de lectura y peticiones escritura.

Una vez tomada esa decisión queda cómo arquitecturar el resto de elementos. Para ello, basta observar el ciclo de vida de un mensaje. Éste debe ser recibido, decodificado (traducido desde el formato de serialización) y procesado (es decir, realizar la invocación sobre el objeto remoto registrado al que va dirigido el mensaje).

Teniendo lo anterior en cuenta, y sabiendo que el mayor problema a la hora de escalar son el número de hilos y que es muy importante la productividad (frente a la velocidad de procesamiento; mayor velocidad de procesamiento por petición no implica mayor productividad y viceversa), el diseño de la librería está basado en un enfoque SEDA (Stage Event Driven Architecture).

Dicho diseño propone partir el diseño en las fases de tratamiento de mensaje, frente a la tentadora posibilidad (y muy común) de diseñar centrándose en el ciclo de vida de la petición. Esta idea no es del todo innovadora, ya que supone la aplicación directa del conocido diseño en “pipeline” de microprocesadores bien conocido por ingenieros en electrónica.

Diseño centrado en petición

diseño centrado en petición

Diseño centrado en fases

diseño centrado en fases

Como se puede observar, la productividad aumenta considerablemente.

Para comprender completamente el diseño de la librería de comunicaciones, es necesario profundizar en el concepto de diseño orientado a etapas.

Se ha mostrado a vista de pájaro el principio SEDA, consistente en dividir el trabajo de procesamiento de una petición en etapas desacopladas mediante colas, y cómo esta organización favorece la productividad del sistema. Es importante entender que de forma subyacente, un sistema SEDA es un sistema de colas. Por tanto se beneficia enormemente de las capacidades de análisis matemático que surgen en dichos sistemas, pudiendo estudiar analíticamente cuando añadir servidores a o colas a una determinada etapa para mejorar el proceso, cuando unir etapas, cuando separarlas, estudiar los cuellos de botella de forma y replicar los recursos involucrados, etc.

Los patrones de diseño relacionados con este tipo de sistemas son los siguientes:

  • Patrón “Wrap”: Se trata de acondicionar una tarea al sistema de procesamiento SEDA, utilizando una cola como elemento de entrada a la etapa.

patron wrap

  • Patrón “Pipeline” y “Partition”: Dividen una tarea en una serie de etapas que permitan mejorar el rendimiento del sistema gracias a una mejora en la localidad o en la arquitectura del sistema colas.

patrón pipeline

  • Patrón “Combine”: Es el patrón opuesto a los anteriores, que permite unir varias etapas en una sola, con la ventaja de reducir la complejidad total del sistema si es que las etapas permiten dicha unión.

patrón combine

  • Patrón “Replicate”: Instancia una etapa más de una vez, generalmente asociada si es posible a un conjunto diferente de recursos bien físicos, bien lógicos. Permite implementar directamente el concepto de redundancia para asegurar la fiabilidad del servicio.

patrón replicate

Para dar más detalles del diseño e implementación de la librería, básicamente hay que tener primero en cuenta otras peculiaridades de los sistemas que van a desarrollarse sobre ella. Generalmente las discusiones sobre servidores o proveedores de servicios se centran en aquellos en que las peticiones son independientes entre sí, no alteran un estado central, es decir, no son transaccionales. Un ejemplo es un servidor de tráfico HTTP. Otro puede ser un servidor de juegos de mundos persistentes (aunque estos necesiten un control microtransaccional) o juegos arcade (también con control microtransaccional). Sin embargo en un juego basado en turnos es muy importante el orden de los mensajes y que estos modifiquen el modelo de forma atómica.

Llegados a este punto se abren dos discusiones; una para juegos arcade donde el modelo es inherentemente multihilo, no es problemático que los mensajes lleguen en desorden parcial y se necesita un protocolo de transporte no orientado a conexión para maximizar el ancho de banda de mensajes. Por otro lado,  están los juegos por turnos, inherentemente monohilo o transaccionales, donde es muy importante asegurar el orden de los mensajes y el protocolo de transporte debe asegurar la entrega de los mensajes de los mismos.

Para cada caso, la librería simplemente elige unos canales de proceso internos diferentes. En el caso arcade, transporte UDP, y el sistema de entrega de mensajes a objetos es simplemente un pool indiferenciado de hilos, escalable hasta el máximo configurado en función de la máquina física. En el caso por turnos, transporte TCP y un pool especializado de hilos donde cada objeto es servido siempre por el mismo hilo para asegurar la atomicidad de la ejecución de mensajes sin sacrificar rendimiento o exponiendo el sistema a una excesiva contención de hilos.

Dada la naturaleza asíncrona del servidor y la necesaria optimización de recursos, se contempla el envío de mensajes retrasados, que la librería (generalmente en el lado cliente) gestionará cuando venzan los contadores del mensaje para permitir al servidor progresar y optimizar sus recursos, de forma que los clientes puedan mostrar adecuadamente datos en las vistas. Asimismo se contemplan mensajes de sincronismo para permitir evolucionar la aplicación de manera igual en todos los clientes.

Como último apunte, la librería permite el registro de observadores de paquetes y estados de conexión, e incluso que estos observadores inyecten paquetes de forma autónoma o los modifiquen e incluso denieguen su posterior procesamiento.

Imágenes extraídas de : An Architecture for Highly Concurrent,Well-Conditioned Internet Services

El diseño de servidores es una tarea compleja que comprende observar multitud de aspectos y detalles. En dicho diseño influyen muchos factores. Por nombrar algunos; carga de usuarios esperada, modelo de procesos y/o hilos, tipo de servicio, etc…

En Ideateca nos hemos liado la manta a la cabeza y hemos decidido apostar por los juegos multijugador. Tras analizar las diferentes posibilidades, nos hemos decantado por un diseño sencillo pero potente con nuestro propio desarrollo.

En este post explicaremos el cómo y el por qué, que aunque parezca que hoy en día ya está todo hecho, aún puede ser útil y necesario un desarrollo ad hoc.

Nuestras necesidades son sencillas, clientes móviles para Android e IPhone, integración con Facebook y portal propio. Una carga esperada modesta inicialmente, pero un sistema que escale de manera adecuada hasta el máximo posible de usuarios por servidor físico. Casi nada…

La elección del modelo de entrada – salida era obvio; asíncrono, ya que queremos escalar bien en situaciones de carga con miles de usuarios. Es bien sabido que el mayor problema para escalar en un modelo síncrono es precisamente su esencia, los hilos. Un sistema puede manejar bien miles de descriptores de I/O (y si no, escribid un driver NDIS para Windows :), pero no maneja tan bien miles de hilos, ya que la sobrecarga para el dispatcher y el paginador empieza a ser excesiva.

Una vez decidido el modelo de I/O, entrábamos en el diseño propiamente dicho del servidor. Para aumentar la productividad, lo más adecuado es un diseño SEDA (Stage Event Driven Architecture). Podéis informaros un poco más aquí, o en éste pdf. He usado las ideas expuestas en la tesis anterior con éxito en otros sistemas software de ámbitos tan diferentes como motores antivirus. Este diseño promueve la diferenciación en etapas del tratamiento de los datos de una petición, favoreciendo la productividad de una forma semejante a las etapas del cauce de ejecución de un microprocesador actual.

Para finalizar la discusión del diseño “a vista de pájaro” de nuestro servidor multijugador de juegos, queda comentar el interfaz de alto nivel usado para exponer servicios y el tipo de protocolo usado para comunicar “peers”.

Buscamos por un lado, facilidad de uso y desarrollo, y por otro eficiencia y rendimiento. Esto lleva de forma lógica a un servicio al estilo RPC, que es sencillo de usar en los “end points” y que use un protocolo de transporte binario, que es efectivo y lo más compacto posible. Nuestro servidor usa un sistema de objetos remotos al estilo CORBA (o DCOM), pero muy simplificado para dar servicio a nuestras necesidades, que sin embargo facilita enormemente la integración de las aplicaciones con nuestro sistema de red y permite bondades tales como desarrollar localmente y de forma extremadamente sencilla convertir la aplicación en distribuida. Respecto al protocolo, poco hay que especificar, ya que serializa clases como estructuras y emite mensajes entendibles en un sistema RPC tales como “Invocación de método sobre objeto remoto” o “Resultado de ejecución de método” principalmente. Para aquellos que les resulte extraño no usar XML para todo, simplemente el sobrecoste del parseo de mensajes lo descalifica directamente para un sistema de alto rendimiento.

En otro “post” comentaré las idiosincracias de nuestro servidor, ya que, por norma general, todo tipo de discusiones sobre servidores se centran en servidores sin estado al estilo HTTP, donde un hilo cualquiera puede servir una petición cualquiera. Sin embargo en nuestro servidor, con estado, para asegurar el orden de mensajes en el caso basado en turnos, limitar la contención entre hilos y mejorar la productividad, se debe observar una estricta política de servicio de hilos a objetos.

Pablo Bravo García trabaja como ingeniero en Ideateca.