历时 3 年半,项目终于上线啦,打开 tig 翻到仓库的开头,看看提交历史,还是有点感慨的,实在没想到要花这么久。简单聊点线上问题吧

这次上线最刺激的是,开服前半小时,发现昨晚压测的服务器崩掉了,原因是聊天服务内存太大导致 OOM。于是用 memlimit 紧急止血,出问题的方法挂到模块上方便后续线上热更,然后打包,重启,紧赶慢赶在开服前几分钟搞完,提前开服错峰。真的非常非常刺激,当然,线上也没有出现这个问题,我们压测的例子相对比较极端,一大堆人每 30 秒集体跳场景,注册场景聊天的地方又做了锁来保序,平台响应聊天只要有点延迟,就会导致堆积,超时设的 10 秒也有点太长了

上线不久,就发现了黑产在活动,常规操作充值 1 块,然后修改订单金额为 648,这种小伎俩当然是拦下来了。没想到物品合成的地方差点出事故。这个接口一般是客户端发来要合成某某物品,数量为 N。然后服务器拿到 N 和配置里的数目相乘,计算最终所需物品数量,交给背包系统做扣除。为了避免被整,第一版代码我就添加了数量不能为负的限制。结果,线上日志发现有人传了一个巨大的 N 过来,乘以配置中的合成所需数量,就会溢出至负数,也就是合一个物品不仅不扣消耗,还反过来给消耗。幸好,背包里做了扣除物品数量不能为负数的设定,兜底成功

另一个是中间件同学反映的问题,晚间活动的时候游戏的包量骤增,导致 goscon 集群负载到达较高水位,CPU 占了一半,系统软中断占了一半。后面通过临时提高硬件规格解决了问题。晚上脑洞了一下解决方案。之前 goscon 是作为完全透明的一层组件叠加到游戏链路中来的,万一有问题可以马上撤掉,不影响业务。但是公司发展到现在,goscon 已久经考验,赢得了大家的信任,自研的线上游戏基本都经过了 goscon,这时候可以考虑做侵入式的改造了。毕竟,“ 软件架构部分取决于公司组织架构 ”XD

比如一个 boss 移动一下,需要将 100 字节左右的包广播到大几百人的连接上,现在是 skynet 反复写几百个 fd,然后 goscon 再读几百个 fd,再分发出去。游戏人数越多,goscon 和 skynet 的连接数越多,同一帧的广播包数量越多,而连接数和包量到达中高水位的时候,机器的性能下降会比较厉害,没法继续线性增长

脑洞中的解决方案,可能是先做一个特殊的 gate server,负责建立 goscon 之间的通讯,暂时叫 goscon gate。skynet 业务写的时候还是按原有方式写,skynet 业务拿的不是 fd,只是一个整型句柄,这个整型可以交由 goscon gate 解析成 goscon 中对应一个客户端的一个标记。短期目标是业务还是写很多个句柄,但是经网络发出去的时候只会经历有限个连接,比如 10 个连接,包体也经过 goscon gate 做装箱再发送,由 goscon 再拆箱分发,避免发送大量小包。长期目标是提供广播接口给业务,业务直接让 goscon gate 给这数千人发指定包,这样业务只要提交一次,goscon gate 往外也只有一个包,goscon 的收包数量就能直接砍半了

涛神提到我们可以自己做合包,比如攒个 0.5 秒再发。这种会增加延时,之前在 M1 也尝试过,效果不太理想,CPU 比较高。不过也可能是思路不够好,当时是房间里的每个人设置一个发送队列,包先发到这里,定时器定期来检查,有包就发出去。可能好一点的是做两套,非战斗状态下,直接发送,不经过定时器;战斗状态下再走定时器这一套。如果攒的包数量不够,还是会出现小包特别多的情况。攒的太多则会导致包体延时过大,人物移动出现拉扯。还是比较难搞

目前项目内部的消息链路是按广播对象做的合包(参见旧文),发一个消息给网关说给 uuids 发包 payload,但是网关还是需要反复提交到网络。注意这里没有严格保序,房间战斗的信息广播和 box 上的业务包,先后顺序是不确定的,但是网络本身是有序的,所以没法多线程给一个客户端发包,得排队等锁,效率还是不够高

线上事故里自己犯过一个比较蠢的,暂时没想到比较好的检查办法,也记录一下。有个玩法是升级的时候检查下所有图纸,看看有没有后续图纸满足等级解锁的,有就解锁掉。代码大概长这样:

for _,blueprint in pairs(orm) do
    if not condition1(blueprint) then
        goto CONTINUE
    end
    if not condition2(blueprint) then
        goto CONTINUE
    end
    if not condition3(blueprint) then
        goto CONTINUE
    end
    unlock(blueprint)
    ::CONTINUE::
end

写完中间那坨条件后,就忘记了不能直接往被 pair 遍历的 table 里塞元素了,unlock 里嵌套了好几层,最后就是塞个元素进去 orm。线下和 QA 尝试很久都复现不出来,但是现在已经有经验了,凡是只有部分人出现,不是必出的 bug,大概率是哪个位置有个 pair 出问题了。比如指定的遍历顺序会出问题,而每个人在不同的 box(agent)上,内存中的遍历顺序是不固定的。再比如上面这种某个条件满足了会往 pair 中的 table 插入元素,这时候 next 函数的行为是未定义的,有可能没有影响,也有可能跳过了一些元素。如果能够在遍历 orm 的时候,给 orm 打上一个标记,在往里插入元素的时候再检查是否在 pairs,可能就能解决了

上线后,为了应对线上问题,周六加班,又为了处理客服问题,周日又值班了,于是连续上了 12 天班,同时还在赶一个线上匹配的功能,压力非常大,每天晚上都失眠,好崩溃。幸好老婆一直在支持我,不然我就坚持不下去了 …… 算是一段难忘的经历吧 ……