为啥特定的 UDP 消息总是低于特定的缓冲区大小?

Posted

技术标签:

【中文标题】为啥特定的 UDP 消息总是低于特定的缓冲区大小?【英文标题】:Why are particular UDP messages always getting dropped below a particular buffer size?为什么特定的 UDP 消息总是低于特定的缓冲区大小? 【发布时间】:2016-05-24 21:21:44 【问题描述】:

3 条不同的消息以不同的速率发送到同一个端口:

消息  大小(字节)  每发送一次 kbd>传输速度高           232                10 ms          100Hz 中等     148                                                                          50Hz                    20                   60 ms         16.6Hz                

我只能每 ~ 6 毫秒处理一条消息。 单线程。阻止读取。


发生了一种奇怪的情况,我对此没有任何解释。 当我将接收缓冲区设置为 4,799 字节时,我的所有低速消息都会被丢弃。 我看到可能有一两个得到处理,然后什么都没有。

当我将接收缓冲区设置为4,800(或更高!)时,似乎所有低速消息都开始得到处理。我看到大约 16/17 秒。


这一直被观察到。发送数据包的应用程序总是在接收应用程序之前启动。接收应用程序在创建套接字之后和开始处理之前总是有很长的延迟。所以处理开始时缓冲区总是满的,而且每次发生测试时它都不是同一个开始缓冲区。这是因为套接字是在发送者已经发送消息之后创建的,因此接收者可能会在发送周期的中间开始监听。

为什么将接收缓冲区大小增加一个字节,会导致低速消息处理发生巨大变化?

我建了一个表格来更好地可视化预期的处理:

随着其中一些消息得到处理,更多消息可能会被放入队列而不是被丢弃。

尽管如此,我希望4,799 字节缓冲区的行为方式与4,800 字节相同。

但这不是我观察到的。


我认为这个问题与低速消息与其他两条消息同时发送的事实有关。它总是在高速/中速消息之后接收。 (这已经通过wireshark得到证实)。

例如,假设缓冲区一开始是空的,很明显低速消息需要比其他消息更长的排队时间。 *每 6 毫秒 1 条消息大约是每 30 毫秒 5 条消息。

这仍然不能解释缓冲区大小。

我们正在运行 VxWorks,并使用他们的 sockLib,它是 Berkeley 套接字的一个实现。这是我们创建套接字的样子的 sn-p:SOCKET_BUFFER_SIZE 是我要更改的内容。

struct sockaddr_in tSocketAddress;                          // Socket address
int     nSocketAddressSize = sizeof(struct sockaddr_in);    // Size of socket address structure
int     nSocketOption = 0;

// Already created
if (*ptParameters->m_pnIDReference != 0)
    return FALSE;

// Create UDP socket
if ((*ptParameters->m_pnIDReference = socket(AF_INET, SOCK_DGRAM, 0)) == ERROR)

    // Error
    CreateSocketMessage(ptParameters, "CreateSocket: Socket create failed with error.");

    // Not successful
    return FALSE;


// Valid local address
if (ptParameters->m_szLocalIPAddress != SOCKET_ADDRESS_NONE_STRING && ptParameters->m_usLocalPort != 0)

    // Set up the local parameters/port
    bzero((char*)&tSocketAddress, nSocketAddressSize);
    tSocketAddress.sin_len = (u_char)nSocketAddressSize;
    tSocketAddress.sin_family = AF_INET;
    tSocketAddress.sin_port = htons(ptParameters->m_usLocalPort);

    // Check for any address
    if (strcmp(ptParameters->m_szLocalIPAddress, SOCKET_ADDRESS_ANY_STRING) == 0)
        tSocketAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    else
    
        // Convert IP address for binding
        if ((tSocketAddress.sin_addr.s_addr = inet_addr(ptParameters->m_szLocalIPAddress)) == ERROR)
        
            // Error
            CreateSocketMessage(ptParameters, "Unknown IP address.");

            // Cleanup socket
            close(*ptParameters->m_pnIDReference);
            *ptParameters->m_pnIDReference = ERROR;

            // Not successful
            return FALSE;
        
    

    // Bind the socket to the local address
    if (bind(*ptParameters->m_pnIDReference, (struct sockaddr *)&tSocketAddress, nSocketAddressSize) == ERROR)
    
        // Error
        CreateSocketMessage(ptParameters, "Socket bind failed.");

        // Cleanup socket
        close(*ptParameters->m_pnIDReference);
        *ptParameters->m_pnIDReference = ERROR;

        // Not successful
        return FALSE;
    


// Receive socket
if (ptParameters->m_eType == SOCKTYPE_RECEIVE || ptParameters->m_eType == SOCKTYPE_RECEIVE_AND_TRANSMIT)

    // Set the receive buffer size
    nSocketOption = SOCKET_BUFFER_SIZE;
    if (setsockopt(*ptParameters->m_pnIDReference, SOL_SOCKET, SO_RCVBUF, (char *)&nSocketOption, sizeof(nSocketOption)) == ERROR)
    
        // Error
        CreateSocketMessage(ptParameters, "Socket buffer size set failed.");

        // Cleanup socket
        close(*ptParameters->m_pnIDReference);
        *ptParameters->m_pnIDReference = ERROR;

        // Not successful
        return FALSE;
    

并且在无限循环中调用的套接字接收: *缓冲区大小肯定足够大

int SocketReceive(int nSocketIndex, char *pBuffer, int nBufferLength)

    int nBytesReceived = 0;
    char szError[256];

    // Invalid index or socket
    if (nSocketIndex < 0 || nSocketIndex >= SOCKET_COUNT || g_pnSocketIDs[nSocketIndex] == 0)
    
        sprintf(szError,"SocketReceive: Invalid socket (%d) or ID (%d)", nSocketIndex, g_pnSocketIDs[nSocketIndex]);
        perror(szError);
        return -1;
    

    // Invalid buffer length
    if (nBufferLength == 0)
    
        perror("SocketReceive: zero buffer length");
        return 0;
    

    // Send data
    nBytesReceived = recv(g_pnSocketIDs[nSocketIndex], pBuffer, nBufferLength, 0);

    // Error in receiving
    if (nBytesReceived == ERROR)
    
        // Create error string
        sprintf(szError, "SocketReceive: Data Receive Failure: <%d> ", errno);

        // Set error message
        perror(szError);

        // Return error
        return ERROR;
    

    // Bytes received
    return nBytesReceived;

任何关于为什么将缓冲区大小增加到 4,800 会导致成功且一致地读取低速消息的线索?

【问题讨论】:

尽管如此,4,799 字节足以预期与 4,800 字节相同的行为。 为什么要这么说? 9,999 字节不足以容纳 10,000 字节的文件。 任何关于为什么将缓冲区大小增加到 4,800 会导致成功且一致地读取低速消息的任何线索? 这不是线索,但我怀疑这是因为 4,800 butes 足够大,而 4,799 则不够大。您可以使用 4,800 字节缓冲区,也可以花费大量时间和精力让 Wind River 告诉您为什么在您的场景中需要使用 4,800 字节缓冲区 - 然后您将使用 4,800 字节的缓冲区。 在setsockopt on vxworks 页面上声明“UDP 保留 41600 字节,足够空间容纳多达 40 个传入数据报(每个 1 KB)。”。您使用的是 1/8.6,因此有足够的空间容纳大约 4 个数据包。我想这一切都取决于 NIC 和驱动程序的实现——它们通常使用“描述符环”,具有许多固定大小的数据包缓冲区来保存大多数数据包。如果这些都是 1kb(并且较大的数据包被分成两个或更多缓冲区),则接收一堆较小的数据包将耗尽数据包缓冲区。 对于实际的套接字接收缓冲区来说,4799 和 4800 都太小了。您至少应该考虑 32-64k。 可能是交换机问题——交换机中的某些东西被配置为避免泛洪 【参考方案1】:

对于为什么SO_RCVBUF 大小为 4799 会导致丢失低速消息而大小为 4800 的问题的基本答案是,随着 UDP 数据包的混合进入,它们的到来速率在,您处理传入数据包的速率,以及 vxWorks 内核中 mbuff 的大小和集群编号允许足够的网络堆栈吞吐量,从而不会丢弃较大大小的低速消息。

上面评论中提到的setsockopt() 手册页中的http://www.vxdev.com/docs/vx55man/vxworks/ref/sockLib.html#setsockopt 中的SO_SNDBUF 选项描述说明了指定的大小和对mbuff 使用的影响:

设置缓冲区最大大小的效果(对于SO_SNDBUF 和 SO_RCVBUF,如下所述)实际上并不是分配 mbufs 来自 mbuf 池。相反,效果是设置高水位线 在协议数据结构中,稍后用于限制 mbuf 分配的数量。

UDP 数据包是离散的单元。如果您发送 10 个大小为 232 的数据包,则在连续内存中不被视为 2320 字节的数据。相反,网络堆栈中有 10 个内存缓冲区,因为 UDP 是离散的数据包,而 TCP 是连续的字节流。

请参阅 DDS 社区网站中的 How do I tune the network buffering in VxWorks 5.4?,其中讨论了 mbuff 大小和网络集群混合的相互依赖关系。

请参阅 DDS 社区网站中的 How do I resolve a problem with VxWorks buffers?。

请参阅此 PDF 幻灯片演示文稿,A New Tool to study Network Stack Exhaustion in VxWorks 从 2004 年开始讨论使用各种工具(例如 mBufShowinetStatShow)来了解网络堆栈中发生的情况。

【讨论】:

【参考方案2】:

如果不详细分析您的 UDP 消息发送路径上的每个网络堆栈实现,几乎不可能说明结果行为。

UDP 实现可以自行决定丢弃任何数据包。通常,当堆栈得出结论认为需要丢弃数据包才能接收新数据包时,就会发生这种情况。没有正式要求丢弃的数据包是最旧的或最新接收的。也可能是由于内部内存管理策略,某个大小类受到的影响更大。

从涉及的 IP 堆栈中,最有趣的是接收机器上的那个。

如果您将接收端更改为有一个接收缓冲区,该缓冲区将在几秒钟内充满预期的消息,您肯定会获得更好的接收体验。我会从至少 10k 开始。

从 4,799 到 4,800 时观察到的行为“变化”可能是由于后者只允许在需要再次删除其中一条小消息之前接收它,而较小的大小只是导致它被稍微删除早些时候。如果接收应用程序足够快地读取待处理消息,您将在一种情况下收到小消息,而在另一种情况下不会收到小消息。

【讨论】:

以上是关于为啥特定的 UDP 消息总是低于特定的缓冲区大小?的主要内容,如果未能解决你的问题,请参考以下文章

到特定远程 IP 的第一条 UDP 消息丢失

为啥 DNS 使用 UDP 作为传输层协议?

为啥有时我们使用特定的位来进行模板测试?

winsocket udp和tcp编程,缓冲区应该设置多大?

JSR 356 WebSocket 最大消息大小配置失败

为啥我们使用指针而不是简单地创建特定大小的数组?