一台服务器最大可以支持的TCP连接
理论情况
TCP连接四元组是源IP地址、源端口、目的IP地址和目的端口。任意一个元素发生了改变,那么就代表的是一条完全不同的连接了。对任意一个网络服务来说,它的端口是固定。另外IP也是固定的,这样目的IP地址、目的端口都是固定的。剩下源IP地址、源端口是可变的。所以理论上该服务最多可以建立2的32次方(ip数)×2的16次方(port数)个连接。这是两百多万亿的一个大数字!!
实际情况
进程每打开一个文件(linux下一切皆文件,包括socket),都会消耗一定的内存资源。如果有不怀好心的人启动一个进程来无限的创建和打开新的文件,会让服务器崩溃。所以linux系统出于安全角度的考虑,在多个位置都限制了可打开的文件描述符的数量,包括系统级、用户级、进程级。这三个限制的含义和修改方式如下:
- 系统级:当前系统可打开的最大数量,通过
fs.file-max
参数可修改
- 用户级:指定用户可打开的最大数量,修改
/etc/security/limits.conf
- 进程级:单个进程可打开的最大数量,通过
fs.nr_open
参数可修改
修改以上内核参数后可以将文件句柄数据加大到100W,但是每一条TCP连接在服务器端都需要file, socket等内核对象。一条空TCP连接大概占用3.3KB内存。
接收区的缓存设置:
1 2 3 4
| $ sudo sysctl -a | grep rmem net.ipv4.tcp_rmem = 4096 131072 629145 net.core.rmem_default = 212992 net.core.rmem_max = 212992
|
其中在tcp_rmem
中的第一个值是为TCP连接所需分配的最少字节数。该值默认是4K,最大的话8MB之多。也就是说有数据发送的时候需要至少为对应的socket再分配4K内存,甚至可能更大。
TCP分配发送缓存区的大小受参数net.ipv4.tcp_wmem
配置影响:
1 2 3 4
| $ sudo sysctl -a | grep wmem net.ipv4.tcp_wmem = 4096 16384 4194304 net.core.wmem_default = 212992 net.core.wmem_max = 212992
|
在net.ipv4.tcp_wmem
中的第一个值是发送缓存区的最小值,默认也是4K。如果数据很大的话,该缓存区实际分配的也会比默认值大。
查询实际消耗
活动连接数量查询:
1
| $ ss -n | grep ESTAB | wc -l
|
查看内存开销:
通过slabtop
命令可以查看到densty、flip、sock_inode_cache、TCP
四个内核对象.
客户端最大建立的TCP连接数
修改ip_local_port_range
和文件描述符
修改该值可以使客户端可用的端口号范围变大:
1
| echo "5000 65000" > /proc/sys/net/ipv4/ip_local_port_range
|
还可以通过给主机增加多个IP地址来实现客户端的并发提高,但还需要修改文件描述符。
文件描述符修改:
1 2 3 4 5 6 7 8 9 10 11
| //修改整个系统能打开的文件描述符为20W echo 200000 > /proc/sys/fs/file-max
//修改所有用户每个进程可打开文件描述符为20W
fs.nr_open=210000
* soft nofile 200000 * hard nofile 200000
|
注意: limits
中的hard limit
不能超过nr_open
, 所以要先改nr_open
。而且最好是在sysctl.conf
中改。避免重启的时候 hard limit
生效了,nr_open
不生效导致启动问题。
连接多个服务进行端口复用
TCP连接的本质是客户端和服务器分别在内存维护一堆socket内核对象,它们只要能找到对方就算一条连接。在连接过程中,端口只是socket对象找到另一半的信物之一。
源码分析:
socket中有一个主要的数据结构sock_common
,在它里面有两个联合体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct sock_common { union { __addrpair skc_addrpair; struct { __be32 skc_daddr; __be32 skc_rcv_saddr; }; }; union { __portpair skc_portpair; struct { __be16 skc_dport; __u16 skc_num; }; }; ...... }
|
其中skc_addrpair
记录的是TCP连接里的IP对,skc_portpair
记录的是端口对。
当客户端的两个连接使用一个端口和服务器的两个服务进行连接时,如何区分服务器给客户端发送的数据属于哪条连接?
在网络包到达网卡之后,依次经历DMA、硬中断、软中断等处理,最后被送到socket的接收队列中。该过程设计到协议层对网络帧的处理,主要的tcp_v4_rcv
代码如下:
1 2 3 4 5 6 7 8 9 10
| int tcp_v4_rcv(struct sk_buff *skb) { ...... th = tcp_hdr(skb); iph = ip_hdr(skb);
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); ...... }
|
连接的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static inline struct sock *__inet_lookup(struct net *net, struct inet_hashinfo *hashinfo, const __be32 saddr, const __be16 sport, const __be32 daddr, const __be16 dport, const int dif) { u16 hnum = ntohs(dport); struct sock *sk = __inet_lookup_established(net, hashinfo, saddr, sport, daddr, hnum, dif);
return sk ? : __inet_lookup_listener(net, hashinfo, saddr, sport, daddr, hnum, dif); }
|
先判断有没有连接状态的socket,这会走到__inet_lookup_established
函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| struct sock *__inet_lookup_established(struct net *net, struct inet_hashinfo *hashinfo, const __be32 saddr, const __be16 sport, const __be32 daddr, const u16 hnum, const int dif) { const __portpair ports = INET_COMBINED_PORTS(sport, hnum); ......
unsigned int hash = inet_ehashfn(net, daddr, hnum, saddr, sport); unsigned int slot = hash & hashinfo->ehash_mask; struct inet_ehash_bucket *head = &hashinfo->ehash[slot];
begin: sk_nulls_for_each_rcu(sk, node, &head->chain) { if (sk->sk_hash != hash) continue; if (likely(INET_MATCH(sk, net, acookie, saddr, daddr, ports, dif))) { if (unlikely(!atomic_inc_not_zero(&sk->sk_refcnt))) goto begintw; if (unlikely(!INET_MATCH(sk, net, acookie, saddr, daddr, ports, dif))) { sock_put(sk); goto begin; } goto out; } } }
|
内核使用hash+链表的方式来管理其维护的socket。服务器端计算完hash值以后找到对应的链表进行遍历。
socket的对比函数(宏)INET_MATCH
:
1 2 3 4 5 6 7 8
| #define INET_MATCH(__sk, __net, __cookie, __saddr, __daddr, __ports, __dif) \ ((inet_sk(__sk)->inet_portpair == (__ports)) && \ (inet_sk(__sk)->inet_daddr == (__saddr)) && \ (inet_sk(__sk)->inet_rcv_saddr == (__daddr)) && \ (!(__sk)->sk_bound_dev_if || \ ((__sk)->sk_bound_dev_if == (__dif))) && \ net_eq(sock_net(__sk), (__net)))
|
在INET_MATCH
中将网络包tcp header
中的__saddr、__daddr、__ports
和Linux中的socket中inet_portpair、inet_daddr、inet_rcv_saddr
进行对比。如果匹配socket就找到了。当然除了ip和端口,INET_MATCH
还比较了其它一些东西,所以TCP还有五元组、七元组之类的说法。
由此:可以把同一个端口用于两条连接,只要server端的ip或两者端口不一样就能正确找到socket,而不是串线。
所以在客户端增加TCP最大并发能力有两个方法。第一个办法,为客户端配置多个ip。第二个办法,连接多个不同的server。
参考
https://mp.weixin.qq.com/s?__biz=MjM5Njg5NDgwNA==&mid=2247484207&idx=1&sn=50ae06628062bcdd5b2aff044f34fa80&chksm=a6e3021491948b0287e4f856791e4d1880ddfb76a76c3de4ea7c8e59a0cb1f2312c49e9ff5ce&cur_album_id=1532487451997454337&scene=189#rd
https://mp.weixin.qq.com/s?__biz=MjM5Njg5NDgwNA==&mid=2247484310&idx=1&sn=025f7787f39a9eef322ab73c4687b910&chksm=a6e302ad91948bbb17479890b03b47b93cc33af7db4ce5f22d63de83c498c424b71da7bc884b&cur_album_id=1532487451997454337&scene=189#rd