如何在香港服务器的CentOS 7环境中通过调整内核参数优化epoll性能,支撑百万级并发连接?

几个月前,我在香港某机房部署了一组用于实时消息推送的业务集群。那几天机房里空调的风声混着硬盘阵列的噪音,耳边一直响着网络监控的报警声。我们需要支撑百万级长连接,而这批业务服务器跑在CentOS 7上,I/O 调度和 epoll 处理成了最大的瓶颈。
当时的情况是:前端接入层不断报延迟,客户端掉线重连频繁,业务端口的 SYN_RECV 状态堆积,系统负载飙升到 30+。我一边看着 top,一边直觉告诉我这是内核参数和 epoll 调度策略的问题。于是我开启了那几天的“硬核调优模式”。
硬件与系统环境
- 机房位置:香港沙田电信机房
- 服务器型号:Dell PowerEdge R740
- CPU:Intel Xeon Gold 6338 × 2 (64 vCPU)
- 内存:512 GB DDR4 ECC
- 存储:NVMe SSD(三星 PM1733,2TB × 2,RAID1)
- 网卡:Mellanox ConnectX-5 25GbE × 2
- 系统版本:CentOS Linux release 7.9.2009 (Core)
- 内核版本:3.10.0-1160.el7.x86_64
这套硬件理论上足够,但默认的 CentOS 7 内核参数远远不适合百万连接场景。
调优目标
- 提高文件描述符上限,避免 epoll EMFILE 报错。
- 优化 TCP 参数,降低 TIME_WAIT 和队列堆积。
- 调整 epoll 等待策略,减少上下文切换。
- 内核级别参数优化,提升百万连接时的稳定性。
内核参数优化过程
1. 文件描述符限制
默认的 nofile 太小,百万连接至少需要上千万 FD。
cat >> /etc/security/limits.conf <<EOF
* soft nofile 1048576
* hard nofile 1048576
EOF
ulimit -n 1048576
验证:
cat /proc/sys/fs/file-max
# 输出 > 2097152 表示可支持
2. sysctl.conf 调整
我当时用表格记录了关键参数和调整前后的差异:
| 参数名 | 默认值 | 调整值 | 作用 |
|---|---|---|---|
net.core.somaxconn |
128 | 65535 | 提高 listen 队列长度 |
net.ipv4.tcp_max_syn_backlog |
2048 | 65535 | 提升半连接队列容量 |
net.ipv4.ip_local_port_range |
32768–61000 | 1024–65535 | 扩大可用端口范围 |
net.ipv4.tcp_tw_reuse |
0 | 1 | 允许复用 TIME_WAIT 连接 |
net.ipv4.tcp_fin_timeout |
60 | 15 | 缩短 FIN-WAIT-2 时间 |
net.core.netdev_max_backlog |
1000 | 65535 | 增加网卡接收队列缓冲 |
net.core.rmem_max |
212992 | 16777216 | 增大 socket 接收缓冲 |
net.core.wmem_max |
212992 | 16777216 | 增大 socket 发送缓冲 |
配置写入 /etc/sysctl.conf:
cat >> /etc/sysctl.conf <<EOF
fs.file-max = 2097152
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 65535
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
EOF
sysctl -p
3. epoll 优化
应用层在 epoll 使用中,要避免 ET (Edge Triggered) 模式下的饿死问题。我的经验是:
高并发场景下尽量使用 LT (Level Triggered) 配合非阻塞 IO。
批量读取事件:epoll_wait 的 maxevents 调大,比如 4096。
示例代码片段(C语言):
int epfd = epoll_create1(0);
struct epoll_event events[4096];
int nfds = epoll_wait(epfd, events, 4096, 1000);
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
handle_read(events[i].data.fd);
}
if (events[i].events & EPOLLOUT) {
handle_write(events[i].data.fd);
}
}
遇到的坑与解决
坑1:TIME_WAIT 过多
在压测时发现 ss -s 显示有几十万个 TIME_WAIT。最初以为是代码没关连接,后来确认是 短连接压测脚本导致的。
解决办法:开启 tcp_tw_reuse 并调整 tcp_fin_timeout。效果立竿见影,TIME_WAIT 数量骤降。
坑2:网卡中断绑核不均
/proc/interrupts 里看到某几个 CPU 核心中断数暴涨,导致 CPU 不均衡。
解决办法:使用 irqbalance 或手动 smp_affinity 绑核,把 25GbE 网卡的中断均匀分布到 NUMA 节点上。
echo ffff,ffff,ffff,ffff > /proc/irq/XX/smp_affinity
坑3:epoll_wait CPU 占用过高
最初 epoll_wait 触发的 wakeup 太频繁。后来把 epoll_wait 的超时调大到 1000ms,结合批量事件处理,CPU 使用率下降了 30%。
压测结果
在优化完成后,我们用 wrk + 自研压测工具 做了百万长连接测试。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| QPS | ~50k | ~200k |
| 平均延迟 | 220ms | 45ms |
| TIME_WAIT 数 | 600k+ | < 50k |
| 系统负载 | 35+ | 8–10 |
最终,集群在百万连接情况下仍然能保持稳定,CPU 使用率控制在合理范围。
这次调优让我深刻体会到,CentOS 7 默认内核参数远远不适合百万级连接的场景。很多时候,问题不在代码层,而在内核的 socket、文件描述符和 epoll 调度机制上。
调优是一种“实战艺术”,需要结合监控指标、日志和 strace、perf 这些工具去逐步拆解瓶颈。现在回想起当时在香港机房调参到凌晨三点的经历,虽然满身疲惫,但看到百万连接稳定跑起来时,心里那股成就感依然很鲜明。
附录
A. sysctl.conf 配置模板
以下是我在 CentOS 7 香港服务器上实际使用的 sysctl.conf 调优配置,经过压测验证,能支撑百万级连接。
# 文件句柄最大值
fs.file-max = 2097152
# socket 队列调优
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# TCP 连接调优
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_syncookies = 1
# 避免端口耗尽
net.ipv4.tcp_max_tw_buckets = 1048576
# 减少 keepalive 探测延迟
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
应用配置:
sysctl -p
B. 压测脚本示例
在调优后,我用 wrk 和自研脚本同时进行压测。这里附上一个简单的 Python 脚本,可以模拟大量长连接。
Python 长连接压测脚本
import socket
import time
import threading
TARGET_IP = "192.168.1.100"
TARGET_PORT = 8080
CONNECTIONS = 1000000 # 模拟百万连接
THREADS = 100
def keep_alive():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
try:
s.connect((TARGET_IP, TARGET_PORT))
while True:
time.sleep(60)
s.send(b'ping\n')
except Exception as e:
pass
def worker():
for _ in range(CONNECTIONS // THREADS):
threading.Thread(target=keep_alive, daemon=True).start()
for _ in range(THREADS):
threading.Thread(target=worker).start()
print(f"模拟建立 {CONNECTIONS} 个长连接...")
while True:
time.sleep(60)
C. wrk 命令示例
wrk 更适合做高 QPS 短连接压测:
wrk -t16 -c1000000 -d60s http://192.168.1.100:8080/
参数说明:
-t16:16 个线程
-c1000000:100 万并发连接
-d60s:持续压测 60 秒
D. 验证与监控
压测过程中,我主要通过以下命令监控:
# 查看 TCP 连接状态
ss -s
# 查看监听队列
netstat -antp | grep :8080 | wc -l
# 监控 CPU/内存
top -H -p $(pidof your_app)
# 查看中断分布
cat /proc/interrupts | grep eth
这样,完整的配置 + 脚本,就能帮助后来者在类似的 CentOS 7 + 香港服务器场景中,快速复现百万级并发连接下的 epoll 优化实践