最近有一些时间,想着把某些基础的东西整理下,毕竟地基很重要,首先从计算机网络这部分入手。
1、网络收发概览
现在绝大部分的系统都是基于TCP协议的可靠传输,从数据的发送到接收的整个过程经历了很多环节,每一个环节也都有其各自的使命,通过大家的协同工作,最终将一个复杂的数据传输问题得以解决。下面是一次TCP通信的示意图
- 应用程序将要发送的数据通过接口发给socket
- 内核将数据从用户空间拷贝到socket的发送缓冲区(可能分多次拷贝)
- TCP协议栈代码读取发送缓冲区的数据进行“粘包或拆包”方式生成TCP段
- IP协议栈代码将TCP段拆分成一个个IP报文(如果需要拆的情况)
- IP报文到达以太网后,根据以太网帧大小的限制又被拆分成多个以太网帧
- 接收端收到以太网帧后去除帧头信息传递给socket继续处理,socket将分段的IP报文组装成完整的IP报文后,再将其IP去除,剩下TCP段部分
- sokcet对TCP报文进行处理,间分段传输的TCP报文组装成完整的报文后,将TCP头去除后发到接收缓冲区。
- 内核将TCP接收缓冲区的数据拷贝到用户空间
- 应用程序根据应用层协议,将分段传输的应用数据组装完整后,再交给业务代码进行逻辑运算
数据传输的快慢会影响到系统的整体性能,我们对网络通信有个整体的大致认识后,也可以为我们在分析系统性能问题方面提供帮助。
帧结构参考:https://info.support.huawei.com/info-finder/encyclopedia/zh/MTU.html
2、连接的建立
要让数据得以发送,首先就是建立通信两端的连接,我们从TCP协议入手看看其大致过程
- 首先服务端建立对某个端口的连接监听
- 客户端发送连接请求,将自己的IP地址、使用的端口以及滑动窗口大小等信息告知服务端,并创建本地的Socket对象获得句柄
- 服务器收到请求后,将该请求放入“半连接队列”,然后发送请求响应,告知自身的活动窗口,初始序列号等信息
- 客户端收到连接响应后,回复服务端确认完成连接的响应
- 服务端收到确认信息后,将连接从“半连接队列”转移到“全接连队列”
- 服务端程序通过accept()方法取到“全连接队列”中的socket
- 至此服务端和客户端的通信都准备完了,双方可以传输数据了
- 假设此时客户端要发送数据到服务器,则通过调用socket对象的接口将要发送的数据内存地址告知内核
- 内核将要发送的数据根据情况分批次拷贝到发送缓冲区
- 协议栈从缓冲区中取得要发送得数据,封装成相应报文后发送出去
- 服务器端将数据恢复成应用数据后将其放入到socket的“接收缓冲”中
- 内核将接收缓冲区的数据拷贝到用户空间供应用程序使用
从图上可知,整个过程中使用到了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:已发送但未收到确认的字节数
2.3 如何配置队列
先看下队列长度的算法
可见队列的大小和以下几个配置有关
- 全连接队列的大小通过
/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
可通过以下的途径对参数进行修改
- 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)假设作为客户端去访问指定服务器的指定端口,那么该客户端可以建立多少连接
从四元组来看,这种情况服务器的IP和端口都是固定的,本地的IP也是固定的,唯一会变的是本地的端口:<远程IP,远程端口,本地IP,本地端口>
那么可以创建的最大连接数就是本地可使用的端口数,即65535
2)假设作为客户端去访问指定服务器上的三个端口建立连接
那么此时,本地端口和远程端口都是变化的:<远程IP,远程端口,本地IP,本地端口>
那么此时可以创建的连接数为:3*65535,共计196605条连接
3)作为客户端,如果还访问了多个服务器以及多个端口会怎么样
2 x 65535 + 3 x 65535 + 4 x 65535= 589815 条连接
4)既做客户端,又作服务会怎么样
当然了,是否真的就这么的算下去呢?其实还有一个限制条件,就是进程能打开的文件句柄数和服务器总的文件句柄数。所以这里计算出的连接数虽然很大却没啥意义。
文件句柄数
linux中一切接文件,每个创建的socket也对应一个文件,对socket的读写其实也是对文件的读写。
在linux中,fs.file-max
参数指定了操作系统级所有能打开的最大文件句柄数(注意:root用户不受该参数的影响)。
而fs.nr_open
参数用于配置可分配给单个进程的最大文件数,且可以针对不同用户配置不同的值。
除此以外,还有一个soft nofile
进程级的参数,也是限制单个进程可以打开的最大文件数。只能在Linux上配置一次,不能针对不同用户配置不同值。
这几个参数的调整有几个注意点:
- 要对
soft nofile
参数进行调整时也要考虑对 hard nofile 参数一起调整,因为实际生效的值取两者最低的 - 对 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:
意思是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一致)
关于Socket的实现,可参考这篇文章:https://juejin.cn/post/7287114372178296847
4、进程内存分配
Socket的创建、建立连接、全/半连接队列、读写缓冲区都在内核区进行,那么内核区有多少空间呢?
在继续前,先列出常用的几个单位和缩写:比特Bit(b), 字节Byte(B), 千字节Kilobyte(KB), 兆字节Megabyte(MB), 吉字节Gigabyte(GB) and 太字节Terabyte(TB)
操作系统会给每个进程分配一个虚拟内存空间,分配多少与操作系统的位数有关系
如上图所示,对于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
标签:tcp,socket,队列,TCP,连接数,计算,net,连接 From: https://blog.51cto.com/dengshuangfu/8340225