首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >再见了TCP(性能优化)

再见了TCP(性能优化)

作者头像
shysh95
发布2021-12-21 20:39:43
发布2021-12-21 20:39:43
1.1K0
举报
文章被收录于专栏:shysh95shysh95

摘要

  1. 三次握手性能提升
  2. 四次挥手性能提示
  3. 数据传输性能提升

三次握手性能提升

三次握手性能提升主要通过以下方面:

  • 调整SYN报文的重传次数
  • 调整SYN半连接队列的长度
  • 调整SYN+ACK报文的重传次数
  • 调整accpet全连接队列的长度
  • 绕过三次握手

SYN报文重传次数的优化

客户端在建立连接时会首先发送SYN报文,但是假设此时你没有收到服务端SYN+ACK的响应报文,客户端此时会重传SYN报文,此时你需要根据实际情况来调整SYN报文的重传次数,以便客户端能够及时得到反馈。

代码语言:javascript
复制
# 查看SYN报文重传次数
cat /proc/sys/net/ipv4/tcp_syn_retries

调整SYN半连接队列的长度

服务端在收到SYN包后,会回复SYN+ACK包,并且把链接放入SYN半连接队列,假设半连接队列增的速度大于取的速度,半连接队列会越来越多,直到无法容纳更多的连接。

此时需要增加半连接队列的大小,增大半连接队列的操作相对还是比较繁琐的:

  • 增大tcp_max_syn_backlog的值
代码语言:javascript
复制
# 查看tcp_max_syn_backlog的值
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
  • 增大somaxconn和backlog(backlog需要应用程序自己调整)
代码语言:javascript
复制
# 查看somaxconn的值
cat /proc/sys/net/core/somaxconn

除了增大半连接队列,还可以通过开启tcp_syncookies避开使用半连接队列:

代码语言:javascript
复制
# 查看tcp_syncookies的默认值
# 0:不开启该功能
# 1:当半连接队列满时,开启该功能
# 2:无条件开启该功能
cat /proc/sys/net/ipv4/tcp_syncookies

调整SYN+ACK报文的重传次数

服务器发送SYN+ACK报文的次数,假设因为网络波动一直收不到ACK报文响应,此时应该略为调大重发次数。重发次数由内核参数tcp_synack_retries控制。

代码语言:javascript
复制
# 查看SYN+ACK报文的最大重传次数
cat /proc/sys/net/ipv4/tcp_synack_retries

调整accpet全连接队列的长度

服务器在收到第三次握手的ACK报文以后,会初始化一个完全的连接并放入全连接队列中等待应用程序取走,假设应用程序无法及时取走就有可能导致全连接队列被放满并溢出。

全连接队列溢出后的行为我们也可以通过参数进行控制,详情请见TCP连接队列这篇文章。

全连接队列的大小=min(somaxconn, backlog),backlog需要应用程序控制。

代码语言:javascript
复制
# 查看somaxconn的值
cat /proc/sys/net/core/somaxconn

绕过三次握手

三次握手造成的影响就是HTTP请求必须在1个RTT以后才可以发送数据,2个RTT才能接收数据。

在Linux3.7内核之后,提供了TCP Fast Open功能,该功能可以减少TCP连接建立的延时,除首次建立TCP连接后续的连接建立过程中在第一次握手就可以发送数据(也就是0 RTT延时),1个RTT以后就可以接收数据。

TCP Fast Open功能由内核参数tcp_fastopen来控制:

代码语言:javascript
复制
# 查看TCP Fast Open功能开启情况
# 0:关闭
# 1:作为客户端使用Fast Open功能
# 2:作为服务端使用Fast Open功能
# 3:无论作为客户端还是服务端,都使用Fast Open功能
cat /proc/sys/net/ipv4/tcp_fastopen

TCP Fast Open的工作原理

在客户端首次建立连接的过程如下:

  1. 客户端发送SYN报文,报文中包含Fast Open选项,且该选项的Cookie为空,表示客户端请求Fast Open Cookie
  2. 服务器假设支持Fast Open功能,会生成Cookie,并将Cookie放置在SYN+ACK报文中的Fast Open选项中返回给客户端
  3. 客户端收到SYN+ACK报文以后,本地缓存Fast Open选项中的Cookie

之后客户端再向服务器建立连接的过程就发生了变化:

  1. 客户端发送SYN报文,该报文包含数据+本地缓存的Fast Open Cookie
  2. 服务器在收到Cookie后会对器进行校验,如果Cookie有效,服务器将在SYN+ACK报文中对SYN和数据进行确认,并且随后会将数据返回给客户端,如果Cookie无效,服务器会丢弃SYN报文中的数据,随后的确认报文只会确认SYN对应的序列号,
  3. 客户端在会将ACK报文和数据发送给服务端,如果第一次握手时数据没有被确认,客户端这里会重新发送数据。

通过上述图和流程可以看出。TCP Fast Open会减少整个数据的RTT延时。

四次挥手性能提升

安全关闭连接必须通过四次挥手,应用程序需要调用close或者shutdown方法发出FIN报文。

TCP四次挥手性能提升主要有以下优化方案:

  • 调整FIN报文的重传次数(主动方)
  • 调整孤儿连接的数量
  • 调整FIN_WAIT_2的状态持续时间
  • 调整TIME_WAIT的上限个数

什么是孤儿连接?

close函数意味着完全断开连接,无法传输数据也不能发送数据,调用了close方法的一方的连接称为孤儿连接。netstat -p会发现连接对应的进程名为空。

shutdown函数的区别

代码语言:javascript
复制
int shutdown(int sock, int howto);

shutdown函数断开连接的方式主要取决于第二个参数:

  • SHUT_RD(0):关闭连接的读,如果接收缓冲区有已接收的数据会被丢弃,并且后续接收的数据也会被丢弃但是会进行ACK确认。
  • SHUT_WR(1):关闭连接的写,如果发送缓冲区还有未发送的数据将会被立即发送出去,并发送一个FIN报文给对端,通常这种连接被称为半关闭的连接。
  • SHUT_RDWR(2):关闭连接的读和写,相当于SHUT_RD和SHUT_WR操作各一次

调整FIN报文的重传次数(主动方)

主动断开方发送FIN报文后,连接处于FIN_WAIT_1状态,但是如果一致收不到被动方的ACK报文,那么连接将会一直处于FIN_WAIT_1状态,并且内核会因为超时重发FIN报文,FIN报文的重发次数由tcp_orphan_retries控制:

代码语言:javascript
复制
# 查看FIN报文的重发次数,默认值是0,表示特指重发8次
cat /proc/sys/net/ipv4/tcp_orphan_retries

如果FIN_WAIT_1状态的连接很多,可以考虑降低tcp_orphan_retries的值,当FIN报文重传次数超过该值时连接会被直接关闭掉。

调整孤儿连接的个数

在遇到恶意攻击,FIN报文无法发出,FIN报文无法发出的原因是:

  • TCP保证报文有序发送,当发送缓冲区还有数据没有发送时,FIN报文也不能提前发送
  • TCP的流量控制,当接收方窗口为0时,发送方无法发送数据,所以攻击者有可能通过下载大文件等具有占用高带宽的操作使得接收窗口变为0,FIN报文则无法发出

这种情况下可以通过调整孤儿连接的数量即可,孤儿连接的数量由tcp_max_orphan控制:

代码语言:javascript
复制
# 查看孤儿连接的数量
cat /proc/sys/net/ipv4/tcp_max_orphan

当孤儿连接数量大于上述的值时,新增的孤儿连接不再走四次挥手,直接发送RST报文强制关闭。

调整FIN_WAIT_2的状态持续时间

主动方在收到ACK报文后,会处于FIN_WAIT_2状态,表示主动方发送通道关闭,等待被动方发送FIN报文,关闭被动方的发送通道。

如果连接使用shutdown函数关闭的,连接可以一直处于FIN_WAIT_2状态,因为它可能还可以发送或接收数据。但对于close函数关闭的孤儿连接,由于无法再发送和接收数据,所以这个状态不可以持续太久,这个状态的最大持续时间受内核参数tcp_fin_timeout控制:

代码语言:javascript
复制
# 查看孤儿连接FIN_WAIT_2的时间,默认值是60s
cat /proc/sys/net/ipv4/tcp_fin_timeout

调整TIME_WAIT状态个数上限

当收到被动方发来的FIN报文后,主动方会立刻回复ACK,表示确认对方的发送通道已经关闭,紧接着进入TIME_WAIT状态。

MSL定义了一个报文在网络中的最长生存时间,TIME_WAIT和FIN_WAIT_2都会保持2MSL时长,在Linux中MSL固定为30s,所以TIME_WAIT和FIN_WAIT_2都是60s。

TIME_WAIT的最大个数受内核参数tcp_max_tw_bucktes控制,当TIME_WAIT的数量超过该参数的限制时,连接关闭将不再经历TIME_WAIT而直接关闭。

代码语言:javascript
复制
# 查看TIME_WAIT的最大个数
cat /proc/sys/net/ipv4/tcp_max_tw_buckets

tcp_max_tw_bucktes的值并不是越大越好,因为内存和端口都是有限资源。

复用TIME_WAIT连接

既然tcp_max_tw_bucktes的参数无法无限变大,还有一种方式就是复用TIME_WAIT状态的连接。是否复用TIME_WAIT的是通过内核参数tcp_tw_reuse参数进行控制,该参数只对客户端(调用connect)有效:

代码语言:javascript
复制
# 查看tcp_tw_reuse功能
cat /proc/sys/net/ipv4/tcp_tw_reuse

使用这个选项,还需要双方都打开对TCP时间戳的支持:

代码语言:javascript
复制
# 查看是否打开时间戳功能
cat /proc/sys/net/ipv4/tcp_timestamps

时间戳带来的好处如下:

  • 重复的数据包会因为时间戳过期被自然丢弃
  • 防止序列号绕回,重复的数据包会由于时间戳过期被自然丢弃

复用TIME_WAIT只使用于连接发起方,并且需要连接在TIME_WAIT状态的时间超过1s才可以复用。

小心大量的close wait状态连接

CLOSE WAIT状态出现在被动方在收到FIN报文以后并发出ACK回应以后的一种状态,当被动方再次发送FIN报文以后便会进入LAST ACK状态,假如有大量的CLOSE WAIT状态的连接,此时一定小心作为被动方的你的应用程序是不是有BUG,没有调用close函数去发送FIN报文。

数据传输性能提升

数据传输性能提升的主要方法为:

  • 扩容滑动窗口发送更多的数据
  • 配置合适的内存指标,缓冲区动态调节

扩充滑动窗口

TCP连接由内核维护,内核会为每个连接建立内存缓冲区。

TCP报文发出去以后,并不会立即从内存中删除,因为重传时还需要使用。

TCP连接在过多时,通过free命令可以看出buff/cache内存是增大的。

流量控制中我们已经讲述了滑动窗口对数据包发送的影响,TCP头部中窗口字段只占用16位(2字节),因此最大可以发送64KB大小的数据,随着网络的高速发展,64KB的窗口其实是很小的,因此在TCP中采用了扩充窗口的方式,具体如下:

在TCP选项字段中定义了窗口扩大因子,其值大小是2^14,这样TCP窗口的位数从16位扩大为30位(2^16 * 2^14 = 2^30),此时窗口最大值可以达到1GB

是否扩充滑动窗口由内核参数tcp_window_scaling控制:

代码语言:javascript
复制
# 查看是否启用扩容滑动窗口
# 默认是打开
cat /proc/sys/net/ipv4/tcp_window_scaling

使用扩充滑动窗口功能需要在各自的SYN报文中发送这个选项,并且被动方必须在主动方的SYN报文包含这个选项时才可以在自己的SYN报文中发送这个选项。

如何确定网络的最大传输速度?

网络是有带宽限制的,带宽描述的是网络传输能力,它与内核缓冲区的计量单位是不同的:

  • 带宽是单位时间内的流量,表示的是速度,比如带宽是100MB/s
  • 内核缓冲区单位是字节,网络速度乘以时间才可以得到字节数

什么是带宽时延积(BDP)?

带宽时延积决定了飞行报文的大小,飞行报文指的是客户端到服务端上的网络数据包。

带宽时延积BDP = RTT * 带宽

假设带宽是100MB/s,RTT为10ms,那么BDP就为1MB的字节,如果在网络上的报文大小超过了1MB,就会导致网络过载,容易丢包。

发送缓冲区决定了发送窗口的上限,发送窗口又决定了已发送未确认的飞行报文的上限,因此发送缓冲区不能超过带宽时延积。

发送缓冲区的大小最好是往带宽时延积靠近。

如何调整缓冲区大小?

Linux中发送缓冲区和接收缓冲区都可以使用参数动态调节。

发送缓冲区由内核tcp_wmem参数控制:

代码语言:javascript
复制
# 查看发送缓冲区的范围
# 默认(单位字节)是4096 16384 4194304
# 第一个数值动态调节的最小值4KB
# 第二个数值是发送缓冲区的初始默认值86KB
# 第三个数值是动态调节的最大值4MB
cat /proc/sys/net/ipv4/tcp_wmem

发送缓冲区是自动调节,当发送方的数据被确认后并且无新数据要发送,发送缓冲区的内存就会被释放。

接收缓冲区由内核参数tcp_moderate_rcvbuf和tcp_rmem参数共同控制:

代码语言:javascript
复制
# 查看是否启用接收缓冲区自动调节
# 默认值是1,表示开启
cat /proc/sys/net/ipv4/tcp_moderate_rcvbuf

# 查看接收缓冲区的范围
# 默认(单位字节)是4096 131072 6291456
# 第一个数值是动态调节的最小值4KB
# 第二个参数是接收缓冲区的初始默认值128KB
# 第三个参数是动态调节的最大值6MB
cat /proc/sys/net/ipv4/tcp_rmem

接收缓冲区可以根据系统空闲内存来调节接收窗口:

  • 如果系统空闲内存多,接收缓冲区会增大,接收窗口相应的也会变大,允许发送方发送更多的数据
  • 如果系统内存紧张,接收缓冲区会减少,接收窗口会变小,虽然传输效率会降低,但可以保证更多的TCP连接正常工作

如何确定内存是否紧张

内存是否紧张是由内核参数tcp_mem控制:

代码语言:javascript
复制
# 查看内存范围
# 默认(单位是页,1页=4KB)是10320 13762 20640
# 当TCP内存小于4KB*10320时,不需要进行调节
# 当TCP内存位于第一个和第二个值时,内核开始调节接收缓冲区的大小
# 当TCP内存大于第三个值时,内核不再为TCP分配新内存,新连接无法建立
cat /proc/sys/net/ipv4/tcp_mem

一定不要在你的应用程序的Socket上设置SO_SNDBUF或者SO_RCVBUF,一旦设置会关闭缓冲区的动态调整功能。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员修炼笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档