在上一节介绍的TCP服务器中,即使使用了epoll I/O多路复用技术,仍然难以达到百万级别的并发处理能力。本文将在原有代码基础上,通过系统性的瓶颈排查与内核参数调优,最终实现一个能够支撑百万连接的TCP服务器。
何为并发量?
并发量通常指服务器每秒能够处理的客户端连接请求数量(QPS)。我们首先对上一节使用epoll实现的单端口TCP服务器进行压力测试。
测试环境准备
- 虚拟机配置:建议使用本机虚拟机进行测试,以减小网络延迟带来的影响。共需四台:
- 服务器:1台,建议配置为4G内存、2核CPU(Ubuntu_130)。
- 客户端:3台,建议配置为2G内存、1核CPU(Ubuntu_128/131/132)。

-
服务端启动:在服务器上编译并运行TCP_Server.c。
gcc -o TCP_Server TCP_Server.c
./TCP_Server 8888
-
客户端测试:在三台客户端虚拟机上同时运行压力测试程序mul_port_client_epoll.c。
gcc -o mul_port_client_epoll mul_port_client_epoll.c
./mul_port_client_epoll 192.168.66.130 8888
测试过程中,我们陆续遇到了以下几个关键瓶颈。
瓶颈一:Connection refused (连接数限制)
当服务器连接数达到约1024个时,客户端开始报错 connect: Connection refused。


原因分析:Linux系统默认限制每个进程最多只能打开1024个文件描述符(包括socket)。使用 ulimit -a 命令可以查看当前限制。

解决方案:修改 open files 限制。
-
临时生效(重启后失效):
sudo su
ulimit -n 1048576 # 临时修改为约100万
-
永久生效:编辑 /etc/security/limits.conf 文件。
sudo vim /etc/security/limits.conf
在文件末尾添加以下两行,然后重启系统。
* hard nofile 1048576
* soft nofile 1048576

瓶颈二:Cannot assign requested address (端口耗尽)
解决了文件描述符限制后,客户端在连接数达到约2万时出现新错误:Cannot assign requested address。


原因分析:一个sockfd对应一个网络连接的五元组(源IP、源端口、目标IP、目标端口、协议)。当客户端机器作为连接发起方时,其本地可用端口号(通常约3万个)会被快速耗尽,导致无法创建新的sockfd。
解决方案:增加服务器监听的端口数量。我们让服务器同时监听100个端口(例如8888-8987),将连接分散到不同的端口上,从而突破单个客户端端口数量的限制。
核心思路:创建100个监听socket,加入epoll事件集。在事件循环中,判断触发的fd是否为监听socket,再进行accept操作。
以下是为实现多端口监听而修改后的服务器核心代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define BUFFER_LENGTH 1024
#define EPOLL_SIZE 1024
#define MAX_PORT 100 // 监听端口数量
// 判断fd是否为监听端口fd
int islistenfd(int fd, int *fds) {
int i = 0;
for (i = 0; i < MAX_PORT; i++) {
if (fd == *(fds + i)) return fd;
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <start_port>\n", argv[0]);
return -1;
}
int start_port = atoi(argv[1]);
int sockfds[MAX_PORT] = {0}; // 存储所有监听fd
int epfd = epoll_create(1);
// 创建并绑定 MAX_PORT 个监听socket
for (int i = 0; i < MAX_PORT; i++) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(start_port + i); // 端口递增
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
perror("bind");
return 2;
}
if (listen(sockfd, 5) < 0) {
perror("listen");
return 3;
}
printf("TCP server listening on port: %d\n", start_port + i);
// 将监听socket加入epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
sockfds[i] = sockfd;
}
struct epoll_event events[EPOLL_SIZE] = {0};
while (1) {
int nready = epoll_wait(epfd, events, EPOLL_SIZE, 5);
if (nready == -1) continue;
for (int i = 0; i < nready; i++) {
int event_fd = events[i].data.fd;
// 判断是否为监听socket
if (islistenfd(event_fd, sockfds)) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int clientfd = accept(event_fd, (struct sockaddr*)&client_addr, &client_len);
// 设置客户端socket为非阻塞模式
fcntl(clientfd, F_SETFL, O_NONBLOCK);
// 设置地址重用,避免TIME_WAIT状态影响
int reuse = 1;
setsockopt(clientfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
// 将新的客户端连接加入epoll,并设置边缘触发(ET)模式
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else {
// 处理客户端数据
int clientfd = event_fd;
char buffer[BUFFER_LENGTH] = {0};
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (len < 0) {
// 错误处理,关闭连接并从epoll移除
close(clientfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL);
} else if (len == 0) {
// 客户端关闭连接
close(clientfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL);
} else {
printf("Recv: %s, %d byte(s), clientfd: %d\n", buffer, len, clientfd);
}
}
}
}
return 0;
}

瓶颈三:Connection timed out (连接跟踪表限制)
启用多端口后,连接数接近65535时,出现 Connection timed out 错误。

检查系统文件打开数上限,已远超65k,排除此原因。

真正原因:Linux内核的连接跟踪表(Conntrack)限制了最大并发连接数。该参数为 nf_conntrack_max。
cat /proc/sys/net/netfilter/nf_conntrack_max

解决方案:修改客户端和服务器端的 nf_conntrack_max 值。
- 编辑
/etc/sysctl.conf 文件,在末尾追加:
net.nf_conntrack_max = 1048576
- 使配置生效:
sudo sysctl -p

可能遇到的错误:如果执行 sudo sysctl -p 时报错 Unknown key,需要先加载 ip_conntrack 内核模块。
sudo modprobe ip_conntrack
sudo sysctl -p

瓶颈四:内存耗尽 (TCP缓冲区优化)
解决了上述问题后,在向百万连接冲刺时,服务器可能因内存占用过高而崩溃。这是因为每个TCP连接都拥有独立的发送和接收缓冲区,默认缓冲区较大,在百万连接下会消耗巨量内存。
解决方案:优化TCP协议栈的内存参数。
编辑 /etc/sysctl.conf,追加以下配置,以显著减少单个连接的内存开销:
# TCP协议栈内存页(4K)限制,分别对应 low, pressure, high 水位
net.ipv4.tcp_mem = 252144 524288 786432
# TCP发送缓冲区大小(字节):最小值,默认值,最大值
net.ipv4.tcp_wmem = 1024 1024 2048
# TCP接收缓冲区大小(字节):最小值,默认值,最大值
net.ipv4.tcp_rmem = 1024 1024 2048
配置生效命令(建议每次测试前执行):
sudo modprobe ip_conntrack
sudo sysctl -p
完成所有调优后,在测试环境中(确保服务器有足够内存,例如关闭不必要的服务如MySQL),重新运行压力测试。

最终,服务器成功突破了百万连接大关!



总结与延伸
通过本次实践,我们系统地解决了Linux系统下实现TCP高并发服务器的四个核心瓶颈:进程文件描述符限制、客户端端口耗尽、内核连接跟踪表限制以及TCP缓冲区内存消耗。这不仅仅是C语言和epoll的应用,更涉及深入的Linux系统调优知识。在实际生产环境中(如使用Nginx、Redis等中间件),类似的系统级参数调优也是构建高性能服务的基础。