Uma série de problemas que duram 16 anos

Não faz muito tempo, no alvorecer deste milênio, em um dia frio de novembro de 2004, sentei-me para escrever um emulador de servidor para um jogo online. Ele foi muito bem escrito para mim, em C # e .Net Framework versão 1.1 atraentes. Não estabeleci metas especiais para mim e tinha relativamente pouca experiência. Por alguma razão, a comunidade gostou deste ofício (talvez porque apareceu antes do lançamento oficial do jogo principal?) E depois de alguns meses eu me deparei com um crescimento explosivo no online, e ao mesmo tempo sérios problemas de desempenho. O projeto viveu por mais de 6 anos, atingiu alturas perceptíveis (2.500 online em seu pico, cerca de 20.000 MAU), e então descansou no Bose. E agora, depois de uma década e meia, decidi fazer meu próprio jogo MMO baseado nos mesmos desenvolvimentos “testados pelo tempo” e enfrentei problemas semelhantes, apesar de já terem sido resolvidos por mim uma vez.



PS Ao escrever este artigo, nem um único IP foi prejudicado , o projeto original, embora estivesse saturado com o espírito da pirataria (servidor gratuito de um jogo pago!), Não violou nenhum direito, o código do detentor dos direitos autorais não foi usado lá e o servidor foi baseado inteiramente na pesquisa de um cliente de jogo comprado honestamente e um som sentido do desenvolvedor. Esta obra fala apenas sobre os desafios enfrentados pelo autor e os métodos originais de resolvê-los, tanto no projeto antigo quanto no moderno. Peço desculpas antecipadamente pelo estilo de narrativa da história, ao invés de simplesmente listar os fatos.



Introdução



Você pode argumentar o quanto quiser que .Net não é para servidores, mas me pareceu então (e agora) uma ideia muito sensata que você pode escrever lógica na forma de scripts, compilá-la e carregá-la em movimento, sem pensar muito sobre alocação de memória, montagem detritos, ponteiros e muito mais. Na verdade, isso permite que você delegue o script da lógica de negócios a desenvolvedores menos qualificados, limitando-se apenas à revisão do código. Mas, para isso, você precisa ter certeza de que o próprio kernel funciona sem falhas, e começou a falhar em 10-15 online, tanto em 2004 quanto em 2020.

Em 2004, tudo girava em torno do Windows Server 2003, .Net 1.1, MSSQL 2000. O servidor e a hospedagem foram fornecidos pelo provedor Wnet e, em seguida, um novo servidor foi montado com doações de jogadores. O projeto não era puramente comercial e alguma receita mínima de banners e contas premium foi usada para atualizações.
O servidor moderno roda em Mono sob Debian em modo de compatibilidade .Net 4.7, com MariaDB para dados, hospedado na nuvem Hetzner. Por muito tempo não existiu tal idealista com olhos ardentes que acreditava que os jogos deveriam ser gratuitos e que a doação e venda de itens do jogo matam todos os juros. Agora, esse personagem ficou bastante grisalho, mudou seu entusiasmo pela experiência e está convencido de que uma startup deve trazer prazer e renda.




Mas a história não é sobre isso, mas sobre servidores auto-escritos e seus problemas.



Capítulo 1. Pestilência





. , , , . , , . , , . 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 .

Tudo isso melhorou significativamente a imagem e o servidor começou a travar com muito menos frequência, principalmente quando a memória acabou.
Em 2020, o aplicativo de servidor era, em princípio, apenas um aplicativo de console, rodando em uma tela separada no Linux. Não havia mais opções para iniciar o Visual Studio, mas o logger se tornou muito avançado com o passar dos anos, UnhandledExceptions pareciam coelhos na rede e não havia código nativo em princípio. Que, no entanto, não salvou de travamentos com OOM e StackOverflowException. A profundidade da pilha no caso de uma StackOverflowException cresceu dez vezes, preenchendo centenas de kilobytes de log com mensagens do mesmo tipo e recusando-se a escrever um rastreamento de pilha normal. Mas, em qualquer caso, redirecionar para >> log.txt rapidamente tornou possível entender quem é o culpado e onde. O bot do Telegram ajudou separadamente, sinalizando que o processo do servidor havia morrido.



Então era apenas uma questão de tecnologia. O estudo dos registros mostrou que o estouro da pilha apenas se manifestou não no núcleo, mas na lógica do negócio: o foguete colidiu com outro foguete ou mina, eles detonaram, isso desencadeou a detonação do primeiro foguete e assim por diante em um círculo. Em suma, este é um momento normal de trabalho, mas foi quando eu senti um estranho déjà vu lutando contra demônios há muito esquecidos do passado. E então apareceu uma nova (ou velha esquecida) causa da peste - a falta de recursos.



Capítulo 2. Que bom





— 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% .
Um servidor moderno com menos de 4 GB de memória causa um sorriso, e você pode adicionar 8-16 gigabytes extras para uma solução em nuvem com alguns cliques e uma reinicialização. No entanto, quando a memória começou a vazar e a carga do processador saltou para 100-150% (com base em 800% para 8 núcleos), novamente me senti como um estudante de 20 anos, queimando gigabytes e gigaflops na fornalha de uma máquina voraz. Era estranho, não normal e estúpido. Foi especialmente desagradável que, como antes, o jogo continuou a correr normalmente (embora com atrasos), mas nada foi interrompido. Bem, até que a memória acabasse, é claro.



Com o passar dos anos, Lightweight Threads (aka Fibers) conseguiram aparecer e desaparecer devido a que não temos mais acesso aos threads do sistema em .Net, apenas aos chamados. Threads gerenciados e no Mono ainda não há acesso ao ProcessThread - há apenas stubs dentro. O diagnóstico de threads ficou muito mais complicado, mas agora eu usei meu próprio Thread Pool, todos os threads foram calculados e nomeados, para cada um deles foram mantidas estatísticas precisas, qual deles está sendo executado no momento, quanto tempo leva uma tarefa específica. Devido a isso, rapidamente descobri que agora os problemas estão no meu código, e não no sistema, e as estatísticas de encadeamento mostraram que o zhor está associado à execução da lógica de negócios, apenas algumas ações são realizadas 100 vezes mais frequentemente do que deveriam. Agora eu não estava limitado em recursos,portanto, calmamente forneci a chamada de cada script e cronômetro com registro adicional, medi o tempo de execução de cada evento e, em uma semana de experimentos, fui capaz de dizer com segurança qual era o problema. Acontece que um certo NPC estava tentando atacar outro NPC e ambos estavam presos em pedras, então eles não podiam se mover e suas tentativas de atirar um no outro foram instantaneamente interrompidas devido à falta de linha de visão. Mas ao mesmo tempo, a cada ciclo de cálculo do comportamento (15ms), eles tentavam calcular o caminho, começavam a atirar, mas devido à impossibilidade de atirar, os canhões não recarregavam e o ciclo seguinte se repetia. Durante vários dias do jogo, centenas desses NPCs foram recrutados e, eventualmente, consumiram todos os recursos do servidor. A solução foi corrigir o comportamento e reduzir as situações de travamento e, ao mesmo tempo, um curto tempo de recarga, mesmo para tiros sem sucesso.



E então o servidor começou a travar.



Capítulo 3. Frio





O outono de 2005 não foi fácil - eu estava em uma situação incerta com meu trabalho e o aluguel do meu apartamento dobrou de repente. Fiquei apenas satisfeito com o servidor de jogos - já havia centenas de jogos online, mas aí também começou o problema - o mundo inteiro começou a congelar. Na melhor das hipóteses, os pings continuaram a funcionar ou alguns temporizadores funcionaram. E às vezes tudo travava, o tráfego parava e era preciso encerrar o aplicativo do servidor e reiniciá-lo. Como antes, era impossível conectar-se com um depurador a um servidor em execução devido ao consumo significativo e aos freios. Por algum motivo, o Visual Studio simplesmente travaria ou congelaria com isso.



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



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



, , Xeon 5130x2 8 . 2000, 2500, . , , , , -, . .
Em um dos dias frios de outubro de 2020, a chegada planejada de transmissões ao vivo foi interrompida porque o servidor congelou de repente. A autorização funcionou, mas era impossível entrar no mundo, o bot do Telegram ficou em silêncio. Uma busca rápida por problemas não mostrou nada nos logs, não havia problemas de memória e nenhum dos threads estava morrendo de fome. Simplesmente parou. Tendo dito várias vezes em voz alta algo sobre um gato da matriz e uma mulher de comportamento indecente, fui procurar um impasse. Depois que a Microsoft comprou Miguel de Icaz e Xamarin, a documentação do Mono é uma visão lamentável - está lá, mas não está atualizada ou não leva a lugar nenhum. Por exemplo, 3/4 dos dados da páginasobre depuração em mono com gdb não é aplicável e não funciona. Consegui me conectar ao servidor congelado via gdb, mas os comandos call mono_pmip e outros deram respostas ininteligíveis, principalmente sobre erros de sintaxe. Por algum milagre, percebi que o gdb quer que eu lance os parâmetros e o resultado dos comandos mono_ * para certos tipos e, portanto, fui capaz de obter uma lista de threads congelados em um bloqueio cruzado. Mas os números na lista não correspondem ao comando ps ou ao ManagedThreadId do servidor. O registro estendido, que fiz para encontrar a queima do processador, ajudou muito - com ele consegui entender quais pacotes e temporizadores foram executados por último e gradualmente comecei a estreitar o círculo de suspeitos. Por ser um mal, o cross-blocking não era com dois threads, mas com três, portanto não era possível obter uma imagem mais detalhada.Então me lembrei do antigo ancinho e comecei a olhar o código para usar bloqueios. Como se viu, várias refatorações se passaram ao longo dos anos e o SpinLock foi gradualmente substituído por Monitor.Enter / Monitor.Exit e, frequentemente, por um simples bloqueio. E então de repente eu chamei minha atençãoEric Gunnerson artigo , que diz que você pode fazer muito mais fácil: use Monitor.TryEnter em todos os lugares com um tempo limite, e se o bloqueio falhar, então lançar uma exceção. Este é um método incrivelmente simples e muito eficaz - se em algum lugar a chamada TryEnter esperou por mais de 30 segundos e caiu (e esses atrasos não são característicos da lógica), então este lugar deve ser investigado e verificado quem poderia ter tomado por tanto tempo e não ter dado o objeto de bloqueio. Polvilhando cinzas na cabeça, percebi que poderia ter limpado tudo assim 15 anos atrás, não era preciso reinventar a roda com o cálculo da “profundidade do buraco”. Mas talvez fosse o melhor então.



Bem, então o 4º piloto veio para um novo projeto, como uma vez para um emulador. Só que ele não teve tempo de se tornar popular. Ainda assim, a presença de até três problemas críticos logo no início do projeto o derrubou rapidamente. E o jogo não saiu do mainstream. Mas este também não é um tópico para este artigo.



PPS O artigo usa ilustrações de um artista desconhecido Parsakoira com a assinatura “ChoW # 227 :: VOTING :: 4 Horsemen of the Apocalypse”, presumivelmente do já falecido site conceptart.com:

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

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



All Articles