怪物移动响应延迟优化

May 9, 2023

背景

前段时间客户端想做怪物移动表现优化,改善现在怪物移动一顿一顿的感觉,希望服务端配合。客户端反馈服务端下发的怪物移动消息延迟很不稳定,会在几十毫秒到几百毫秒中波动,客户端平滑插值表现完之后,还没有收到服务端下发的下一个移动消息,导致怪物还是会出现移动卡顿的表现。

为了解决这个问题,我花了不少时间排查和优化,因此记录一下。

响应慢的原因

socket线程收包延迟

这里说的延迟不是指网络本身的延迟,因为在内网,所以这里暂时忽略网络部分的延迟。Socket线程的处理逻辑大致如下:

while (!msgpipe.empty()):
    processmsg()

foreach socket :// 怪物移动由怪服控制,怪服也是通过socket连接到游服
    recv a packet;
    send the packet to logic thread;
    send buffering packet;
    
if there is no msg in msgpipe: // <== 问题点
    sleep(40);
else:
    continue;

这个逻辑看上去足够简单,似乎也没什么问题,但是如果仔细分析问题点,就会发现问题:
如果是正常外服,玩家数量很多时,消息管道中可能总是会有新的消息,这时候就不会有延迟。而内网调试时,只有一个客户端,那么就有很大的几率出现管道中没有消息而休眠40ms,这是一个可能导致延迟的重要原因。
但是历史遗留代码这么写,一般可能会有一定的原因(当然也有可能没有😂)。写代码的人早已经离职了,那么就只能通过场景进行合理推断了。

  • 空闲的时候sleep是必要的,否则可能导致线程有非常高的cpu占用。
  • 但是40ms其实过长了,这么写的原因,猜测是为了限制收包的速率,防止收包过快导致业务线程压力太大。

根据上述推断,主要还是为了解决空闲时对cpu的高占用和限流。因此我决定进行如下修改:

  1. 引入令牌桶机制对socket进行限流。每个socket每隔40ms可获得一个令牌,消耗一个令牌可消费一个packet,这样就可以达到原来同等的限流目的,而又不会影响消息的延迟。具体的令牌桶算法可以google,这里就不赘述了。
  2. 同时将消息管道空闲时的休眠时间减少到1ms,加快socket线程的吞吐速率。
    修改后的伪代码如下:
while (!msgpipe.empty()):
    processmsg()

foreach socket :
    if socket has token:
        spendtoken(1);
        recv a packet;
        send the packet to logic thread;
        send buffering packet;

if there is no msg in msgpipe:
    sleep(1);
else:
    continue;

发包缓存延迟

上述socket收到怪服的移动消息后,转发给业务线程处理移动,完成后会向周围的玩家广播移动消息。而业务线程有个消息缓存的机制,会将要发送的消息进行一次聚合,当缓冲达到4K或者下一个定时周期(目前是750ms,这也是个大坑,后面会单独写篇文章来说)触发时,进行发送。很明显,这里一个怪物移动消息,是不可能达到4k的大小的,因此消息会等到下一次定时器触发时才往下推送。由于目前游服的实现机制,定时器最高的精度就是750ms,无法调整,所以只能采用了一个临时方案:

  • 每个消息包处理完成以后,刷一次消息缓存。

这个方案必然会影响降低合包的经济性,但是可以极大的提高响应延迟,根据目前的游服负载来评估,我认为是可以接受的。
这同时也让我下定决心对游服定时器方案进行修改。具体的方案改天再写个文章细说。

额外的优化

在做延迟排查和优化的过程中,发现原来怪服和游服间的socket消息是没有压缩的,因此趁此机会也加上了和客户端消息相同的消息压缩。

总结

经过以上优化,现在怪物移动间隔基本已经能够稳定在250~300ms之间(怪服每隔250ms发起一次移动),目前来看也没有其他的副作用,效果还是比较明显的。