队列介绍
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
半连接队列,也称 SYN 队列;
全连接队列,也称 accepet 队列;
服务端收到客户端发起的 SYN 请求后【syn_recv】,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接【eastablished】,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
信息查看
ss -lnt 查看全连接队列大小
netstat -s | grep overflowed 查看全连接队列溢出情况
netstat -natp | grep SYN_RECV |wc -l 查看当前半连接队列长度【就是syn_recv状态的连接数】
netstat -s | grep "SYNs to LISTEN" 查看半连接队列溢出情况
netstat -s | grep "LISTEN" 查看半连接队列和全连接队列的溢出情况
丢包场景
TCP 第一次握手(收到 SYN 包)时会被丢弃的三种条件:
1.如果半连接队列满了,并且没有开启 tcp_syncookies,则会丢弃;
2.若全连接队列满了,且没有重传 SYN+ACK 包的连接请求多于 1 个,则会丢弃;
3.如果没有开启 tcp_syncookies,并且 max_syn_backlog 减去 当前半连接队列长度小于 (max_syn_backlog >> 2),则会丢弃;
TCP第三次握手丢包:
当超过了 TCP 最大全连接队列,服务端则会丢掉后续进来的 TCP 连接。
全连接队列丢包处理
发生 TCP 全连接队溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。实际上,丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST 复位报文,告诉客户端连接已经建立失败【否则客户端会等到超时,非常影响用户体验】。
【echo 1 -> /proc/sys/net/ipv4/tcp_abort_on_overflow】
tcp_abort_on_overflow 共有两个值分别是 0 和 1,其分别表示:
0 :表示如果全连接队列满了,那么 server 扔掉 client 发过来的 ack ;
1 :表示如果全连接队列满了,那么 server 发送一个 reset 包给 client,表示废掉这个握手过程和这个连接;
如果要想知道客户端连接不上服务端,是不是服务端 TCP 全连接队列满的原因,那么可以把 tcp_abort_on_overflow 设置为 1,这时如果在客户端异常中可以看到很多 connection reset by peer 的错误,那么就可以证明是由于服务端 TCP 全连接队列溢出的问题。
通常情况下,应当把 tcp_abort_on_overflow 设置为 0,因为这样更有利于应对突发流量。
举个例子,当 TCP 全连接队列满导致服务器丢掉了 ACK,与此同时,客户端的连接状态却是 ESTABLISHED,进程就在建立好的连接上发送请求。只要服务器没有为请求回复 ACK,请求就会被多次重发。如果服务器上的进程只是短暂的繁忙造成 accept 队列满,那么当 TCP 全连接队列有空位时,再次接收到的请求报文由于含有 ACK,仍然会触发服务器端成功建立连接。
所以,tcp_abort_on_overflow 设为 0 可以提高连接建立的成功率,只有你非常肯定 TCP 全连接队列会长期溢出时,才能设置为 1 以尽快通知客户端。
如何增大 TCP 全连接队列呢?
TCP 全连接队列足最大值取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog)
somaxconn 是 Linux 内核的参数,默认值是 128,可以通过 /proc/sys/net/core/somaxconn 来设置其值;
backlog 是 listen(int sockfd, int backlog) 函数中的 backlog 大小,Nginx 默认值是 511,可以通过修改配置文件设置其长度;
syncookies 功能介绍
如果 SYN 半连接队列已满,只能丢弃连接吗?
并不是这样,开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接。
syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功。
syncookies 参数主要有以下三个值:
0 值,表示关闭该功能;
1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
2 值,表示无条件开启功能;
那么在应对 SYN 攻击时,只需要设置为 1 即可:
【echo 1 -> /proc/sys/net/ipv4/tcp_syncookies】
这里给出几种防御 SYN 攻击的方法:
1.增大半连接队列;
2.开启 tcp_syncookies 功能
3.减少 SYN+ACK 重传次数
要想增大半连接队列,我们得知不能只单纯增大 tcp_max_syn_backlog 的值,还需一同增大 somaxconn 和 backlog,也就是增大全连接队列。否则,只单纯增大 tcp_max_syn_backlog 是无效的。
【echo 1024 > /proc/sys/net/ipv4/tcp_max_syn_backlog】
【echo 1024 > /proc/sys/net/core/somaxconn】
【backlog的参数大小,需要在具体应用的listen系统调用的时候传入】
开启 tcp_syncookies 功能
【echo 1 -> /proc/sys/net/ipv4/tcp_syncookies】
减少 SYN+ACK 重传次数
当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。那么针对 SYN 攻击的场景,我们可以减少 SYN+ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。
【echo 1-> /proc/sys/net/ipv4/tcp_synack_retries】重传次数设置为1次
具体case
半连接队列满了导致连接失败: 半连接队列满了,丢包,导致客户端一直在发syn但是连不上。
全连接队列满了导致客户端误认为连接建立成功:
Client 认为成功与 Server 端建立 tcp socket 连接,后续发送数据失败,持续 RETRY;Server 端认为 TCP 连接未建立,一直在发送SYN+ACK
上图可以看到如下请求:
1.Client 端向 Server 端发送 SYN 发起握手
2.Server 端收到 Client 端 SYN 后,向 Client 端回复 SYN+ACK,socket 连接存储到半连接队列(SYN Queue)
3.Client 端收到 Server 端 SYN+ACK 后,向 Server 端回复 ACK,Client 端进入 ESTABLISHED状态(重要:此时仅仅是 Client 端认为 tcp 连接建立成功)
4.由于 Client 端认为 TCP 连接已经建立完成,所以向 Server 端发送数据 [PSH,ACK],但是一直未收到 Server 端的确认 ACK,所以一直在 RETRY
5.Server 端一直在 RETRY 发送 SYN+ACK
为什么会出现上述情况?Server 端为什么一直在 RETRY 发送 SYN+ACK?Server 端不是已经收到了 Client 端的 ACK 确认了吗?
上述情况是由于 Server 端 socket 连接进入了半连接队列,在收到 Client 端 ACK 后,本应将 socket 连接存储到全连接队列,但是全连接队列已满,所以 Server 端 DROP 了该 ACK 请求。之所以 Server 端一直在 RETRY 发送 SYN+ACK,是因为 DROP 了 client 端的 ACK 请求,所以 socket 连接仍旧在半连接队列中,等待 Client 端回复 ACK【自己drop了ack,却又在等待ack,这是tcp的一个缺陷】。
tcp_abort_on_overflow 参数控制全连接队列满DROP 请求是默认行为,可以通过设置/proc/sys/net/ipv4/tcp_abort_on_overflow使 Server 端在全连接队列满时,向 Client 端发送 RST 报文。tcp_abort_on_overflow 有两种可选值:
0:如果全连接队列满了,Server 端 DROP Client 端回复的 ACK
1:如果全连接队列满了,Server 端向 Client 端发送 RST 报文,终止 TCP socket 链接
经验教训
长连模块 应该提前配置好tcp的相关参数,否则,建立连接过快的时候,就会导致建连失败。 还应该配置好fd的参数限制,系统级别,用户级别,线程级别,supervisor级别
半连接队列溢出、全连接队列溢出这类问题很容易被忽略,同时这类问题又很致命。
当半连接队列、全连接队列溢出时 Server 端,从监控上来看系统 cpu 水位、内存水位、网络连接数等一切正常,然而却会持续影响 Client 端业务请求。
对于高负载上游使用短连接的情况,出现这类问题的可能性更大。
RST的理解
RST出现的情况
1. 客户端不ack,服务器发送tcp_synack_retries(大概63s)次 SYN+ACK之后会发送rst。
一个特殊情况,通过上面的63s,可能导致服务器半连接队列 SYN-Queue满,此时新的SYN连接受tcp_syncookies影响,tcp_syncookies:
0的时候直接丢弃;
1的时候accept-queue 满并且 qlen_young>1直接丢弃,1 生成syncookie继续完成连接过程,比三次握手要复杂点
2. accept-queue满,受tcp_abort_on_overflow影响
tcp_abort_on_overflow:0时,重启SYN+ACK机制,tcp_synack_retries后直接丢弃
tcp_abort_on_overflow:1时,直接回复rst
3. 连接未被listen的端口,服务端直接回复rst
4. 服务端crash,再有客户端数据过来,就直接回复rst
5. so_linger选项会导致 客户端直接发送rst,而不走4次握手断开流程
6. 主动关闭方在关闭时socket接收缓冲区还有未处理数据
在调用close时如果接收缓冲区还有数据,则直接清空并发送rst;如果发送缓冲区还有数据,就发送完,然后走4次握手断开。
RST对应的是URG标志位,告诉对方要快速关闭连接。读到对方发送的RST包,会报Connection reset by peer
broken pipe错误:当本地socket已经关闭,或者收到了对端的RST报文,还在继续写数据,就会报错broken pipe
TCP调优参数
1. 建立连接阶段
net.ipv4.tcp_syn_retries
控制三次握手第一步客户端发送syn得不到服务端响应时重传syn的次数,如果是内网环境,中间链路少,网络稳定,服务端无响应很可能是服务端应用出了问题,重传多次的意义不大,还会加大服务端压力,可以调低重传次数,让客户端尽快去尝试连接其他服务端。
net.ipv4.tcp_syncookies
开启SYN Cookies,默认开启,建议保持默认值,可以提升SYN Flood攻击的防护能力。
net.ipv4.tcp_synack_retries
控制三次握手第二步服务端发送syn+ack得不到客户端响应时重传syn+ack的次数,如果是内网环境,中间链路少,网络稳定,客户端无响应很可能是客户端出了问题,重传多次的意义不大,可以调低重传次数。
net.ipv4.tcp_max_syn_backlog
控制半连接队列大小,所谓半连接是指还没有完成TCP三次握手的连接。服务端收到了客户端的SYN包后,就会把这个连接放到半连接队列中,然后再向客户端发送SYN+ACK,为了应对新建连接数暴增的场景,建议调大,半连接队列溢出观察方法:netstat -s | grep "SYNs to LISTEN"
net.core.somaxconn
全连接队列=min(somaxconn,backlog),所谓全连接,是指服务端已经收到客户端三次握手第三步的ACK,然后就会把这个连接放到全连接队列中,全连接队列中的连接还需要被 accept()系统调用取走,服务端应用才可以开始处理客户端的请求,建议适当调大,全连接队列溢出观察方法:netstat -s | grep "listen queue"
net.ipv4.tcp_abort_on_overflow
当全连接队列满了之后,新的连接就会被丢弃掉。服务端在丢弃新连接时,默认行为是直接丢弃不去通知客户端,有的时候需要发送reset来通知客户端,这样客户端就不会再次重试,至于是否需要给客户端发送reset,是由tcp_abort_on_overflow参数控制,默认为0,即不发送reset给客户端,如非特殊需求,建议保持默认值。
2. 数据传输阶段
net.ipv4.tcp_wmem
tcp发送缓冲区大小,包含min、default、max三个值,内核会控制发送缓冲区在min-max之间动态调整,可根据实际业务场景和服务器配置适当调大,如果设置了socket的SO_SNDBUF,动态调整功能失效,一般不建议设置。
net.core.wmem_max
socket发送缓冲区的最大值,需要设置net.core.wmem_max的值大于等于 net.ipv4.tcp_wmem的max值。
net.ipv4.tcp_mem
系统中所有tcp连接最多可消耗的内存,有三个值,当TCP总内存小于第1个值时,不需要内核进行自动调节,在第1和第2个值之间时,内核开始调节缓冲区的大小,大于第3个值时,内核不再为TCP分配新内存,此时无法新建连接,需要注意的是,三个值的单位都是内存页,也就是4KB。
net.ipv4.tcp_rmem
tcp接收缓冲区大小,包含min、default、max三个值,内核会控制接收缓冲区在min-max之间动态调整,可根据实际业务场景和服务器配置适当调大,如果设置了socket的 SO_RECVBUF或者关闭了net.ipv4.tcp_moderate_rcvbuf,动态调整功能失效。
net.core.rmem_max
socket接收缓冲区的最大值,需要设置net.core.rmem_max的值大于等于net.ipv4.tcp_rmem 的max值。
net.ipv4.tcp_moderate_rcvbuf
接收缓冲区动态调整功能,默认打开,建议保持默认配置。
net.ipv4.tcp_window_scaling
扩充滑动窗口,tcp头部中,窗口字段只有2个字节,最多只能达到2的16次方,即65535字节大小的窗口,打开此开关可以扩充窗口大小,默认打开,建议保持默认配置。
net.ipv4.tcp_keepalive_probes
keepalive探测失败后通知应用前的重试次数,建议适当调低。
net.ipv4.tcp_keepalive_intvl
keepalive探测包的发送间隔时间,建议适当调低。
net.ipv4.tcp_keepalive_time
最后一次数据包到keepalive探测包的间隔时间,建议适当调低。
net.ipv4.tcp_available_congestion_control
查看内核支持的拥塞控制算法。
net.ipv4.tcp_congestion_control
配置拥塞控制算法,默认cubic,内核4.9版本后支持BBR,弱网络条件下建议配置成BBR。
3. 断开连接阶段
net.ipv4.tcp_fin_timeout
是从Fin_WAIT_2到TIME_WAIT的超时时间,长时间收不到对端FIN包,大概率是对端机器有问题,不能及时调用close()关闭连接,建议调低,避免等待时间太长,资源开销过大。
net.ipv4.tcp_max_tw_buckets
系统TIME_WAIT连接的最大数量,根据实际业务需要调整,超过最大值后dmesg会有报错TCP: time wait bucket table overflow。
net.ipv4.tcp_tw_reuse
允许TIME_WAIT状态的连接占用的端口用到新建连接中,客户端可开启。
net.ipv4.tcp_tw_recycle
开启后,TIME_WAIT状态的连接无需等待2MSL时间就可用于新建连接,在NAT环境下,开启tcp_tw_recycle参数会触发PAWS机制导致丢包,建议不开启,事实上,内核在4.1版本后就把这个参数删除了。
参考文章: blog.csdn.net/qq_23350817… zhuanlan.zhihu.com/p/514391329