最近一段时间,项目增加了小游戏平台的支持,而小游戏的网络要求是 websocket,因此服务器也对 websocket 网络进行了支持。
最近,开始压测新的架构,发现了一个很有意思的问题:
- 当并发较少时,如100人同时在线,一切正常
- 当并发增大,比如增加到1500人,客户端的延迟显著增加,甚至将正常玩的玩家踢下线。
实际感受下来,客户端的延迟能达到1.5s左右,基本不可玩。
于是开始定位这个并发造成的延迟问题。
在现有的架构中,消息流转的方向如下:
client → LB → server → LB → client
整个流程涉及三方:客户端client、负责负载均衡的LB、以及服务器server本身。
这三方都可能会有问题,于是我设计了几个 case 来依次排除:
1)第一个可能就是客户端问题,比如客户端队列调度延迟或者其他bug。排除的方法就是用压测机器人和真实客户端来对比,如果压测机器人也有较高的延迟,那就可以排除client问题。测试下来,机器人统计的延迟也是很高,90%的耗时为900多ms,因此排除客户端client问题。
2)第二个可能是LB问题,有时候如果LB限制了带宽,也可能造成较高的延迟。排除的方法是将LB替换成其他不走LB的形式,在我们K8S服务器集群中是将LB换成 NodePort 的形式供客户端链接。测试下来,换成 NodePort 方式连接后,延迟依然还在,因此排除LB问题。
3)最后一种可能就是服务器server本身的问题。但是之前非小游戏的情况下,压测是正常的,因此我的怀疑点就落到websocket 网络层实现上,当然,还有一个可能是服务器的业务逻辑层的处理比较耗时。
先来看看是否是逻辑层处理比较耗时。
这个比较简单,就是在逻辑处理的入口和出口处增加时间统计。结果是逻辑处理非常快,基本是毫秒级别的耗时,因此排除了逻辑层的问题。
那最后的怀疑点就落到了网络层上。为了证实自己的猜想,我将压测机器人还原为 tcp 网络,发现延迟显著下降到正常水平(10ms以内),于是根据逻辑推理,问题出在 websocket 服务上。
现在的网络层 loop 是这样的流程:
c++
while (IsRunning())
{
// 处理 websocket 网络IO
_ProcWebSocket();
// 处理TCP网络IO (内部TCP通信)
_ProcTcpEpoll();
// 处理逻辑层过来的消息(消息队列)
_ProcNetQueue()
}
根据以往的经验,很可能是调度循环 loop 里处理消息发送的流程积压的比较多,也就是 _ProcWebSocket 调度时间不足导致的。
查看代码,发现是调用的是 RunOnce, 它的实现也比较简单:
arduino
void CWSServer::RunOnce()
{
m_endpoint.poll_one();
}
翻看 websocket 库文档后,发现除了 poll_one 之外还有个 poll,他们的区别就是一次处理多少 websocket 的IO 事件:

很明显,一次循环调度一次 poll_one 造成了底层IO 的挤压,我调试时候发现,并发超过1500人之后,poll_one 调用10次都不够。
最终的修改方案也比较简单,使用 poll 来尽量多的调度触发的事件,有多少调用多少,类似 epoll(获取有多少个事件,然后依次处理):
arduino
void CWSServer::RunOnce()
{
// Start the Asio io_service run loop
// 非阻塞调用,会一次处理多个事件
m_endpoint.poll();
}
修改完毕之后,大并发下,延迟下降到10ms,基本满足需求。
更进一步:这次问题的定位还是比较耗时的,如果引入一下消息追踪的工具,能较快的定位到延迟出在哪个环节; 另外,如果能增加一些消息延迟的监控或者统计,能更快发现问题。