找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1186

积分

0

好友

210

主题
发表于 3 天前 | 查看: 7| 回复: 0

上一节介绍的TCP服务器中,即使使用了epoll I/O多路复用技术,仍然难以达到百万级别的并发处理能力。本文将在原有代码基础上,通过系统性的瓶颈排查与内核参数调优,最终实现一个能够支撑百万连接的TCP服务器。

何为并发量?
并发量通常指服务器每秒能够处理的客户端连接请求数量(QPS)。我们首先对上一节使用epoll实现的单端口TCP服务器进行压力测试。

测试环境准备

  1. 虚拟机配置:建议使用本机虚拟机进行测试,以减小网络延迟带来的影响。共需四台:
    • 服务器:1台,建议配置为4G内存、2核CPU(Ubuntu_130)。
    • 客户端:3台,建议配置为2G内存、1核CPU(Ubuntu_128/131/132)。

客户端虚拟机文件夹列表

  1. 服务端启动:在服务器上编译并运行TCP_Server.c

    gcc -o TCP_Server TCP_Server.c
    ./TCP_Server 8888
  2. 客户端测试:在三台客户端虚拟机上同时运行压力测试程序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

客户端连接被拒绝错误
服务器连接数达到1024

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

使用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

    limits.conf配置文件修改示例

瓶颈二:Cannot assign requested address (端口耗尽)

解决了文件描述符限制后,客户端在连接数达到约2万时出现新错误:Cannot assign requested address

客户端地址无法分配错误
服务器连接数卡在2万级别

原因分析:一个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;
}

基于epoll与C语言的TCP服务器:突破百万并发连接的瓶颈分析与调优实践 - 图片 - 1

瓶颈三:Connection timed out (连接跟踪表限制)

启用多端口后,连接数接近65535时,出现 Connection timed out 错误。

连接超时错误

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

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

cat /proc/sys/net/netfilter/nf_conntrack_max

查看nf_conntrack_max值

解决方案:修改客户端和服务器端的 nf_conntrack_max 值。

  1. 编辑 /etc/sysctl.conf 文件,在末尾追加:
    net.nf_conntrack_max = 1048576
  2. 使配置生效:
    sudo sysctl -p

    执行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等中间件),类似的系统级参数调优也是构建高性能服务的基础。




上一篇:锂电池SOC主动均衡控制:基于双向反激变换器的六串电池组设计与实现
下一篇:校园二手交易小程序:解决学生痛点,构建数字社区新生态
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 20:12 , Processed in 0.132360 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表