http://blog.163.com/xychenbaihu@yeah/blog/static/13222965520138128552553/
一、TCP长链接思考:
有一台机器,它有一个 IP,上面运行了一个 TCP 服务程序,程序只侦听一个端口,问:从理论上讲(只考虑 TCP/IP 这一层面,不考虑IPv6)这个服务程序可以支持多少并发 TCP 连接?
具体来说,这个问题等价于:
有一个 TCP 服务程序的地址是 1.2.3.4:8765,问它从理论上能接受多少个并发连接?
在只考虑 IPv4 的情况下,并发数的理论上限是 2**48。考虑某些 IP 段被保留了,这个上界可适当缩小,但数量级不变。实际的限制是操作系统全局文件描述符的数量(
/proc/sys/fs/file-max
),以及内存大小(CPU不是瓶颈)。
一个 TCP 连接有两个 end points,每个 end point 是 {ip, port},具体是{serverip,serverport}和{clientip,clientport},题目说其中一个 end point 已经固定,即{serverip,serverport},
那么留下一个 end point 的自由度,即 2 ** 48。原因客户端 IP 的上限是 2**32 个(IPV4,所有的IP数),每个客户端IP发起连接的上限是 2**16(local_port_range是一个unsigned short类型,
其实一般还要保留至少0~1024段),乘到一起得理论上限。(实际由于ip预留了一段作为私有IP,port预留了一段作为系统用,所以实际比这个要小,但是量级不变),即便客户端使用 NAT,也不影响这
个理论上限。(为什么?因为IP已经全部被计算在里面了)。
二、建立链接的速度限制:
Listen
的端口有一个backlog配置,它的大小会影响监听端口上等待建立连接的队列长度,通过调整该值,
可以加速连接建立过程
。建议在
listen
函数和内核参数(
/proc/sys/net/core/somaxconn
)都设置为
1024
。
三、增加链接会影响链接上的发送和接收包速率吗?
TCP
长连接个数对单个连接的收发包量和速率基本上不造成影响。Linux内核对连接采用4元组(serverip,serverport,destip,destport)的hash管理,因此时间复杂度基本不随连接数目的变化而上升。
四、防火墙对TCP连接上收发包性能的影响:
如果
系统配置了防火墙策略,并且开启防火墙的相关模块(
nf_conntrack
等),系统会对每条连接进行跟踪。收发数据包时,需要在连接跟踪中查找相关连接信息,要消耗大量的
CPU
资源。
防火墙操作的时间复杂度为O(N*M),N为连接数,M为数据包数,因此在海量连接情况下更加消耗CPU。
参考资料:
http://hi.baidu.com/farmerluo/item/f49bbdc0e390a52dee4665bb
五、在真实的 Linux 系统中,TCP链接或者长连接受限受到内核参数的限制,如果想支持上百万的并发链接,就需要对内核参数进行调整:
可以参看:
http://rdc.taobao.com/blog/cs/?p=1062
http://qa.blog.163.com/blog/static/19014700220134132571261/?latestBlog
http://qa.blog.163.com/blog/static/190147002201342115957410/
http://urbanairship.com/blog/2010/09/29/linux-kernel-tuning-for-c500k/
http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-3
需要调整的内核参数如下:
1、ulimit -HSn 2000000
以上命令中,H指定了硬性大小,S指定了软性大小,n表示设定单个进程最大的打开文件句柄数量。
ulimit 命令的理解:
a、这个限制是针对单个程序的限制;
系统限制单个进程打开文件输的限制是:/proc/sys/fs/file-max,可以通过cat查看目前的值,用echo来立刻修改;
另外还有一个是/proc/sys/fs/file-nr只读,可以看到整个系统目前使用的文件句柄数量。
/proc/sys/fs/nr_open是进程打开文件资源的限制,可以用过echo来修改。
b、这个限制不会改变之前已经运行了的程序的限制;
c、对这个值的修改,退出了当前的shell就会消失,如果想保存其中一个方法,是想ulimit修改命令放入/etc/profile里面,但是这个做法并不好,
好的做法,应该是修改/etc/security/limits.conf里面有很详细的注释,比如
* soft nofile 1500000
* hard nofile 2000000
就可以将文件句柄限制统一改成软1500000,硬20000000,这里涉及另外一个问题,什么是软限制,什么是硬限制,硬限制是实际的限制,而软限制,是warnning限制,
只会做出warning,其实ulimit命令本身就有分软硬设置,加-H就是硬,加-S就是软,默认显示的是软限制,如果修改的时候没有加上的话,就是两个一起改,
配置文件最前面的一位是domain,设置为星号代表全局,另外你也可以针对不同的用户做出不同的限制,修改了,重新登录用ulimit一看就立刻生效了,不过之
前启动过的程序要重新启动才能使用新的值。我用的是CentOS,似乎有些系统需要重启才能生效。
2、net.ipv4.ip_local_port_range
#本地IP端口的范围
3、net.ipv4.tcp_rmem
4、net.ipv4.tcp_wmem
5、net.ipv4.tcp_mem
#3-5三个值的含义:a、低于此值,TCP没有内存压力;b、在此值下,进入内存压力阶段;c、高于此值,TCP拒绝分配socket,内存单位是页,而不是字节。
更多关于内核的网络参数详解及调优,可以参看:
http://lijichao.blog.51cto.com/67487/308509
6、net.ipv4.tcp_max_orphans
#系统中最多有多少个TCP套接字不被关联到任何一个用户文件句柄上。如果超过这个数字,孤儿连接将即刻被复位并打印出警告信息,
这个限制仅仅是为了防止简单的DoS攻击,你绝对不能过分依靠它或者人为地减小这个值,更应该增加这个值(如果增加了内存之后)。
7、epoll监听fd个数的限制 一般是在/proc/sys/fs/epool/max_user_watches,在centos中好像是在fs.inotify.max_user_watches 中;
8、/proc/sys/core/somaxconn listen fd backlog
在测试过程中,可以用dmesg查看内核有没有报一些错误。
六、几乎大家用到的测试方法都是使用很多客户端机器(也可能是虚拟机),是否可以通过虚拟网卡的方式来实现呢?
通过IP别名的方式进行最大链接数测试,
但是不幸的是,还是受到了ip_local_port_range的限制,还是不能超过0~65535的限制,没能成功(需要好好研究下),测试方法如下:
设置网卡的ip别名:
/sbin/ifconfig eth1:0 217.30.116.82 netmask 255.255.255.0 up;
/sbin/ifconfig eth1:1 217.30.116.83 netmask 255.255.255.0 up;
清除网卡的ip别名:
/sbin/ifconfig eth1:0 down;
ech1:*
//虚拟网络接口,建立在eth1上,取值范围0~255
192.168.11.XXX
//增加ip别名,想加多少就加多少~~~
使用/sbin/ifconfig查看是否生效,使用 ping 192.168.11.XXX查看通不通。
注意:
在设置ip别名时,如果增加的是和局域网同一个网段的ip(如192.168.11.100),那么
除了本机外局域网内其他机器都可以ping通这个机器ip,如果增加的是奇形的ip,那么只
有本机ping通而已,后者主要用户本机测试需要。
测试代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main(int argc,char *argv[])
{
if(argc<5)
{
printf("usage:%s localip serverip port connectnum\n",argv[0]);
exit(0);
}
unsigned int connect_num =strtoul(argv[4],NULL,10);
set sock_set;
unsigned int csock_failed_num=0;
unsigned int setop_failed_num=0;
unsigned int bind_failed_num=0;
unsigned int conn_failed_num=0;
unsigned int index=0;
for(index=0;index
{
int sock_fd=0;
sock_fd = socket(AF_INET,SOCK_STREAM,0);
//创建socket
if(sock_fd < 0)
{
perror("creat sock_fd fail:");
//exit(0);
close(sock_fd);
csock_failed_num++;
continue;
}
int option_value = 1;
//端口复用
int ret= setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,(char *)&option_value,sizeof(option_value));
if(ret < 0)
{
perror("set sock opt fail:");
close(sock_fd);
//exit(0);
close(sock_fd);
setop_failed_num++;
continue;
}
struct sockaddr_in connect_addr;
//客户端地址,主要绑定IP(
使用IP别名
)
connect_addr.sin_family = AF_INET;
connect_addr.sin_port = 0;
//0表示端口由操作系统分配
connect_addr.sin_addr.s_addr = inet_addr(argv[1]);
memset(connect_addr.sin_zero,'\0',sizeof(connect_addr.sin_zero));
//int ret;
ret=bind(sock_fd,(struct sockaddr*)&connect_addr,sizeof(connect_addr));
//绑定客户端IP
if(ret<0)
{
perror("bind client ip failed:");
//exit(0);
close(sock_fd);
bind_failed_num++;
continue;
}
struct sockaddr_in server_addr;
//服务端地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[3]));
server_addr.sin_addr.s_addr = inet_addr(argv[2]);
memset(server_addr.sin_zero,'\0',sizeof(server_addr.sin_zero));
socklen_t addr_len = sizeof(struct sockaddr_in);
ret = connect(sock_fd,(struct sockaddr*)&server_addr,addr_len);
//可以适当坐下频率控制,防止server处理不过来
if(ret<0)
{
perror("connect to server failed:");
//exit(0);
close(sock_fd);
conn_failed_num++;
continue;
}
sock_set.insert(sock_fd);
}
while(true)
{
printf("connect socket num: conn=%u csock_failed_num=%u setop_failed_num=%u \
bind_failed_num=%u conn_failed_num=%u\n",
sock_set.size(),csock_failed_num,setop_failed_num,bind_failed_num,conn_failed_num);
sleep(10);
}
//close(sock_fd);
return 0;
}
IP别名的这种测试方法,理论上应该是可以的,可是自己此时没有成功,非常希望有相关经验的网友,能够留言,指出问题可能出现在哪里。