在游戏服务器开发中,对于服务器的性能要求特别高,主要的指标就是整个系统的吞吐量,就是平时所说的QPS。目前使用的服务器都是多核的,想要提升系统的QPS,就需要使用到多线程,但是线程资源对于服务器来说非常的珍贵与稀缺,受到内存与CPU核数的限制,所以要对线程的使用进行合理的划分与管理,才能真挖掘服务器的服务能力。 从整个系统来看,主要的线程分类有以下几类:
- 网络层线程池
- 业务逻辑线程池
- IO处理线程池
网络层线程池
这里说的网络层是指游戏服务与客户端直接建立连接,而不是经过网关转发,当然,即使是经过网关转发,也会有这样一个网络层线程池,它主要是负责与请求方建立连接,监听网络消息的接收与发送,维护连接消息的解码与编码。
对于我们使用Java开发的游戏服务器,是使用Netty框架开发的网络层,这里主要配置两个线程池,一个是boss线程池,一个是work线程池。这里经常被面试官问到的是boss线程池和worker线程池各设置多少个线程合适?暂时先不管多少个线程,要回答这个问题,需要涉及以下几个条件,我们以最理想的条件来说吧:
- 一个游戏服务进程创建的连接数不超过1万。
- work线程池是用来处理channel中的消息的,在处理过程中,没有任何的阻塞操作或长时间任务处理。
基于以上两个条件,boss线程数1,work线程数为2即可。但是这个参数只是用于能考,在真实环境于,还要自己做相关的压力测试,这是上线必不可少的步骤。
业务逻辑线程池
在游戏服务开发中,用户的数据都会在登陆的时候被加载到内部之后,这样在用户操作游戏的过程中,就不会频繁的操作数据库了,数据在内存中即可处理完成。而我们都知道,内存中的数据处理速率是非常快的,而业务逻辑线程池就是用来处理这些数据的,所以它有一个非常明显的特殊要求,即在数据处理过程中,不能有需要长时间处理的大任务,或IO阻塞类的操作。
这类线程池处理的数据一般是来自网络层的消息请求。当网络层收到客户端的请求消息后,完成消息的解码的操作,就会把请求消息丢给业务逻辑线程池来处理,所以,玩家在登陆成功之后,就会加载玩家的数据到内存之中,并由业务逻辑线程池进行管理。
这种类型的线程池一般用于游戏操作比较多且频繁的游戏,比如卡牌类,mmorpg类。像有些小的休闲游戏,数据量本身就不是太大,需要操作的数据请求也不多,就不需要单独分享一个业务逻辑线程池了,可以直接和IO线程池共用一个线程池。
在我的这个单服框架里面,对应的业务逻辑线程池管理类是GameChannelService,在服务启动之后,会根据配置的线程数量,初始化N多个单线程池:
private void initExecutor(int threadCount) {
this.eventExecutors = new EventExecutor[threadCount];
for (int i = 0; i < threadCount; i++) {
eventExecutors[i] = new DefaultEventExecutor();
}
}
这里使用的是Netty的单线程池EventExecutor,创建一个EventExecutor[]是为了方便给某一个连接分配一个单独的EventExecutor,这样这个连接所有的消息都丢在这个单线程池中执行,未执行的消息会在线程池中排队,这样就可以保证同一个连接的消息都是按顺序执行了,也不用加锁了。
为了方便使用,对应于netty的channel,我封装了一个逻辑的GameChannel,会为它分配 一个业务逻辑处理单线程池,它会在用户的连接认证成功之后,与netty的channel绑定,这样netty收到消息之后,就可以把这个消息的处理直接丢到GameChannel之中的单线程池中处理。
public class GameChannel<P extends BasePlayer> {
private static final Logger logger = LoggerFactory.getLogger(GameChannel.class);
private P player;
private Channel nettyChannel;
private EventExecutor executor;
private GameChannelRemoveListener<P> gameChannelRemoveListener;
}
具体的实现,可以参考源码工程:gitee.com/wgslucky/xi…
IO线程池
在游戏服务器开发中,尽管将用户的操作都放在了业务逻辑线程池之中,但是与其它服务的交互也是避免不了的,比如数据库交互,Redis交互,第三方接口服务,以及内部的服务之间的调用等。这些IO操作都是同步的,会阻塞当前线程的执行,等待网络请求返回结果,如果这些请求没有放在单独的IO线程池之中,就会阻塞业务逻辑线程池中的线程执行,降低游戏用户操作的吞吐量,在线用户稍微多一些就会感觉到消息延迟很高。
但是话说回来,一般在一个系统中,IO操作相对占的比例比较低才值得单独设计一个IO线程池来管理这些IO操作,比如10%左右,如果你的游戏服务器50%以上都需要IO操作,那就没有必要再分了,直接和业务逻辑线程池使用同一个就可以了,线程数可以设置多一些,因为在发生IO阻塞时,CPU是空闲的,可以用来执行其它的线程,增加消息处理的并发量。
在我的单服框架之中,管理IO线程池的类是GameIOTaskService,在服务器启动之后,会根据配置的线程数量,初始一个单线程池数组,这个也是netty的EventExecutor:
@PostConstruct
public void init() {
int threadCount = gameServerConfig.getIoThreadCount();
if (threadCount < 1) {
throw new IllegalArgumentException("游戏异步任务线程数必须大于0");
}
executors = new EventExecutor[threadCount];
for (int i = 0; i < threadCount; i++) {
executors[i] = new DefaultEventExecutor(r -> {
return new Thread(r, "游戏异步单线程池");
});
}
}
既然在一个服务器系统中,有这三种不同的线程池处理数据,那么一个不可避免的问题就出现了:如何在多线程之间交互数据且保证数据的线程安全性? 这个下一篇文章再详细介绍!
您对公众号的关注对我的鼓励非常大,它可以给我带来些微薄的收入,让我继续创作,谢谢