我们通常使用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也是没有意义的。终究会随着时间的推移,导致接收缓冲区的数据不断累积…你能放多少?
无论何时,应当保证你的消耗速率(从内核中将读缓冲区的数据读到用户态缓冲区)大于你的生产速率(内核收到来自网络的数据包,将其从网卡中写入内核的写缓冲区的速率)。
接收缓冲区有堆积是一定有问题的,增大缓冲区并不能解决问题,要从服务器角度考虑,优化、增强其处理性能,快快地将接收缓冲区滞留的待读数据处理完毕。