100万并发连接服务器

      著名的 C10K 问题提出的时候, 正是 2001 年, 到如今 12 年后的 2013 年, C10K 已经不是问题了, 任何一个普通的程序员, 都能利用手边的语言和库, 轻松地写出 C10K 的服务器. 这既得益于软件的进步, 也得益于硬件性能的提高.

      现在, 该是考虑 C1000K, 也就是百万连接的问题的时候了. 像 Twitter, weibo, Facebook 这些网站, 它们的同时在线用户有上千万, 同时又希望消息能接近实时地推送给用户, 这就需要服务器能维持和上千万用户的 TCP 网络连接, 虽然可以使用成百上千台服务器来支撑这么多用户, 但如果每台服务器能支持一百万连接(C1000K), 那么只需要十台服务器.

      有很多技术声称能解决 C1000K 问题, 例如 Erlang, Java NIO 等等, 不过, 我们应该首先弄明白, 什么因素限制了 C1000K 问题的解决. 主要是这几点:

      1.操作系统能否支持百万连接?

      对于绝大部分 Linux 操作系统, 默认情况下确实不支持 C1000K! 因为操作系统包含最大打开文件数(Max Open Files)限制, 分为系统全局的, 和进程级的限制.

      2.操作系统维持百万连接需要多少内存?

      看看内存的占用情况. 首先, 是操作系统本身维护这些连接的内存占用. 对于 Linux 操作系统, socket(fd) 是一个整数, 所以, 猜想操作系统管理一百万个连接所占用的内存应该是 4M/8M, 再包括一些管理信息, 应该会是 100M 左右.

      3.应用程序维持百万连接需要多少内存?

      另外应用程序维持百万个空闲的连接, 只会占用操作系统的内存, 应用程序本身几乎不占用内存.

      4.百万连接的吞吐量是否超过了网络限制?

      假设百万连接中有 20% 是活跃的, 每个连接每秒传输 1KB 的数据, 那么需要的网络带宽是 0.2M x 1KB/s x 8 = 1.6Gbps, 要求服务器至少是万兆网卡(10Gbps).     

环境准备 

      测试一个非常简单服务器如何达到100万(1M=1024K连接)的并发连接,并且这些连接一旦连接上服务器,就不会断开,一直连着。
      环境受限,服务器使用一台I3 6G的PC机器,测试机使用VMWare开3个虚拟机,每个2核2G内存,测试端和服务器端都选用较为熟悉的64位Centos 7.1,64位系统,后面根据测试调整。IO密集型应用,对CPU要求不是很高。另外服务器确保安装上gcc,那就可以开工了。

  • 测试程序也很简陋,一个C语言所写服务器程序,没有任何业务存在,收到请求后发送一些头部,不断开连接
  • 测试端程序也是使用C语言所写,发送请求,然后等待接收数据,仅此而已
  • 理论上200万的并发连接(IO密集型),加上业务,40G-50G的内存大概能够保证

 

服务器端程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <sys/select.h>


int main(int argc, char **argv){

	struct sockaddr_in addr;
	const char *ip = "0.0.0.0";
	int opt = 1;
	int bufsize;
	socklen_t optlen;
	int connections = 0;
	int port = 8000;

	inet_pton(AF_INET, ip, &addr.sin_addr);
	memset(&addr, sizeof(addr), 0);
	addr.sin_family = AF_INET;
	addr.sin_port = htons((short)port);
	inet_pton(AF_INET, ip, &addr.sin_addr);

	int serv_sock;
	if((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){
		goto sock_err;
	}
	if(setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1){
		goto sock_err;
	}
	if(bind(serv_sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){
		goto sock_err;
	}
	if(listen(serv_sock, 1024) == -1){
		goto sock_err;
	}

	printf("server listen on port: %d\n", port);

	while(1){
		fd_set readset;
		FD_ZERO(&readset);
		int maxfd = 0;
		FD_SET(serv_sock, &readset);
		if(serv_sock > maxfd){
			maxfd = serv_sock;
		}
		int ret = select(maxfd + 1, &readset, NULL, NULL, NULL);
		if(ret < 0){
			if(errno == EINTR){
				continue;
			}else{
				printf("select error! %s\n", strerror(errno));
				exit(0);
			}
		}
		if(ret == 0){
			continue;
		}
		if(!FD_ISSET(serv_sock, &readset)){
			continue;
		}
		socklen_t addrlen = sizeof(addr);
		int sock = accept(serv_sock, (struct sockaddr *)&addr, &addrlen);
		if(sock == -1){
			goto sock_err;
		}
		connections ++;
		if(connections % 1000 == 999){
			printf("connections: %d, fd: %d\n", connections, sock);
		}

		bufsize = 5000;
		setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
		setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
	}
	return 0;
sock_err:
	printf("connections: %d\n", connections);
	printf("error: %s\n", strerror(errno));
	return 0;
}

编译

gcc server.c -o server

运行

./server

默认监听了8000端口

server listen on port: 8000

测试端程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>

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

	struct sockaddr_in addr;
	const char *ip = argv[1];
	int base_port = atoi(argv[2]);
	int opt = 1;
	int bufsize;
	socklen_t optlen;
	int connections = 0;

	memset(&addr, sizeof(addr), 0);
	addr.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &addr.sin_addr);

	char tmp_data[10];

	int index = 1000;
	while(1){
		int port = index;
		index++;
		//printf("connect to %s:%d\n", ip, base_port);
		addr.sin_port = htons((short)base_port);
		int sock;
		if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){
			goto sock_err;
		}
		if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){
			goto sock_err;
		}
		connections ++;

		if(connections % 1000 == 999){
			printf("connections: %d, fd: %d\n", connections, sock);
		}
		usleep(1 * 1000);

		bufsize = 5000;
		setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
		setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
	}

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

编译

gcc -o client client.c

运行

./client  192.168.1.100 8000

 

第一个遇到的问题:文件句柄受限(服务端、客户端)

测试端程序输出

看看测试端程序client输出的错误信息:

connections: 999, fd: 1001
connections: 1021
error: Too many open files

服务器端程序输出

服务器端最后一条日志为

server listen on port: 8000
connections: 999, fd: 1002
connections: 1020
error: Too many open files

此时默认情况下服务器和客户端文件句柄已经达到极限(1024)

# ulimit  -n
1024

root用户编辑/etc/security/limits.conf文件添加:

* soft nofile 1048576
* hard nofile 1048576
  • soft是一个警告值,而hard则是一个真正意义的阀值,超过就会报错。
  • soft 指的是当前系统生效的设置值。hard 表明系统中所能设定的最大值
  • nofile - 打开文件的最大数目
  • 星号表示针对所有用户,若仅针对某个用户登录ID,请替换星号

注意:
1024K x 1024 = 1048576K = 1M,1百万多一点。

备注:测试端和服务器端都需要作此设置,保存退出,然后reboot即可生效。不重启可以使用命令临时修改。ulimit -n 1048576

 

第二个遇到的问题:端口数量受限(客户端)

测试端程序输出

看看测试端程序client输出的错误信息:

connections: 999, fd: 1001
connections: 1999, fd: 2001
connections: 2999, fd: 3001
connections: 3999, fd: 4001
connections: 4999, fd: 5001
connections: 5999, fd: 6001
connections: 6999, fd: 7001
connections: 7999, fd: 8001
connections: 8999, fd: 9001
connections: 9999, fd: 10001
connections: 10999, fd: 11001
connections: 11999, fd: 12001
connections: 12999, fd: 13001
connections: 13999, fd: 14001
connections: 14999, fd: 15001
connections: 15999, fd: 16001
connections: 16999, fd: 17001
connections: 17999, fd: 18001
connections: 18999, fd: 19001
connections: 19999, fd: 20001
connections: 20999, fd: 21001
connections: 21999, fd: 22001
connections: 22999, fd: 23001
connections: 23999, fd: 24001
connections: 24999, fd: 25001
connections: 25999, fd: 26001
connections: 26999, fd: 27001
connections: 27999, fd: 28001
connections: 28233
error: Cannot assign requested address

这个是程序端口不够用的异常,但可以通过增加端口进行解决。

一般来说,单独对外提供请求的服务不用考虑端口数量问题,监听某一个端口即可。但是向提供代理服务器,就不得不考虑端口数量受限问题了。当前的1M并发连接测试,也需要在客户端突破6万可用端口的限制。

单机端口上限为65536

端口为16进制,那么2的16次方值为65536,在linux系统里面,1024以下端口都是超级管理员用户(如root)才可以使用,普通用户只能使用大于1024的端口值。
系统提供了默认的端口范围:

cat /proc/sys/net/ipv4/ip_local_port_range
32768 61000

大概也就是共61000-32768=28232个端口可以使用,单个IP对外只能发送28232个TCP请求。
以管理员身份,把端口的范围区间增到最大:

echo "1000 65535"> /proc/sys/net/ipv4/ip_local_port_range

现在有64535个端口可用.
以上做法只是临时,系统下次重启,会还原。 更为稳妥的做法是修改/etc/sysctl.conf文件,增加一行内容

net.ipv4.ip_local_port_range= 1000 65535

保存,然后使之生效:

sysctl -p 

现在可以使用的端口达到64535个(假设系统所有运行的服务器是没有占用大于1000的端口的,较为纯净的centos系统可以做到),要想达到50万请求,还得再想办法。

修改客户端程序(增加绑定源IP和端口)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>

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

	struct sockaddr_in addr;
	const char *ip = argv[1];
	int base_port = atoi(argv[2]);
	
	int bufsize;
	socklen_t optlen;
	int connections = 0;

	memset(&addr, sizeof(addr), 0);
	addr.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &addr.sin_addr);

	char tmp_data[10];
	int i = 0;
	for(i=3; i<=argc; i++) {
		int index = 1000;
		const char *srcip = argv[i];
		while(index < 65535){
			int port = index;
			index++;
			//printf("connect to %s:%d\n", ip, base_port);
			addr.sin_port = htons((short)base_port);
			int sock;
			if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){
				goto sock_err;
			}

			struct sockaddr_in client;
			client.sin_family = AF_INET;
			client.sin_addr.s_addr = inet_addr(srcip);
			client.sin_port = htons((short)port);
			if (bind(sock, (struct sockaddr *) &client, sizeof(client)) == -1) {
				goto sock_err;
			}

			if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){
				goto sock_err;
			}
			connections ++;

			if(connections % 1000 == 999){
				printf("connections: %d, fd: %d\n", connections, sock);
			}
			usleep(1 * 1000);

			bufsize = 5000;
			setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
			setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
		}
	}
	
	printf("max connections: %d\n", connections);
	int number = 0;
	scanf ("%d",&number);
	return 0;
sock_err:
	printf("connections: %d\n", connections);
	printf("error: %s\n", strerror(errno));
	return 0;
}

编译

gcc -o client1 client1.c

为本机增加虚拟IP

ifconfig eth0:0 add  192.168.1.200
ifconfig eth0:1 add  192.168.1.201
ifconfig eth0:2 add  192.168.1.202
ifconfig eth0:3 add  192.168.1.203

运行(第三个参数起为本地IP)

./client1 192.168.1.100 8000 192.168.1.200 192.168.1.201 192.168.1.202 192.168.1.203

多次测试期间,需要开启TCP快速回收,不然处于TIME_OUT的端口不能及时回收,影响测试

echo "1" > /proc/sys/net/ipv4/tcp_tw_recycle
echo "1" > /proc/sys/net/ipv4/tcp_tw_reuse

第三个遇到的问题:iptables的问题(服务端、客户端)

在服务器端连接数达到65535后,客户端再也连接不上,提示连接超时,客户端发出的连接数超过65535后也是。

connections: 29999, fd: 30001
connections: 30999, fd: 31001
connections: 31999, fd: 32001
connections: 32999, fd: 33001
error: Connection timed out

查看服务器端日志

#tailf /var/log/messages
Jan 13 05:36:27 localhost kernel: net_ratelimit: 1628 callbacks suppressed
Jan 13 05:36:27 localhost kernel: nf_conntrack: table full, dropping packet
Jan 13 05:36:27 localhost kernel: nf_conntrack: table full, dropping packet
Jan 13 05:36:28 localhost kernel: nf_conntrack: table full, dropping packet
Jan 13 05:36:28 localhost kernel: nf_conntrack: table full, dropping packet

这是iptables的报错信息“连接跟踪表已满,开始丢包”,此时需要关闭服务器端防火墙

Centos7 系统
systemctl stop firewalld.service

Centos6 系统
service iptables stop

此时再测试,可以继续往下测试

第四个遇到的问题:fs.file-max的问题(客户端)

测试端程序在发出的数量大于某个值(大概为18万时)是,查看服务器端日志查看会得到大量警告信息:

#tailf /var/log/messages
Jan 13 06:09:07 localhost kernel: VFS: file-max limit 186374 reached

此时,就需要检查/proc/sys/fs/file-max参数了。

查看一下系统对fs.file-max的说明

file-max表示系统所有进程最多允许同时打开所有的文件句柄数,系统级硬限制。Linux系统在启动时根据系统硬件资源状况计算出来的最佳的最大同时打开文件数限制(一般为内存大小(KB)的10%来计算, 可以使用如下shell命令来计算grep -r MemTotal /proc/meminfo | awk '{printf("%d",$2/10)}'),如果没有特殊需要,不用修改,除非打开的文件句柄数超过此值。在为测试机分配2G内存时,对应的fs.file-max值为186374,很显然打开的文件句柄很受限,18万个左右。 很显然,无论是测试端还是服务端,都应该将此值调大些,一定要大于等于/etc/security/limits.conf送所设置的soft nofile和soft nofile值。
注意ulimit -n,仅仅设置当前shell以及由它启动的进程的资源限制。

备注:以上参数,具有包含和被包含的关系。

当前会话修改,可以这么做:

echo 1048576 > /proc/sys/fs/file-max

但系统重启后消失。

永久修改,要添加到 /etc/sysctl.conf 文件中:

fs.file-max = 1048576

再测,就不会出现此问题了。另外,把虚拟机内存调整到4G,大概可以支持38万个。

一台4G内存机器测试机,分配6个网卡,对外发出64500 * 6 = 387000个对外持久请求,只要3个虚拟机即可做到100W。

最终测试端组成如下:

  • 三台虚拟机,每个虚拟机6个网卡,每个发出64500个请求

服务器端也需要调整这个参数。

第五个遇到的问题:tcp_mem(服务端)

在服务端,连接达到一定数量,诸如50W时,有些隐藏很深的问题,就不断的抛出来。

#tailf  /var/log/messages
Jan 13 05:07:12 localhost kernel: TCP: too many orphaned sockets

也很正常,下面到了需要调整tcp socket参数的时候了。

第一个需要调整的是tcp_rmem,即TCP读取缓冲区,单位为字节,查看默认值

cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 4161536

默认值为87380 byte ≈ 86K,最小为4096 byte=4K,最大值为4064K。

第二个需要调整的是tcp_wmem,发送缓冲区,单位是字节,默认值

cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4161536

解释同上

第三个需要调整的tcp_mem,调整TCP的内存大小,其单位是页,1页等于4096字节。系统默认值:

cat /proc/sys/net/ipv4/tcp_mem
932448 1243264 1864896

tcp_mem(3个INTEGER变量):low, pressure, high

  • low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。
  • pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入pressure模式,当内存消耗低于low值时则退出pressure状态。
  • high:允许所有tcp sockets用于排队缓冲数据报的页面量,当内存占用超过此值,系统拒绝分配socket,后台日志输出“TCP: too many of orphaned sockets”。

一般情况下这些值是在系统启动时根据系统内存数量计算得到的。 根据当前tcp_mem最大内存页面数是1864896,当内存为(1864896*4)/1024K=7284.75M时,系统将无法为新的socket连接分配内存,即TCP连接将被拒绝。

实际测试环境中,据观察大概在99万个连接左右的时候(零头不算),进程被杀死,触发out of socket memory错误(dmesg命令查看获得)。每一个连接大致占用7.5K内存(下面给出计算方式),大致可算的此时内存占用情况(990000 * 7.5 / 1024K = 7251M)。

这样和tcp_mem最大页面值数量比较吻合,因此此值也需要修改。

三个TCP调整语句为:

echo "net.ipv4.tcp_mem = 786432 2097152 3145728">> /etc/sysctl.conf
echo "net.ipv4.tcp_rmem = 4096 4096 16777216">> /etc/sysctl.conf
echo "net.ipv4.tcp_wmem = 4096 4096 16777216">> /etc/sysctl.conf

备注: 为了节省内存,设置tcp读、写缓冲区都为4K大小,tcp_mem三个值分别为3G 8G 16G,tcp_rmemtcp_wmem最大值也是16G。

目标达成

经过若干次的尝试,最终达到目标,1024000个持久连接。1024000数字是怎么得来的呢,两台物理机器各自发出64000个请求,两个配置为6G左右的centos测试端机器(绑定7个桥接或NAT连接)各自发出640007 = 448000。也就是 1024000 = (64000) + (64000) + (640007) + (64000*7), 共使用了16个网卡(物理网卡+虚拟网卡)。

终端输出

......
online user 1023990
online user 1023991
online user 1023992
online user 1023993
online user 1023994
online user 1023995
online user 1023996
online user 1023997
online user 1023998
online user 1023999
online user 1024000

在线用户目标达到1024000个!

服务器状态信息

服务启动时内存占用:

                 total       used       free     shared    buffers     cached
     Mem:         10442        271      10171          0         22         78
     -/+ buffers/cache:        171      10271
     Swap:         8127          0       8127

系统达到1024000个连接后的内存情况(执行三次 free -m 命令,获取三次结果):

                 total       used       free     shared    buffers     cached
     Mem:         10442       7781       2661          0         22         78
     -/+ buffers/cache:       7680       2762
     Swap:         8127          0       8127
 
                  total       used       free     shared    buffers     cached
     Mem:         10442       7793       2649          0         22         78
     -/+ buffers/cache:       7692       2750
     Swap:         8127          0       8127
 
                  total       used       free     shared    buffers     cached
     Mem:         10442       7804       2638          0         22         79
     -/+ buffers/cache:       7702       2740
     Swap:         8127          0       8127

这三次内存使用分别是7680,7692,7702,这次不取平均值,取一个中等偏上的值,定为7701M。那么程序接收1024000个连接,共消耗了 7701M-171M = 7530M内存, 7530M*1024K / 1024000 = 7.53K, 每一个连接消耗内存在为7.5K左右,这和在连接达到512000时所计算较为吻合。
虚拟机运行Centos内存占用,不太稳定,但一般相差不大,以上数值,仅供参考。

执行top -p 某刻输出信息:

     top - 17:23:17 up 18 min,  4 users,  load average: 0.33, 0.12, 0.11
     Tasks:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
     Cpu(s):  0.2%us,  6.3%sy,  0.0%ni, 80.2%id,  0.0%wa,  4.5%hi,  8.8%si,  0.0%st
     Mem:  10693580k total,  6479980k used,  4213600k free,    22916k buffers
     Swap:  8323056k total,        0k used,  8323056k free,    80360k cached
 
     PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                      
     2924 yongboy   20   0 82776  74m  508 R 51.3  0.7   3:53.95 server 

执行vmstate:

vmstat
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
 r b swpd free buff cache si so bi bo in cs us sy id wa st
 0 0 0 2725572 23008 80360 0 0 21 2 1012 894 0 9 89 2 0 

获取当前socket连接状态统计信息:

cat /proc/net/sockstat
sockets: used 1024380
TCP: inuse 1024009 orphan 0 tw 0 alloc 1024014 mem 2
UDP: inuse 11 mem 1
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

获取当前系统打开的文件句柄:

sysctl -a | grep file
fs.file-nr = 1025216 0 1048576
fs.file-max = 1048576

此时任何类似于下面查询操作都是一个慢,等待若干时间还不见得执行完毕。

netstat -nat|grep -i "8000"|grep ESTABLISHED|wc -l 
netstat -n | grep -i "8000" | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

以上两个命令在二三十分钟过去了,还未执行完毕,只好停止。

小结

本次从头到尾的测试,所需要有的linux系统需要调整的参数也就是那么几个,汇总一下:

     echo "* - nofile 1048576" >> /etc/security/limits.conf
 
     echo "fs.file-max = 1048576" >> /etc/sysctl.conf
     echo "net.ipv4.ip_local_port_range = 1024 65535" >> /etc/sysctl.conf
 
     echo "net.ipv4.tcp_mem = 786432 2097152 3145728" >> /etc/sysctl.conf
     echo "net.ipv4.tcp_rmem = 4096 4096 16777216" >> /etc/sysctl.conf
     echo "net.ipv4.tcp_wmem = 4096 4096 16777216" >> /etc/sysctl.conf

其它没有调整的参数,仅仅因为它们暂时对本次测试没有带来什么影响,实际环境中需要结合需要调整类似于SO_KEEPALIVE、tcpmax_orphans等大量参数。

 

内容整理自网络,再加上部分修改

http://www.blogjava.net/yongboy/archive/2013/04/09/397559.html

https://github.com/ideawu/c1000k

http://www.ideawu.net/blog/archives/740.html

转载于:https://my.oschina.net/mywiki/blog/822274

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值