Linux服务器百万并发实现与问题排查

前言

  实现一台服务器的百万并发,服务器支撑百万连接会出现哪些问题,如何排查与解决这些问题 是本文的重点
  百万并发的这个并发,很多人理解为与服务器同时建立连接的数量,其实不够准确。不仅仅是同时连接还需要一些别的条件。
  服务器的并发量:一个服务器能够同时承载客户端的数量;
  承载:服务器能够稳定的维持这些连接,能够响应请求,在200ms内返回响应就认为是ok的,其中这200ms包括数据库的操作,网络带宽,内存操作,日志等时间。

客服端代码

  这个代码为接下来测试做准备

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>


#define MAX_BUFFER		128
#define MAX_EPOLLSIZE	(384*1024)
#define MAX_PORT		100

#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

int isContinue = 0;

static int ntySetNonblock(int fd) {
	int flags;

	flags = fcntl(fd, F_GETFL, 0);
	if (flags < 0) return flags;
	flags |= O_NONBLOCK;
	if (fcntl(fd, F_SETFL, flags) < 0) return -1;
	return 0;
}

static int ntySetReUseAddr(int fd) {
	int reuse = 1;
	return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
}



int main(int argc, char **argv) {
	if (argc <= 2) {
		printf("Usage: %s ip port\n", argv[0]);
		exit(0);
	}

	const char *ip = argv[1];
	int port = atoi(argv[2]);
	int connections = 0;
	char buffer[128] = {0};
	int i = 0, index = 0;

	struct epoll_event events[MAX_EPOLLSIZE];
	
	int epoll_fd = epoll_create(MAX_EPOLLSIZE);
	
	strcpy(buffer, " Data From MulClient\n");
		
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));
	
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(ip);

	struct timeval tv_begin;
	gettimeofday(&tv_begin, NULL);

	while (1) {
		if (++index >= MAX_PORT) index = 0;
		
		struct epoll_event ev;
		int sockfd = 0;

		if (connections < 340000 && !isContinue) {
			sockfd = socket(AF_INET, SOCK_STREAM, 0);
			if (sockfd == -1) {
				perror("socket");
				goto err;
			}

			//ntySetReUseAddr(sockfd);
			addr.sin_port = htons(port+index);

			if (connect(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
				perror("connect");
				goto err;
			}
			ntySetNonblock(sockfd);
			ntySetReUseAddr(sockfd);

			sprintf(buffer, "Hello Server: client --> %d\n", connections);
			send(sockfd, buffer, strlen(buffer), 0);

			ev.data.fd = sockfd;
			ev.events = EPOLLIN | EPOLLOUT;
			epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
		
			connections ++;
		}
		//connections ++;
		if (connections % 1000 == 999 || connections >= 340000) {
			struct timeval tv_cur;
			memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
			
			gettimeofday(&tv_begin, NULL);

			int time_used = TIME_SUB_MS(tv_begin, tv_cur);
			printf("connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, time_used);

			int nfds = epoll_wait(epoll_fd, events, connections, 100);
			for (i = 0;i < nfds;i ++) {
				int clientfd = events[i].data.fd;

				if (events[i].events & EPOLLOUT) {
					sprintf(buffer, "data from %d\n", clientfd);
					send(sockfd, buffer, strlen(buffer), 0);
				} else if (events[i].events & EPOLLIN) {
					char rBuffer[MAX_BUFFER] = {0};				
					ssize_t length = recv(sockfd, rBuffer, MAX_BUFFER, 0);
					if (length > 0) {
						printf(" RecvBuffer:%s\n", rBuffer);

						if (!strcmp(rBuffer, "quit")) {
							isContinue = 0;
						}
						
					} else if (length == 0) {
						printf(" Disconnect clientfd:%d\n", clientfd);
						connections --;
						close(clientfd);
					} else {
						if (errno == EINTR) continue;

						printf(" Error clientfd:%d, errno:%d\n", clientfd, errno);
						close(clientfd);
					}
				} else {
					printf(" clientfd:%d, errno:%d\n", clientfd, errno);
					close(clientfd);
				}
			}
		}

		usleep(1 * 1000);
	}

	return 0;

err:
	printf("error : %s\n", strerror(errno));
	return 0;
	
}

error : Too many open files

  先使用以下简易的epoll服务端,代码如下,主要记录文件描述符的打开次数。

int SocketFdcount=2;
int epofd;
char buf[1024]={0};
//set eventstruct
struct eventstruct{
    int fd;
    int (* callback )(int ,void *);
};

//set not socketfd callback
int read_cb(int fd,void *arg){
    memset(buf,0,sizeof(buf));
    int n=read(fd,buf,sizeof(buf));
    if(n<=0) {
        printf("have con break \n");
        epoll_ctl(epofd,EPOLL_CTL_DEL,fd,NULL);
        close(fd);
        free(arg);
        return -1;
    }else {
        printf("buf=[%s]\n",buf);
        write(fd,"OK\n",4);
    }

    return 0;
}

//set socketfd callback
int accept_cb(int fd,void *arg){
    int confd=accept(fd,NULL,NULL);
    if(confd<0){
        return -1;
    }
    SocketFdcount++;
    printf("have new con SocketFdCount=[%d]\n",SocketFdcount);
    struct eventstruct *ev= (struct eventstruct *)malloc(sizeof(struct eventstruct));
    ev->fd=confd;
    ev->callback=read_cb;
    struct epoll_event temp;
    temp.events=EPOLLIN;
    temp.data.ptr=ev;

    epoll_ctl(epofd,EPOLL_CTL_ADD,confd,&temp);

    return 0;
}
int main()
{
    //socket()
    int socketfd= socket(AF_INET,SOCK_STREAM,0);
    if(socketfd<0){
        return -1;
    }

    //bind()
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port= htons(8002);
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    int bindret=bind(socketfd,(struct sockaddr *)&addr,sizeof(addr));
    if(bindret<0){
        return -2;
    }

    //listen()
    int listenret=listen(socketfd,128);
    if(listenret<0){
        return -3;
    }

    //set epoll
    epofd= epoll_create(1);
    if(epofd<0){
        return -1;
    }

    struct epoll_event node[1024];
    memset(&node,0,sizeof(node));

    struct epoll_event temp;
    struct eventstruct *ev= (struct eventstruct*)malloc(sizeof(struct eventstruct));
    temp.events=EPOLLIN;
    ev->fd=socketfd;
    ev->callback=accept_cb;
    temp.data.ptr=ev;
    int retc= epoll_ctl(epofd,EPOLL_CTL_ADD,socketfd,&temp);
    if(retc<0) {
        return -1;
    }

    while(1){
        int nready= epoll_wait(epofd,node,1024,-1);
        if(nready<0){
            return -9;
        }

        for(int i=0;i<nready;i++){
            struct eventstruct *snacks=(struct eventstruct*)node[i].data.ptr;
            if(node[i].events & EPOLLIN){
                snacks->callback(snacks->fd,snacks);
            }
        }

    }

    close(socketfd);
    return 0;
}

在这里插入图片描述
  这是代码实现的遇到的问题,程序执行到一半,创建了1023个连接后,然后报错,为什么这里是1021呢,因为代码中创建socket套接字的时候我没有++,epoll地基创建的时候也会占一个但是我没有++,上面代码文件描述符从2开始是因为0-2默认被占了,分别是标准输入、标准输出、标准错误输出。这也验证了文件系统默认允许打开文件描述符数量个数为1024个。
  那么怎么解决这个问题呢?使用ulimit -a查看open files的数量,这个open files就是一个进程文件描述符的数量,我们设置一下就行。
在这里插入图片描述
在这里插入图片描述
  通过ulimit这个命令 输入每行参数对应的 -n或者-c进行设置,就能进行更改。注意这个只能暂时的更改只在当前这个会话有效,重启这个终端或者重新启动这台服务器都会还原默认设置。ulimit -n是限制当前shell以及该shell启动的进程打开的文件数量。
  非root用户只能越设置越小,不能越设置越大,但是root用户不受这种限制。
  一个用户可能会同时通过多个shell连接到系统,所以下面可以设置每个用户能打开的最大文件数量

root@root:/etc/security# vim /etc/security/limits.conf  //编辑
*    soft    nofile          1048576
*    hard    nofile          1048576
root@root:/etc/security# reboot   //重启生效

在这里插入图片描述

  • 软限制:超出软限制会发出警告
  • 硬限制:绝对限制,在任何情况下都不允许用户超过这个限制

  这个 *代表的是全部用户包括root。
  对于非root用户, /etc/security/limits.conf会限制ulimit -n,但是限制不了root用户。

  还需要了解一个东西file-max : 系统一共可以打开的最大文件数(所有进程加起来)

root@root:/etc/security# cat /proc/sys/fs/file-max
1048576

# 编辑内核参数配置文件
vim /etc/sysctl.conf

# 修改fs.file-max参数
fs.file-max = 1048576

# 重新加载配置文件
sysctl -p

  /proc/sys/fs/file-max是系统给出的建议值,系统会计算资源给出一个和合理值,一般跟内存有关系,内存越大,改值越大,但是仅仅是一个建议值,limits.conf的设定完全可以超过/proc/sys/fs/file-max。

  总的来说,一个shell里面执行进程还是设置ulimit来的直接了当,但是只设置这个是远远不够的,需要多重考虑结合着来看。
(测试的时候记得给客户端的限制也修改一下)

Cannot assign requested address

  当我们设置好最大文件描述符打开个数的时候又会发现到了2.8w个左右连接就没法再连接了,客户端error:Cannot assign requested address,这代表着客户端端口耗尽.
  理解这个问题之前,我们需要先了解下fd代表着什么?
  一个fd代表着一个五元组,那这个组里面有哪五个成员呢?分别是这五个< 源IP地址 , 源端口 , 目的IP地址 , 目的端口 , 协议 >
  其中源ip就是自己的ip,源端口就是自己的端口,目的ip就是连接的ip,目的端口就是连接的端口,协议就是使用的TCP还是UDP协议等。
  上面的代码服务端只用了一个端口8002,源ip和目的ip以及协议都是可以确定的,唯一可以产生不同的fd就是改变源端口的值,每一个不同的值都是一个不同的fd。简单来说就是一个五元组确定了一个fd,当里面的任一一个值不一样,那么就是另一个fd。
  我们看到大概创建了2.8w的fd , 可是我们知道端口一个有6w多个,也就是说有6w个端口,为什么我们只使用了2.8w个?
  是因为Linux中有限定端口的使用范围:60999 - 32768 = 2.8w ,与我们上面实验结果相符:
  The /proc/sys/net/ipv4/ip_local_port_range defines the local port range that is used by TCP and UDP traffic to choose the local port. You will see in the parameters of this file two numbers: The first number is the first local port allowed for TCP and UDP traffic on the server, the second is the last local port number. For high-usage systems you may change its default parameters to 32768-61000 -first-last.proc/sys/net/ipv4/ip_local_port_range(范围定义TCP和UDP通信用于选择本地端口的本地端口范围。您将在该文件的参数中看到两个数字:第一个数字是服务器上允许TCP和UDP通信的第一个本地端口,第二个是最后一个本地端口号。对于高使用率的系统,您可以将其默认参数更改为32768-61000(first-last))
  也就是说用于TCP和UDP通信的端口只有2.8w,所以只能到2.8w。
  那么问题应该如何解决呢?
  修改net.ipv4.ip_local_port_range的范围?一般不这样做,我们这里研究的是服务器,怎么会去对客户端进行修改呢.
  想想五元组里面还能通过什么来形成不同的fd呢?那就是开放更多的服务器端口,比如说开放了100个端口来连接,那么一个客户端就可以连接280w了。

error : Connection timed out

当把上述问题解决后发现连接数达到13w的时候又停止了,出现了error:Connection timed out,译为连接超时,也就是说,client发送的请求超时了,那么这个超时有两种情况:
第一种:三次握手第一次的SYN没发出去。
第二种:三次握手第二次ACK没收到。

在这里插入图片描述
  在想这个之前我们需要了解一下,网卡接收的数据,会通过sk_buff将数据传到协议栈里面,协议栈处理完再交给应用程序。由于操作系统在使用的时候,为防止被攻击,在数据发送给协议栈之前进行一个过滤,在协议栈前面加了一个小组件:过滤器,叫做netfilter
在这里插入图片描述
  netfilter不管对发送的数据,还是对接收的数据,都是可以过滤的。当连接数量达到一定数量的时候,netfilter就会不允许再对外发连接了。
  可以进文件中查看nf_conntrack_max的值为多少刚好就为13w左右,

root@root:/etc# cat /etc/sysctl.conf 

  这个问题进到相应文件进行修改即可,修改完后记得重新加载配置文件或者重启一下。因为发送和接收都是可以过滤的,如果只用两台虚拟机来测试就需要都进行设置。

# 重新加载配置文件
sysctl -p

killed(已杀死)

  这种情况的发生主要是因为fd本身也是占内存的,fd太多了,内存不足就把进程杀死了,每个fd都有一个tcp接收缓冲区和tcp发送缓冲区。而默认的太大了,导致Linux内存不足,进程被杀死,所以我们需要适当的缩小。
  还是在这个文件/etc/sysctl.conf中:
在这里插入图片描述

					最小值 默认值  最大值
net.ipv4.tcp_mem = 252144 524288 786432	# tcp协议栈的大小,单位为内存页(4K),分别是 1G 2G 3G,如果大于2G也就是这个默认值,tcp协议栈会进行一定的优化,大于3G的话就不给使用了。
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都有一个发送缓存区

  缓冲区当超过1024这个默认值会给你调大,当你用的很少的时候又会帮你回收,调到小一些的值。
  这里一个fd有两个缓冲区,也就是1024字节+1024字节,要百万并发的话就最起码需要(1024byte+1024byte)*1000000大约2G左右的大小。

  • 如果服务器是用来接收大文件,传输量很大的时候,就需要把send buffer和read buffer调大。
  • 如果服务器只是接收小数据字符的时候。把buffer调小是为了把fd的数量做到更多,并发数量能做到更大。

总结

  如果想要实现服务器百万并发的话:

  • 1.相应用户需要设置好最大的打开文件描述符的数量,必要时在相应的在当前shell里通过ulimit -n 设置。数量设置需大于等于100w。
  • 2.在相应的环境下考虑是否需要开放多个端口。
  • 3.设置netfilter允许对外最大连接数量100w以上。
  • 4.根据内存和场景,适当调整net.ipv4.tcp_mem,net.ipv4.tcp_wmem,net.ipv4.tcp_rmem。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值