TCP 连接的建立是一个从 Client 侧调用 connect(), 到 Server 侧 accept() 成功返回的过程。你可以看到,在整个 TCP 建立连接的过程中,各 个行为都有配置选项来进行控制。

重传配置

Client 会给 Server 发送一个 SYN 包,但是该 SYN 包可能会在传输过程中丢失,或 者因为其他原因导致 Server 无法处理,此时 Client 这一侧就会触发超时重传机制。但是 也不能一直重传下去,重传的次数也是有限制的,这就是 tcp_syn_retries 这个配置项来决 定的。

对于 tcp_syn_retries 为 3 而言,总共会重传 3 次,也就是说从第一次发出 SYN 包后,会 一直等待(1 + 2 + 4 + 8)秒,如果还没有收到 Server 的响应,connect() 就会产生 ETIMEOUT 的错误。

tcp_syn_retries 的默认值是 6,也就是说如果 SYN 一直发送失败,会在(1 + 2 + 4 + 8 + 16+ 32 + 64)秒,即 127 秒后产生 ETIMEOUT 的错误。

通常情况下,我们都会将数据中心内部服务器的 tcp_syn_retries 给调小,这里推荐 设置为 2,来减少阻塞的时间。配置项为net.ipv4.tcp_syn_retries = 2

半连接

Server 没有响应 Client 的 SYN,除了我们刚才提到的 Server 已经不存在了这种情况 外,还有可能是因为 Server 太忙没有来得及响应,或者是 Server 已经积压了太多的半连 接(incomplete)而无法及时去处理。

半连接,即收到了 SYN 后还没有回复 SYNACK 的连接,Server 每收到一个新的 SYN 包,都会创建一个半连接,然后把该半连接加入到半连接队列(syn queue)中。syn queue 的长度就是 tcp_max_syn_backlog 这个配置项来决定的,当系统中积压的半连接 个数超过了该值后,新的 SYN 包就会被丢弃。对于服务器而言,可能瞬间会有非常多的新 建连接,所以我们可以适当地调大该值,以免 SYN 包被丢弃而导致 Client 收不到 SYNACK:

net.ipv4.tcp_max_syn_backlog = 16384

Server 中积压的半连接较多,也有可能是因为有些恶意的 Client 在进行 SYN Flood 攻 击。为了防止 SYN Flood 攻击,Linux 内核引入了 SYN Cookies 机制

推荐开启 SYN Cookies: net.ipv4.tcp_syncookies = 1

全连接

就像半连接队列(syn queue)的长度有限制一样,全连接队列(accept queue) 的长度也有限制,目的就是为了防止 Server 不能及时调用 accept() 而浪费太多的系统资 源。

全连接队列(accept queue)的长度是由 listen(sockfd, backlog) 这个函数里的 backlog 控制的,而该 backlog 的最大值则是 somaxconn。somaxconn 在 5.4 之前的内核中, 默认都是 128(5.4 开始调整为了默认 4096),建议将该值适当调大一些: net.core.somaxconn = 16384

tcp_abort_on_overflow

当服务器中积压的全连接个数超过该值后,新的全连接就会被丢弃掉。Server 在将新连接 丢弃时,有的时候需要发送 reset 来通知 Client,这样 Client 就不会再次重试了。不过, 默认行为是直接丢弃不去通知 Client。至于是否需要给 Client 发送 reset,是由 tcp_abort_on_overflow 这个配置项来控制的,该值默认为 0,即不发送 reset 给 Client。推荐也是将该值配置为 0:net.ipv4.tcp_abort_on_overflow = 0

设置为0的原因:Server 如果来不及 accept() 而导致全连接队列满,这往往是由瞬间有大量新建 连接请求导致的,正常情况下 Server 很快就能恢复,然后 Client 再次重试后就可以建连 成功了。也就是说,将 tcp_abort_on_overflow 配置为 0,给了 Client 一个重试的机会。 当然,你可以根据你的实际情况来决定是否要使能该选项

tcp_fin_timeout

调用 close() 的一侧是 active close(主动关闭);而接收到对端的 FIN 包后再调用 close() 来关闭的一侧,称之为 passive close(被动关闭)

FIN_WAIT_2 状态,TCP 进入到这个状态后,如果本端迟迟收不到对端的 FIN 包,那就会一直处于这个状态,于是就会一直消耗系统资源。Linux 为了防止这种资源 的开销,设置了这个状态的超时时间 tcp_fin_timeout,默认为 60s,超过这个时间后就会 自动销毁该连接。

TIME_WAIT

TIME_WAIT 状态,TIME_WAIT 状态存在的意义是:最后发送的这个 ACK 包 可能会被丢弃掉或者有延迟,这样对端就会再次发送 FIN 包。如果不维持 TIME_WAIT 这 个状态,那么再次收到对端的 FIN 包后,本端就会回一个 Reset 包,这可能会产生一些异 常。所以维持 TIME_WAIT 状态一段时间,可以保障 TCP 连接正常断开。TIME_WAIT 的默认 存活时间在 Linux 上是 60s(TCP_TIMEWAIT_LEN),这个时间对于数据中心而言可能还 是有些长了,所以有的时候也会修改内核做些优化来减小该值,或者将该值设置为可通过 sysctl 来调节

TIME_WAIT 状态存在这么长时间,也是对系统资源的一个浪费,所以系统也有配置项来限 制该状态的最大个数,该配置选项就是 tcp_max_tw_buckets。对于数据中心而言,网络 是相对很稳定的,基本不会存在 FIN 包的异常,所以建议将该值调小一些:
net.ipv4.tcp_max_tw_buckets = 10000

Client 关闭跟 Server 的连接后,也有可能很快再次跟 Server 之间建立一个新的连接,而 由于 TCP 端口最多只有 65536 个,如果不去复用处于 TIME_WAIT 状态的连接,就可能 在快速重启应用程序时,出现端口被占用而无法创建新连接的情况。所以建议你打开复用 TIME_WAIT 的选项:
net.ipv4.tcp_tw_reuse = 1

还有另外一个选项 tcp_tw_recycle 来控制 TIME_WAIT 状态,但是该选项是很危险的,因 为它可能会引起意料不到的问题,比如可能会引起 NAT 环境下的丢包问题。所以建议将该 选项关闭:
net.ipv4.tcp_tw_recycle = 0

发送问题

TCP发送缓冲区配置

TCP 发送缓冲区的大小默认是受 net.ipv4.tcp_wmem 来控制:

tcp_wmem 中这三个数字的含义分别为 min、default、max。TCP 发送缓冲区的大小会 在 min 和 max 之间动态调整,初始的大小是 default,这个动态调整的过程是由内核自动 来做的

tcp_wmem 中的 max 不能超过 net.core.wmem_max 这个配置项的值,如果超过了, TCP 发送缓冲区最大就是 net.core.wmem_max

TCP 发送缓冲区太小,也会导致 业务延迟很大的问题

可以通过 setsockopt(2) 里的 SO_SNDBUF 来设置固定的缓冲区大小。一旦进行了这种设置后,tcp_wmem 就会失效,而且这个缓冲区大小设置的是固定值,内核也不会 对它进行动态调整。

SO_SNDBUF 设置的最大值不能超过 net.core.wmem_max,如果超过了该值,内 核会把它强制设置为 net.core.wmem_max

TCP 连接消耗的总内存也有限制:
net.ipv4.tcp_mem = 8388608 12582912 16777216

该选项中这些值的单位是 Page(页数),也就是 4K。它也有 3 个值:min、pressure、max。当所有 TCP 连接消 耗的内存总和达到 max 后,也会因达到限制而无法再往外发包

tcp_mem 达到限制而无法发包或者产生抖动的问题,我们也是可以观测到的观察时你只需要打开 tracepiont(需要 4.16+ 的 内核版本):
$ echo 1 > /sys/kernel/debug/tracing/events/sock/sock_exceed_buf_limit/enable
然后去看是否有该事件发生:
$ cat /sys/kernel/debug/tracing/trace_pipe
如果有日志输出(即发生了该事件),就意味着你需要调大 tcp_mem 了,或者是需要断 开一些 TCP 连接了。

IP 层

net.ipv4.ip_local_port_range 这个配置选项,它是指和其他服务器建立 IP 连接时本地端 口(local port)的范围

net.ipv4.ip_local_port_range = 1024 65535

为了能够对 TCP/IP 数据流进行流控,Linux 内核在 IP 层实现了 qdisc(排队规则)

$ ip -s -s link ls dev eth0

TX: bytes packets errors dropped carrier collsns 3263284 25060 0 0 0 0
如果观察到 dropped 这一项不为 0,那就有可能是 txqueuelen 太小导致的。当遇到这种 情况时,你就需要增大该值了

txqueuelen 太小也会导致数据包被丢弃的 情况

TCP 数据包的接收

TCP 硬中断poll数据大小

TCP 数据包的接收流程在整体上与发送流程类似,只是方向是相反的。 数据包到达网卡后,就会触发中断(IRQ)来告诉 CPU 读取这个数据包。但是在高性能网 络场景下,数据包的数量会非常大,如果每来一个数据包都要产生一个中断,那 CPU 的处 理效率就会大打折扣,所以就产生了 NAPI(New API)这种机制让 CPU 一次性地去轮询 (poll)多个数据包,以批量处理的方式来提升效率,降低网卡中断带来的性能开销。
那在 poll 的过程中,一次可以 poll 多少个呢?这个 poll 的个数可以通过 sysctl 选项来控 制:
net.core.netdev_budget = 600
该控制选项的默认值是 300,在网络吞吐量较大的场景中,我们可以适当地增大该值,比 如增大到 600。增大该值可以一次性地处理更多的数据包。但是这种调整也是有缺陷的, 因为这会导致 CPU 在这里 poll 的时间增加,如果系统中运行的任务很多的话,其他任务 的调度延迟就会增加。

TCP 接收缓冲区

默认都是使 用 tcp_rmem 来控制缓冲区的大小。同样地,我们也会适当地增大这几个值的默认值,来 获取更好的网络性能,调整为如下数值:
net.ipv4.tcp_rmem = 8192 87380 16777216

3 个字段:min、default、max。TCP 接收缓冲区大小也是在 min 和 max 之间动 态调整 ,不过跟发送缓冲区不同的是,这个动态调整是可以通过控制选项来关闭的,这个 选项是 tcp_moderate_rcvbuf 。通常我们都是打开它,这也是它的默认值:
net.ipv4.tcp_moderate_rcvbuf = 1

之所以接收缓冲区有选项可以控制自动调节,而发送缓冲区没有,那是因为 TCP 接收缓冲 区会直接影响 TCP 拥塞控制,进而影响到对端的发包,所以使用该控制选项可以更加灵活 地控制对端的发包行为

SO_RCVBUF 这个标记,那么 TCP 接收缓冲区的动态调整就是关闭,即使 tcp_moderate_rcvbuf 为 1,接收缓冲区的大小始终就为设置的 SO_RCVBUF 这个值

SO_RCVBUF 设置的值最大也不能超过 net.core.rmem_max。通常情况下,我们也需要设置 net.core.rmem_max 的值大于等于 net.ipv4.tcp_rmem 的 max:
net.core.rmem_max = 16777216

TCP 接收缓冲区的限制而引发的丢包问题。但是这 类问题不是那么好追踪的

![image-20220920080639876](/Users/mac/Library/Application Support/typora-user-images/image-20220920080639876.png)

![image-20220921074809241](/Users/mac/Library/Application Support/typora-user-images/image-20220921074809241.png)

引起 TCP 重传的情况在整体上可以分为如下两类

丢包
TCP 数据包在网络传输过程中可能会被丢弃;接收端也可能会把该数据包给丢弃;接收 端回的 ACK 也可能在网络传输过程中被丢弃;数据包在传输过程中发生错误而被接收端 给丢弃……这些情况都会导致发送端重传该 TCP 数据包。

拥塞
TCP 数据包在网络传输过程中可能会在某个交换机 / 路由器上排队,比如臭名昭著的 Bufferbloat(缓冲膨胀);TCP 数据包在网络传输过程中因为路由变化而产生的乱序; 接收端回的 ACK 在某个交换机 / 路由器上排队……这些情况都会导致发送端再次重传该 TCP 数据包。


, ,