容易的--露天藏身的三只虫子

2020-08-31 17:10:19

我写了很多关于棘手错误的调查-CPU缺陷、内核错误、临时的4-GB内存分配-但大多数错误并不那么深奥。有时,跟踪bug就像关注服务器仪表板、花几分钟在分析器中,或者查看编译器警告一样简单。

下面是我发现并修复的三个重要的错误,它们正坐在空地上,等待着有人注意到。

几年前,我花了几周时间对实时游戏服务器进行内存调查。远程数据中心的服务器都在运行Linux,所以我的大部分时间都花在获取权限上,这样我就可以通过隧道连接到服务器,并学习如何有效地使用Perf和其他Linux诊断工具。我发现了一系列错误,这些错误导致内存使用量达到所需的三倍,我修复了这些错误:

我发现地图ID不匹配,这意味着为每个游戏加载约20MB的新数据副本,而不是重复使用。我发现未使用的(!)50MB(!!)。全局变量Memset为零(!),从而确保它消耗每个进程的物理RAM。

在花时间学习了如何分析我们的游戏服务器之后,我想我应该更深入地了解一下,所以我在服务器上为我们的另一个游戏运行了perf。我分析的第一个服务器进程是…。奇怪。CPU采样数据的实时视图显示,单个函数占用了100%的CPU时间。在该函数中,似乎只有14条指令在执行。但这说不通。

我的第一个假设是我错误地使用了perf或曲解了数据。我查看了其他一些服务器进程,发现其中大约有一半处于这种奇怪的状态。另一半人的CPU配置文件看起来更正常。

所讨论的函数正在遍历导航节点的链接列表。我四处打听,找到一位程序员,他说浮点精度问题可能会导致游戏生成带有循环的导航列表。他们一直想要限制要遍历的节点数量,但是从来没有做到这一点。

所以,谜团解开了,对吧?浮点不稳定性会导致导航列表中出现循环,游戏会无休止地遍历它们,并解释其行为。

但是…。这种解释意味着,无论何时发生这种情况,服务器进程都将进入无限循环,所有玩家都必须断开连接,服务器进程将无限期地消耗整个CPU核心。如果发生这种情况,我们最终不会耗尽服务器机器上的资源吗?难道不会有人,你知道,注意到吗?

我追踪了服务器监控,发现了一个类似以下内容的图表:

早在监控开始的时候(一两年),我就可以看到服务器负载的每日和每周波动,上面覆盖的是每月的模式。CPU使用率将逐渐增加,然后下降到零。再多询问一下,就会发现服务器机器每个月都会重启一次。最后,一切都变得有意义了:

在游戏的任何特定运行中,服务器进程都有可能陷入无限循环,当这种情况发生时,玩家将断开连接,服务器进程将保持在此循环中,直到月底重新启动机器。CPU监视仪表板清楚地显示,此错误平均减少了大约50%的服务器容量。

修复方法是编写几行代码,在20个导航节点之后停止遍历,大概可以节省数百万美元的服务器和电力成本。我不是通过查看监控图表来发现这个错误的,但是任何看过它们的人都可能发现了这个错误。

我喜欢错误的频率被完美地设置为最大限度地增加成本,而不会造成足够严重的问题,以至于它被捕获。它就像一种病毒,进化后会让人咳嗽,但不会杀死他们。

软件开发人员的工作效率与编辑/编译/链接/调试周期的延迟密切相关。也就是说,在对源文件进行更改之后,运行包含该更改的新二进制文件需要多长时间?多年来,我在减少编译/链接时间方面做了很多工作,但启动时间也很重要。有些游戏每次上线都要做大量的工作。我很不耐烦,我经常是第一个花几个小时或几天试图让游戏启动快几秒钟的人。

在本例中,我运行了我最喜欢的分析器,并查看了初始加载期间的CPU使用情况。有一个阶段看起来最有希望:初始化一些照明数据花费了大约10秒钟。我希望有一些方法可以加速这些计算,也许可以节省5秒左右的启动时间。在深入研究之前,我咨询了图形专家。他们说:

“我们在这个游戏中不使用照明数据”--“只需删除通话即可。”

通过半小时的分析和一行更改,主菜单的启动时间缩短了一半,不需要特别的努力。

Printf格式中的变量参数意味着很容易获得类型不匹配。实际效果差别很大:

Printf(“0x%08lx”,p);//在64位printf(“%d,%f”,f,i)上将指针打印为整型截断或更糟;//交换浮点型和整型-可能打印无意义,或者可能实际工作(!)。Printf(“%s%d”,i,s);//交换字符串和int的顺序-可能会崩溃。

该标准说,这些不匹配是未定义的行为,因此从技术上讲,任何事情都可能发生,一些编译器会生成在任何这些不匹配时故意崩溃的代码,但以下是一些最有可能的结果(另外:理解为什么#2经常打印所需的结果是一个很好的ABI难题)。

这些错误非常容易犯,所以现代编译器都有办法在出现不匹配时警告开发人员。GCC和clang有针对printf样式函数的注释,并且可以警告不匹配(尽管遗憾的是,这些注释不能用于wprintf样式函数)。VC++有(不幸的是不同的)注释,/Analyze可以用来警告不匹配,但是如果您没有使用/Analyze,那么它只会警告CRT定义的printf/wprintf样式的函数,而不是您自己的自定义函数。

我工作的公司已经注释了他们的printf样式的函数,以便GCC/clang会发出警告,但后来决定忽略这些警告。这是一个奇怪的决定,因为这些警告是错误的100%可靠的指示器-信噪比是无限的。

我决定开始使用VC++的注释和/分析来清理这些错误,以确保我找到了所有的错误。我已经完成了大部分错误,并在提交之前有一个最后的更改等待代码审查。

那个周末,我们的数据中心停电了,所有的服务器都停机了(可能有一些电源配置错误)。随叫随到的人争先恐后地在损失太多钱之前让事情恢复正常。

关于printf bug的有趣之处在于,它们通常在执行时100%都会行为不端。也就是说,如果它们要打印不正确的数据或崩溃,那么它们通常在每次运行时都会这样做。因此,这些错误存在的唯一方式是它们位于从未读取的日志代码中,或者位于很少执行的错误处理代码中。

事实证明,“同时重启所有服务器”命中了一些不能正常执行的代码路径。正在启动的服务器寻找其他服务器,但找不到它们,并打印如下消息:

Fprintf(log,“找不到服务器%s。错误代码%d。\n”,Err,server_name);

随叫随到的人现在有了一个额外的问题。服务器需要重新启动,但是直到检查了崩溃转储、发现了错误、修复了错误、重新构建了服务器二进制文件并部署了新构建之后,才能重新启动服务器。这是一个相当快的过程-我相信只需要几个小时-但完全可以避免。

它感觉像是一个完美的故事来演示为什么我们应该花时间来解决这些警告-为什么要忽略那些告诉您代码在执行时肯定会崩溃或行为不当的警告呢?然而,似乎没有人关心修复这类警告是否会为我们节省几个小时的停机时间。实际上,公司文化似乎对这些修正中的任何一个都不感兴趣。但直到这最后一个漏洞,我才意识到我需要跳槽到另一家公司。

如果项目中的每个人都把所有的时间都花在特性和已知的bug上,那么很可能会有一些简单的bug隐藏在普通的站点中。花一些时间查看日志,清理编译器警告(不过,如果您真的有编译器警告,您需要重新考虑您的生活选择),并花几分钟运行一个分析器。如果您添加自定义日志记录、启用一些新警告或使用其他人没有使用的分析器,则需要额外加分。

如果你做了出色的修复,改善了内存/CPU/稳定性,但没有人关心,也许可以找一家他们关心的公司。

此条目发布在Bugs、代码分析、代码可靠性、调试、浮点、Linux、性能和标记编码值中。为固定链接添加书签。