Una serie de problemas que duran 16 años.

No hace mucho, en los albores de este milenio, en un frío día de noviembre de 2004, me senté a escribir un emulador de servidor para un juego en línea. Fue escrito muy bien para mí, en C # y .Net Framework versión 1.1. No me fijé metas especiales y tenía relativamente poca experiencia. Por alguna razón, la comunidad apreció este arte (¿quizás porque apareció antes del lanzamiento oficial del juego principal?) Y después de unos meses me enfrenté a un crecimiento explosivo en línea y al mismo tiempo a serios problemas de rendimiento. El proyecto vivió durante más de 6 años, alcanzó alturas notables (2500 en línea en su punto máximo, alrededor de 20,000 MAU), y luego descansó en el Bose. Y ahora, después de una década y media, decidí hacer mi propio juego MMO basado en los mismos desarrollos “probados en el tiempo” y enfrenté problemas similares, a pesar de que ya los había resuelto una vez.



PD Al escribir este artículo, no se dañó ni una sola IP , el proyecto original, aunque estaba saturado con el espíritu de la piratería (¡servidor gratuito de un juego pago!), No violó ningún derecho, el código del titular de los derechos de autor no se usó allí, y el servidor se basó completamente en la investigación de un cliente y sonido de juego comprado honestamente. sentido del desarrollador. Esta obra habla solo sobre los desafíos que enfrentó el autor y los métodos originales para resolverlos, tanto en el proyecto antiguo como en el moderno. Me disculpo de antemano por el estilo narrativo de la historia, en lugar de simplemente enumerar los hechos.



Introducción



Puede argumentar tanto como quiera que .Net no es para servidores, pero entonces (y ahora) me pareció una idea muy sensata que puede escribir lógica en forma de scripts, compilar y cargar sobre la marcha, sin pensar mucho en la asignación de memoria, ensamblaje escombros, punteros y más. De hecho, esto le permite delegar la secuencia de comandos de la lógica empresarial a desarrolladores menos calificados, limitándose solo a Revisión de código. Pero para hacer esto, debe asegurarse de que el kernel funcione sin fallas, y comenzó a fallar incluso en 10-15 en línea, tanto en 2004 como en 2020.

En 2004, todo giraba en Windows Server 2003, .Net 1.1, MSSQL 2000. El proveedor de Wnet proporcionó el servidor y el alojamiento, y luego se construyó un nuevo servidor con donaciones de los jugadores. El proyecto no era puramente comercial y se utilizaron algunos ingresos mínimos de los banners y las cuentas premium para las actualizaciones.
El servidor moderno se ejecuta en Mono bajo Debian en modo de compatibilidad .Net 4.7, con MariaDB para datos, alojado en la nube Hetzner. Durante mucho tiempo ya no existe un idealista con ojos ardientes que creyera que los juegos deberían ser gratuitos, y la donación y venta de artículos de juego mata todo interés. Ahora este personaje se ha vuelto bastante gris, cambió su entusiasmo por la experiencia y está convencido de que una startup debe traer tanto placer como ingresos.




Pero la historia no se trata de eso, sino de servidores escritos por ellos mismos y sus problemas.



Capítulo 1. Pestilencia





. , , , . , , . , , . Visual Studio, - , . EventLog .



— , Console.Out Console.Error. UnhandledExceptionHandler, . AutoFlush = true, , .



cmd — , . , , , - — , . - — .Net >> log.txt.



UnhandledExceptionHandler : OutOfMemoryException ( ), StackOverflowException Unmanaged . , — Access Violation - OOM.

Access Violation — ZLib ( ICSharpCode.SharpZipLib), OpenSSL ( SRP-6), MySQL ( System.Data MSSQL ).



, Socket.BeginReceive . .Net Thread Pool ( , IO Threads) , UnhandledExceptionHandler. , BeginReceive->EndReceive->BeginReceive , BeginReceive .

Todo esto mejoró significativamente la imagen y el servidor comenzó a fallar con mucha menos frecuencia, principalmente solo cuando se agotó la memoria.
En 2020, la aplicación de servidor era, en principio, solo una aplicación de consola, ejecutándose en una pantalla separada en Linux. No había más opciones para el lanzamiento en Visual Studio, pero el registrador se volvió muy avanzado a lo largo de los años, las UnhandledExceptions se encontraron como conejitos en la red y, en principio, no había código nativo. Lo que, sin embargo, no lo salvó de fallas con OOM y StackOverflowException. La profundidad de la pila en el caso de una StackOverflowException se ha multiplicado por diez, llenando cientos de kilobytes de registro con mensajes del mismo tipo y negándose a escribir un seguimiento de pila normal. Pero en cualquier caso, redirigir a >> log.txt rápidamente hizo posible comprender quién tiene la culpa y dónde. El bot de Telegram ayudó por separado, lo que indica que el proceso del servidor había muerto.



Entonces fue solo una cuestión de tecnología. El estudio de los registros mostró que el desbordamiento de la pila simplemente se manifestó no en el núcleo, sino en la lógica empresarial: el cohete chocó con otro cohete o con la mía, detonaron, esto provocó la detonación del primer cohete, y así sucesivamente en un círculo. En general, este es un momento de trabajo normal, pero fue entonces cuando sentí un extraño déjà vu luchando contra demonios olvidados del pasado. Y luego apareció una nueva (o antigua causa olvidada) de la pestilencia: la falta de recursos.



Capítulo 2. Me alegro





— 256 , ! - , , , , — , OOM - . , — Visual Studio ( , ), WinDbg (), - dotTrace (). , . — , 1.7, . . 100%. , , , — ~100 . Maoni Stephens Rico Mariani GC, LOH (Large Object Heap) .Net. , (pin) , Gen 2, — LOH, . — , , , (, .Net 1.1 Generics!). — , - , . Marshal.AllocHGlobal ( - , ). , , . , , , 100% CPU - . Interop WSASend/WSAReceive ( Windows , .Net) . - , .Net : BeginSend/BeginReceive , , 100% CPU.



, , , , , . , - 100% , !



, 2005 Workstation GC Server GC .Net 2.0 Preview. — , GC , 5-10% CPU.



, , Thread Pool Net 1.1 Workstation GC , ( !) ( 100% ).

BeginSend/BeginReceive Windows IOCP . , , , OOM 100% .
Un servidor moderno con menos de 4 GB de memoria provoca una mueca, y puede agregar de 8 a 16 gigabytes adicionales para una solución en la nube con un par de clics y un reinicio. Sin embargo, cuando la memoria comenzó a filtrarse y la carga del procesador saltó al 100-150% (basado en 800% para 8 núcleos), nuevamente me sentí como un estudiante de 20 años, quemando gigabytes y gigaflops en la cámara de combustión de una máquina voraz. Era extraño, no normal y estúpido. Fue especialmente desagradable que, como antes, el juego continuara funcionando normalmente (aunque con retrasos), pero nada se interrumpió. Bueno, hasta que se acabó la memoria, claro.



A lo largo de los años, Lightweight Threads (también conocidos como Fibers) lograron aparecer y desaparecer debido a que ya no tenemos acceso a los hilos del sistema en .Net, solo a los llamados. Managed Threads, y en Mono todavía no hay acceso a ProcessThread, solo hay stubs adentro. El diagnóstico de subprocesos se volvió mucho más complicado, pero ahora usé mi propio Thread Pool, todos los subprocesos se calcularon y nombraron, para cada uno de ellos se mantuvieron estadísticas precisas, cuál de ellos se está ejecutando actualmente, cuánto tiempo toma una tarea específica. Debido a esto, rápidamente resultó rastrear que ahora los problemas están en mi código, y no en el del sistema, y ​​las estadísticas del hilo mostraron que el zhor está asociado con la ejecución de la lógica empresarial, solo algunas acciones se realizan 100 veces más a menudo de lo que deberían. Ahora no estaba limitado en recursos,por lo tanto, proporcioné con bastante calma la llamada de cada script y temporizador con registros adicionales, medí el tiempo de ejecución de cada evento y, en una semana de experimentos, pude decir con seguridad cuál era el problema. Resultó que cierto NPC estaba tratando de atacar a otro NPC y ambos estaban atrapados en rocas, por lo que no podían moverse y sus intentos de dispararse entre sí se interrumpieron instantáneamente debido a la falta de Línea de visión. Pero al mismo tiempo, en cada ciclo de cálculo del comportamiento (15ms), intentaron calcular la trayectoria, empezaron a disparar, pero debido a la imposibilidad de disparar, los cañones no recargaron y se repitió el siguiente ciclo. Durante varios días del juego, se reclutaron cientos de estos NPC y, finalmente, consumieron todos los recursos del servidor. La solución fue corregir el comportamiento y reducir las situaciones de atasco y, al mismo tiempo, un tiempo de recarga corto incluso para tiros fallidos.



Y luego el servidor comenzó a congelarse.



Capítulo 3. Frío





El otoño de 2005 no fue fácil: tuve una situación incierta con mi trabajo, el alquiler del apartamento se duplicó repentinamente. Solo estaba satisfecho con el servidor del juego, ya había cientos de juegos en línea, pero el problema también comenzó allí, el mundo entero comenzó a congelarse. En el mejor de los casos, los pings continuaron caminando o algunos temporizadores funcionaron. Y a veces todo se congeló, el tráfico se detuvo y tuvo que cerrar la aplicación del servidor e iniciarla nuevamente. Como antes, era imposible conectarse con un depurador a un servidor en ejecución debido al consumo y los frenos importantes. Por alguna razón, Visual Studio simplemente se bloqueó o se colgó de esto.



— , . , - . , - . SOS.dll. Son Of Strike WinDbg .Net , , . , .Net GC. - sos.dll 50. , , , . , — deadlock!



, . — . — , , , , ! , . SpinLock try/finally . , , — , SpinLock , , , , , . 8 , . , : , , “ ”. , . , , — .



, , Xeon 5130x2 8 . 2000, 2500, . , , , , -, . .
En uno de los fríos días de octubre de 2020, la llegada prevista de transmisores en vivo se interrumpió porque el servidor colgó de repente. La autorización funcionó, pero era imposible entrar al mundo, el bot de Telegram estaba en silencio. Una búsqueda rápida de problemas no mostró nada en los registros, no hubo problemas de memoria y ninguno de los hilos se estaba muriendo de hambre. Simplemente se detuvo. Después de decir en voz alta varias veces algo sobre un gato de la matriz y una mujer de comportamiento indecente, fui a buscar un punto muerto. Después de que Microsoft compró Miguel de Icaz y Xamarin, la documentación de Mono es lamentable: está ahí, pero no está actualizada o no conduce a ninguna parte. Por ejemplo, 3/4 de los datos de la páginaacerca de la depuración en mono con gdb no es aplicable y no funciona. Pude conectarme al servidor congelado a través de gdb, pero los comandos call mono_pmip y otros dieron respuestas ininteligibles, principalmente sobre errores de sintaxis. Por algún milagro, me di cuenta de que gdb quiere que transmita los parámetros y el resultado de los comandos mono_ * a ciertos tipos, por lo que terminé pudiendo obtener una lista de subprocesos congelados en el bloqueo cruzado. Pero los números de la lista no coincidían ni con el comando ps ni con el ManagedThreadId del servidor. El registro extendido, que hice para encontrar el procesador quemado, ayudó mucho; a partir de él pude comprender qué paquetes y temporizadores se ejecutaron en último lugar y gradualmente comencé a reducir el círculo de sospechosos. Como malvado, el bloqueo cruzado no fue con dos hilos, sino con tres, por lo que no fue posible obtener una imagen más detallada.Luego me acordé del antiguo rastrillo y comencé a mirar el código para usar candados. Al final resultó que, varias refactorizaciones han pasado a lo largo de los años y SpinLock ha sido reemplazado gradualmente por Monitor.Enter / Monitor.Salir, y a menudo por un simple candado. Y de repente me llamó la atenciónEl artículo de Eric Gunnerson, que dice que puedes hacerlo mucho más fácil: usa Monitor. TryEnter en todas partes con un tiempo de espera, y si el bloqueo falla, lanza una excepción. Este es un método increíblemente simple y muy efectivo: si en algún lugar la llamada TryEnter esperó durante más de 30 segundos y se cayó (y tales demoras no son típicas de la lógica), entonces este lugar debe investigarse y verificarse quién pudo haber tomado durante tanto tiempo y no se le dio el objeto de bloqueo. Espolvoreando cenizas en mi cabeza, me di cuenta de que podría haber limpiado todo de esta manera hace 15 años, no era necesario reinventar la rueda con el cálculo de la “profundidad del agujero”. Pero tal vez fue lo mejor entonces.



Bueno, entonces el cuarto piloto llegó a un nuevo proyecto, como una vez a un emulador. Solo que no tuvo tiempo de hacerse popular. Aún así, la presencia de hasta tres problemas críticos justo al comienzo del proyecto lo derribó rápidamente. Y el juego no salió del todo convencional. Pero este tampoco es un tema para este artículo.



PPS El artículo utiliza ilustraciones de un artista desconocido Parsakoira con la firma “ChoW # 227 :: VOTING :: 4 Horsemen of the Apocalypse”, presumiblemente del sitio ya fallecido conceptart.com:

https://www.pinterest.com/pin/460141286926583086/

https : //www.pinterest.com/pin/490681321879914768/



All Articles