09_百万并发的TCP服务器
百万并发的TCP服务器介绍
上一节已经介绍了TCP服务器的两个版本的实现,跳转:08_TCP服务器:一请求一线程 & epoll。
但是即使是使用了epoll也无法做到100万的并发量。本节将针对上一节 08_TCP服务器:一请求一线程 & epoll的代码基础上进行优化,最终实现百万并发的TCP服务器。
并发量是什么?
并发量指的是一秒钟处理的客户端的请求数量,即qbs。
我们首先来测试一下上节使用epoll实现的TCP服务器的TCP_Server.c代码的并发数量, 运行该程序需要指定服务器的端口 (只有1个端口)。
准备工作:
-
准备四台虚拟机(不建议使用云服务器,因为网络连接的时间开销很大,建议使用本机的虚拟机):
- 其中一台4G内存,2核CPU (最低配置,当然越大越好)服务器Server的作用:Ubuntu_130
- 其他三台2G内存,1核CPU (最低配置) 客户端Client的作用:Ubuntu_128/131/132

-
服务器运行代码
TCP_Server.c(可在08_TCP服务器:一请求一线程 & epoll获取)
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 服务器ip地址 8888
陆续发现以下几个问题。
一. Connection refused问题
当运行客户端测试代码,当服务器连接数达到1024时(clientfd从0开始),此时客户端报错connection refused.


服务器连接数到达1024后不允许连接,原因是Linux的文件系统默认每个进程的fd最多只可以有1024个。
我们可以使用ulimit -a命令可以查看。

解决方法: 修改open files, 有以下两种方案:
- 使用
ulimit -n来修改, 这种方式是临时生效的方案。
su #切换管理员
ulimit -n 1048576 # 临时修改至1024*1024 => 100w
- 编辑
/etc/security/limits.conf,永久生效方案。
sudo vim /etc/security/limits.conf
在文件尾部追加, 然后reboot重启即可生效:
* hard nofile 1048576
* soft nofile 1048576

二. Cannot assign requested address
即使我们已经修改了open files,但是再次测试,在客户端上发现了新的问题Cannot assign requested address, 提示连接请求的地址被占用。

服务器最大连接数量当前为2万级别:

那么这个问题指的是客户端的地址还是服务端地址呢?
思考:sockfd与网络地址之间有什么关系? \(\Rightarrow\) sockfd与ip地址之间有什么关系?
当我们进行send、recv时,传参中的sockfd,包含了五个信息,可以表示为五元组:
sockfd \(\leftarrow \rightarrow\) (远程ip, 远程端口,本机ip,本机端口,协议proto)
那这就会有一种情况,当五元组被用完了,sockfd也就不能继续分配了。
当前版本的代码里,服务器的远程ip和远程端口192.168.66.130:8888已经确定, 对于客户端来说,ip地址肯定不变,那只有一种情况,客户端的本机端口被用完了,不能再分配了。
解决方法:增加服务器的端口数量 \(\Rightarrow\) 开100个端口
使用循环,创建100个listen端口,sockfd保存在sockfds数组里,然后在主事件中判断当前epoll可读的sockfd是否为listen_fd,即判断是否在sockfds数组里。
TCP服务器-100个端口代码
#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
int islistenfd(int fd, int *fds) {
int i = 0;
for (i = 0;i < MAX_PORT;i ++) {
if (fd == *(fds+i)) return fd;
}
return 0;
}
// ./tcp_server 8888
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Param Error\n");
return -1;
}
int port = atoi(argv[1]); // start
int sockfds[MAX_PORT] = {0}; // listen fd
int epfd = epoll_create(1);
int i = 0;
for (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(port+i); // 8888 8889 8890 8891 .... 8987
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 listen on port : %d\n", port + i);
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); // -1, 0, 5
if (nready == -1) continue;
int i = 0;
for (i = 0;i < nready;i ++) {
// 判断当前sockfd是否为listen,即是否在数组sockfd[]里
int sockfd = islistenfd(events[i].data.fd, sockfds);
if (sockfd) { // listen 2
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len = sizeof(client_addr);
int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
fcntl(clientfd, F_SETFL, O_NONBLOCK);
int reuse = 1;
setsockopt(clientfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else {
int clientfd = events[i].data.fd;
char buffer[BUFFER_LENGTH] = {0};
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (len < 0) {
close(clientfd);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
} else if (len == 0) { // disconnect
close(clientfd);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
} else {
printf("Recv: %s, %d byte(s), clientfd: %d\n", buffer, len, clientfd);
}
}
}
}
return 0;
}
三. Connection timed out
客户端出现Connection timed out问题

- sockfd很像65535,先检查是否为文件系统的配置问题,发现 $ >100w$, 排除。

- 检查是否与防火墙有关
nf_conntrack_max:是 Linux 内核中控制连接跟踪系统(Conntrack)最大条目数的关键参数, 也就是防火墙设置了对外连接的最大数目。
cat /proc/sys/net/netfilter/nf_conntrack_max

修改客户端和服务器的nf_conntrack_max:
vim /etc/sysctl.conf # 在最后追加 net.nf_conntrack_max = 1048576
sudo sysctl -p # 生效

如果服务器未修改nf_conntrack_max,会报错误Cannot open /porc/meminfo: Too many open files in system。
不过使用sudo sysctl -p生效可能会报下面的错误:

需要使用下面的命令:
sudo modprobe ip_conntrack
sudo sysctl -p # 生效
四. 内存分配
使用上述代码,重新运行,还是会有遇到新问题,原因是服务器的内存占用拉满,崩溃了。
当服务器的CPU占用率急剧增加的时候,系统会触发系统本身的保护,检测出来哪个进程CPU占用率过高,然后系统会自动回收 内存回收。
因为程序每个fd都有一个tcp接收缓冲区和tcp发送缓冲区。而默认的太大了,导致Linux内存不足,进程被杀死,所以我们需要适当的缩小。进程空间,代码段,堆栈都是要占用内存的。
我们只需要对net.ipv4.tcp_mem,net.ipv4.tcp_wmem,net.ipv4.tcp_rmem进行适合的修改即可
同样的,编辑sysctl.conf文件,sudo vim /etc/sysctl.conf, 在后面追加这几个参数的配置。
net.ipv4.tcp_mem = 252144 524288 786432
# tcp协议栈的大小,单位为内存页(4K),分别是 1G 2G 3G,如果大于2G,tcp协议栈会进行一定的优化,进行内存回收
net.ipv4.tcp_wmem = 1024 1024 2048
# tcp接收缓存区(用于tcp接受滑动窗口)的最小值,默认值和最大值(单位byte)1k 1k 2k,每一个连接fd都有一个接收缓存区
net.ipv4.tcp_rmem = 1024 1024 2048
# tcp发送缓存区(用于tcp发送滑动窗口)的最小值,默认值和最大值(单位byte)1k 1k 2k,每一个连接fd都有一个发送缓存区
同样使用下面的命令生效,建议每次运行前都运行一下,不然重启虚拟机,可能出现未生效的情况。
sudo modprobe ip_conntrack
sudo sysctl -p # 生效
然后重新运行测试,由于我的服务器虚拟机版本是ubuntu 22.04,修改完后,只能达到85万的连接数,于是我通过htop后台打开,发现有许多其他进程占用内存资源(比如我之前安装的mysql占了600Mb),于是我给它又分配了一个gb的内存重新测试。

此时,我们再运行测试,最终目标完成了,达到了100w并发量!

服务器的极限连接数量为1020103,因为客户端的测试命令最多只能达到340000,最终我们实现了百万连接的TCP服务器!



浙公网安备 33010602011771号