Caso de estudio: Fantasy Westward Journey Server Optimization

  

La aplicación de parches a la ingeniería histórica es una molestia.

Los primeros dos días hablan sobre la optimización del servidor de Fantasy Westward Journey. Me he alojado en Guangzhou estos días y planeo pasar una semana especializándome en este asunto. Como solía estar en el chat en línea, solo puedo entender realmente el problema si me siento juntos.

En la actualidad, Fantasy Westward Journey utiliza solo una máquina, con un máximo de 8 CPU y 8G de memoria. Incluso los servidores más activos no pueden usar estos recursos (aproximadamente 3 CPU y la mitad de la memoria). El programa central fue escrito hace casi 10 años, y continúa desde Westward Journey to the West. He estado disfrutando de un almuerzo gratis durante los últimos dos años. Con la actualización de la configuración del hardware, el servidor único tiene una capacidad en línea de 12,000. Un gráfico que observa la velocidad de respuesta del servidor revela que el problema actual es que hay una respuesta lenta a intervalos regulares. Periódicamente, el tiempo de respuesta del servidor superará los 1000ms. La razón de esto fue que el disco IO estaba muy congestionado en ese momento. El uso de IO de los servicios que regularmente almacenan datos del jugador, así como las secuencias de comandos que SA realiza copias de seguridad de los datos con regularidad, requieren mucho tiempo de IO. Finalmente la máquina se sobrecargó.

Cómo la sobrecarga de IO afecta en última instancia al rendimiento de los servicios de juego no es una discusión exhaustiva. He analizado principalmente la estructura del sistema en los últimos dos días y he pensado en el plan de mejora.

De hecho, el sistema antiguo no es complicado y la cantidad de código es bastante pequeña. El código de servicio relevante es solo unos pocos miles de líneas de código C limpio. Nadie lo ha estado moviendo porque es un asunto importante, que involucra a millones de usuarios y procesos de facturación. Independientemente de si el diseño es bueno o malo, el rendimiento alcanzado es problemático y da paso a la estabilidad. Las "razones históricas" causadas por todo tipo de quejas, solo pueden quejarse cuando se chatea, si se rediseñó, ciertamente no se escribió. En los últimos dos años, me he vuelto más y más indiferente a la refactorización de este asunto. ¿Por qué no hacerlo, por qué no? En más casos, es solo el almuerzo de los programadores. Una vez que se escribe cada sistema, está lleno de arrepentimientos. Si funciona, la mayor posibilidad es que se siga utilizando. Deja todas las nuevas ideas para la próxima vez.

Para un sistema antiguo que ha estado funcionando de manera estable durante muchos años, es de poca importancia encontrar una buena manera de transformarlo. Lo más importante es cómo agregar algo al sistema existente con un impacto mínimo y mejorar el rendimiento. La clara división entre módulos es bastante importante. La independencia del servicio también es necesaria. Es un gran error colocar un servicio de datos en ejecución y un servicio de facturación y autenticación de usuario en un programa de servicio. Nos hace muy difícil eliminar los datos.

El servicio de datos utiliza una estructura C /S. Pero en lugar de utilizar la base de datos, el sistema de archivos local se usa directamente. Todo el diseño es bueno, pero el mecanismo del servicio de datos en sí es muy malo. La memoria compartida se utiliza para intercambiar datos entre C y S para mejorar el rendimiento de IPC. Solo hay una C, que es el proceso principal del juego, y puede haber más de una S. Los servicios se pueden proporcionar al mismo tiempo. Pipeline ordena entre múltiples Ss y Cs para intercambiar datos con la memoria compartida. La intención es buena, pero el diseño del protocolo es problemático. Debido a que C manipula directamente el área de datos y tiene singularidad, el resultado está diseñado para colocar la administración de bloques del área de datos en C en lugar de S.

Por ejemplo, si el proceso del juego (C) necesita cargar los datos de un usuario, primero busca el espacio en el área de datos y luego le dice a S que cargue los datos del usuario en su ubicación de datos especificada. La limpieza del área de datos también se realiza por C. Esto hace que sea imposible para S hacer caché directamente en el área de datos. Si necesita datos de caché que no se usan temporalmente (como un jugador sin conexión), debe hacerlo usted mismo. O agregue otro servicio de caché (esto requiere el doble de memoria y operaciones de copia de memoria). Me di cuenta de que la implementación probablemente está considerando la necesidad de que múltiples Ss sirvan una C al mismo tiempo, pero solo puedo pensar en ello como un diseño. Pobre

El resultado es que todo el servicio de datos, ya sea de lectura o escritura, está libre de caché. El caché se basa únicamente en el sistema operativo para hacerlo. Esto no es un problema cuando es un orden de magnitud menor. Sin embargo, después de que el número de usuarios en línea llegó a 10,000, el problema fue revelado. Después de todo, cuanta más personalización tenga para la demanda final, más podrá sacar el máximo provecho de su hardware.

El siguiente es un registro del diseño de la base de datos de memoria /clave que he implementado.

Para lograr la estrategia de salvar la diferencia hace unos días, solo la estrategia de guardar la información de la diferencia (la operación de IO medida se puede reducir en un 90%), la ubicación del servicio de lectura /escritura de datos se debe unificar primero. No se puede confiar en el sistema de archivos local para el intercambio de datos. Anteriormente, he examinado varias bases de datos en memoria, como Redis, y finalmente decidí implementar una yo mismo. Como ya conozco muy bien los requisitos, puedo personalizar el algoritmo para maximizar la potencia del hardware. La cantidad de código no será demasiado grande.
(Controlado dentro de 500 líneas de código C y finalmente anotado, pero 300 líneas de programa C)

Nuestra demanda es la siguiente: el programa de servicio se detendrá una vez por semana. El número total de datos de jugadores involucrados por semana es de 100.000. Cada conjunto de datos está entre 4k y 32K. Todos son datos de texto. Se puede ver como una identificación del servicio de almacenamiento de datos de clave /valor de cadena de datos. Se estima que los datos totales se pueden poner en la memoria. Los datos se actualizan con frecuencia y la duración cambiará después de la actualización.

Pasé un día implementando este servicio de datos de memoria k /v. Con el fin de maximizar el uso de la memoria, garantizando la eficiencia y la simplicidad de la implementación del código. Utilicé un esquema que asignaba previamente todo el bloque de memoria para cortar la memoria en bloques de 1K. Y usa una lista enlazada individualmente para unirla. Considere la eficiencia de éxito de la memoria caché. El puntero de la lista enlazada en sí está separado del área de almacenamiento de datos.
(La mayoría de las veces, solo necesitamos acceder a los punteros de la lista vinculada sin tener que acceder a datos específicos).

Los punteros de la lista vinculada utilizan números de serie en lugar de direcciones de memoria. Esto permite un índice de 4 bytes (que se puede utilizar para administrar hasta 4 datos, incluso en un sistema de 64 bits). Una lista enlazada individualmente puede guardar la mitad de la operación del puntero y guardar una pequeña cantidad de memoria en comparación con una lista enlazada por duplicado. El precio es que el código es un poco más complicado de escribir.

Todos los bloques de memoria se dividen en dos partes: bloques libres y bloques usados. Al principio todo el espacio es libre. Una vez que una parte de los datos se coloca hacia adentro, se toman suficientes bloques de la lista libre y se colocan al final de la lista utilizada. Si el espacio de la memoria caché está lleno, se retira un poco del encabezado de la lista de bloque usado y se devuelve al bloque de espacio (las áreas de datos no se visitan durante mucho tiempo). Cada vez que lea un dato, ajústelo al final de la lista utilizada para asegurarse de que finalmente se limpie.

Además, se asigna una tabla hash desde id al encabezado del segmento de bloque en el caché (ya que es una lista enlazada individualmente, el nodo anterior debe guardarse en la implementación). De esta manera, el tiempo O (1) se puede usar para consultar el área de datos correspondiente a la identificación especificada.

Los datos almacenados en el caché no tienen que ser completamente continuos en la dirección, que es como la administración de clústeres del disco. A diferencia de los discos, la memoria tiene menos rendimiento de acceso aleatorio y rendimiento de acceso secuencial. Esto es beneficioso para la eficiencia de la utilización del espacio de memoria.

Copyright © Conocimiento de Windows All Rights Reserved