TCP 三次握手优化

客户端

客户端优化的手段主要是控制 SYN 报文的重传次数

比如,在公司内网,或者服务之间相互调用,不需要多次重传(网络环境比较好),就可以适当减少 SYN 报文的重传次数,以尽快向应用报告错误

SYN 的重传次数可以通过修改 /proc/sys/net/ipv4/tcp_syn_retries

服务端

服务端可以通过优化:

  • SYN + ACK 报文的重传次数:tcp_synack_retries
  • 半连接队列的大小:调整 tcp_max_syn_backlog、listen 的 backlog、somaxconn
  • 启用 syn_cookies 防止 SYN 攻击
  • 全连接队列的大小:调整 listen 的 backlog、somaxconn

“绕过”三次握手

这里的「绕过」不是真正绕过三次握手,而是 TCP 的 快启动 机制,在 第一次握手时,携带应用层数据 + cookie,减少请求数

例如,对于 HTTP 而言,通常的形式是这样:

如果启用了快启动,是这样的:

图片来自小林 coding

  • 第一次握手,携带了 请求 Cookies,即要求 Server 启用快启动机制
  • Server 如果支持快启动,会在本地生成一个 cookie,在第二次握手时发送给 Client
  • 第三次握手,带上 HTTP 请求,这一步与之前一样

重点来了,下一次客户端第一次握手时,除了 SYN 以外,还会带上 Server 之前发来的 cookie + HTTP GET

Server 除了发送 ACK 以外,还会判断 Cookie 是否有效(过期):

  • 如果有效,发送 GET 请求对应的资源给 Client
  • 如果无效,忽略 Cookie

可以发现,启用了 快启动机制,可以减少一次 RTT 的消耗

此外,cookie 的值是放在 TCP 首部的 Opinion 中的

那么在 Linux 下,如何启用快启动呢?

修改 /proc/sys/net/ipv4/tcp_fastopn,有三个值可选:

  • 0:关闭
  • 1:作为 Client 启用
  • 2:作为 Server 启用
  • 3:都启用

注意:需要通信双方都启用才行

TCP 四次挥手优化

主动方

FIN_WAIT_1 的优化

可以通过 tcp_orphan_retries 调整 FIN 报文的重传次数

tcp_orphan_retries 的默认值为 0,但实际上对应的是重传 8 次

FIN_WAIT_2 的优化

如果调用的是 close 来关闭的 TCP 连接,因为 close 后的套接字,收发能力都不具备(因此也叫做 孤儿连接),不应该在 FIN_WAIT_2 等待过长时间,因此可以通过调整 tcp_fin_timeout 来修改等待的时间

默认值为 2MSL,与 TIME_WAIT 的时间一样

但是,如果是调用的 shutdown 来关闭的 TCP 连接,就不能修改等待时间了,shutdown 关闭的套接字,FIN_WAIT_2 的时间是不受 tcp_fin_timeout 控制的

此外,还可以调整 tcp_max_orphans 来修改 孤儿连接 的最大数,以避免太多的连接处于 FIN_WAIT_2

TIME_WAIT

如果有太多连接处于 TIME_WAIT,会造成资源浪费

可以通过修改 tcp_max_tw_buckets 来修改处于 TIME_WAIT 的连接数上限,超过上限以后,如果还有连接要释放,就会直接释放而不进入 TIME_WAIT

此外,连接发起方 还可以 复用 TIME_WAIT 的连接,需要修改两个参数:

  • tcp_tw_reuse:1
  • tcp_timestamps:1(为报文引入时间戳)

被动方

被动方,可以优化的点就只有 FIN 报文的重传次数了,还是修改 tcp_orphan_retries

此外,还需要注意及时调用 close 来避免大量连接处于 CLOSE_WAIT 状态

TCP 传输数据的优化

滑动窗口

TCP 首部滑动窗口的大小默认只有 16 位,即 65535 字节(64KB)

对于当今的网络来说,64 KB 太小了,要发送一个包,可能得分很多次,这无疑降低了效率

在 TCP 选项字段定义了窗口扩大因子,用于扩大 TCP 通告窗口,最大可以到 1G

因此,可以根据实际情况动态调整窗口的大小

在 Linux 中可以通过修改 tcp_window_scaling 的值为 1 来打开窗口扩大选项(默认已经打开)

当然,窗口也不能真的有 1G 大(除非网络带宽很大)

窗口大小还要考虑一个重要参数:带宽时延积

例如,带宽为 100M,往返时延 RTT 为 10ms,那么,带宽时延积就为 100 * 0.01 = 1M

窗口的大小不应该超过带宽时延积,否则可能会造成网络负载太高,丢包率上升

缓冲区

仅仅修改窗口的大小还不够,OS 会根据发送(接收)缓冲区的大小动态调整发送(接收)窗口的大小

因此,即使启用了窗口扩大选项,如果缓冲区很小,窗口的大小也会很小

如何修改缓冲区的大小呢?

可以通过修改 tcp_wmemtcp_rmem 来调整发送(接收)缓冲区的大小范围,格式如下:

root@SkyLee:~# cat /proc/sys/net/ipv4/tcp_wmem 
4096	16384	4194304
  • 4096:下限为 4096 字节,即使内存有压力的情况下也要保证的
  • 16384:初识值为 16384 字节
  • 4194304:上限为 4194304 字节

前面提到,窗口的大小不应该超过带宽时延积,因此,这里就要注意设置 缓冲区上限 <= 带宽时延积

在 Linux 中,默认 已经启用了动态修改 发送缓冲区 的大小,但接收缓冲区的调节功能需要修改 tcp_moderate_rcvbuf 为 1 来开启

接收缓冲区的调整是基于 tcp_mem 的:

root@SkyLee:~# cat /proc/sys/net/ipv4/tcp_mem 
18603	24804	37206

上面的数字不是字节数,而是页面数(一般来说,1 页 = 4K)

当 TCP 使用内存:

  • 小于 18603 * 4KB 时,不需要调整接收缓冲区
  • 18603 * 4KB <= used <= 24804 * 4KB 时,开始调整接收缓冲区
  • 大于 37206 * 4KB 时,不允许新的 TCP 连接(TCP 可用内存为 0)

注意: 如果要启用自动调整功能,不要在设置 socket 的 SO_SNDBUF 或者 SO_RCVBUF 选项,设置后会关闭自动调整功能

对于 Server 来说,应该提高 TCP 内存的上限,如果内存紧张,可以降低 buffer 的下限,以允许更多的连接数,提高并发量