无法向任何人隐藏的错误的三个示例





我写了很多有关研究棘手错误的文章-CPU错误,内核错误,中间4 GB内存分配,但是大多数错误并不是那么奇特。有时,要查找错误,您只需要查看服务器仪表板,在事件探查器中花费几分钟或阅读编译器警告即可。



在本文中,我将介绍已经发现并修复的三个主要错误;他们都根本没有躲起来,只是等着有人注意到他们。



服务器处理器的惊喜







几年前,我花了几周的时间研究实时游戏服务器上的内存行为。这些服务器在远程数据中心中运行Linux,因此大部分时间都花在了获取必要的权限上,这样我就可以隧道连接到服务器,并学习如何与perf和其他Linux诊断工具一起有效地工作我发现了一系列错误,这些错误导致内存消耗比必要的多三倍,并进行了修复:



  • 我发现地图ID不匹配,这导致每个游戏不使用大约20MB数据的相同副本,而是加载一个新副本。
  • 我发现一个未使用的(!)50 MB全局变量(!!),该变量设置为零内存集(!!!),这导致它在每个进程中消耗物理RAM。
  • 各种不太严重的错误。


但是我们的故事不会是关于那个的。



花时间学习了如何配置我们的游戏服务器后,我意识到我可以对此进行更深入的研究。因此,我跑PERF我们的游戏之一的服务器上。我剖析的第一个服务器进程很奇怪。观看采样的处理器数据“实时”时,我看到一个功能正在消耗100%的CPU时间。但是,此功能仅执行了14条指令。这没有任何意义。



起初我以为我使用的Perf不正确或误解数据。我查看了其他一些服务器进程,发现其中大约一半处于异常状态。下半部分具有更正常的CPU配置文件。



我们感兴趣的功能通过导航节点的链接列表传递。我问我的同事,发现一个程序员,他说浮点精度问题可能导致游戏生成循环的导航列表。他们一直想限制他们可以行走的最大节点数,但是他们从来没有绕过它。



那么难题解决了吗?浮点计算的不稳定性会导致导航列表中出现循环,这使游戏无休止地绕过循环-就是这样,说明了行为。



但是...这样的解释意味着,当这种情况发生时,服务器进程将进入无限循环,所有参与者都将不得不与其断开连接,并且服务器进程将无休止地消耗整个处理器核心。如果真是这样,我们最终会不会耗尽服务器上的资源?没人会注意到吗?



我查找了服务器监视数据,发现如下所示:







在整个监视期间(一到两年),我观察到服务器负载的每日和每周波动,这些波动被每月模式叠加。处理器利用率逐渐提高,然后降至零。问了几番之后,我发现服务器每月重新启动一次。最后,逻辑出现在所有这一切中:



  • , .
  • , , .
  • CPU , 50%.
  • .


修复此错误后,添加了几行代码,停止了二十个导航节点后的遍历列表,从而节省了数百万美元的服务器和电源成本。我没有通过查看监视图来发现此错误,但是查看它们的任何人都可以做到。



我喜欢这样一个事实,即错误的发生频率与它的成本最大化完全吻合;同时,他从未造成足够严重的问题被发现。这类似于一种病毒的作用,这种病毒演变成使人们打喷嚏而不是杀死他们。



加载缓慢







软件开发人员的生产率与编辑/编译/链接/调试周期的速度密切相关。换句话说,这取决于对源文件进行更改后要花多长时间才能运行完成的新二进制文件。多年来,我在减少编译/链接时间方面做得很出色,但是加载时间也很重要。有些游戏每次启动时都会做大量工作。我很不耐烦,因此通常是第一个花费数小时或数天才能使游戏更快加载几秒钟的人。



在这种情况下,我运行了我最喜欢的探查器,并在游戏的初始加载阶段查看了CPU使用情况图。看起来最有希望的一步是:花了大约十秒钟来初始化一些照明数据。我希望可以找到某种方法,通过在启动阶段节省五秒钟来加快这些计算。在深入研究之前,我咨询了图形专家。他说:



“我们不在游戏中使用这些照明数据。只需消除这一挑战。”



哦,太好了。很容易。



通过花费半小时进行概要分析并更改一行,我可以将主菜单的加载时间减半,而这并不需要花费很多精力。



不合时宜的出发



由于格式中的参数个数任意printf很容易出现类型不匹配错误。实际上,结果可能相差很大:



  1. printf(“ 0x%08lx”,p);//将指针打印为int-在64位上截断或更差
  2. printf(“%d,%f”,f,i);//更改float和int的位置-可能显示废话,或者可能起作用(!)
  3. printf(“%s%d”,i,s);//更改字符串和整数的顺序-最有可能导致崩溃


该标准说,这种类型的不匹配是未定义的行为,一些编译器生成的代码会故意使任何这些不匹配而崩溃,但是上面列出了最可能的结果(注意:为什么第二段经常产生期望的结果是好的ABI知识难题)。



这样的错误很容易产生,因此所有现代编译器都有能力警告开发人员发生不匹配。 gcc和clang都具有函数的printf样式注释,它们可以警告不匹配(但是,不幸的是,注释不适用于wprintf样式函数)。 VC ++带有/ analyge注释(不幸的是其他注释),可用于警告不匹配,但是,如果您不使用/ analyst,则只会警告关于printf / wprintf风格的CRT风格的函数,而不是您的自定义函数...



我工作的公司以printf样式注释了它们的功能,以便gcc / clang发出警告,但后来决定忽略警告。这是一个奇怪的决定,因为这样的警告可以完美地指示错误-信噪比是无限的。



我决定开始使用VC ++清理这些错误,并//分析注释以准确找到所有错误。我解决了大多数错误,并做了很大的更改,等待代码被检查后再提交。







那个周末,数据中心发生了停电,我们的所有服务器都掉了(可能是由于电源配置错误)。紧急人员赶在损失太多钱之前重建并修复了所有东西。



printf错误的有趣之处在于它们100%的时间行为不当。也就是说,如果他们将要显示不正确的数据或导致程序崩溃,则每次都会发生这种情况。因此,只有当它们处于从未读取的日志记录代码或很少执行的错误处理代码中时,它们才能保留在程序中。



原来,“同时重启所有服务器”事件导致代码沿着通常不会执行的路径移动。起始服务器开始寻找其他服务器,找不到它们,并显示类似以下消息的内容:



fprintf(日志,“找不到服务器%s。错误代码%d。\ n”,err,server_name);


哎呀。类型不匹配的任意数量的参数。和出发。



紧急响应人员还有另一个问题。需要重新启动服务器,但是在检查故障转储,发现错误,未重建服务器二进制文件并发布新的构建之前,无法完成此操作。这是一个相当快的过程-似乎不超过几个小时,但完全可以避免。



我认为这个故事完美地说明了为什么我们应该花时间对这些警告的原因进行排查-为什么忽略那些告诉我们代码在执行时肯定会崩溃或表现不佳的警告?但是,没有人担心消除此类警告可以为我们节省几个小时的停机时间。事实上,该公司的文化似乎并不关心任何这些修补程序。但这是最后一个错误,使我意识到现在是时候转移到另一家公司了。



从中可以学到什么教训?



如果涉及的每个人都在努力开发产品功能并修复众所周知的错误,那么公开显示的很可能就是非常简单的错误。花一些时间研究日志,清理编译器警告(尽管实际上,如果您有编译器警告,那么可能值得重新考虑您在生命中所做的决定),运行探查器几分钟。如果您添加自己的日志记录系统,启用新警告或使用除您之外没有其他人使用的探查器,您将获得额外的加分。



如果您要进行出色的修复以提高内存/ cpu的使用或稳定性,而又没人在乎,请找一家对此表示赞赏的公司。



黑客新闻的讨论在这里,reddit的讨论在这里,微博讨论在这里






广告



可靠的租金服务器和正确的费率计划选择将使您不必再因令人讨厌的监视通知而分心-一切都会顺利进行,并具有很高的正常运行时间!









All Articles