全连接队列

0.半连接全连接队列示意

1.对于 LISTEN 状态的 socket (ss -lnt)

Recv-Q:当前全连接队列的占用大小,即已完成三次握手等待应用程序 accept() 的 TCP 链接
Send-Q:全连接队列的最大长度,即全连接队列的大小

2.对于非 LISTEN 状态的 socket (ss -nt)

Recv-Q:已收到但未被应用程序读取的字节数
Send-Q:已发送但未收到确认的字节数

3.说明

  • (1)当Recv-Q ≤ Send-Q 正常工作,当Recv-Q > Send-Q 时数据包被DROP或者回复RST。
  • (2)可以通过设置/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 链接
    
  • (3) netstat 命令通过netstat -s命令可以查看 TCP 半连接队列、全连接队列的溢出情况

      $ netstat -s | grep -i "listen"
          189088 times the listen queue of a socket overflowed
          30140232 SYNs to LISTEN sockets dropped
    
  • (4) 全连接队列最大长度控制TCP 全连接队列的最大长度由min(somaxconn, backlog)控制,其中: somaxconn 是 Linux 内核参数,由 /proc/sys/net/core/somaxconn 指定backlog 是 TCP 协议中 listen 函数的参数之一,即 int listen(int sockfd, int backlog) 函数中的 backlog 大小。在 Golang 中,listen 的 backlog 参数使用的是/proc/sys/net/core/somaxconn文件中的值。

4.关键参数

参数 作用
/proc/sys/net/core/somaxconn TCP全连接队列的最大长度Send-Q的设定
/proc/sys/net/ipv4/tcp_abort_on_overflow 当TCP队列满时的处理方式设定(RST/DROP)

5.模拟

5.1 server代码

import (
	"log"
	"net"
	"time"
)

func main() {
	l, err := net.Listen("tcp4", ":8888")
	if err != nil {
		log.Printf("failed to listen due to %v", err)
	}
	defer l.Close()
	log.Println("listen :8888 success")

	for {
		time.Sleep(time.Second * 100)
	}
}

5.2 client代码

package net

import (
	"context"
	"log"
	"net"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

var wg sync.WaitGroup

func establishConn(ctx context.Context, i int) {
	defer wg.Done()
	conn, err := net.DialTimeout("tcp", "10.168.0.30:8888", time.Second*5)
	if err != nil {
		log.Printf("%d, dial error: %v", i, err)
		return
	}
	log.Printf("%d, dial success", i)
	_, err = conn.Write([]byte("hello world"))
	if err != nil {
		log.Printf("%d, send error: %v", i, err)
		return
	}
	select {
	case <-ctx.Done():
		log.Printf("%d, dail close", i)
	}
}

func StartClient() {
	ctx, cancel := context.WithCancel(context.Background())
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go establishConn(ctx, i)
	}

	go func() {
		sc := make(chan os.Signal, 1)
		signal.Notify(sc, syscall.SIGINT)
		select {
		case <-sc:
			cancel()
		}
	}()

	wg.Wait()
	log.Printf("client exit")
}

5.3 开始模拟

5.3.1 运行服务

1、查看系统默认值
# cat /proc/sys/net/core/somaxconn 
4096

2、在默认值下运行server后查看队列情况,
# ss -ant | grep 8888
State      Recv-Q Send-Q    Local Address:Port         Peer Address:Port
LISTEN     0      4096            *:8888                     *:*         

3、临时修改队列大小为5
# echo 5 > /proc/sys/net/core/somaxconn

# cat /proc/sys/net/core/somaxconn       
5

5、修改后运行server
# ss -ant | grep 8888
State      Recv-Q Send-Q    Local Address:Port         Peer Address:Port
LISTEN     0      5            *:8888                     *:* 

6、运行客户端发送10个请求,修改代码中i < 10控制
# go run client.go

5.3.2 查看TCP队列情况

5.3.2.1 查看客户端情况

发现客户端有一些成功,一些请求超时,我们得到的结果是有8个成功2个超时(记住这个结果),每次执行的结果可能不一样,但是超时的个数不会超过4个,因为我们的队列能够容忍的上限是6个;

=== RUN   TestStartClient
2022/10/08 17:55:05 5, dial success
2022/10/08 17:55:05 7, dial success
2022/10/08 17:55:05 1, dial success
2022/10/08 17:55:05 8, dial success
2022/10/08 17:55:05 0, dial success
2022/10/08 17:55:05 9, dial success
2022/10/08 17:55:05 2, dial success
2022/10/08 17:55:05 6, dial success
2022/10/08 17:55:10 3, dial error: dial tcp 10.168.0.30:8888: i/o timeout
2022/10/08 17:55:10 4, dial error: dial tcp 10.168.0.30:8888: i/o timeout

5.3.2.2 查看服务端情况
# 在server端执行,马上执行ss -ant | grep 8888,输出如下:
State      Recv-Q Send-Q    Local Address:Port         Peer Address:Port
LISTEN     6      5            *:8888                     *:*                  
SYN-RECV   0      0      10.168.0.30:8888               192.168.10.42:62516              
SYN-RECV   0      0      10.168.0.30:8888               192.168.10.42:62525              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62514              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62515              
SYN-RECV   0      0      10.168.0.30:8888               192.168.10.42:62523              
SYN-RECV   0      0      10.168.0.30:8888               192.168.10.42:62527              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62513              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62512              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62519              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62520

发现处于LISTEN状态的,Recv-Q 当前全连接队列的大小是 6 ,大于了Send-Q 全连接队列最大长度是 5,并且出现了大量的半连接状态。

# 等待一段时间持续执行ss -ant | grep 8888,输出如下:

LISTEN     6      5            *:8888                     *:*                  
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62805              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62806              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62801              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62799              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62802              
ESTAB      11     0      10.168.0.30:8888               192.168.10.42:62800 

我们可以看到处于ESTAB连接状态的只会有6个,达到全连接队列的上限。

5.3.2.3 抓包分析

(1) 在我们客户端发起访问请求的时候,我们通过tcpdump在服务端进行抓包分析,tcpdump -i ens192 tcp port 8888 -w server.pcap 将抓取的server.pcap包导入到wireshark进行分析。

(2) 我的客户端是windows,直接在windows中运行wireshark进行抓包。

我们尝试建立10个连接,所以tcp的数据流也只会有10个,一个数据流包含n个数据包,可以通过 tcp.stream eq 0 ~ tcp.stream eq 9在wireshark中进行过滤。

记住刚刚从日志打印看到success 8 个 fail 2个,

但是:

(1)实际数据流0-5共6个才是真正成功的,6个就是TCP队列能够容忍的上限;

(2)6-7的2个是客户端发送了数据但是没有收到server端的确认,客户端以为成功;

(3)8-9的2个是完全失败的;

情况一:Client 成功与 Server 端建立 tcp socket 链接,发送数据成功tcp.stream eq 0 ~ tcp.stream eq 5

tcp.stream eq 0 ~ tcp.stream eq 5 实际数据流0-5共6个才是真正成功的,6个就是TCP队列能够容忍的上限;

情况二:Client 认为成功与 Server 端建立 tcp socket 连接,后续发送数据失败,持续 RETRY;Server 端认为 TCP 连接未建立,一直在发送SYN+ACK

tcp.stream eq 6 ~ tcp.stream eq 7 的2个是客户端发送了数据但是没有收到server端的确认,客户端以为成功;

TCP状态解释

  • retransmission 超时重传;
  • spurious retransmission 意味着发送端认为发送的包已经丢失了,然后就重传了,尽管此时接收端已经发送了对这些包的确认(确认还没收到或者已经丢失了)。这是需要在发送端抓包,分析原因。如果接收端以前收到过相同的包,接收端就会报spurious retransmission,但是发送端是不会报spurious retransmission的;
  • dup ack XXX#X 就是重复应答#前的表示报文到哪个序号丢失,#后面的是表示第几次丢失;

情况三:Client 向 Server 发送 SYN 未得到响应,一直在 RETRY

tcp.stream eq 8 ~ tcp.stream eq 9 8-9的2个是tcp建立连接完全失败的;

一点实用的东西

(1)如果要想知道客户端连接不上服务端,是不是服务端 TCP 全连接队列满的原因,那么可以把 tcp_abort_on_overflow 设置为 1,这时如果在客户端异常中可以看到很多 connection reset by peer 的错误,那么就可以证明是由于服务端 TCP 全连接队列溢出的问题

参考

https://www.cnblogs.com/xiaolincoding/p/12995358.html