百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

一文读懂全连接队列

bigegpt 2024-09-09 01:26 45 浏览

简介

我们在编写服务端程序时,总是需要先 listen 一下。listen 最重要的就是初始化全连接和半连接队列,本文通过 Java 程序来验证全连接队列溢出情况。

全连接队列


上图是三次握手和网络交互的流程图,通过调用 accept 从全连接队列中获取 socket 进行读写。因此,我们在服务端只 listen 不调用 accept 就可以模拟全连接溢出场景。

队列长度

全连接队列长度与两个参数有关,backlog 和 somaxconn。

backlog 可以通过程序指定,在 ServerSocket 构造函数中,第二个参数 backlog 作用就在这儿。

// https://github.com/torvalds/linux/blob/448b3fe5a0eab5b625a7e15c67c7972169e47ff8/net/socket.c#L1867-L1886
int __sys_listen(int fd, int backlog)
{
 struct socket *sock;
 int err, fput_needed;
 int somaxconn;

 sock = sockfd_lookup_light(fd, &err, &fput_needed);
 if (sock) {
  // 获取 net.core.somaxconn,与 backlog 取较小值。
  **somaxconn = READ_ONCE(sock_net(sock->sk)->core.sysctl_somaxconn);
  if ((unsigned int)backlog > somaxconn)
   backlog = somaxconn;**

  err = security_socket_listen(sock, backlog);
  if (!err)
   err = READ_ONCE(sock->ops)->listen(sock, backlog);

  fput_light(sock->file, fput_needed);
 }
 return err;
}

第二个参数在 Linux 中,可以修改 /etc/sysctl.conf 文件。在我的机器上该值为 4096。


动手实践

根据上面的判断,可以推断出下面程序的全连接队列的长度为:min(1, 4096) = 1

/**
 * 全连接队列溢出
 */
public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888, 1);
        System.out.println("Started on 8888!");
        Thread.currentThread().join();
    }
}
/**
 * 全连接队列溢出
 */
public class Client {
    public static void main(String[] args) throws Exception {
        System.out.println("Started on: " + System.currentTimeMillis());
        try {
            for (int i = 0; i < 10; i++) {
                Socket socket = new Socket("127.0.0.1", 8888);
                System.out.println(socket.getPort());
            }
        } finally {
            System.out.println("End on: " + System.currentTimeMillis());
        }
    }
}

Linux 5.10 版本

当我们分别执行 java Server 和 java Client 后,在 client 的控制台会打印两次 8888。

Started on: 1715755832881
8888
8888
End on: 1715755963822
Exception in thread "main" java.net.ConnectException: Connection timed out (Connection timed out)
        at java.net.PlainSocketImpl.socketConnect(Native Method)
        at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
        at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
        at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
        at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
        at java.net.Socket.connect(Socket.java:607)
        at java.net.Socket.connect(Socket.java:556)
        at java.net.Socket.<init>(Socket.java:452)
        at java.net.Socket.<init>(Socket.java:229)
        at Client.main(Client.java:13)

通过 ss 查看全连接队列情况时,可以看出 Send-Q(全连接队列最大长度) 值为 1,Recv-Q(全连接队列实际长度) 值为 2。


为什么实际长度要比最大长度大呢? 根据 Linux 作者描述,全连接队列下标是从零开始的,因此连接数要比指定数字多一个。参考:[NET]: Revert incorrect accept queue backlog changes. · torvalds/linux@64a1465 (github.com)

备注:MacOS 上两个长度一致,都为 1。

抓包验证

通过 tcpdump -i lo -w rece.pcap 抓 lo 网卡的包,执行上面的程序。


可以发现在进行过前两次成功的三次握手后,第三次 56128 向 8888 发送 syn 包,但 8888 没有响应,56128 进行了六次重传,最后重传 syn 包失败,程序退出,已经建立成功的连接向 8888 发送 fin 包,销毁连接。

重试次数由 /proc/sys/net/ipv4/tcp_syn_retries 控制,默认为 6 次。重试策略为指数退避,1、2、4、8、16、32 秒进行重试。

不同的 Linux 版本中,全连接队列满后,行为是不一样的。上述是在 5.10 版本的表现。


在 5.10 版本中,只要是全连接队列满了,就会抛掉 syn 包。

// net/ipv4/tcp_input.c tcp_conn_request
// 需要切换到 5.10 tag
// 如果 accept queue 已经满了
if (sk_acceptq_is_full(sk)) {
 NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
 goto drop;
}

Linux 3.10 版本

当我们在 3.10 版本测试上面的程序时,现象比较奇怪,抓到的包则更为奇怪。

// 程序输出结果
Started on: 1715928123222
8888
8888
8888
8888
8888
8888
8888
8888
8888
8888
End on: 1715928129242


  • 从输出结果看,在全连接队列限制为 1 的前提下建立了 10 个连接
  • 抓包显示,建立成功的连接也不止 2 个,并且还有多次 syn 包重传 和 dup ack。

通过 ss -lnt 看出,全连接队列大小确实为 1,连接成功确实为 2,符合我们之前的预计结果。

那么这些「建立成功」的连接是怎么回事儿呢?

答案是 Client 端收到了 syn ack,发送 ack 就认为连接创建成功了。但 Server 端因为全连接队列满了,抛弃掉了部分 ack 包和 syn 包。所以出现了已经建立成功的连接的 Client 重传 ack 包,已经发送 syn-ack 的 Server 重传 syn-ack 包。

那么 Server 端何时丢 syn 包,何时丢 ack 包呢?(这里我们暂时不讨论半连接溢出的情况)

丢弃 syn 包

// net/ipv4/tcp_ip_v4.c
// 需要切换到 3.10 tag
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
 goto drop;
}

上面的程序和 5.10 版本的类似,只不过后面多了 inet_csk_reqsk_queue_young(sk) > 1 的判断。含义是半连接队列中刚到达的 syn,但是没有被超时重传 syn-ack 的数量,意义在于衡量 Server 的处理能力。因为正常三次握手都会很快,所以该值通常为 0。但如果 > 1,意味着此时 Server 端的处理能力不足,因此就要抛弃掉 syn 包来减轻 Server 的处理压力。

我们可以通过 systemtap 工具打印 qlen_young 的值,观察丢包情况。

probe kernel.function("tcp_v4_conn_request") {
    tcphdr = __get_skb_tcphdr($skb);
    dport = __tcp_skb_dport(tcphdr);
    if (dport == 8888)
    {
        // 获取当前时间的秒数和微秒数
        sec = gettimeofday_s();
        usec = gettimeofday_us() % 1000000;

        // 手动格式化时间
        min = (day_sec % 3600) / 60;
        sec_only = day_sec % 60;

        printf("current: %02d:%02d.%06d \n",
               min, sec_only, usec);

        // 当前 syn 排队队列的大小
        syn_qlen = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->listen_opt->qlen;
        // 当前 syn 排队中 young 的大小
        syn_young_qlen = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->listen_opt->qlen_young;

        printf("%02d:%02d.%06d syn queue: syn_qlen=%d, syn_young_qlen=%d\n",
                min, sec_only, usec,
               syn_qlen, syn_young_qlen);

    }
}

将上面的程序保存为 rece_request.c,然后调用 stap -g rece_request.c 观察 syn_yyoung_qlen 的值,当其大于 1 时就会触发丢掉 syn 包操作,再数数抓包文件中 syn 重传,这两个值应该是一致的。

丢弃 ack 包

只要是全连接队列满了,就会丢弃 ack 包。

/*
 * The three way handshake has completed - we got a valid synack -
 * now create the new socket.
 */
 // 三次握手最后一次 ack 处理,完事儿
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
      struct request_sock *req,
      struct dst_entry *dst)
{
     // 全连接队是否满了,直接 goto 到溢出,不处理 ack 包。
 if (sk_acceptq_is_full(sk))
  goto exit_overflow;
}      

总结

  • 我们在调用 listen 时,主要创建了全连接(accept queue)和半连接(syn queue)队列。
  • 全连接队列的大小取决于 min(backlog, somaxconn)
  • 不同版本对于全连接队列溢出的策略是不同的,但本质都是通过丢弃 Client 请求来保证 Server 的稳定。
  • 全连接队列主要供 accept 调用,因此只要使用先进先出的队列结构即可。全连接队列的数据主要来自于半连接,需要根据不同的 Client 获取,因此被设计成查询高效的哈希表结构。

相关推荐

Java 泛型大揭秘:类型参数、通配符与最佳实践

引言在编程世界中,代码的可重用性和可维护性是至关重要的。为了实现这些目标,Java5引入了一种名为泛型(Generics)的强大功能。本文将详细介绍Java泛型的概念、优势和局限性,以及如何在...

K8s 的标签与选择器:流畅运维的秘诀

在Kubernetes的世界里,**标签(Label)和选择器(Selector)**并不是最炫酷的技术,但却是贯穿整个集群管理与运维流程的核心机制。正是它们让复杂的资源调度、查询、自动化运维变得...

哈希Hash算法:原理、应用(哈希算法 知乎)

原作者:Linux教程,原文地址:「链接」什么是哈希算法?哈希算法(HashAlgorithm),又称为散列算法或杂凑算法,是一种将任意长度的数据输入转换为固定长度输出值的数学函数。其输出结果通常被...

C#学习:基于LLM的简历评估程序(c# 简历)

前言在pocketflow的例子中看到了一个基于LLM的简历评估程序的例子,感觉还挺好玩的,为了练习一下C#,我最近使用C#重写了一个。准备不同的简历:image-20250528183949844查...

55顺位,砍41+14+3!季后赛也成得分王,难道他也是一名球星?

雷霆队最不可思议的新星:一个55号秀的疯狂逆袭!你是不是也觉得NBA最底层的55号秀,就只能当饮水机管理员?今年的55号秀阿龙·威金斯恐怕要打破你的认知了!常规赛阶段,这位二轮秀就像开了窍的天才,直接...

5分钟读懂C#字典对象(c# 字典获取值)

什么是字典对象在C#中,使用Dictionary类来管理由键值对组成的集合,这类集合被称为字典。字典最大的特点就是能够根据键来快速查找集合中的值,其键的定义不能重复,具有唯一性,相当于数组索引值,字典...

c#窗体传值(c# 跨窗体传递数据)

在WinForm编程中我们经常需要进行俩个窗体间的传值。下面我给出了两种方法,来实现传值一、在输入数据的界面中定义一个属性,供接受数据的窗体使用1、子窗体usingSystem;usingSyst...

C#入门篇章—委托(c#委托的理解)

C#委托1.委托的定义和使用委托的作用:如果要把方法作为函数来进行传递的话,就要用到委托。委托是一个类型,这个类型可以赋值一个方法的引用。C#的委托通过delegate关键字来声明。声明委托的...

C#.NET in、out、ref详解(c#.net framework)

简介在C#中,in、ref和out是用于修改方法参数传递方式的关键字,它们决定了参数是按值传递还是按引用传递,以及参数是否必须在传递前初始化。基本语义对比修饰符传递方式可读写性必须初始化调用...

C#广义表(广义表headtail)

在C#中,广义表(GeneralizedList)是一种特殊的数据结构,它是线性表的推广。广义表可以包含单个元素(称为原子),也可以包含另一个广义表(称为子表)。以下是一个简单的C#广义表示例代...

「C#.NET 拾遗补漏」04:你必须知道的反射

阅读本文大概需要3分钟。通常,反射用于动态获取对象的类型、属性和方法等信息。今天带你玩转反射,来汇总一下反射的各种常见操作,捡漏看看有没有你不知道的。获取类型的成员Type类的GetMembe...

C#启动外部程序的问题(c#怎么启动)

IT&OT的深度融合是智能制造的基石。本公众号将聚焦于PLC编程与上位机开发。除理论知识外,也会结合我们团队在开发过程中遇到的具体问题介绍一些项目经验。在使用C#开发上位机时,有时会需要启动外部的一些...

全网最狠C#面试拷问:这20道题没答出来,别说你懂.NET!

在竞争激烈的C#开发岗位求职过程中,面试是必经的一道关卡。而一场高质量的面试,不仅能筛选出真正掌握C#和.NET技术精髓的人才,也能让求职者对自身技术水平有更清晰的认知。今天,就为大家精心准备了20道...

C#匿名方法(c#匿名方法与匿名类)

C#中的匿名方法是一种没有名称只有主体的方法,它提供了一种传递代码块作为委托参数的技术。以下是关于C#匿名方法的一些重要特点和用法:特点省略参数列表:使用匿名方法可省略参数列表,这意味着匿名方法...

C# Windows窗体(.Net Framework)知识总结

Windows窗体可大致分为Form窗体和MDI窗体,Form窗体没什么好细说的,知识点总结都在思维导图里面了,下文将围绕MDI窗体来讲述。MDI(MultipleDocumentInterfac...