JavaScript is required
新闻中心
7*24 小时获取专业工程师的帮助,快速解决您的问题
关注获取即时动态
< 返回

单机百万并发:某游戏平台服务器架构优化全复盘

发布时间:2026-09-18 15:24:52   访问量:6

从 3 万到 120 万,我们踩过的坑与总结的经验,全在这了。

2023 年初,我们游戏平台迎来了一次“生死大考”。旗下一款竞技类手游日活突破 800 万,晚高峰同时在线玩家峰值达到 470 万。原有服务器架构在 3 万并发时就已经 CPU 告警,扩容到 40 台机器仍频繁出现卡顿、掉线、匹配超时。

运维团队一度考虑继续堆机器,但成本失控。最终,我们决定对单机架构动刀——目标:单机支撑百万并发连接

三个月后,单台 32 核 64G 服务器稳定支撑 120 万长连接,CPU 利用率不到 60%,消息延迟 P99 控制在 8ms 以内。以下是完整复盘。

一、问题定位:旧架构为什么撑不住?

旧架构是典型的“一连接一线程”模型:

  • 每个玩家连接创建一个独立线程

  • 线程池默认 2000,扩容到 5000 后上下文切换开销爆炸

  • 大量时间浪费在锁竞争和线程调度上

压测数据很直观:

并发连接数CPU 使用率平均延迟掉线率
1 万35%12ms0.1%
3 万78%45ms1.2%
5 万95%220ms8.7%

根本瓶颈不在网络带宽,而在线程模型内存管理

二、架构重构:五个关键优化

1. 从多线程到协程 + epoll

我们放弃线程池,改用 epoll 边缘触发 + 协程调度。每个连接不再绑定线程,而是由少量事件循环线程管理。

核心代码逻辑简化如下:

// epoll 事件循环
int epfd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

while (1) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        if (events[i].data.fd == listen_fd) {
            accept_conn();
        } else {
            // 投递到协程调度器
            coroutine_resume(events[i].data.ptr);
        }
    }
}

效果:单机可管理连接数从 5 万跃升至 80 万,线程数从 5000 降到 32。

2. 无锁化与内存池

旧架构中,每个消息都要加锁写入共享队列。高并发下锁竞争消耗了 40% 的 CPU。

我们做了两件事:

  • 每线程独立队列:消息按连接哈希到不同事件循环,避免跨线程竞争

  • 内存池预分配:连接对象、消息包、缓冲区全部从内存池分配,消除 malloc/free 抖动

内存池核心结构:

typedef struct {
    void *blocks;
    size_t block_size;
    int free_count;
    void **free_list;
} mem_pool_t;

void* pool_alloc(mem_pool_t *pool) {
    if (pool->free_count == 0) return NULL;
    void *ptr = pool->free_list[--pool->free_count];
    return ptr;
}

效果:CPU 缓存命中率提升 3 倍,消息处理耗时降低 65%。

3. 协议层优化:二进制 + 批量合并

原协议用 JSON over WebSocket,单条消息平均 400 字节,序列化耗时 0.3ms。

改为 Protobuf 二进制协议后,消息体积缩小到 90 字节。更关键的是批量合并:同一帧内多个小消息合并为一个 TCP 包发送。

message GameMessage {
    uint32 type = 1;
    bytes payload = 2;
    repeated uint64 targets = 3;  // 批量目标
}

效果:网络 IO 次数减少 70%,带宽占用下降 55%。

4. 内核参数调优

默认 Linux 参数完全无法支撑百万连接。我们调整了:

# 文件描述符
ulimit -n 2000000
echo 2000000 > /proc/sys/fs/nr_open

# TCP 优化
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_rmem = 4096 87380 6291456
net.ipv4.tcp_wmem = 4096 65536 6291456

# 网卡多队列
ethtool -L eth0 combined 32

同时开启 SO_REUSEPORT,让多个事件循环线程各自绑定同一端口,由内核负载均衡。

5. CPU 亲和性绑定

将 32 个事件循环线程绑定到 32 个物理核,避免跨 NUMA 节点访问内存。

taskset -cp 0-31 <pid>

配合 numactl --interleave=all,内存访问延迟降低 40%。

三、压测结果:从 3 万到 120 万

优化完成后,我们用自研压测工具模拟真实玩家行为(登录、匹配、对战、聊天、心跳)。

指标优化前优化后
单机最大并发3 万120 万
CPU 使用率(100万连接)58%
内存占用(100万连接)22GB
平均延迟45ms3.2ms
P99 延迟220ms8ms
掉线率8.7%0.02%
服务器数量40 台4 台

成本直接下降 90%。

四、踩过的坑

坑 1:epoll 边缘触发漏读
ET 模式下必须一次性读完所有数据,否则不会再触发。早期忘记循环读,导致消息丢失。

坑 2:协程栈溢出
默认协程栈 8KB,复杂业务逻辑递归后溢出。改为动态扩容栈,并限制递归深度。

坑 3:TIME_WAIT 堆积
短连接频繁创建导致端口耗尽。开启 tcp_tw_reuse 并改用长连接心跳保活。

坑 4:内存池碎片
不同大小对象混用同一内存池,导致碎片率 30%。改为多级内存池,按大小分类。

五、可复用的经验总结

  1. 先定位瓶颈,再动手:用 perf、火焰图找到真正的热点,不要凭感觉优化。

  2. 协程 + epoll 是百万并发的基石:线程模型不升级,堆机器也没用。

  3. 内存管理决定上限:无锁队列 + 内存池,比任何算法优化都直接。

  4. 内核参数是隐藏开关:默认配置只适合小规模,百万连接必须调优。

  5. 压测要模拟真实行为:只测连接数没意义,要模拟消息频率、大小、分布。

六、后续演进

单机百万并发不是终点。我们正在探索:

  • io_uring 替代 epoll,进一步降低系统调用开销

  • DPDK 用户态网络协议栈,绕过内核瓶颈

  • 多机分布式网关,单集群支撑 5000 万连接

架构优化没有银弹,只有对每一个细节的极致打磨。希望这篇复盘能帮到正在经历类似挑战的你。