TCP连接数计算
  I6B6oQKIoM4W 2023年11月15日 29 0

最近有一些时间,想着把某些基础的东西整理下,毕竟地基很重要,首先从计算机网络这部分入手。


1、网络收发概览

现在绝大部分的系统都是基于TCP协议的可靠传输,从数据的发送到接收的整个过程经历了很多环节,每一个环节也都有其各自的使命,通过大家的协同工作,最终将一个复杂的数据传输问题得以解决。下面是一次TCP通信的示意图

TCP连接数计算_TCP

  1. 应用程序将要发送的数据通过接口发给socket
  2. 内核将数据从用户空间拷贝到socket的发送缓冲区(可能分多次拷贝)
  3. TCP协议栈代码读取发送缓冲区的数据进行“粘包或拆包”方式生成TCP段
  4. IP协议栈代码将TCP段拆分成一个个IP报文(如果需要拆的情况)
  5. IP报文到达以太网后,根据以太网帧大小的限制又被拆分成多个以太网帧
  6. 接收端收到以太网帧后去除帧头信息传递给socket继续处理,socket将分段的IP报文组装成完整的IP报文后,再将其IP去除,剩下TCP段部分
  7. sokcet对TCP报文进行处理,间分段传输的TCP报文组装成完整的报文后,将TCP头去除后发到接收缓冲区
  8. 内核将TCP接收缓冲区的数据拷贝到用户空间
  9. 应用程序根据应用层协议,将分段传输的应用数据组装完整后,再交给业务代码进行逻辑运算

数据传输的快慢会影响到系统的整体性能,我们对网络通信有个整体的大致认识后,也可以为我们在分析系统性能问题方面提供帮助。

帧结构参考:https://info.support.huawei.com/info-finder/encyclopedia/zh/MTU.html


2、连接的建立

要让数据得以发送,首先就是建立通信两端的连接,我们从TCP协议入手看看其大致过程

TCP连接数计算_客户端_02

  1. 首先服务端建立对某个端口的连接监听
  2. 客户端发送连接请求,将自己的IP地址、使用的端口以及滑动窗口大小等信息告知服务端,并创建本地的Socket对象获得句柄
  3. 服务器收到请求后,将该请求放入“半连接队列”,然后发送请求响应,告知自身的活动窗口,初始序列号等信息
  4. 客户端收到连接响应后,回复服务端确认完成连接的响应
  5. 服务端收到确认信息后,将连接从“半连接队列”转移到“全接连队列”
  6. 服务端程序通过accept()方法取到“全连接队列”中的socket
  7. 至此服务端和客户端的通信都准备完了,双方可以传输数据了
  8. 假设此时客户端要发送数据到服务器,则通过调用socket对象的接口将要发送的数据内存地址告知内核
  9. 内核将要发送的数据根据情况分批次拷贝到发送缓冲区
  10. 协议栈从缓冲区中取得要发送得数据,封装成相应报文后发送出去
  11. 服务器端将数据恢复成应用数据后将其放入到socket的“接收缓冲”中
  12. 内核将接收缓冲区的数据拷贝到用户空间供应用程序使用

从图上可知,整个过程中使用到了Socket的“半连接队列”、“全连接队列”、“发送缓冲”和“接收缓冲”,这几个区域也是我们在性能调优时需要关注的。


2.1 连接队列溢出

如果半连接队列满了,可能是因为突发创建连接的客户端太多,也有可能是网络原因导致三次握手过程太慢,还有可能是导致了恶意的Dos攻击。

半连接队列满了便会拒绝客户端的连接请求,为了避免太多的连接被拒绝,我们可以通过增大队列长度来延长对列被填满的时间。这样在队列被填满前,随着许多的连接请求被成功建立并移除了半连接队列后,半连接队列又可以接收更多的连接请求。这个过程就是用空间来争取时间。


如果全连接队列满了,那就是内核没能及时的将连接对象返回给应用程序,很可能的原因是突发请求迅速增高,导致连接的速度超过了处理的速度,很快就把队列填满了。与半连接队列一样,我们也可以通过增加队列长度来争取时间。

当全连接队列溢出后,内核会根据配置参数net.ipv4.tcp_abort_on_overflow来决定如何处理

  • 如果值为0,则服务端丢弃客户端发送过来的ack,此时服务端处于【syn_rcvd】的状态,客户端处于【established】的状态。在该状态下会有一个定时器重传服务端 SYN/ACK 给客户端(不超过过/proc/sys/net/ipv4/tcp_synack_retries 指定的次数,Linux下默认5)。超过后,服务器不在重传,后续也不会有任何动作。如果此时客户端发送数据过来,服务端会返回RST。
  • 如果值为1,则服务端发送reset响应给客户端,表示废掉这个握手过程和这个连接(返回复位标记RST),客户端会报connection reset by peer


2.2 查看队列指标

如果你发现服务器的资源利用率不高,但却出现了大量的错误请求,此时请关注下这两个队列的使用情况。我们可以同过以下的命令来查看

ss -lnt

ss命令参数:

  • -l 显示正在Listener 的socket
  • -n 不解析服务名称
  • -t 只显示tcp

输出含义(LISTEN状态):

  • Recv-Q:完成三次握手并等待服务端 accept() 的 TCP 全连接总数
  • Send-Q:全连接队列大小

输出含义(非LISTEN状态):

  • Recv-Q:已收到但未被应用进程读取的字节数
  • Send-Q:已发送但未收到确认的字节数

TCP连接数计算_TCP_03


2.3 如何配置队列

先看下队列长度的算法

TCP连接数计算_客户端_04

可见队列的大小和以下几个配置有关

  • 全连接队列的大小通过/proc/sys/net/core/somaxconn配置和listen函数的backlog参数共同决定,取二者的较小值。
  • 半连接队列的大小受/proc/sys/net/ipv4/tcp_max_syn_backlog配置和全连接队列大小的影响。

可通过以下的方式来查看配置

# cat /proc/sys/net/core/somaxconn
# cat /proc/sys/net/ipv4/tcp_max_syn_backlog

也可以通过sysctl -a命令来查看所有内核参数和值,例如:

sysctl -a |grep somaxconn

TCP连接数计算_IP_05

可通过以下的途径对参数进行修改

  • sysctl -w net.core.somaxcnotallow=1024,该方法在重启系统之后会失效,参数值重新恢复成最初的128
  • 修改/etc/sysctl.conf文件,新增或修改值,如:net.core.somaxcnotallow=1024 ,然后执行sysctl -p命令使其生效。该方法可永久生效。


2.4 修改backlog

从上图可知,全连接队列还和backlog参数有关,该参数是服务端Socket构建时候可以传递的参数。许多的中间件都提供了修改的路径,例如

  • Tomcat 默认参数是100,server.xml配置文件中connector的acceptCount 配置就是backlog的值。而SpringBoot内置的Tomcat修改server.tomcat.accept-count参数即可
  • Nginx 配置 server{ listen 8080 default_server backlog=512}
  • Redis 配置redis.conf文件 tcp-backlog 511参数


3、再看Socket


Socket四元组

每次TCP连接建立的过程其实就是服务端和客户端信息交换的过程,每个TCP连接都对应一个服务端和客户端的Socket对象,在Socket对象中记录了所交换的信息。

Socket通过四元组来表示,也就是一条连接通过四个数据来唯一标识

<远程IP,远程端口,本地IP,本地端口>

基于该关系,我们来看下一台机器可以创建多少连接呢?


理论最大连接数

在TCP协议中,存储端口号的字段为2字节(16位),也就最多能表达的端口号个数为2^16,共计65535个。

在IP协议中,存储地址的字段为4字节(32位),也就是最多能表达的IP地址个数为2^32,共计4294967296

对于操作系统来讲,你可以通过“cat /etc/sysctl.conf”来查看“net.ipv4.ip_local_port_range”的配置,例如返回的结果

net.ipv4.ip_local_port_range = 1024 65535  ##默认

即1024~65535范围的端口提供给你使用,该值基本不需要改。


1)假设作为客户端去访问指定服务器的指定端口,那么该客户端可以建立多少连接

TCP连接数计算_TCP_06

从四元组来看,这种情况服务器的IP和端口都是固定的,本地的IP也是固定的,唯一会变的是本地的端口:<远程IP,远程端口,本地IP,本地端口>

那么可以创建的最大连接数就是本地可使用的端口数,即65535


2)假设作为客户端去访问指定服务器上的三个端口建立连接

TCP连接数计算_TCP_07

那么此时,本地端口和远程端口都是变化的:<远程IP,远程端口,本地IP,本地端口>

那么此时可以创建的连接数为:3*65535,共计196605条连接


3)作为客户端,如果还访问了多个服务器以及多个端口会怎么样

TCP连接数计算_TCP_08

2 x 65535 + 3 x 65535 + 4 x 65535=  589815 条连接


4)既做客户端,又作服务会怎么样

TCP连接数计算_IP_09

当然了,是否真的就这么的算下去呢?其实还有一个限制条件,就是进程能打开的文件句柄数和服务器总的文件句柄数所以这里计算出的连接数虽然很大却没啥意义


文件句柄数

linux中一切接文件,每个创建的socket也对应一个文件,对socket的读写其实也是对文件的读写。

在linux中,fs.file-max参数指定了操作系统级所有能打开的最大文件句柄数(注意:root用户不受该参数的影响)。

fs.nr_open参数用于配置可分配给单个进程的最大文件数,且可以针对不同用户配置不同的值。

除此以外,还有一个soft nofile进程级的参数,也是限制单个进程可以打开的最大文件数。只能在Linux上配置一次,不能针对不同用户配置不同值。

这几个参数的调整有几个注意点:

  1. 要对soft nofile参数进行调整时也要考虑对 hard nofile 参数一起调整,因为实际生效的值取两者最低的
  2. hard nofile参数调整,那么fs.nr_open也应该一起调整(fs.nr_open参数值一定要大于hard nofile参数值,否则后果可能很严重,会导致该用户无法登录,如果设置的是*,那么所有用户都不能登录)

通过执行“sysctl -a | grep fs.file-max”或“cat /proc/sys/fs/file-max”可以查看配置的值。

通过执行“sysctl -a | grep nr_open”或“cat /proc/sys/fs/nr_open”可看到所配置的值,默认值是:1048576(1024*1024)。

通过执行”sysctl -a | grep fs.file-nr“或“cat /proc/sys/fs/file-nr”可以查看配置的值。


修改file-max和nr_pen:

看到别处说在kernel文档中对该参数的默认值有以下说明

The value  in  file-max  denotes  the  maximum number of file handles that the
Linux kernel will allocate. When you get a lot of error messages about running
out of  file handles, you might want to raise this limit. The default value is
10% of  RAM in kilobytes.  To  change it, just  write the new number  into the
file:

https://www.cnblogs.com/wangqingyong/p/11078741.html

意思是file-max一般为内存大小(KB)的10%来计算,如果使用shell,可以这样计算:

grep -r MemTotal /proc/meminfo | awk '{printf("%d",$2/10)}'

file-max的合理值计算方法:取决于内存,每1M内存可增加100个。默认情况下,不要将超过10%的内存用于文件。比如1G内核内存,应该配置的值为:1x 0.1x 1024 x100=10240

vi /etc/sysctl.conf
fs.file-max=2621440
fs.nr_open=2621440

然后执行 sysctl -p 使配置生效

vi /etc/security/limits.conf
soft nofile 1000000
hard nofile 1000000


还可以通过以下方式查看系统中当前打开的文件句柄的数量

# sysctl -a | grep fs.file-nr
 fs.file-nr = 2720       0       782441

结果中的第一个数值:表示已经分配了的文件描述符数量;第二个数值表示空闲的文件句柄数量(待重新分配的);第三个数值表示能够打开文件句柄的最大值(与fs.file-max一致)

TCP连接数计算_客户端_10

关于Socket的实现,可参考这篇文章:https://juejin.cn/post/7287114372178296847


4、进程内存分配

Socket的创建、建立连接、全/半连接队列、读写缓冲区都在内核区进行,那么内核区有多少空间呢?

在继续前,先列出常用的几个单位和缩写:比特Bit(b), 字节Byte(B), 千字节Kilobyte(KB), 兆字节Megabyte(MB), 吉字节Gigabyte(GB) and 太字节Terabyte(TB)

操作系统会给每个进程分配一个虚拟内存空间,分配多少与操作系统的位数有关系

TCP连接数计算_IP_11

如上图所示,对于32位操作系统,每个进程都会分配3G的用户空间内存和1G的内核空间内存,但需要注意的是,内核空间内存是所有进程共享的,用户空间内存是进程独享的,也就是,虽然每个进程都分配了虚拟内核空间,但每个进程映射到实际内存上的区域都是一样的

64位进程原则上最多可以寻址264 bytes (16EB)。在x86_64架构上,目前每个进程的地址空间限制为128TB。


连接使用内存量

Socket在内核创建,每次创建一个socket对象后也会使用内存空间,有人计算出一个socket大约占用3.3k的空间(不含收发的缓冲区大小)

应用程序通过socket系统调用和远程主机进行通讯,每一个socket都有一个读写缓冲区。读缓冲区保存了远程主机发送过来的数据,如果缓冲区已满,则数据会被丢弃;写缓冲区保存了要发送到远程主机的数据,如果写缓冲区已满,则系统的应用程序在写入数据时会阻塞。

大规模Linux环境下,需要优化系统的缓存区大小,以免影响应用程序运行过程的整体性能。

通过/etc/sysctl.conf文件可以调整socket缓冲区大小

#所有协议(例如TCP,UDP)的通用设置,单位字节
net.core.wmem_default = 8388608 
net.core.rmem_default = 8388608 
#TCP连接写缓冲最大内存,单位字节 16MB
net.core.wmem_max = 16777216
#TCP连接读缓冲最大内存,单位字节 16MB
net.core.rmem_max = 16777216


#配置TCP的内存大小,单位是页(每页4K)。也就是可用于TCP连接的缓存
#页大小可通过 getconf PAGESIZE 查看,大小字节
#945000*4/1024/1024=3.6G
#9150000*4/1024/1024=34.9G
#9270000*4/1024/1024=35.36G
#也就是说最大有35.36内存可以用作TCP连接
#这三个量也同时代表了三个阀值,TCP的使用小于第二个值时kernel不会有任何提示
#操作,当大于第二个值时进入压力模式,当高于第三个值时将不接受新的TCP连接,
#同时会报出“Out  of  socket memory”
#或者“TCP:too many of orphaned sockets”。
net.ipv4.tcp_mem = 94500000 915000000 927000000

#写缓冲区设置,大小字节,覆盖上面 4k,16k,4MB
#三个值分表表示最小、默认和最大写缓冲区大小
#第二个默认值,会覆盖net.core.wmem_default
#第三个最大值,最终取与net.core.wmem_max的最小值
net.ipv4.tcp_wmem = 4096 16384 4194304  

#读缓冲区设置,覆盖上面 4k,86k,4MB
net.ipv4.tcp_rmem = 4096 87380 4194304
  • wmem_default:套接字发送缓冲区大小的缺省值,单位字节[所有协议]
  • rmem_default: 套接字读取缓冲区大小的缺省值,单位字节[所有协议]
  • wmem_max:套接字发送缓冲区大小的最大值,单位字节 [所有协议]
  • rmem_max: 套接字读取缓冲区大小的最大值,单位字节[所有协议]
  • tcp_wmem:套接字写缓存区最小、默认和最大值,单位字节[TCP协议]
  • tcp_rmem: 套接字读缓存区最小、默认和最大值,单位字节[TCP协议]
  • tcp_mem: 可以用TCP缓冲的三个阈值,单位[TCP协议]

当一个TCP建立间接并进行互相通信的时候,使用的内存情况

  • 最小:3.3k(socket自身消耗)+4k(读缓冲)+4k(写缓冲) = 11.3k
  • 默认:3.3k(socket自身消耗)+86k(读缓冲)+16k(写缓冲) = 105.3k
  • 最大:3.3k(socket自身消耗)+4MB(读缓冲)+4MB(写缓冲) = 8MB

从上面的配置来看,TCP的缓冲区使用量不能超过35.36G,那么最多可以建立的连接数为:35.36 x 1024 x 1024 / 11.3 = 3218207; 最少能建立的连接数为:35.36 x 1024 / 8 = 4526;按默认值计算约为:35.36 x 1024 x 1024 / 105.3 = 352114

注意:发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能:


小结:一台服务器能服务的TCP连接数主要会受到“最大文件句柄数”、“半连接队列大小”、“全连接队列大小”、“每个TCP读写缓冲区大小”、“所有TCP连接读写缓冲区允许的最大使用量”的影响。


附:常见优化参数

参数

说明

fs.file-max

整个操作系统最大能打开的文件描述符数

net.ipv4.tcp_syncookies

默认为0,1表示开启

net.ipv4.tcp_max_syn_backlog

SYNC队列长度。只有当tcp_syncookies被禁用时才生效(注意此参数过大可能遭遇到Syn flood攻击,即对方发送多个Syn报文端填充满Syn队列,使server无法继续接受其他连接)

net.core.somaxconn

Accept队列长度。如果在listen函数中传入了backlog值,则取两则最小值。如果该队列满了则新连接被拒绝

net.ipv4.tcp_keepalive_time

保活心跳包间隔时间,用于检测连接是否已断开,是服务器对客户端进行发送查看客户端是否在线

net.ipv4.tcp_tw_recycle

表示开启状态为TIME-WAIT的sockets的快速回收,默认为0,表示关闭

net.ipv4.tcp_tw_reuse

表示开启状态为TIME-WAIT的socket重用,用于新的TCP连接,默认为0,表示关闭

net.ipv4.tcp_fin_timeout

修改time_wait状的存在时间,默认的2MSL

net.ipv4.tcp_max_tw_buckets 

表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印告警信息

net.ipv4.ip_local_port_range

表示对外连接的端口范围。


net.core.wmem_default

各种类型的socket默认读写缓冲器大小

net.core.wmem_max

各种类型的socket默认读写缓冲器最大值

net.core.rmem_default

指定了接收套接字缓冲区大小的缺省值(以字节为单位)

net.core.rmem_max

指定了接收套接字缓冲区大小的最大值

net.ipv4.tcp_wmem 

三个值分别表示socket 的发送缓冲区分配的最少字节数、默认值、最大字节数;这里设置的值会覆盖wmem_default、wmem_max

net.ipv4.tcp_rmem

三个数值分别表示:T分配的最小内存、缺省内存、最大内存


附参考地址:

https://bbs.huaweicloud.com/blogs/300136

https://developer.aliyun.com/article/804896

https://learnku.com/articles/46249

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月15日 0

暂无评论

推荐阅读
  uIMxVj27KMVR   2023年12月24日   44   0   0 PodIPJavaJavaIPPod
I6B6oQKIoM4W