我们通常使用netstat查看网络的诸多状态,其中包含Send-Q与Recv-Q。

我们知道:

  • 每一个Socket对象在系统中都被映射为一个Socket文件;
  • 每一个Socket对象在系统中都关联有两个内核缓冲区:一个接收缓冲区(读缓冲区),一个发送缓冲区(写缓冲区);

Send-Q:指代的是内核中Socket对应的发送缓冲区尚未发送完毕的字节数;
Recv-Q:指代的是内核中Socket对应的接收缓冲区尚未被用户收走(read)而滞留在接收缓冲区的字节数;

下面请看示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

static int totalMsg = 0;

void sigINT(int dwsigno)
{
    printf("totalMsg: %d\n", totalMsg);
    exit(0);
}

int openServer()
{
    struct addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_socktype = SOCK_DGRAM;

    struct addrinfo *res;
    static char *port = "4020";
    int e = getaddrinfo(NULL, port, &hints, &res);
    if (e == EAI_SYSTEM)
    {
        printf("openServer: getaddrinfo error=%d(%s)!!!\n", errno, strerror(errno));
        return -1;
    }
    else if (e != 0)
    {
        printf("openServer: getaddrinfo error=%s!!!\n", gai_strerror(e));
        return -1;
    }

    int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (fd < 0)
    {
        printf("openServer: create socket error=%d(%s)!!!\n", errno, strerror(errno));
        freeaddrinfo(res);
        return -1;
    }

    int rcvBufSize = 131071; // 系统默认可设置缓冲区大小
    socklen_t optlen = sizeof(rcvBufSize);
    if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0)
    {
        printf("openServer: setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
        freeaddrinfo(res);
        return -1;
    }
    int rcvRealSize = -1;
    if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvRealSize, &optlen) < 0)
    {
        printf("openServer: getsockopt error=%d(%s)!!!\n", errno, strerror(errno));
        freeaddrinfo(res);
        return -1;
    }
    printf("recrive-buff-size: %d\n", rcvRealSize);

    if (bind(fd, (struct sockaddr *)res->ai_addr, res->ai_addrlen) < 0)
    {
        printf("openServer: bind error=%d(%s)!!!\n", errno, strerror(errno));
        freeaddrinfo(res);
        return -1;
    }
    printf("create udp socket(%d) ok!\n", fd);
    return fd;
}

void monUdpSock(int udpSock)
{
    static fd_set fds;
    FD_ZERO(&fds);
    FD_SET(udpSock, &fds);

    static struct timeval tv = {0, 20000};
    int readyNum = select(udpSock+1, &fds, NULL, NULL, &tv);
    if (readyNum < 0)
    {
        printf("monUdpSock: select error=%d(%s)!!!\n", errno, strerror(errno));
        // 异常处理
        return;
    }
    else if (readyNum == 0)
        return; // select超时,do nothing
    else
        ; // 存在可读写fd

    if (!FD_ISSET(udpSock, &fds))
        return;

    static char udpMsg[1024*64]; // 64KB
    int rbytes = read(udpSock, udpMsg, sizeof(udpMsg));
    if (rbytes <= 0)
        return;
    // 处理收到的Udp消息
    totalMsg++;
}

int main()
{
    if (signal(SIGINT,sigINT) == SIG_ERR)
    {
        printf("set single handler error!\n");
        exit(1);
    }

    int udpSock = openServer();

    while (1)
    {
        monUdpSock(udpSock);
        usleep(10); // sleep 10 us
    }
}

上面的代码是一个简单的udp服务器,用以接收来自udp客户端的数据报并且统计总共收到了多少个udp数据报。

我们编译并且运行这个udp服务器:

[udpdriver@eb6347 0329]$ gcc -o main main.c
[udpdriver@eb6347 0329]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!

结合代码,我们看到当前Socket拥有的接收缓冲区大小为262142字节。

注:为啥设置的缓冲区大小是131071,但实际返回的是262142?

通常来说,setsockopt可以设置的缓冲区是系统设置的Socket最大缓冲区数值大小的一半。

[udpdriver@eb6347 0329]$ cat /proc/sys/net/core/rmem_max
131071

系统内核设置的这个接收缓冲区大小值,就是udp socket默认的最大接收缓冲区值的一半。

实际一个udp socket的接收缓冲区最大为:131071*2=262142.

我们再通过netstat来查看此socket的接收缓冲区中滞留的,尚未读取(到用户态缓冲区)的字节数量。

我们可以写一个简单的shell脚本,每秒调用一次netstat,来观察其运行期的缓冲区滞留数值。

#!/bin/bash

while [ true ]; do
    sleep 1
    netstat -an | grep $1
done

添加权限并且启动脚本:

[udpdriver@eb6347 0329]$ chmod a+x netstat.sh
[udpdriver@eb6347 0329]$ ./netstat.sh 4020
udp        0      0 :::4020                     :::*                                    
udp        0      0 :::4020                     :::*                                    
udp        0      0 :::4020                     :::*                                    
udp        0      0 :::4020                     :::*                                    
udp        0      0 :::4020                     :::*

第二列即为我们说的Recv-Q,第三列即为我们说的Send-Q。

目前尚未有udp客户端发送数据,所以滞留在udp服务端socket的接收缓冲区,尚未被读取的字节数为0。

我们使用一个压测拟每条udp 200字节左右,800caps,总量80000,来对服务器进行压测。

[udpdriver@eb6347 pmtest]$ ./main 80000 800
totalUdp: 80000
maxRate: 800
udp data len: 258
loaded 258 Bytes Data

此时netstat脚本的输出中,我们可以观察到接收缓冲区的堆积:

udp        0      0 :::4020                     :::*                                    
udp        0      0 :::4020                     :::*                                    
udp        0      0 :::4020                     :::*                                    
udp     5056      0 :::4020                     :::*                                    
udp     2528      0 :::4020                     :::*                                    
udp     2528      0 :::4020                     :::*                                    
udp    11376      0 :::4020                     :::*                                    
udp     9480      0 :::4020                     :::*                                    
udp    12008      0 :::4020                     :::*                                    
udp    12008      0 :::4020                     :::*                                    
udp     9480      0 :::4020                     :::*                                    
udp     3160      0 :::4020                     :::*                                    
udp    10744      0 :::4020                     :::*                                    
udp    43608      0 :::4020                     :::*                                    
udp   147888      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261016      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261016      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261648      0 :::4020                     :::*                                    
udp   261016      0 :::4020                     :::*

Recv-Q越来越大,说明udp服务器来不及从内核中的接收缓冲区收取数据,导致大量udp数据包堆积在内核缓冲区。

当内核缓冲区中数据达到设置的上限(262142字节),再有udp包到内核,内核就将其丢弃,也就是常说的:

UDP接收缓冲区溢出,发生丢包现象。

我们客户端实际发送80000个udp数据包,可以通过在服务端Ctrl+C发个信号,查看当前udp服务端已处理的udp包总数:

[udpdriver@eb6347 0329]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!
totalMsg: 69770

69770<80000,丢失UDP包10230个。

如果你够仔细,你会发现,Recv-Q的最大值,就是我们的udp内核接收缓冲区的实际值。

无论发包多快,多大,在Recv-Q永远不会超过getsockopt得出的实际的udp socket内核接收缓冲区的max值。

到此我们明白:getsockopt获取的接收缓冲区的大小,等价于在netstat中Recv-Q中可滞留在接收缓冲区数据的最大值。如果数据加入不到Recv-Q(内核读缓冲区)中,那内核就将其丢弃(特指udp)。此时,我们应该考虑的是提高服务器的性能,而不是扩大接收缓冲区的大小。

原因:缓冲区大小是防止“突变”的一种机制,防止在某一特殊时刻大量数据到来,导致丢失数据包。

如果接收缓冲区通常为0,偶尔有个波动,来个3k 5k的数据,那是正常的,没关系的,可能由于系统调度等原因导致。

如果接收缓冲区有堆积并且无法归零,说明服务器read慢了,跟不上客户端的发送速率,这样,即使你暂时设置缓冲区为1G(假定能设置这么大),2G,3G也是没有意义的。终究会随着时间的推移,导致接收缓冲区的数据不断累积…你能放多少?

无论何时,应当保证你的消耗速率(从内核中将读缓冲区的数据读到用户态缓冲区)大于你的生产速率(内核收到来自网络的数据包,将其从网卡中写入内核的写缓冲区的速率)。

接收缓冲区有堆积是一定有问题的,增大缓冲区并不能解决问题,要从服务器角度考虑,优化、增强其处理性能,快快地将接收缓冲区滞留的待读数据处理完毕。