一起 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 天前已经有人提了 。