持续16年的一系列问题

不久之前,在这个千年的黎明中,在2004年11月的一个寒冷的日子里,我坐下来写了一个用于在线游戏的服务器模拟器。对于我而言,关于令人愉悦的C#和.Net Framework 1.1版,它写得很好。我没有为自己设定特殊目标,而且经验相对较少。出于某种原因,社区对此技术表示赞赏(也许是因为它出现在主游戏正式发布之前?),几个月后,我面临着网络爆炸性增长,同时又出现严重的性能问题。该项目历时6年以上,达到了显着的高度(高峰时在线2500个,约20,000 MAU),然后在Bose中休息。而现在,经过十年半的努力,我决定根据相同的“经过时间考验”的开发成果制作自己的MMO游戏,并面临类似的问题,尽管事实上它们已经由我解决了。



PS在撰写本文时,没有一个IP受到损害,它是原始项目,尽管它充满了盗版精神(付费游戏的免费服务器!),没有侵犯任何权利,未在此使用版权所有者的代码,并且该服务器完全基于对诚实购买的游戏客户端和声音的研究。开发商感。这个作品仅说明了在旧项目和现代项目中作者所面临的挑战以及解决这些挑战的原始方法。我提前为故事的叙述风格道歉,而不是简单地列举事实。



介绍



您可以说尽可能多的论点。碎片,指针等。实际上,这使您可以将业务逻辑的脚本委托给资格较低的开发人员,而仅限于“代码审查”。但是要做到这一点,您需要确保内核本身可以正常运行,并且在2004年和2020年在10-15在线时都开始出现故障。

在2004年,一切都在Windows Server 2003,.Net 1.1,MSSQL 2000上旋转。服务器和托管由Wnet提供程序提供,然后使用玩家的捐款建造了新服务器。该项目并非纯粹是商业性的,并且从标语和高级帐户获得的少量收入用于升级。
现代服务器在.Net 4.7兼容模式下在Debian下的Mono上运行,MariaDB用于数据存储在Hetzner云中。长期以来,还没有这样的理想主义者,他们的眼睛发亮,他们认为游戏应该是免费的,捐赠和出售游戏物品会扼杀所有利益。现在,这个角色已经变得非常灰暗,改变了他对体验的热情,并相信初创公司应该带来乐趣和收入。




但是这个故事不是关于这个的,而是关于自写服务器及其问题的。



第1章。





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

所有这些都极大地改善了画面,并且服务器开始崩溃的频率越来越少,主要是仅在内存用尽时。
原则上,在2020年,服务器应用程序只是一个控制台应用程序,在Linux中的单独屏幕中运行。在Visual Studio中没有更多的启动选项,但是记录器在过去的几年中变得非常先进,UnhandledExceptions就像网络中的兔子一样出现,并且原则上没有本地代码。但是,这并没有使我免于OOM和StackOverflowException崩溃的困扰。在StackOverflowException的情况下,堆栈深度增加了十倍,用相同类型的消息填充了数百KB的日志,并拒绝编写正常的堆栈跟踪。但是无论如何,快速重定向到>> log.txt使得了解谁应该归罪于谁以及在何处成为可能。Telegram机器人单独提供了帮助,表明服务器进程已终止。



然后,这只是技术问题。对原木的研究表明,堆栈溢出不是在核心中而是在业务逻辑中表现出来:火箭与另一枚火​​箭或地雷相撞,它们被引爆,这引发了第一枚火箭的引爆,依此类推。总而言之,这是正常的工作时刻,但是那时候我感到奇怪的déjàvu与过去被遗忘的恶魔作斗争。然后出现了一种新的(或久被遗忘的)瘟疫原因-缺乏资源。



第二章高兴





— 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% .
内存不足4GB的现代服务器会让人发笑,您只需单击几次并重新启动,即可为云解决方案增加8-16 GB的额外空间。但是,当内存开始泄漏并且处理器负载跃升至100-150%(基于8核的800%)时,我再次感到自己像个20岁的学生,在一台贪婪的机器的火炉中燃烧了千兆字节和千兆字节的闪存。这很奇怪,不正常,很愚蠢。像以前一样,游戏继续正常运行(尽管有滞后),却没有任何中断,这尤其令人不快。好吧,当然要等到内存用完为止。



多年来,轻量级线程(又名光纤)设法出现和消失,因此我们不再可以访问.Net中的系统线程,而只能访问所谓的。托管线程,在Mono上,仍然无法访问ProcessThread-内部仅存根。线程的诊断变得更加复杂,但是现在我使用了自己的线程池,计算并命名了所有线程,为每个线程保留了准确的统计信息,它们当前正在执行中,特定任务需要花费多长时间。因此,很快就可以跟踪到问题出在我的代码中,而不是系统中的问题,并且线程统计数据表明zhor与业务逻辑的执行相关联,只是某些动作的执行频率比应执行的次数高100倍。现在我的资源不受限,因此,我非常冷静地为每个脚本和计时器的调用提供了额外的日志记录,测量了每个事件的执行时间,并且在一周的实验中,我能够自信地说出问题所在。事实证明,某个NPC正试图攻击另一个NPC,而且它们都被卡在石头上,因此它们无法移动,并且由于缺乏视线,他们互相射击的尝试立即被打断。但与此同时,他们在计算行为的每个周期(15毫秒)中尝试计算路径并开始射击,但由于无法发射,因此炮弹未装弹,因此重复了下一个周期。在游戏的几天中,招募了数百个这样的NPC,它们最终吞噬了服务器的所有资源。解决方案是纠正行为并减少卡死情况,同时即使是拍摄失败也要缩短装弹时间。



然后服务器开始冻结。



第三章感冒





2005年秋天并不容易-我的工作情况不确定,公寓租金突然翻了一番。我只对游戏服务器感到满意-已经有成百上千的在线游戏,但问题也从那里开始-整个世界开始冻结。充其量是,Ping继续走动或一些计时器起作用。有时,一切都冻结了,流量停止了,您不得不杀死服务器应用程序并重新启动它。和以前一样,由于消耗大量的资源和刹车,不可能将调试器连接到正在运行的服务器。由于某种原因,Visual Studio可能因此崩溃或冻结。



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



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



, , Xeon 5130x2 8 . 2000, 2500, . , , , , -, . .
在2020年10月最寒冷的日子之一,由于服务器突然冻结,直播广播的预定到来被中断了。授权有效,但无法进入世界,Telegram机器人保持沉默。快速搜索问题不会在日志中显示任何内容,也没有内存问题,并且所有线程都不会挨饿。才停下来在大声说了一些关于矩阵中的猫和不雅行为的女人的事后,我去寻找一个僵局。在Microsoft收购Miguel de Icaz和Xamarin之后,Mono文档是一个可怜的景象-虽然存在,但不是最新的或无处可寻。例如,页面中3/4的数据有关使用gdb在mono中进行调试的说明不适用,也不起作用。我可以通过gdb连接到冻结的服务器,但是命令mono_pmip和其他命令给出了难以理解的答案,主要是关于语法错误的信息。通过一个奇迹,我意识到gdb希望我将参数和mono_ *命令的结果转换为某些类型,因此我最终能够获得冻结在交叉阻塞中的线程列表。但是列表中的数字与ps命令或服务器上的ManagedThreadId不匹配。我确实发现处理器烧毁的扩展日志记录有很大帮助-从中我可以了解最后执行了哪些程序包和计时器,并逐渐开始缩小可疑范围。邪恶的是,交叉阻塞不是使用两个线程,而是使用三个线程,因此无法获得更详细的图片。然后我想起了以前的耙子,开始查看使用锁的代码。事实证明,这些年来已经进行了多次重构,SpinLock已逐渐由Monitor.Enter / Monitor.Exit取代,并且通常由简单的锁取代。然后突然我引起了我的注意Eric Gunnerson的文章,您可以轻松得多:在有超时的地方使用Monitor.TryEnter,如果锁定失败,则抛出异常。这是一种非常简单且非常有效的方法-如果某个地方的TryEnter调用等待了30秒钟以上而失败了(并且这种延迟不是逻辑上的典型现象),那么必须对此地点进行调查并检查谁可能花费了这么长时间并且没有给出锁定对象。当我在头上撒上灰烬时,我意识到我可以15年前用这种方法清理所有东西,因此不必通过计算“孔深”来重新发明轮子。但这也许是最好的。



好吧,然后第4个车手来到了一个新项目,一次进入了模拟器。只有他没有时间变得受欢迎。尽管如此,在项目开始之初就存在多达三个关键问题,很快就使他失望了。游戏根本不是主流。但这也不是本文的主题。



PPS本文采用的插图由一位不知名的艺术家Parsakoira与签名“周先生#227 ::投票:4骑士启示录”,想必从已经去世的网站conceptart.com:

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

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



All Articles