企业为什么需要网站/营销活动推广策划

企业为什么需要网站,营销活动推广策划,唐河网站建设,疫情什么时候结束本文是对 从一次线上问题说起,详解 TCP 半连接队列、全连接队列 这篇文章的实验复现和总结,借此加深对 TCP 半连接队列、全连接队列的理解。 实验环境 两台腾讯云服务器 node2(172.19.0.12) 和 node3(172.19.0.15&am…

本文是对 从一次线上问题说起,详解 TCP 半连接队列、全连接队列 这篇文章的实验复现和总结,借此加深对 TCP 半连接队列、全连接队列的理解。

实验环境

两台腾讯云服务器 node2(172.19.0.12) 和 node3(172.19.0.15)配置为 2C4G,Ubuntu 系统,内核版本 5.15.0-130-generic 。

全连接半连接队列简介

在 TCP 三次握手过程中,Linux 会维护两个队列分别是:

  • SYN Queue 半连接队列
  • Accept Queue 全连接队列

创建连接时,两个队列作用如下:

  • 客户端向服务端发送 SYN 包,客户端进入 SYN_SENT 状态
  • 服务端收到 SYN 包后,进入 SYN_RECV 状态,内核将连接信息放入 SYN Queue 队列,然后向客户端发送 SYN+ACK 包
  • 客户端收到 SYN+ACK 包后,发送 ACK 包,客户端进入 ESTABLISHED 状态
  • 服务端收到 ACK 包后,将连接从 SYN Queue 队列中取出移到 Accept Queue 队列,Server 端进入 ESTABLISHED。
  • 服务端应用程序调用 accept 函数处理数据,连接从 Accept Queue 队列移除。


图片来自:从一次线上问题说起,详解 TCP 半连接队列、全连接队列

图片来自Cloudflare Blog: SYN Packet Handling in the Wild

两个队列的长度都是有限的,当队列满了之后,新建连接时内核会将 SYN 包丢弃或者直接返回 RST 包。

全连接队列实战

全连接队列长度控制

TCP 全连接队列的长度计算公式为:

min(somaxconn, backlog)

  • somaxconn Linux 内核参数 net.core.somaxconn 的值,默认为 4096。可以通过修改该参数来控制全连接队列的长度。
  • backlog 是系统调用 listen 函数 int listen(int sockfd, int backlog) 的 backlog 参数, Golang 中默认使用系统 somaxconn 的值。

下面是 Linux 5.15.130 内核源码中计算全连接队列长度的代码:

源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/socket.c#L1716

我们修改 somaxconn 的值,然后运行实验代码查看全连接队列的长度变化。

  • 服务端实验代码
package mainimport ("log""net""time"
)func main() {l, err := net.Listen("tcp", ":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)}
}

首先我们修改 somaxconn 为 128:

sudo sysctl -w net.core.somaxconn=128

启动服务后查看全连接队列的长度:

$ go run server.go
2025/02/13 09:53:01 listen :8888 success$ ss -lnt
State             Recv-Q            Send-Q                         Local Address:Port                         Peer Address:Port            Process
LISTEN            0                 128                                        *:8888                                    *:*
...

这里简单解释下 ss 命令输出的含义:

  • 对于 Listen 状态的 socket,Recv-Q 表示当前全连接队列的长度,也就是已经完成三次握手,等待应用层调用 accept 的 TCP 连接数;Send-Q 表示全连接队列的最大长度。

  • 对于非 Listen 状态的 socket,Recv-Q 表示已经收到但尚未被应用读取的字节数;Send-Q 表示已发送但尚未收到确认的字节数。

再次修改 somaxconn 为 1024 重启服务后,查看全连接队列的长度已经变成了 1024。

$ sudo sysctl -w net.core.somaxconn=1024
$ go run server.go
2025/02/13 09:53:01 listen :8888 success$ ss -lnt
State             Recv-Q            Send-Q                         Local Address:Port                         Peer Address:Port            Process
LISTEN            0                 1024                                       *:8888                                    *:*
...

全连接队列溢出

下面我们让服务端只 Listen 端口但不执行 accept() 处理数据,模拟全连接队列溢出的情况。

  • 服务端代码
// server 端监听 8888 tcp 端口 
package main import ( "log" "net" "time" 
) func main() { l, err := net.Listen("tcp", ":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) } 
}
  • 客户端代码

和原实验相比加了 time.Sleep(500 * time.Millisecond) 一行代码,让连接一个个建立,可以更精准的复现全连接队列已满的情况。

package main 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", ":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 main() { ctx, cancel := context.WithCancel(context.Background()) // 并发请求 10 次服务端,连接建立成功后发送数据for i := 0; i < 10; i++ { wg.Add(1) time.Sleep(500 * time.Millisecond)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:

$ sudo sysctl -w net.core.somaxconn=5$ cat /proc/sys/net/core/somaxconn
5

运行服务端和客户端后,查看全连接队列情况:

  • 服务端 socket 情况
$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN     6      5                         *:8888                       *:*
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40148
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40162
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40128
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40132
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40110
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40112
  • 客户端 socket 情况
$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q Local Address:Port    Peer Address:Port Process
ESTAB      0      0        172.19.0.15:40132    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40162    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40148    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51906    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40112    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40128    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51912    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:40176    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40110    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51926    172.19.0.12:8888
  • 客户端日志输出
$ go run client.go
2025/02/19 11:14:22 0, dial success
2025/02/19 11:14:22 1, dial success
2025/02/19 11:14:23 2, dial success
2025/02/19 11:14:23 3, dial success
2025/02/19 11:14:24 4, dial success
2025/02/19 11:14:24 5, dial success
2025/02/19 11:14:30 6, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:30 7, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:31 8, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:31 9, dial error: dial tcp 172.19.0.12:8888: i/o timeout

我们来分析下上述结果:

1. 全连接队列是否已满

服务端 Listen 状态的 socket 显示 Send-Q 为 5,表示该 socket 的全连接队列最大值为 5;Recv-Q 为 6,表示当前 Accept queue 中数量为 6,我们看有 6 条 ESTAB 状态的连接,符合观察结果。Linux 内核的判断依据是 > 而不是 >=,所以实际的连接数为比队列的最大值多 1 个。5.15.0-130-generic 内核代码如下:

// 源码地址
// https://elixir.bootlin.com/linux/v5.15.130/source/include/net/sock.h#L980
/* Note: If you think the test should be:*	return READ_ONCE(sk->sk_ack_backlog) >= READ_ONCE(sk->sk_max_ack_backlog);* Then please take a look at commit 64a146513f8f ("[NET]: Revert incorrect accept queue backlog changes.")*/
static inline bool sk_acceptq_is_full(const struct sock *sk)
{return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}

之所以这样做,是为了保证在 backlog 设置为 0 时,依然可以有一个连接进入全连接队列,具体可以查看以下 commit 信息:

https://github.com/torvalds/linux/commit/64a146513f8f12ba204b7bf5cb7e9505594ead42[NET]: Revert incorrect accept queue backlog changes.
This reverts two changes:8488df8
248f067A backlog value of N really does mean allow "N + 1" connections
to queue to a listening socket.  This allows one to specify
"0" as the backlog and still get 1 connection.Noticed by Gerrit Renker and Rick Jones.Signed-off-by: David S. Miller <davem@davemloft.net>
2. 内核 drop 包处理逻辑

客户端有 6 个 ESTAB 状态的 socket,另外还有 4 个 SYN-SENT 状态的 socket,对应着 4 条 timeout 报错信息。我们只改了全连接队列大小为 5,半连接队列大小依然为默认的 net.ipv4.tcp_max_syn_backlog=256,所以第 6 个请连接建立后 Accept Queue 满了但 SYN Queue 还没有满。按理说从第 7 个请求开始服务端可以接收 SYN 但不能在处理客户端的 ACK 进入 Accept Queue,服务端会有 4 条 SYN-RECV 状态的连接,而实际情况是服务端不存在 SYN_RECV 状态的连接,这是因为当 Accept Queue 被占满时,即使 SYN Queue 没有满,Linux 内核也会将新来的 SYN 请求丢弃掉。 5.15.0-130-generic 内核处理这部分逻辑的代码如下::

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848int tcp_conn_request(struct request_sock_ops *rsk_ops,const struct tcp_request_sock_ops *af_ops,struct sock *sk, struct sk_buff *skb)
{// ... 代码省略syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);/* TW buckets are converted to open requests without* limitations, they conserve resources and peer is* evidently real one.*/// 强制启用 SYN cookie 或者半连接队列已满// !isn 表示是一个新的请求连接建立的 SYNif ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {// 这里表示是否启用 SYN cookie 机制;如果不开启,则直接 drop,如果开启,则继续执行。want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);if (!want_cookie)goto drop;}// 如果 accept queue 满了则 dropif (sk_acceptq_is_full(sk)) {NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}static bool tcp_syn_flood_action(const struct sock *sk, const char *proto)
{struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;const char *msg = "Dropping request";struct net *net = sock_net(sk);bool want_cookie = false;u8 syncookies;syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);// 开启 SYN Cookie 机制
#ifdef CONFIG_SYN_COOKIESif (syncookies) {msg = "Sending cookies";want_cookie = true;__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);} else
#endif// 没有启用 syncookies,统计丢弃包的数量__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);// 如果启用了 SYN cookie 机制,发送警告if (!queue->synflood_warned && syncookies != 2 &&xchg(&queue->synflood_warned, 1) == 0)net_info_ratelimited("%s: Possible SYN flooding on port %d. %s.  Check SNMP counters.\n",proto, sk->sk_num, msg);return want_cookie;
}// 判断半连接队列是否满,用的是半连接队列的长度是否大于等于全连接队列的最大长度
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

从代码中可以推测出 net.ipv4.tcp_syncookies 参数值的含义和 Linux 的处理机制:

  • 2:强制开启 SYN Cookie 机制,发送警告
  • 1:当半连接队列满时,开启 SYN Cookie 机制,发送警告
  • 0:不开启 SYN Cookie 机制,并统计丢弃包的数量

这里判断半连接队列是否满的依据是 inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog,也就是说当半连接队列长度不小于全连接队列的最大长度时,如果不开启 SYN Cookie 机制,就会将 SYN 包丢弃。

回到我们的实验环境,net.ipv4.tcp_syncookies 设置为 1 并且半连接队列没满,因此不会开启 SYN Cookie 机制,继续往后执行时会因为 Accept Queue 满了将包丢弃。可以通过 netstat -s 命令查看丢弃包的数量。

$ date;netstat -s | grep -i "SYNs to LISTEN"
Wed Feb 19 12:05:51 PM CST 20251289 SYNs to LISTEN sockets dropped$ date;netstat -s | grep -i "SYNs to LISTEN"
Wed Feb 19 12:06:05 PM CST 20251301 SYNs to LISTEN sockets dropped

可以看到有 12 个 SYN 包被 DROP 了,查看抓包情况可以看到,我们有 4 个请求连接超时,每个请求传了 3 次 SYN(一次发起 + 两次重传)。

查看客户端 socket 状态能够看到重传计时器在工作,这里重传了两次和默认的 net.ipv4.tcp_syn_retries = 6 有出入,是因为代码 conn, err := net.DialTimeout("tcp", "172.19.0.12:8888", time.Second*5)设置了 5s 超时,操作系统的默认重传间隔大约为 1s、2s、4s、8s、16s、32s,第 3 次重传会发生在 7s 以后,客户端已经主动断开连接了。

$ sudo netstat -anpo | grep -E "Recv-Q|8888"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.15:57384       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (7.57/0/0)
tcp        0      0 172.19.0.15:57388       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (8.07/0/0)
tcp        0      0 172.19.0.15:60276       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (9.58/0/0)
tcp        0      1 172.19.0.15:60304       172.19.0.12:8888        SYN_SENT    3123924/client       on (0.08/1/0)
tcp        0      1 172.19.0.15:60286       172.19.0.12:8888        SYN_SENT    3123924/client       on (2.60/2/0)
tcp        0      0 172.19.0.15:60270       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (9.08/0/0)
tcp        0      0 172.19.0.15:60280       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (10.08/0/0)
tcp        0      1 172.19.0.15:60292       172.19.0.12:8888        SYN_SENT    3123924/client       on (3.11/2/0)
tcp        0      0 172.19.0.15:57398       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (8.57/0/0)
tcp        0      1 172.19.0.15:60294       172.19.0.12:8888        SYN_SENT    3123924/client       on (3.62/2/0)
3. overflow 参数控制

当全连接队列满时,Linux 默认会 drop 掉包,这个受 net.ipv4.tcp_abort_on_overflow 参数控制,默认为 0 表示直接 drop,为 1 则表示中断连接,服务端会返回 RST 包。可以通过如下方式修改

$ sudo sysctl -w net.ipv4.tcp_abort_on_overflow=1或者echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow

我们修改参数后再次执行客户端请求,会出现 connection reset by peer 错误,抓包能看到 RST 包。(在实验时,如果客户端不加时间间隔,会出现返回 RST 包的情况,如果加了则不会出现这种情况,应该是和两者的生效机制有关,SYN Cookie 和全连接队列满 drop 发生在 tcp_conn_request 函数,而 abort_on_overflow 发生在 tcp_check_req 函数, 先挖个坑,等后续梳理整个网络传输流程时在做进一步分析)。

$ go run client.go
2025/03/01 13:36:55 2, dial success
2025/03/01 13:36:55 5, dial success
2025/03/01 13:36:55 4, dial success
2025/03/01 13:36:55 1, dial success
2025/03/01 13:36:55 3, dial success
2025/03/01 13:36:55 0, dial success
2025/03/01 13:36:55 7, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer
2025/03/01 13:36:55 6, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer

4. ss 命令展示含义

服务端有 6 条 ESTAB 状态的 socket,RECV_Q 的值为 11,与客户端发送的数据 []byte("hello world") 数据长度一致,因为我们的没有执行 accept 接收数据,所以 RECV_Q 会展示这部分数据的大小;

客户端 6 条 ESTAB 状态的 socket,其 RECV_Q 和 SEND_Q 均为 0;而 4 条 SYN-SENT 状态的 SEND-Q 为 1,这是因为 6 条已建立连接的 socket 包可以被正常 ACK,而 4 条建立连接失败的 socket,其 SYN 包没有收到 ACK 包,因为 SEND-Q 显示为 1。由此我们可以再次总结下 ss 的展示含义:

对于 LISTEN 状态的 socket

  • Recv-Q:表示当前全连接队列的大小,即已完成三次握手等待应用程序 accept() 的 TCP 连接数。
  • Send-Q:全连接队列的最大长度,即全连接队列所能容纳的 socket 数量。

对于非 LISTEN 状态的 socket

  • Recv-Q:表示已被接收但尚未执行 accept 被应用程序读取的数据字节数,通常在服务端能观察到。
  • Send-Q:表示已经发送但尚未收到 ACK 确认的字节数。

内核代码如下:

// https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_diag.c#L18
static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,void *_info)
{struct tcp_info *info = _info;if (inet_sk_state_load(sk) == TCP_LISTEN) { // LISTEN 状态的连接// 当前已完成三次握手但未被 accept 的连接数r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog); // 最大队列长度r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog);} else if (sk->sk_type == SOCK_STREAM) { // 非 LISTEN 状态的普通连接const struct tcp_sock *tp = tcp_sk(sk);// TCP 读队列,即接收缓冲区中未被应用层读取的数据量,单位是字节r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) -READ_ONCE(tp->copied_seq), 0);// TCP 写队列,即已经发送但尚未被对方 ACK 确认的数据量,单位是字节r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;}if (info)tcp_get_info(sk, info);
}
5. SYN+ACK 重传

原实验有三种情况:

  • 三次握手成功,数据正常发送
  • 客户端认为连接建立成功,但服务端一直处于 SYN-RECV 状态,不断重传 SYN + ACK
  • 客户端发送 SYN 未得到响应一直在重传

我们复现了第 1 中和第 3 种,之所以没有第二种情况是因为每次请求加了 500ms 的间隔,这样下一个请求发起 SYN 时,上一个请求已经完成三次握手,服务端的 socket 已经进入全连接队列了。如果我们去掉时间间隔,请求可能会一下子发出去全部进入半连接队列,等到服务端在接收到客户端的 ACK 包时,全连接队列已经满了,从而导致服务端的 socket 无法进入全连接队列,从而 DROP 掉 ACK 包出现第二种情况。这里我们去掉时间间隔尝试复现,此时可以看到服务端有 SYN-RECV 状态的连接,

$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN     6      5                         *:8888                       *:*
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33430
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33458
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33482
SYN-RECV   0      0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33512
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33442
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33428
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33472
SYN-RECV   0      0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33496

查看抓包结果可以看到 SYN-ACK 包重传。

全连接队列的实验就到这里,下面我们来看半连接队列的实验。

半连接队列实战

半连接队列的最大长度计算有些麻烦,网络上资料也很繁杂,本着 talk is cheap, show me the code 的原则,这里还是直接看 Linux 的源码来分析,还是 tcp_conn_request 函数。

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848int tcp_conn_request(struct request_sock_ops *rsk_ops,const struct tcp_request_sock_ops *af_ops,struct sock *sk, struct sk_buff *skb)
{// ... 代码省略u8 syncookies;// 第一部分,基于 syncookies 和半连接队列是否超过全连接队列长度、半连接队列是否已满来判断是否 dropsyncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);if ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);if (!want_cookie)goto drop;}// 第二部分,判断全连接队列是否已满if (sk_acceptq_is_full(sk)) {NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);if (!req)goto drop;// ... 代码省略if (!want_cookie && !isn) {// 获取系统参数 ``net.ipv4.tcp_max_syn_backlog`` 的值int max_syn_backlog = READ_ONCE(net->ipv4.sysctl_max_syn_backlog);/* Kill the following clause, if you dislike this way. */// 第三部分:判断半连接队列是否超过长度限制if (!syncookies &&(max_syn_backlog - inet_csk_reqsk_queue_len(sk) <(max_syn_backlog >> 2)) &&!tcp_peer_is_proven(req, dst)) {/* Without syncookies last quarter of* backlog is filled with destinations,* proven to be alive.* It means that we continue to communicate* to destinations, already remembered* to the moment of synflood.*/pr_drop_req(req, ntohs(tcp_hdr(skb)->source),rsk_ops->family);goto drop_and_release;}isn = af_ops->init_seq(skb);}tcp_ecn_create_request(req, skb, sk, dst);if (want_cookie) {isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);if (!tmp_opt.tstamp_ok)inet_rsk(req)->ecn_ok = 0;}return 0;}

核心计算逻辑是 (max_syn_backlog - inet_csk_reqsk_queue_len(sk) < (max_syn_backlog >> 2)),即 max_syn_backlog 的值减去当前半连接队列的长度的值小于 max_syn_backlog 的 1/4 时,就会将 SYN 包丢弃。简单来说就是半连接队列长度不能超过 max_syn_backlog 的 3/4。因为比较条件是 > 而不是 >=,所以在不开启 syncookies 的情况下,实际的半连接队列长度应该是 max_syn_backlog 的 3/4 + 1。大致计算如下:

  • max_syn_backlog 为 128,则半连接队列长度最大为 97
  • max_syn_backlog 为 256,则半连接队列长度最大为 193
  • max_syn_backlog 为 512,则半连接队列长度最大为 385
  • max_syn_backlog 为 1024,则半连接队列长度最大为 769

结合上面全连接实验中的代码分析,我们可以总结下 Linux 5.15.30 内核下 SYN 包的 Drop 机制:

我们修改参数验证下上述三种情况。

实验一:关闭 syncookies,半连接长度超过全连接最大长度

客户端我们使用 iptables 将服务端的包拦截,模拟 SYN Flood 攻击,这样服务端不会收到 ACK 包,也就不会进入全连接队列。系统参数 syn_cookies=0,max_syn_backlog=128,somaxconn=64,理论上会有 64 个 SYN-RECV 状态连接,其余的包被丢弃。

# 拦截服务端 8888 端口的包
$ sudo iptables -A INPUT -p tcp --sport 8888 -j DROP# 发送 SYN 包
$ sudo hping3 -S 172.19.0.12 -p 8888 --flood

查看服务端情况

$ ss -ant | grep -E "Recv|8888"
State     Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN    0      64                        *:8888                       *:*# ubuntu @ node2 in ~ [11:58:11]
$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
64

结果符合预期。这里可以用 go 客户端做更精确的验证,我们使用 Go 程序发送 100 个请求,然后查看服务端连接数和 DROP 数

$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:01:58 PM CST 20253030591019 SYNs to LISTEN sockets dropped$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
64$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:02:14 PM CST 20253030591127 SYNs to LISTEN sockets dropped

可以看到服务端只有 64 个 SYN-RECV 状态连接,程序执行有有 3030591127-3030591019=108 个 SYN 包被丢弃。上面我们分析过,因为客户端设置了超时时间为 5s,所以 SYN 只会重传 2 次,也就是每个被 DROP 的连接都会发送 3 次 SYN。100 - 64 = 36,36 * 3 = 108,符合我们预期。

实验二:关闭 syncookies,全连接队列已满

修改服务端系统参数 syn_cookies=0,max_syn_backlog=128,somaxconn=64,这样全连接队列最大长度为 64,当有 65 个连接建立时,全连接队列就会满,此时再有 SYN 包建立连接时就会被丢弃。

首先我们清理掉客户端机器的 iptables 规则,是的三次握手能够正常进程。

$ sudo iptables -F

设置系统参数

$ sudo sysctl -w net.ipv4.tcp_syncookies=0
$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128
$ sudo sysctl -w net.core.somaxconn=64

我们再次用 Go 客户端发送 100 个请求,然后查看服务端状态,可以看到有 65 个 ESTAB 状态连接,没有 SYN-RECV 状态连接,因为全连接队列已满,所有 SYN 包都会被丢弃。

$ ss -ant | grep -E "Recv|8888"
State     Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN    65     64                        *:8888$ sudo netstat -nat | grep :8888 | grep ESTAB  | wc -l
65# ubuntu @ node2 in ~ [12:18:27] C:130
$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
0

按照以上逻辑,会有 35 个连接被拒绝,一共有 35 * 3 = 105 个 SYN 包被丢弃。我们查看统计信息可以验证,3030591766 - 3030591661 = 105,符合预期。

$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:18:19 PM CST 20253030591661 SYNs to LISTEN sockets dropped# ubuntu @ node2 in ~ [12:18:19]
$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:18:34 PM CST 20253030591766 SYNs to LISTEN sockets dropped

实验三:关闭 syncookies,半连接队列长度超过 max_syn_backlog 的 3/4

现在我们将全连接队列长度调大 net.core.somaxconn 设置为 4096,使用 iptables 拦截服务端 8888 端口的包,这样全连接队列始终不会填满,然后 max_syn_backlog 分别设置为:

  • 128,预期有 97 个 SYN-RECV 状态连接
  • 256,预期有 193 个 SYN-RECV 状态连接
  • 512,预期有 385 个 SYN-RECV 状态连接
  • 1024,预期有 769 个 SYN-RECV 状态连接

分别设置并发送请求后,服务端显示结果如下,基本符合预期。

# 客户端设置 iptables 拦截服务端
sudo iptables -A INPUT -p tcp --sport 8888 -j DROP# 服务端查看 SYN-RECV 状态连接数
$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
97$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=256
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
193$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=512
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
385$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=1024
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
769

执行过程数值会有变化,但最大半连接队列长度符合预期。

实验四:开启 syncookies,半连接队列长度取决于 max(somaxconn, backlog)

当开启 syncookies 时,半连接队列不在保留 1/4 的限制,而是取决于 max(somaxconn, backlog)。这里源码判断是 >=,因此最大长度应该会等于 max(somaxconn, backlog)

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/include/net/inet_connection_sock.h#L280
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

我们分别设置 net.core.somaxconn 为 512,1024,4096并设置 net.ipv4.tcp_syncookies=1 开启 syncookies,每次设置完重启服务端,然后在发起请求,理论上会有 512,1024,4096 个 SYN-RECV 状态连接。

修改服务端 somaxconn 并重启后,使用 watch 命令查看 SYN-RECV 状态连接数,结果如下,符合预期。

$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:07:22 2025512$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:08:15 20251024$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:09:11 20254096

简要总结

  • 半连接队列受限于全连接队列长度,而全连接队列会受应用的影响,尽量不要将 somaxconn 设置的过小,否则会影响服务器的性能。
  • 尽量开启 syncookies,可以有效防止 SYN Flood 攻击,同时可以避免半连接队列被大量占用。
  • ss、netstat 的熟练使用对探查网络状态非常重要,要熟练掌握。
  • 代码之下无秘密,一定要结合源码去理解 Linux 的网络工作机制,不要只是死记硬背协议。
  • 动手!动手!动手!实践出真知。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/72526.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型训练微调技术介绍

大模型训练微调技术是人工智能领域中的一项重要技术&#xff0c;旨在通过少量特定领域的数据对预训练模型进行进一步训练&#xff0c;使其更好地适应具体任务或应用场景。以下是关于大模型训练微调技术的详细介绍&#xff1a; 1. 微调技术的定义与意义 微调&#xff08;Fine-…

javaweb自用笔记:Vue

Vue 什么是vue vue案例 1、引入vue.js文件 2、定义vue对象 3、定义vue接管的区域el 4、定义数据模型data 5、定义视图div 6、通过标签v-model来绑定数据模型 7、{{message}}直接将数据模型message展示出来 8、由于vue的双向数据绑定&#xff0c;当视图层标签input里的…

基于eRDMA实测DeepSeek开源的3FS

DeepSeek昨天开源了3FS分布式文件系统, 通过180个存储节点提供了 6.6TiB/s的存储性能, 全面支持大模型的训练和推理的KVCache转存以及向量数据库等能力, 每个客户端节点支持40GB/s峰值吞吐用于KVCache查找. 发布后, 我们在阿里云ECS上进行了快速的复现, 并进行了性能测试, ECS…

计算机毕业设计SpringBoot+Vue.js医院挂号就诊系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

Linux的用户与权限--第二天

认知root用户&#xff08;超级管理员&#xff09; root用户用于最大的系统操作权限 普通用户的权限&#xff0c;一般在HOME目录内部不受限制 su与exit命令 su命令&#xff1a; su [-] 用户名 -符号是可选的&#xff0c;表示切换用户后加载环境变量 参数为用户名&#xff0c…

计算机网络软考

1.物理层 1.两个主机之间发送数据的过程 自上而下的封装数据&#xff0c;自下而上的解封装数据&#xff0c;实现数据的传输 2.数据、信号、码元 码元就是数字通信里用来表示信息的基本信号单元。比如在二进制中&#xff0c;用高电平代表 “1”、低电平代表 “0”&#xff0c…

第四十一:Axios 模型的 get ,post请求

Axios 的 get 请求方式 9.双向数据绑定 v-model - 邓瑞编程 Axios 的 post 请求方式&#xff1a;

【JQuery—前端快速入门】JQuery 操作元素

JQuery 操作元素 1. 获取/修改元素内容 三个简单的获取元素的方法&#xff1a; 这三个方法即可以获取元素的内容&#xff0c;又可以设置元素的内容. 有参数时&#xff0c;就进行元素的值设置&#xff0c;没有参数时&#xff0c;就进行元素内容的获取. 接下来&#xff0c;我们需…

2025年4月1日-2日AutoCable 中国汽车线束线缆及连接技术创新峰会即将开幕

正如人体的心脏与四肢之间需要靠神经和血管连接&#xff0c;汽车的各个部件&#xff0c;也要靠各种电线、管道连接。线束&#xff0c;就是汽车的神经和血管&#xff0c;车主向汽车下达的每一个功能指令&#xff0c;都通过线束来传递&#xff0c;看似不起眼的线束&#xff0c;却…

深度学习神经网络分类原理

每一个神经元做的是一个类似回归的操作 最后一层是softmax函数&#xff0c;每一个输出就会变成一个0到1之间的数&#xff0c;也就是概率&#xff0c;然后他们之间的和加起来等于1&#xff0c;到底是哪一个分类就是看哪个神经元的这个值最大。 那么如何算损失呢&#xff1a; 加…

硬核技术组合!用 DeepSeek R1、Ollama、Docker、RAGFlow 打造专属本地知识库

文章目录 一、引言二、安装Ollama部署DeepSeekR1三、安装Docker四、安装使用RAGFlow4.1 系统架构4.2 部署流程4.3 使用RAGFlow4.4 在RAGFlow中新增模型4.5 创建知识库4.6 创建私人助理使用RGA 一、引言 本地部署DeepSeek R1 Ollama RAGFlow构建个人知识库&#xff0c;通过将…

前端实现OSS上传图片(Vue3+vant)

首先&#xff0c;下面这些信息从阿里云服务器OSS管理中获取 aliyun:oss:file:endpoint: "oss-cn-beijing.aliyuncs.com"keyid: "xxxxxxxxx"keysecret: "xxxxxxxxxxxx"bucketname: "xxxx"一、安装OSS npm install ali-oss 二、以下步…

huggingface NLP主要知识点以及超级详解使用

1.安装huggingface依赖库 pip install transformers pip install datasets pip install pytorch pip install tokenizers pip install diffusers pip install accelerate pip install evaluate pip install optimum pip install pillow pip install requests pip install gr…

SQL注入练习场:PHPStudy+SQLI-LABS靶场搭建教程(零基础友好版)

注意&#xff1a;文中涉及演示均为模拟测试&#xff0c;切勿用于真实环境&#xff0c;任何未授权测试都是违法行为&#xff01; 一、环境准备 下载PHPStudy 官网下载地址&#xff1a;https://www.xp.cn/php-study&#xff08;选择Windows版&#xff09; 安装时建议选择自定…

现今大语言模型性能(准确率)比较

现今大语言模型性能(准确率)比较 表头信息:表的标题为“大语言模型性能比较结果”(英文:Table 1: Large Language Model Performance Comparison Results),表明该表是用于对比不同大语言模型的性能。列信息: 模型:列出参与比较的不同大语言模型名称,包括LLAMA3(70B)…

Docker创建自定义网桥并指定网段

前言 docker0是Docker默认网络的核心组件, 通过虚拟网桥和NAT技术, 实现了容器间的通信以及容器与外部网络的交互。然而, docker0网段是固定的(通常是172.17.0.0/16), 为了更灵活地管理容器网络&#xff0c;Docker支持创建自定义网桥&#xff0c;允许用户指定网段。 例如, 在…

蓝桥杯每日一题:第一周周四哞叫时间

蓝桥杯每日一题&#xff1a;第一周周四哞叫时间 疑惑&#xff1a;如何把复杂度控制在Q&#xff08;n&#xff09;&#xff0c;怎么枚举a和b&#xff0c;longlong的形式又该怎么输入&#xff08;考虑用string&#xff09; 思路&#xff1a;枚举倒数第二个b前面有多少个a 这是一…

在 macOS 使用 .pem 私钥免密登录腾讯云服务器

前言 在腾讯云上创建服务器时&#xff0c;如果选择了「密钥对」的登录方式&#xff0c;就会得到一个 .pem 文件作为私钥。很多小伙伴在使用 macOS 系统时&#xff0c;可能不清楚如何使用这个私钥文件来 SSH 免密登录远程服务器。本文将详细介绍如何在本地配置 .pem 私钥文件并…

腾讯 TDF 即将开源 Kuikly 跨端框架,Kotlin 支持全平台

今天&#xff0c;在腾讯的 Shiply 平台看 Flutter 动态化自研框架 Conch 时&#xff0c;在侧边栏看到了有「跨端开发框架」的介绍&#xff0c;点开发现有两个产品&#xff1a; Hippy&#xff1a;面向前端技术栈的跨端开发框架&#xff0c;Web原生开发体验&#xff0c;支持 Rea…

播放器系列3——解码

FFmpeg解码过程详解 解码流程 #mermaid-svg-FGu92IEtteOdO2tO {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-FGu92IEtteOdO2tO .error-icon{fill:#552222;}#mermaid-svg-FGu92IEtteOdO2tO .error-text{fill:#5522…