« 如何让玩家相信游戏是公平的 | 返回首页 | 谈谈陌陌争霸在数据库方面踩过的坑(前篇) »

一起 select 引起的崩溃

2 月 13 日,陌陌争霸 计划在下午 16:00 例行维护,之前已经稳定运行了很长时间了。没想到在 14:30 左右,有一台从机意外崩溃。再次之前,从机崩溃并不会引起系统坏死,只需要新启动一台从机即可。但这次似乎不一样,所有玩家均不能登陆游戏,不得已,提前进行了长达两小时的紧急维护(将例行维护的工作合并)。

这次从机崩溃进行的大规模事故,可简单描述为:当从机上 5K 玩家掉线后,引起这批用户立刻连接其它从机。而事故发生后另一个小 bug 导致没有及时从系统中把这批用户重置,导致这批用户不可能立刻重新进入系统,反复重试。而每台从机上的网关配置最大连接数过小,导致他们阻塞了其他用户的登陆。

维护后修正了后面几个问题,由于时间紧迫,没有细查崩溃的直接原因。(另一个小插曲是,core 文件被输出到一个容量很小的分区,分区容量不足,导致 core 文件被截断,增加了分析难度。)系统重开后,不到半小时又有一台从机崩溃,这次没有产生雪崩,立刻启用了备用机替换。可在 5 分钟后,新的一起崩溃事件在另一台从机上发生。这使我们意识到问题的严重性。

由于后来的崩溃拿到了完整的 core 文件,可以看到 C 层的崩溃发生在游戏服务器和我们的运营平台服务器(分属两个机房)的对接环节。由于机房间网络状态不稳定,发生了一次重连操作,再重新建立连接时,程序出错。但是程序调用栈已经被部分破坏。同时,错误 log 中有大量玩家领奖品失败的记录 。最快速的判断是,两者有一定的相关性。我们已经两个月没有修改 C 代码了,之前一直很稳定,过年期间无人守护都没有发生这种严重的事故,所以很难理解 bug 的成因。最近只在 lua 曾增加了一些业务逻辑,比如一些奖品发放。不太能确定到底是何为因何为果。

到了 13 日晚饭时间,依然没有头绪。为了减少晚上再次崩溃的可能,我决定关闭奖品领取模块,再慢慢追查 bug 。


core 文件中的调用栈破坏使得从现场做分析变得有些困难。所以按一般的 bug 追查策略,我们推测是由于这次发奖的某种流程导致的问题。结合三次崩溃的现象,大致认为是某个玩家领取奖励触发了某个很难测试到的分支。花了一点时间在三次崩溃前 20 分钟的 log 中获得了几万个玩家的 id 。放在一起比对,只有一个玩家恰好出现在三个名单中。

但事后表明,这个玩家的出现时间完全是巧合而已。我们没有从这个玩家的信息中获得线索。但三次崩溃都出现在游戏服务器和运营平台对接重连中,这使得这部分相关代码变得最为可疑。

写这块代码的蜗牛同学正在马来西亚度婚假,所以就不打搅他了。13 日晚上,我阅读了所有我们编写的平台对接代码,没有找到问题。弄得这一整晚,晓靖和我都非常不爽。

14 日,我改去查蜗牛用到的 rabbitmq 的 C 库 ,初看下来也没啥问题。

在毫无线索的情况下,只好开始在 core 文件中重新寻找蛛丝马迹。把未损坏的堆栈数据和反编译的代码逐个比对。发现在 amqp_open_socket_noblock 函数中 portnumber 这个局部数组全部为 0 ,但 sockfd 却不是 -1 。这说明 socket 被创建后,portnumber 数组却被覆盖掉了。

再仔细阅读才发现,原来是 rabbitmq-c 为了控制 connect 的超时时间,使用了非阻塞 connect ,然后使用 select 判断 fd 是否可写来判定连接是否建立。 select 调用未考虑 sockfd 大于 FD_SETSIZE 。linux 的 select 手册中有这样一行注释:

Anfd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.

这是一个很容易被程序员忽视的坑。FD_SET 其实是一个位数组,linux 默认是 1024 bit 。而 FD_SET 只是简单的把 fd 当作一个序号按位向位数组写数据。select 应该也是如此。所以当 fd 大于 1024 时,就会写越界。正确的方法是当需要 select 的 fd 大于等于 1024 时,最好在堆上分配 FD_SET ,分配空间应至少保证 fd/8 + 1 以上;或者用 poll / WSAPoll 更好。

一般的程序不会有这个问题是因为通常一个进程需要的文件 fd 不会超过 1024 。而我们的系统管理了大量的外部连接,超过 1024 轻而易举。之前,机房间的网络正常,只在程序启动时创建连接一次连接。这时尚无外部连接,这个连接的 fd 一定是小于 1024 的,而之后也不会发生重连。


btw, 当我想去提一个 Issue 时,发现刚好是 2 天前已经有人提了

Comments

遇到一模一样的问题了,server端使用了epoll,程序里面还有个client用了select,重连时fd超过1024,程序崩溃,堆栈被破坏。
果然是大坑,推荐使用epoll来判断socket connect之后的状态,来实现connect超时阻塞的功能。
“只好开始在 core 文件中重新寻找蛛丝马迹。把未损坏的堆栈数据和反编译的代码逐个比对”,调用栈如果都被破坏的情况下,是否还能有其它的检查办法?
RabbitMQ是不是主要就是用于日志的分发?
RabbitMQ是不是主要就是用于日志的分发?
RabbitMQ是不是主要就是用于日志的分发?
RabbitMQ是不是主要就是用于日志的分发?
RabbitMQ是不是主要就是用于日志的分发?
RabbitMQ是不是主要就是用于日志的分发?
@davidxu valgrind可以检查这种越界的,建议@cloud长期在内网测试服跑valgrind
写的不错,不过我记得直接修改大于1024没有用处,libc和内核都要改才行。当然最好选择依然是用epoll
我们也遇到过同样的问题,当时堆栈完全挂了,手工从main函数的偏移找到了第一个函数入口,然后根据每个函数sp的大小一直上推最后找到最上层的函数,看到堆栈完全被覆盖。最后才想到是fd_set的大小超了。我们的方案是直接手工define __FD_SETSIZE,把代码里面所有select需要的fd_set都调成65536/8了
之前在用mosquitto的客户端库时也遇到这个坑,查了很久。
这个 BUG 值得注意。
神奇啊,server端还不用epoll
使用第三方库就得冒这样的风险,尤其是网络层,出了问题还难以调试。
in windows: FD_SETSIZE is the max count of fd in linux: FD_SETSIZE is the max value of fd
所以,大家一般共识的select的fd不能超过1024,是不准确的。实际上,可以超过1024。因为fd_set是个默认的size,可以重新被分配。
看来我记错了,应该是purify可能检测内存问题,valgrind好像只能检测memory leak. :-(
这种情况用valgrind也许可以捕捉到错误,不过速度可能有影响,在紧急情况下也不太适用。平时上线前最好过一下valgrind,也许又帮助。
可能产生1024个以上的fd的场合不该用select啊,低级错误。
NEW BEE的程序员都是血和泪的坑历练出来的~
@cloud 请问您用的是哪个decompiler反编译的代码,以前没试过反编译,想学习一下。谢谢!
过界,常犯的错。
感谢分享! 确实是个常见的坑,redis 在2012年修复过这个问题(换成poll来multiplexing了):https://github.com/antirez/redis/issues/267
Select这个syscall应当obsolete了。

Post a comment

非这个主题相关的留言请到:留言本