MMO服务器开发--多核提高并发能力构想其线程模型设计
MMO服务器开发--多核提高并发能力构想其线程模型设计
前言
目前的架构是epoll单核网络模型,但是所有的请求都在同一个核心中运行。现在想要设计个能支持多核的线程模型。我先来介绍目前的单核架构。项目是mmo网络框架,网络层和业务层解耦,网络层采用epoll reactor架构,业务层采用etc架构,提供场景服务,字节流解析后需要到场景服务中进行消息处理。现在网络层解析完字节流后直接就到场景服务中去执行,是个单线程模型。
需求
设计多线程模型,实现网络层执行和业务层解耦,防止业务层数据处理阻塞到网络层的数据接收。网络层中,连接要实现并发读,同时读写相互不阻塞。业务层也要实现分场景并发处理业务。
多Reactor + 场景Actor模型 / Message Driven
架构总览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+------------------+
| Acceptor Thread |
| (Main Thread) |
+--------+---------+
|
| 分发新连接
v
+-----------------------------+-------------------------------+
| | |
+-------v--------+ +--------v--------+ +--------v--------+
| Reactor Thread | | Reactor Thread | ... | Reactor Thread |
| 1 | | 2 | | N |
| - epoll_wait | | - epoll_wait | | - epoll_wait |
| - 并发读写 | | - 并发读写 | | - 并发读写 |
| - 解析字节流 | | - 解析字节流 | | - 解析字节流 |
| - 投递消息 | | - 投递消息 | | - 投递消息 |
+-------+--------+ +--------+--------+ +--------+--------+
| | |
| (消息队列) | (消息队列) | (消息队列)
v v v
+-------v--------+ +--------v--------+ +--------v--------+
| Scene Actor | | Scene Actor | ... | Scene Actor |
| Scene-1 | | Scene-2 | | Scene-K |
| - 消息循环 | | - 消息循环 | | - 消息循环 |
| - 顺序处理 | | - 顺序处理 | | - 顺序处理 |
| - 无锁执行 | | - 无锁执行 | | - 无锁执行 |
+----------------+ +-----------------+ +-----------------+
🧱 核心组件详解
1. Acceptor 线程(1 个) / 分发连接
- 职责:监听端口,
accept()新连接。 - 行为:使用 轮询(Round-Robin) 或 最少连接数 策略,将新连接分发给某个 Reactor 线程。
- 优势:避免
accept成为瓶颈。
第一个负载均衡的点:连接的负载均衡。首先所有的连接优先级平等,所以连接要被均匀的分配到不同的reactor线程。实现均匀分配的方式有两种,一是round-robin轮询;二是最小连接数策,将新连接分发给某个拥有连接最小的reactor。
2. Reactor 线程池(N 个) / 处理读写事件,消息路由
每个 Reactor 线程内部也是独立运行 epoll 事件循环,职责如下:
✅ 并发读写不阻塞
- 每个连接的读写操作在同一个 Reactor 线程内串行处理,但多个连接之间并行。
- 使用
EPOLLET(边缘触发) + 非阻塞 socket。 - 读操作:
epoll_wait触发后,立即read()到缓冲区,解析成消息对象。 - 写操作:使用 output buffer,消息发送请求先写入 buffer,
epoll监听EPOLLOUT,在可写时批量发送。 - ✅ 读写分离:读在
EPOLLIN回调中处理,写通过EPOLLOUT触发,互不阻塞。
📌 关键点:Reactor 线程只负责 I/O 和基本的字节流分包,绝不执行业务逻辑。
reactor thread 数量如何决定呢?
首先我们要明晰reactor thread的类型,reactor thread 存在大量的网络IO事件,所以是IO密集型,IO密集型的线程池数量最好是核心 * 2。
3. 消息解析线程池
🎯 为什么需要独立的“消息解析线程池”?
- I/O 线程必须轻量化:
read()后应尽快返回,避免阻塞其他连接的收包。 - 解析可能耗时:Protobuf 反序列化、CRC 校验、加密解密等操作可能占用毫秒级 CPU 时间。
- 避免 I/O 线程被拖慢:如果在 Reactor 线程中直接解析,会导致
epoll_wait延迟,影响所有连接。
对比网络层的reactor thread,消息解析分发路由是计算密集型,伴随着大量的计算任务,需要核心数个线程,消息解析分发路由层的线程模型直接使用公平的线程池即可,分发也就使用round-robin。路由层解析完消息就把消息投递到对应的scene actor的mailbox。
4. Scene Actor 模型(业务层)
这是 Actor 模型的核心实现。每个场景(Scene)是一个独立的 Actor。
Actor 特性:
- 封装状态:每个 Scene Actor 持有该场景内的所有玩家、NPC、状态等。
- 消息驱动:只通过接收消息来触发行为。
- 顺序处理:Actor 内部单线程处理消息,无需锁,保证状态一致性。
- 位置透明:网络层无需关心 Actor 运行在哪个线程。
实现方式:
- 每个 Scene Actor 绑定一个 专属线程 或从线程池中调度(推荐绑定线程,减少切换)。
- 每个 Actor 拥有一个 私有消息队列(
boost::lockfree::queue)。 - Actor 线程持续从队列中
pop消息并处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SceneActor {
uint32_t scene_id_;
std::thread thread_;
boost::lockfree::queue<Message> mailbox_;
void Run() {
while (running_) {
Message msg;
if (mailbox_.pop(msg)) {
HandleMessage(msg); // 无锁处理
} else {
std::this_thread::yield(); // 空闲时让出 CPU
}
}
}
};
✅ 分场景并发
- 不同 Scene Actor 运行在不同线程,天然支持跨场景并发。
- 场景间通信通过 跨 Actor 消息投递 实现(如广播、跨场景传送)。
This post is licensed under CC BY 4.0 by the author.