服务器客户端的最大TCP连接数

一台服务器最大可以支持的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

查看内存开销:

1
$ cat /proc/meminfo

通过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
#vi /etc/sysctl.conf
fs.nr_open=210000
#sysctl -p
#vi /etc/security/limits.conf
* 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
// file: include/net/sock.h
struct sock_common {
union {
__addrpair skc_addrpair; //TCP连接IP对
struct {
__be32 skc_daddr;
__be32 skc_rcv_saddr;
};
};
union {
__portpair skc_portpair; //TCP连接端口对
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
// file: net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
......
th = tcp_hdr(skb); //获取tcp header
iph = ip_hdr(skb); //获取ip header

sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); // 返回socket,这里设计到连接
......
}

连接的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// file: include/net/inet_hashtables.h
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)
{
//将源端口、目的端口拼成一个32位int整数
const __portpair ports = INET_COMBINED_PORTS(sport, hnum);
......

//内核用hash的方法加速socket的查找
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
// include/net/inet_hashtables.h
#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