超高性能的物理机
从训练到推理,全栈GPU护航您的AI之旅
安全可靠且超五星的服务器托管服务
海量资源,提供多种线路可选
安全稳定、可弹性扩展的高性能云服务器
火数云3.2Ghz频率高性能独立服务器
快速、稳定、可靠的全球加速服务
从 3 万到 120 万,我们踩过的坑与总结的经验,全在这了。
2023 年初,我们游戏平台迎来了一次“生死大考”。旗下一款竞技类手游日活突破 800 万,晚高峰同时在线玩家峰值达到 470 万。原有服务器架构在 3 万并发时就已经 CPU 告警,扩容到 40 台机器仍频繁出现卡顿、掉线、匹配超时。
运维团队一度考虑继续堆机器,但成本失控。最终,我们决定对单机架构动刀——目标:单机支撑百万并发连接。
三个月后,单台 32 核 64G 服务器稳定支撑 120 万长连接,CPU 利用率不到 60%,消息延迟 P99 控制在 8ms 以内。以下是完整复盘。
旧架构是典型的“一连接一线程”模型:
每个玩家连接创建一个独立线程
线程池默认 2000,扩容到 5000 后上下文切换开销爆炸
大量时间浪费在锁竞争和线程调度上
压测数据很直观:
根本瓶颈不在网络带宽,而在线程模型和内存管理。
我们放弃线程池,改用 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。
旧架构中,每个消息都要加锁写入共享队列。高并发下锁竞争消耗了 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%。
原协议用 JSON over WebSocket,单条消息平均 400 字节,序列化耗时 0.3ms。
改为 Protobuf 二进制协议后,消息体积缩小到 90 字节。更关键的是批量合并:同一帧内多个小消息合并为一个 TCP 包发送。
message GameMessage { uint32 type = 1; bytes payload = 2; repeated uint64 targets = 3; // 批量目标 }
效果:网络 IO 次数减少 70%,带宽占用下降 55%。
默认 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,让多个事件循环线程各自绑定同一端口,由内核负载均衡。
SO_REUSEPORT
将 32 个事件循环线程绑定到 32 个物理核,避免跨 NUMA 节点访问内存。
taskset -cp 0-31 <pid>
配合 numactl --interleave=all,内存访问延迟降低 40%。
numactl --interleave=all
优化完成后,我们用自研压测工具模拟真实玩家行为(登录、匹配、对战、聊天、心跳)。
成本直接下降 90%。
坑 1:epoll 边缘触发漏读ET 模式下必须一次性读完所有数据,否则不会再触发。早期忘记循环读,导致消息丢失。
坑 2:协程栈溢出默认协程栈 8KB,复杂业务逻辑递归后溢出。改为动态扩容栈,并限制递归深度。
坑 3:TIME_WAIT 堆积短连接频繁创建导致端口耗尽。开启 tcp_tw_reuse 并改用长连接心跳保活。
tcp_tw_reuse
坑 4:内存池碎片不同大小对象混用同一内存池,导致碎片率 30%。改为多级内存池,按大小分类。
先定位瓶颈,再动手:用 perf、火焰图找到真正的热点,不要凭感觉优化。
协程 + epoll 是百万并发的基石:线程模型不升级,堆机器也没用。
内存管理决定上限:无锁队列 + 内存池,比任何算法优化都直接。
内核参数是隐藏开关:默认配置只适合小规模,百万连接必须调优。
压测要模拟真实行为:只测连接数没意义,要模拟消息频率、大小、分布。
单机百万并发不是终点。我们正在探索:
io_uring 替代 epoll,进一步降低系统调用开销
DPDK 用户态网络协议栈,绕过内核瓶颈
多机分布式网关,单集群支撑 5000 万连接
架构优化没有银弹,只有对每一个细节的极致打磨。希望这篇复盘能帮到正在经历类似挑战的你。
2023 年初,我们游戏平台迎来了一次“生死大考”。旗下一款竞技类手游日活突破 800 万,晚高峰同时在线玩家峰值达到 470 万。原有服务器架构在 3 万并发时就已经 CPU 告警,扩容到 40 台机器仍频繁出现卡顿、掉线、匹配超时。
运维团队一度考虑继续堆机器,但成本失控。最终,我们决定对单机架构动刀——目标:单机支撑百万并发连接。
三个月后,单台 32 核 64G 服务器稳定支撑 120 万长连接,CPU 利用率不到 60%,消息延迟 P99 控制在 8ms 以内。以下是完整复盘。
一、问题定位:旧架构为什么撑不住?
旧架构是典型的“一连接一线程”模型:
每个玩家连接创建一个独立线程
线程池默认 2000,扩容到 5000 后上下文切换开销爆炸
大量时间浪费在锁竞争和线程调度上
压测数据很直观:
根本瓶颈不在网络带宽,而在线程模型和内存管理。
二、架构重构:五个关键优化
1. 从多线程到协程 + epoll
我们放弃线程池,改用
epoll边缘触发 + 协程调度。每个连接不再绑定线程,而是由少量事件循环线程管理。核心代码逻辑简化如下:
效果:单机可管理连接数从 5 万跃升至 80 万,线程数从 5000 降到 32。
2. 无锁化与内存池
旧架构中,每个消息都要加锁写入共享队列。高并发下锁竞争消耗了 40% 的 CPU。
我们做了两件事:
每线程独立队列:消息按连接哈希到不同事件循环,避免跨线程竞争
内存池预分配:连接对象、消息包、缓冲区全部从内存池分配,消除 malloc/free 抖动
内存池核心结构:
效果:CPU 缓存命中率提升 3 倍,消息处理耗时降低 65%。
3. 协议层优化:二进制 + 批量合并
原协议用 JSON over WebSocket,单条消息平均 400 字节,序列化耗时 0.3ms。
改为 Protobuf 二进制协议后,消息体积缩小到 90 字节。更关键的是批量合并:同一帧内多个小消息合并为一个 TCP 包发送。
效果:网络 IO 次数减少 70%,带宽占用下降 55%。
4. 内核参数调优
默认 Linux 参数完全无法支撑百万连接。我们调整了:
同时开启
SO_REUSEPORT,让多个事件循环线程各自绑定同一端口,由内核负载均衡。5. CPU 亲和性绑定
将 32 个事件循环线程绑定到 32 个物理核,避免跨 NUMA 节点访问内存。
配合
numactl --interleave=all,内存访问延迟降低 40%。三、压测结果:从 3 万到 120 万
优化完成后,我们用自研压测工具模拟真实玩家行为(登录、匹配、对战、聊天、心跳)。
成本直接下降 90%。
四、踩过的坑
坑 1:epoll 边缘触发漏读
ET 模式下必须一次性读完所有数据,否则不会再触发。早期忘记循环读,导致消息丢失。
坑 2:协程栈溢出
默认协程栈 8KB,复杂业务逻辑递归后溢出。改为动态扩容栈,并限制递归深度。
坑 3:TIME_WAIT 堆积
短连接频繁创建导致端口耗尽。开启
tcp_tw_reuse并改用长连接心跳保活。坑 4:内存池碎片
不同大小对象混用同一内存池,导致碎片率 30%。改为多级内存池,按大小分类。
五、可复用的经验总结
先定位瓶颈,再动手:用 perf、火焰图找到真正的热点,不要凭感觉优化。
协程 + epoll 是百万并发的基石:线程模型不升级,堆机器也没用。
内存管理决定上限:无锁队列 + 内存池,比任何算法优化都直接。
内核参数是隐藏开关:默认配置只适合小规模,百万连接必须调优。
压测要模拟真实行为:只测连接数没意义,要模拟消息频率、大小、分布。
六、后续演进
单机百万并发不是终点。我们正在探索:
io_uring 替代 epoll,进一步降低系统调用开销
DPDK 用户态网络协议栈,绕过内核瓶颈
多机分布式网关,单集群支撑 5000 万连接
架构优化没有银弹,只有对每一个细节的极致打磨。希望这篇复盘能帮到正在经历类似挑战的你。