如何优化TCP?
TCP三次挥手的性能提升
TCP 是面向连接的、可靠的、双向传输的传输层通信协议,所以在传输数据之前需要经过三次握手才能建立连接。
我们可以通过调整三次握手中的参数,来提高TCP三次握手的性能。
上图可见,客户端和服务端都可以针对三次握手优化性能,但是两者的优化方式是不同的。
三次握手建立连接的首要目的是同步序列号。只有同步了序列号才有可靠传输,TCP的许多特性都依赖于序列号实现,比如流量控制、丢包重传。SYN的全称就叫做Synchronized Sequence Numbers(同步序列号)
-
客户端优化
-
优化SYN_SENT状态
SYN_SENT状态是客户端发送SYN包之后的状态,客户端在等待ACK报文,正常情况下,服务器会在几毫秒内返回SYN + ACK,如果长时间没有收到SYN + ACK,客户端会重发SYN包,重发次数由
tcp_syn_retries
(tcp同步复审)参数决定。一般来说,每次超时重传时间是上一次的2倍。
-
-
服务端优化
-
调整SYN半连接队列大小
当服务端SYN半连接队列溢出,会导致后续连接被丢弃,可以通过nestat -s 观察半连接队列溢出的情况,如果SYN半连接队列溢出情况比较严重,可以通过tcp_max_syn_backlog、somaxconn、backlog参数来调整SYN半连接队列大小。
-
不使用SYN半连接队列
开启syncookies功能,可以在不使用SYN半连接队列的情况下成功建立连接。
-
SYN_RECV状态优化
当客户端收到SYN + ACK报文后,就会回复ACK给服务器,客户端的连接状态从SYN_SENT转变为ESTABLISHED,表示建立连接成功。
服务端收到ACK后,服务端的连接才转变为ESTABLISHED,如果没有收到ACK,就会重发SYN +ACK,我们可以通过
tcp_synack_retries
参数调整重发次数。 -
调整accept队列策略
我们可以通过
tcp_abort_on_overflow
(在溢出时中止)参数,来调整当accept队列满的策略。- 0:如果accept队列满了,那么server扔掉client发过来的ack;
- 1:如果accept队列满了,那么server发送一个RST(Reset the connection)给client,表示废掉握手过程和连接。
通常情况下,如果服务器上的进程只是因为短暂的繁忙造成accept队列满,当accept队列为空时,再次接收到ACK报文,仍然会重新成功建立连接。
所以,
tcp_abort_on_overflow
设为 0 可以提高连接建立的成功率,只有你非常肯定 TCP 全连接队列会长期溢出时,才能设置为 1 以尽快通知客户端。 -
调整accept队列的长度
accept 队列的长度取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog),其中:
- somaxconn 是 Linux 内核的参数,默认值是 128,可以通过
net.core.somaxconn
来设置其值; - backlog 是
listen(int sockfd, int backlog)
函数中的 backlog 大小;
- somaxconn 是 Linux 内核的参数,默认值是 128,可以通过
-
绕过三次握手
-
TCP四次挥手的性能提升
在TCP四次挥手中,客户端和服务端双方都可以主动断开连接,通常先关闭连接的一方称为主动方,后关闭连接的一方为被动方。
四次挥手只涉及了两种报文,分别是FIN 和ACK。
- FIN就是结束连接,谁发出FIN,就表示它不会再发送任何数据,关闭这一方的传输通道。
- ACK 就是确认,用来通知对方,你方的发送通道已经关闭。
四次挥手的过程:
- 当主动方关闭连接时,会发送 FIN 报文,此时发送方的 TCP 连接将从 ESTABLISHED 变成 FIN_WAIT1。
- 当被动方收到 FIN 报文后,内核会自动回复 ACK 报文,连接状态将从 ESTABLISHED 变成 CLOSE_WAIT,表示被动方在等待进程调用 close 函数关闭连接。
- 当主动方收到这个 ACK 后,连接状态由 FIN_WAIT1 变为 FIN_WAIT2,也就是表示主动方的发送通道就关闭了。
- 当被动方进入 CLOSE_WAIT 时,被动方还会继续处理数据,等到进程的 read 函数返回 0 后,应用程序就会调用 close 函数,进而触发内核发送 FIN 报文,此时被动方的连接状态变为 LAST_ACK。
- 当主动方收到这个 FIN 报文后,内核会回复 ACK 报文给被动方,同时主动方的连接状态由 FIN_WAIT2 变为 TIME_WAIT,在 Linux 系统下大约等待 1 分钟后,TIME_WAIT 状态的连接才会彻底关闭。
- 当被动方收到最后的 ACK 报文后,被动方的连接就会关闭。
你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
-
主动方的优化
-
调用close()函数和shutdown()函数有什么区别?
close()函数意味完全断开连接,无法发送/传输数据。调用close()一方的连接叫做孤儿连接。
而使用shutdown(),可以控制只关闭一个方向的连接。其中的参数可以决定连接断开的方式。
- SHUT_RD(0):关闭连接的「读」这个方向,如果接收缓冲区有已接收的数据,则将会被丢弃,并且后续再收到新的数据,会对数据进行 ACK,然后悄悄地丢弃。也就是说,对端还是会接收到 ACK,在这种情况下根本不知道数据已经被丢弃了。
- SHUT_WR(1):关闭连接的「写」这个方向,这就是常被称为「半关闭」的连接。如果发送缓冲区还有未发送的数据,将被立即发送出去,并发送一个 FIN 报文给对端。
- SHUT_RDWR(2):相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。
-
FIN_WAIT1状态的优化
主动方发送FIN后,就到了FIN_WAIT1的状态,如果迟迟收不到ACK,就会重发FIN报文。重发次数可以由tcp_orphan_retries (orphan:孤儿)参数控制。
如果 FIN_WAIT1 状态连接很多,我们就需要考虑降低 tcp_orphan_retries 的值,当重传次数超过 tcp_orphan_retries 时,连接就会直接关闭掉。
但是如果遭到恶意攻击,FIN报文无法发送出去,主要由两个特性决定:
- TCP报文是有序的,当缓冲区还有数据,FIN报文不能提前发送。
- 其次,当TCP有流量控制的功能时,接收方接收窗口为0,发送方就不再发送数据,攻击者可以将接收窗口设置为0,就会使得FIN报文无法发送,连接会一直处于FIN_WAIT1的状态。
可以通过调整tcp_max_orphans参数,来调整孤儿连接的最大数量。
当进程调用了
close
函数关闭连接,此时连接就会是「孤儿连接」,因为它无法再发送和接收数据。Linux 系统为了防止孤儿连接过多,导致系统资源长时间被占用,就提供了tcp_max_orphans
参数。如果孤儿连接数量大于它,新增的孤儿连接将不再走四次挥手,而是直接发送 RST 复位报文强制关闭。 -
FIN_WAIT2状态的优化
如果连接是用 shutdown 函数关闭的,连接可以一直处于 FIN_WAIT2 状态,因为它可能还可以发送或接收数据。但对于 close 函数关闭的孤儿连接,由于无法再发送和接收数据,所以这个状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长
-
TIME_WAIT状态的优化
TIME_WAIT 状态的连接,在主动方看来确实快已经关闭了。然后,被动方没有收到 ACK 报文前,还是处于 LAST_ACK 状态。如果这个 ACK 报文没有到达被动方,被动方就会重发 FIN 报文。重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。
TIME-WAIT 的状态尤其重要,主要是两个原因:
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
- 保证「被动关闭连接」的一方,能被正确的关闭;
-
优化方式一:
Linux 提供了 tcp_max_tw_buckets 参数,当 TIME_WAIT 的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT 而直接关闭:
当服务器的并发连接增多时,相应地,同时处于 TIME_WAIT 状态的连接数量也会变多,此时就应当调大
tcp_max_tw_buckets
参数,减少不同连接间数据错乱的概率。tcp_max_tw_buckets 也不是越大越好,毕竟系统资源是有限的。 -
优化方式二:
有一种方式可以在建立新连接时,复用处于 TIME_WAIT 状态的连接,那就是打开 tcp_tw_reuse 参数。但是需要注意,该参数是只用于客户端(建立连接的发起方),因为是在调用 connect() 时起作用的,而对于服务端(被动连接方)是没有用的。
-
优化方式三:
我们可以在程序中设置 socket 选项,来设置调用 close 关闭连接行为。如果
l_onoff
为非 0, 且l_linger
值为 0,那么调用 close 后,会立该发送一个 RST 标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了 TIME_WAIT 状态,直接关闭。这种方式只推荐在客户端使用,服务端千万不要使用。因为服务端一调用 close,就发送 RST 报文的话,客户端就总是看到 TCP 连接错误 “connnection reset by peer”。(对等方重置连接)
-
-
被动方的优化
-
被动关闭的连接方应对非常简单,它在回复 ACK 后就进入了 CLOSE_WAIT 状态,等待进程调用 close 函数关闭连接。因此,出现大量 CLOSE_WAIT 状态的连接时,应当从应用程序中找问题。
当被动方发送 FIN 报文后,连接就进入 LAST_ACK 状态,在未等到 ACK 时,会在
tcp_orphan_retries
参数的控制下重发 FIN 报文。
-
TCP传输数据的性能提升
之前说的是在三次握手和四次挥手的优化策略,接下来主要介绍的是 TCP 传输数据时的优化策略。
-
滑动窗口如何影响传输速度?
由于TCP报文发出,必须接收到对方返回的确认报文ACK,如果未收到,就会超时重发该报文,直到收到ACK为止。所以,TCP报文发出后,并不会立刻在内存中删除。
如果TCP每次发一条数据,接收方每次发送一条确认应答,这样效率很低,所以我们并行批量发送报文,再批量确认报文。
这时就要考虑一个问题,就是接收方的处理能力,所以,TCP提供了一种机制可以让发送方根据接收方的实际接收能力,来控制发送的数据量,这就是滑动窗口。
接收方根据它的缓冲区,可以计算出后续能够接收多少字节的报文,这个数字叫做接收窗口。当内核接收到报文时,必须用缓冲区存放它们,这样剩余缓冲区空间变小,接收窗口也就变小了;当进程调用 read 函数后,数据被读入了用户空间,内核缓冲区就被清空,这意味着主机可以接收更多的报文,接收窗口就会变大。
因此,接收窗口并不是恒定不变的,接收方会把当前可接收的大小放在 TCP 报文头部中的窗口字段,这样就可以起到窗口大小通知的作用。
窗口字段只有两个字节,因此最多能表达65535字节大小的窗口,也就是64KB大小。后续又提出了扩充窗口的想法:
在 TCP 选项字段定义了窗口扩大因子,用于扩大 TCP 通告窗口,其值大小是 2^14,这样就使 TCP 的窗口大小从 16 位扩大为 30 位(2^16 * 2^ 14 = 2^30),所以此时窗口的最大值可以达到 1GB。
要使用窗口扩大选项,通讯双方必须在各自的 SYN 报文中发送这个选项:
- 主动建立连接的一方在 SYN 报文中发送这个选项;
- 而被动建立连接的一方只有在收到带窗口扩大选项的 SYN 报文之后才能发送这个选项。
但是,网络传输的能力也是有限的,当发送方依据发送窗口,发送超过网络处理能力的报文时,路由器会直接丢弃这些报文。因此,缓冲区的内存并不是越大越好。
-
如何确定最大传输速度?
窗口大小由内核缓冲区大小决定。如果缓冲区与网络传输能力匹配,那么缓冲区的利用率就达到了最大化。
网络是有「带宽」限制的,带宽描述的是网络传输能力,它与内核缓冲区的计量单位不同:
- 带宽是单位时间内的流量,表达是「速度」,比如常见的带宽 100 MB/s;
- 缓冲区单位是字节,当网络速度乘以时间才能得到字节数;
如果最大带宽是 100 MB/s,网络时延(RTT)是 10ms 时,意味着客户端到服务端的网络一共可以存放 100MB/s * 0.01s = 1MB 的字节。
这个 1MB 是带宽和时延的乘积,所以它就叫「带宽时延积」(缩写为 BDP,Bandwidth Delay Product)。同时,这 1MB 也表示「飞行中」的 TCP 报文大小,它们就在网络线路、路由器等网络设备上。如果飞行报文超过了 1 MB,就会导致网络过载,容易丢包。
由于发送缓冲区大小决定了发送窗口的上限,而发送窗口又决定了「已发送未确认」的飞行报文的上限。因此,发送缓冲区不能超过「带宽时延积」。
发送缓冲区与带宽时延积的关系:
- 如果发送缓冲区「超过」带宽时延积,超出的部分就没办法有效的网络传输,同时导致网络过载,容易丢包;
- 如果发送缓冲区「小于」带宽时延积,就不能很好的发挥出网络的传输效率。
所以,发送缓冲区的大小最好是往带宽时延积靠近。
-
怎样调整缓冲区大小?
发送缓冲区是自行调节的,当发送方发送的数据被确认后,并且没有新的数据要发送,就会把发送缓冲区的内存释放掉。可以通过tcp_wmem 参数配置。
**接收缓冲区可以根据系统空闲内存的大小来调节接收窗口:**需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能:
- 如果系统的空闲内存很多,就可以自动把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而提升发送方发送的传输数据数量;
- 反之,如果系统的内存很紧张,就会减少缓冲区,这虽然会降低传输效率,可以保证更多的并发连接正常工作;
-
怎样知道当前内存是否紧张或充分呢?
可以通过 tcp_mem 配置完成。表示页面大小,1页表示4KB,如果计算的值大于tep_mem的最大值,系统将无法为新的TCP连接分配内存,即TCP连接被拒绝。
-
实际场景的调节策略?
在高并发服务器中,为了兼顾网速与大量的并发连接,我们应当保证缓冲区的动态调整的最大值达到带宽时延积,而最小值保持默认的 4K 不变即可。而对于内存紧张的服务而言,调低默认值是提高并发的有效手段。
同时,如果这是网络 IO 型服务器,那么,调大 tcp_mem 的上限可以让 TCP 连接使用更多的系统内存,这有利于提升并发能力。需要注意的是,tcp_wmem 和 tcp_rmem 的单位是字节,而 tcp_mem 的单位是页面大小。而且,千万不要在 socket 上直接设置 SO_SNDBUF 或者 SO_RCVBUF,这样会关闭缓冲区的动态调整功能。