本文所运用的Linux内核版别信息为 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

什么是TCP Backlog

backlog的中文意义是 积压 的意思,在Linux网络中,意味着网络数据包的积压,在Linux表现为半衔接行列和全衔接行列存储这些积压的数据包。backlog参数的巨细,则会影响半衔接行列和全衔接行列缓存数据包的多少。

其间,半衔接行列和全衔接行列的意义如图所示(此处引用张师傅博客中的图)

  • 半衔接行列(Incomplete connection queue),又称 SYN 行列
  • 全衔接行列(Completed connection queue),又称 Accept 行列

使用SystemTap观测TCP Backlog

从服务端角度看待TCP三次握手的进程,有以下几步:

  1. 调用 listen 函数时,TCP 的状况被从 CLOSE 状况变为 LISTEN,此刻内核就创建了半衔接行列和全衔接行列。backlog参数便是在listen的时分指定的。
int listen(int sockfd, int backlog);
  1. 在TCP进行三次握手的时分,收到SYN报文会先将数据包放到半衔接行列,然后发出SYN+ACK
  2. 接着当收到对端的SYN+ACK的时分,再将这个衔接恳求的数据包移动到全衔接行列,等待应用程序经过accept() 函数读取。

咱们能够经过listen函数传入backlog参数值,且backlog参数值会影响到半衔接行列和全衔接行列的巨细,可是咱们该怎样观测到终究操作体系运用的backlog的巨细呢?又怎样观测到半衔接行列、全衔接行列中的缓存的包数量呢?backlog参数和半衔接行列、全衔接行列的巨细之间又有什么关系呢?

实验环境搭建

先在本地电脑上发动了两个虚拟机,Linux虚拟机1(命名为L1,ip: 10.211.55.6)和Linux虚拟机2(命名为L2,ip: 10.211.55.8),以 L1 作为服务器,L2作为客户端。

使用SystemTap观测TCP Backlog

观测Linux终究选用的backlog巨细

为确认backlog值经过listen函数设置进去之后,操作体系终究选用的数值,能够经过systemtap东西来确认。安装好systemtap东西之后,编写探测脚本如下:

probe kernel.function("tcp_v4_conn_request") {
    tcphdr = __get_skb_tcphdr($skb);  
    dport = __tcp_skb_dport(tcphdr);  
    if (dport == 9090)  
    {  
        printf("reach here\n");  
        printf("socket struct: %s \n", $sk$);  
        syn_qlen = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->qlen;  
        max_backlog=$sk->sk_max_ack_backlog;  
        printf("qlen: %d, max_backlog: %d  \n", syn_len, max_backlog);  
    }  
}

这个脚本做的事情,便是对linux中 tcp_v4_conn_request 这个内核函数做了探针,只需调用到这个内核函数,且端口号为9090,就会履行一系列的打印操作。其间,会将socket目标打印出来,也会将socket目标中的 sk_max_ack_backlog 变量打印出来,这个变量正是linux终究选用的backlog值。

将这个脚本放到机器L1中的任一用户目录下,脚本命名为 tcp_backlog.stp,然后用指令履行:

sudo stap -v tcp_backlog.stp

如果运转成功,则会看到在终端上显示正在运转的提示:

使用SystemTap观测TCP Backlog

此刻,为防止编程言语的干扰,用C言语准备一段服务器的发动代码,backlog值能够经过修改常量来更改,这儿运用backlog值为20


// main.c
#include <sys/socket.h>  
#include <stdio.h>  
#include <netinet/in.h>  
#include <unistd.h>  
#include <string.h>  
#include <stdlib.h>  
#include <sys/shm.h>  
#define MYPORT  9090  
#define BACKLOG 20  
#define BUFFER_SIZE 1024  
int main()  
{  
    ///定义sockfd  
    int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);  
    ///定义sockaddr_in  
    struct sockaddr_in server_sockaddr;  
    server_sockaddr.sin_family = AF_INET;  
    server_sockaddr.sin_port = htons(MYPORT);  
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    ///bind,成功回来0,犯错回来-1  
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)  
    {  
        perror("bind");  
        exit(1);  
    }  
    ///listen,成功回来0,犯错回来-1  
    if(listen(server_sockfd, BACKLOG) == -1)  
    {  
        perror("listen");  
        exit(1);  
    }  
    ///客户端套接字  
    char buffer[BUFFER_SIZE];  
    char message[100] = "已成功接收!";  
    struct sockaddr_in client_addr;  
    socklen_t length = sizeof(client_addr);  
    ///成功回来非负描绘字,犯错回来-1  
    int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);  
    if(conn<0)  
    {  
        perror("connect");  
        exit(1);  
    }  
    while(1)  
    {  
        memset(buffer,0,sizeof(buffer));  
        int size = read(conn, buffer, 1024);  
        if(strcmp(buffer,"exit\n")==0)  
            break;  
        strncat(buffer, message, 100);  
        fputs(buffer, stdout);  
        write(conn,buffer,strlen(buffer)+1);  
    }  
    close(conn);  
    close(server_sockfd);  
    return 0;  
}

在L1上经过指令编译sk.c 并发动:

gcc main.c -o sk.o && ./sk.o

发动后,在L2上经过nc指令衔接L1的9090端口:

nc 10.211.55.6 9090

接着调查 tcp_backlog.stp 探针脚本的输出:

使用SystemTap观测TCP Backlog

可见此刻运用的backlog值为20,经过这个办法,咱们能够观测到linux终究选用的 backlog值的巨细是多少了。

体系变量对backlog巨细的影响

backlog尽管能够经过listen设置进去,可是依照张师傅的博客所说,终究的巨细会遭到操作体系的配置影响。可经过sysctl指令检查这两个体系变量:

sysctl net.ipv4.tcp_max_syn_backlog
# net.ipv4.tcp_max_syn_backlog = 128
sysctl net.core.somaxconn
# net.core.somaxconn = 4096

依照上述观测的办法,函数传入的backlog值别离在 小于128,大于128但小于4096,大于4096这三个区间取一个值。设置backlog巨细为 20、200、6000,别离观测操作体系终究选用的backlog值如下:

listen backlog值为200时,操作体系选用的backlog值为200

使用SystemTap观测TCP Backlog

listen backlog值为6000时,操作体系选用的backlog值为4096,和体系变量 net.core.somaxconn 坚持一样。

使用SystemTap观测TCP Backlog

将上述测试数据总结如下:

listen backlog值 操作体系实际选用的backlog值
20 20
200 200
6000 4096

在张师傅的博客中说到, Linux内核版别在3.10.0的时分,会遭到 net.ipv4.tcp_max_syn_backlognet.core.somaxconn 的影响,且受这两个变量影响的逻辑还比较复杂。可是在 5.15.0版别中,现已做了简化,代码如下:

// net/socket.c
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) {
    # sysctl_somaxconn对应体系变量net.core.somaxconn的值
    somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
    if ((unsigned int)backlog > somaxconn)
      backlog = somaxconn;
    err = security_socket_listen(sock, backlog);
    if (!err)
      err = sock->ops->listen(sock, backlog);
    fput_light(sock->file, fput_needed);
  }
  return err;
}

再简化一下中心逻辑,中心逻辑的伪代码如下:

backlog = listen_backlog;
somaxconn = valuOf(`net.core.somaxconn`);
if(backlog > somaxconn) {
	backlog = somaxconn;
}

按张师傅的博客所说,在内核版别为3.10.0中,backlog 值会在这个时分依次传递给 __sys_listen() -> inet_listen()->inet_csk_listen_start()->reqsk_queue_alloc(),终究在 reqsk_queue_alloc函数中依据这两个体系变量经历一系列复杂的核算,终究得到操作体系运用的backlog值。可是这些操作,在5.x版别的内核都去掉了,reqsk_queue_alloc函数中不再对backlog做过任何处理:

// net/ipv4/inet_connection_sock.c
// 在这个函数中,尽管传入了backlog,可是在后续的处理中完全没有用上,由此证明backlog的赋值,在 __sys_listen 函数中现已完结
int inet_csk_listen_start(struct sock *sk, int backlog)
{
  struct inet_connection_sock *icsk = inet_csk(sk);
  struct inet_sock *inet = inet_sk(sk);
  int err = -EADDRINUSE;
  reqsk_queue_alloc(&icsk->icsk_accept_queue);
  sk->sk_ack_backlog = 0;
  inet_csk_delack_init(sk);
  /* There is race window here: we announce ourselves listening,
   * but this transition is still not validated by get_port().
   * It is OK, because this socket enters to hash table only
   * after validation is complete.
   */
  inet_sk_state_store(sk, TCP_LISTEN);
  if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
    inet->inet_sport = htons(inet->inet_num);
    sk_dst_reset(sk);
    err = sk->sk_prot->hash(sk);
    if (likely(!err))
      return 0;
  }
  inet_sk_set_state(sk, TCP_CLOSE);
  return err;
}
// net/core/request_sock.c
void reqsk_queue_alloc(struct request_sock_queue *queue)
{
  spin_lock_init(&queue->rskq_lock);
  spin_lock_init(&queue->fastopenq.lock);
  queue->fastopenq.rskq_rst_head = NULL;
  queue->fastopenq.rskq_rst_tail = NULL;
  queue->fastopenq.qlen = 0;
  queue->rskq_accept_head = NULL;
}

观测半衔接行列巨细

在三次握手的进程中,服务端收到握手恳求包之后,会先把它放到半衔接行列中,然后回复SYN+ACK。接着接收到客户端回来的ACK报文时,再把这个数据包从半衔接行列移动到全衔接行列中。在正常情况下,SYN报文在半衔接行列逗留的时刻会很快,观测半衔接行列巨细要做点处理。

依照张师傅博客提供的办法,能够在客户端设置防火墙,把服务端回来的ACK包都扔掉,这样在服务端就不会收到ACK报文了。

// 在L2机器上设置这条防火墙规则
sudo iptables --append INPUT --match tcp --protocol tcp --src 10.211.55.6 --sport 9090 --tcp-flags SYN SYN --jump DROP
// 检查防火墙规则是否设置成功
sudo iptables -L

接着用上述的服务端代码发动服务后,在L2上经过nc指令衔接上:

nc 10.211.55.6 9090

接着能够经过以下指令调查到,当时有多少个衔接处于SYN_RECV状况:

sudo netstat -lnpa | grep :9090  | awk '{print $6}' | sort | uniq -c | sort -rn

使用SystemTap观测TCP Backlog

处于SYN_RECV状况的衔接,意味着接收到了客户端的SYN报文但未接收到ACK报文。此刻衔接就处于SYN_RECV状况。经过这个点能够观测到半衔接行列此刻的巨细是多少。你也能够在L2上通进程序建议多次衔接,看看SYN_RECV状况的衔接数是否有变化,此处就不再叙述了。

观测全衔接行列巨细

当恳求收到ACK之后,就会从半衔接行列挪到全衔接行列,此刻衔接现已完全建立,衔接状况就会从LISTEN变成ESTABLISHED状况,等待应用程序调用accept函数从全衔接行列中取走数据。所以,要调查全衔接行列的巨细,只需调查ESTABLISHED状况的衔接数即可。同样能够选用netstat指令:

netstat -lnpa | grep :9090 | awk '{print $6}' | sort | uniq -c | sort -rn

也能够运用ss指令来进行观测。运用指令如下:

ss -lnt | grep :9090
  • 处于 LISTEN 状况的 socket,Recv-Q 表明 accept 行列排队的衔接个数,Send-Q 表明全衔接行列(也便是 accept 行列)的总巨细
  • 对于非 LISTEN 状况的 socket,Recv-Q 表明 receive queue 的字节巨细,Send-Q 表明 send queue 的字节巨细

总结

SystemTap是一个很有力的东西,用好这个东西,能够实实在在地观测到Linux内部的状况,让自己对操作体系有个更深刻的认识。