ZeroMQ经销商 - 与winsock相比,经销商的高延迟

时间:2014-10-07 12:01:37

标签: c++ visual-c++ zeromq winsock low-latency

我的公司正在研究使用ZeroMQ作为传输机制。首先,我对性能进行了基准测试,以便预测我正在玩的东西。

所以我创建了一个应用程序,将zmq经销商与经销商的设置与winsock进行比较。我确保了从客户端向服务器发送同步消息然后计算平均值的往返时间。

这里服务器运行winsock:

DWORD RunServerWINSOCKTest(DWORD dwPort)
{
    WSADATA wsaData;
    int iRet = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iRet != NO_ERROR)
    {
        printf("WSAStartup failed with error: %d\n", iRet);
        return iRet;
    }

    struct addrinfo hints;
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;

    struct addrinfo *result = NULL;
    iRet = getaddrinfo(NULL, std::to_string(dwPort).c_str(), &hints, &result);
    if (iRet != 0)
    {
        WSACleanup();
        return iRet;
    }

    SOCKET ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET)
    {
        freeaddrinfo(result);
        WSACleanup();
        return WSAGetLastError();
    }

    iRet = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iRet == SOCKET_ERROR)
    {
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        return WSAGetLastError();
    }

    freeaddrinfo(result);
    iRet = listen(ListenSocket, SOMAXCONN);
    if (iRet == SOCKET_ERROR)
    {
        closesocket(ListenSocket);
        WSACleanup();
        return WSAGetLastError();
    }

    while (true)
    {
        SOCKET ClientSocket = accept(ListenSocket, NULL, NULL);
        if (ClientSocket == INVALID_SOCKET)
        {
            closesocket(ListenSocket);
            WSACleanup();
            return WSAGetLastError();
        }
        char value = 0;
        setsockopt(ClientSocket, IPPROTO_TCP, TCP_NODELAY, &value, sizeof(value));

        char recvbuf[DEFAULT_BUFLEN];
        int recvbuflen = DEFAULT_BUFLEN;
        do {

            iRet = recv(ClientSocket, recvbuf, recvbuflen, 0);
            if (iRet > 0) {
            // Echo the buffer back to the sender
                int iSendResult = send(ClientSocket, recvbuf, iRet, 0);
                if (iSendResult == SOCKET_ERROR)
                {
                    closesocket(ClientSocket);
                    WSACleanup();
                    return WSAGetLastError();
                }
            }
            else if (iRet == 0)
                printf("Connection closing...\n");
            else  {
                closesocket(ClientSocket);
                WSACleanup();
                return 1;
            }

        } while (iRet > 0);

        iRet = shutdown(ClientSocket, SD_SEND);
        if (iRet == SOCKET_ERROR)
        {
            closesocket(ClientSocket);
            WSACleanup();
            return WSAGetLastError();
        }
        closesocket(ClientSocket);
    }
    closesocket(ListenSocket);

    return WSACleanup();
}

这是运行winsock的客户端:

DWORD RunClientWINSOCKTest(std::string strAddress, DWORD dwPort, DWORD dwMessageSize)
{
    WSADATA wsaData;
    int iRet = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iRet != NO_ERROR)
    {
        return iRet;
    }

    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo *result = NULL,  *ptr = NULL, hints;


    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    int iResult = getaddrinfo(strAddress.c_str(), std::to_string(dwPort).c_str(), &hints, &result);
    if (iResult != 0) {
        WSACleanup();
        return 1;
    }

    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            WSACleanup();
            return 1;
        }

        iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        WSACleanup();
        return 1;
    }


    // Statistics
    UINT64 uint64BytesTransmitted = 0;
    UINT64 uint64StartTime = s_TimeStampGenerator.GetHighResolutionTimeStamp();
    UINT64 uint64WaitForResponse = 0;

    DWORD dwMessageCount = 1000000;

    CHAR cRecvMsg[DEFAULT_BUFLEN];
    SecureZeroMemory(&cRecvMsg, DEFAULT_BUFLEN);

    std::string strSendMsg(dwMessageSize, 'X');

    for (DWORD dwI = 0; dwI < dwMessageCount; dwI++)
    {
        int iRet = send(ConnectSocket, strSendMsg.data(), strSendMsg.size(), 0);
        if (iRet == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }
        uint64BytesTransmitted += strSendMsg.size();

        UINT64 uint64BeforeRespone = s_TimeStampGenerator.GetHighResolutionTimeStamp();
        iRet = recv(ConnectSocket, cRecvMsg, DEFAULT_BUFLEN, 0);
        if (iRet < 1)
        {
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }
        std::string strMessage(cRecvMsg);

        if (strMessage.compare(strSendMsg) == 0)
        {
            uint64WaitForResponse += (s_TimeStampGenerator.GetHighResolutionTimeStamp() - uint64BeforeRespone);
        }
        else
        {
            return NO_ERROR;
        }
}

    UINT64 uint64ElapsedTime = s_TimeStampGenerator.GetHighResolutionTimeStamp() - uint64StartTime;
    PrintResult(uint64ElapsedTime, uint64WaitForResponse, dwMessageCount, uint64BytesTransmitted, dwMessageSize);

    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }
    closesocket(ConnectSocket);
    return WSACleanup();
}

这是运行ZMQ(经销商)的服务器

DWORD RunServerZMQTest(DWORD dwPort)
{
    try
    {
        zmq::context_t context(1);
        zmq::socket_t server(context, ZMQ_DEALER);

        // Set options here
        std::string strIdentity = s_set_id(server);
        printf("Created server connection with ID: %s\n", strIdentity.c_str());

        std::string strConnect = "tcp://*:" + std::to_string(dwPort);
        server.bind(strConnect.c_str());

        bool bRunning = true;
        while (bRunning)
        {
            std::string strMessage = s_recv(server);

            if (!s_send(server, strMessage))
            {
                return NO_ERROR;
            }
        }
    }
    catch (zmq::error_t& e)
    {
        return (DWORD)e.num();
    }

return NO_ERROR;

}

这是运行ZMQ(经销商)的客户

DWORD RunClientZMQTest(std::string strAddress, DWORD dwPort, DWORD dwMessageSize)
{
    try
    {
        zmq::context_t ctx(1);
        zmq::socket_t client(ctx, ZMQ_DEALER); // ZMQ_REQ

        // Set options here
        std::string strIdentity = s_set_id(client);

        std::string strConnect = "tcp://" + strAddress + ":" + std::to_string(dwPort);
        client.connect(strConnect.c_str());

        if(s_send(client, "INIT"))
        {
            std::string strMessage = s_recv(client);
            if (strMessage.compare("INIT") == 0)
            {
                printf("Client[%s] connected to: %s\n", strIdentity.c_str(), strConnect.c_str());
            }
            else
            {
                return NO_ERROR;
            }
        }
        else
        {
            return NO_ERROR;
        }


        // Statistics
        UINT64 uint64BytesTransmitted   = 0;
        UINT64 uint64StartTime          = s_TimeStampGenerator.GetHighResolutionTimeStamp();
        UINT64 uint64WaitForResponse    = 0;

        DWORD dwMessageCount = 10000000;


        std::string strSendMsg(dwMessageSize, 'X');
        for (DWORD dwI = 0; dwI < dwMessageCount; dwI++)
        {
            if (s_send(client, strSendMsg))
            {
                uint64BytesTransmitted += strSendMsg.size();

                UINT64 uint64BeforeRespone = s_TimeStampGenerator.GetHighResolutionTimeStamp();
                std::string strRecvMsg = s_recv(client);
                if (strRecvMsg.compare(strSendMsg) == 0)
                {
                    uint64WaitForResponse += (s_TimeStampGenerator.GetHighResolutionTimeStamp() - uint64BeforeRespone);
                }
                else
                {
                    return NO_ERROR;
                }
            }
            else
            {
                return NO_ERROR;
            }
        }
        UINT64 uint64ElapsedTime = s_TimeStampGenerator.GetHighResolutionTimeStamp() - uint64StartTime;
        PrintResult(uint64ElapsedTime, uint64WaitForResponse, dwMessageCount, uint64BytesTransmitted, dwMessageSize);
    }
    catch (zmq::error_t& e)
    {
        return (DWORD)e.num();
    }

    return NO_ERROR;
    }

我在本地运行基准测试,消息大小为5个字节,我得到以下结果:

WINSOCK

Messages sent:                 1 000 000
Time elapsed (us):            48 019 415
Time elapsed (s):                     48.019 415
Message size (bytes):                  5
Msg/s:                            20 825
Bytes/s:                         104 125
Mb/s:                                  0.099
Total   response time (us):   24 537 376
Average repsonse time (us):           24.0

ZeroMQ

Messages sent:                 1 000 000
Time elapsed (us):           158 290 708
Time elapsed (s):                    158.290 708    
Message size (bytes):                  5
Msg/s:                             6 317
Bytes/s:                          31 587
Mb/s:                                  0.030
Total   response time (us):  125 524 178    
Average response time (us):          125.0

有人可以解释为什么使用ZMQ时平均响应时间会高得多吗?

目标是找到一个设置,我可以异步发送和接收消息,而无需回复。如果这可以通过与经销商 - 经销商不同的设置来实现,请告诉我!

3 个答案:

答案 0 :(得分:4)

您说您想要异步发送和接收消息而无需回复。然而到目前为止所做的测试都是完全同步的,基本上是请求 - 回复,但是在经销商 - 经销商插座上。那里没有东西计算。为什么不运行更接近你所针对的设计的测试呢?

ZeroMQ比TCP&#34;更快地得到了相当数量的#34;通过将排队的消息聚合到单个消息中来实现性能。显然,该机制无法在纯粹的同步设计中激活,一次只有一条消息在飞行中。

至于为什么这个特殊的测试,即纯粹同步发送和接收的非常小的消息,相对较慢,我不能说。你做过剖析了吗?我要再说一遍的是,如果它看起来不像你的最终设计,那么运行这个测试并根据它做出决定是没有意义的。

看起来奇怪的一件事是ZeroMQ代码中的try / catch块。这看起来并不公平,因为winsock测试并没有这样写。众所周知,try / catch中存在相当大的开销。

答案 1 :(得分:4)

这只是对你问题的一小部分的回答,但这里有 -

为什么需要经销商/经销商?我假设因为沟通可以从任何一点发起?您并非绑定给经销商/经销商,特别是它限制您只有两个端点,如果您在通信的任何一侧添加另一个端点,比如第二个客户端,则每个客户只收到一半的邮件,因为经销商是严格循环的。

异步通信所需要的是经销商和/或路由器插座的某种组合。两者都不需要响应,主要区别在于他们如何选择向哪个连接的对等方发送消息:

  • 经销商,如上所述,严格循环,它会发送给每个连接的同行串联
  • 路由器严格来说是一个已被寻址的消息,您必须知道&#34; name&#34;您要发送到的同行以获取该消息。

这两种插座类型协同工作,因为经销商套接字(以及请求套接字,经销商是一个&#34;请求类型&#34;套接字)发送他们的&#34;名称&#34;作为消息的一部分,路由器套接字可以用来发回数据。这是一个请求/回复范例,你会在the guide中的所有示例中看到这种范式,但你可以将这种范式转变为你正在寻找的范例,特别是经销商和路由器都不需要回复。

在不了解您的全部要求的情况下,我无法告诉您我会选择哪种ZMQ架构,但总的来说我更喜欢路由器套接字的可扩展性,它比处理它更容易处理适当的寻址将所有东西都塞进一个同行......你会看到警告不要做路由器/路由器,我同意他们的意见,你应该在尝试之前了解你做了什么,但要了解你和#39;重新开始,实施并不难。


如果符合您的要求,您还可以选择使用pub套接字设置每个端口,并且每个端口都有一个子套接字,如果没有任何回复永远。如果它严格来说是从源到目标的数据馈送,并且两个对等体都不需要对它发送的内容有任何反馈,那么这可能是最好的选择,即使这意味着你每个端口都要处理两个套接字而不是而不是一个。


这些都没有直接解决性能问题,但重要的是要理解zmq套接字针对特定用例进行了优化,正如John Jefferies所指出的那样。回答,您通过严格同步测试中的消息来打破经销商套接字的用例。首先要做的是确定你的ZMQ架构,然后模拟一个实际的消息流,特别是添加任意等待和同步,这将必然改变方式吞吐量看起来就像你正在测试它一样,几乎就是定义。

答案 2 :(得分:2)

OP问题是吞吐量问题,而不是延迟问题,可能是所提供示例中使用的模式问题。但是,您可能总是发现ZeroMQ具有更高的延迟,我会解释,尽管在这种情况下它可能对OP没有用。

ZeroMQ通过缓冲消息来工作。想象一下(仅作为一个基本的例子)创建 std::string 并为其添加许多小字符串(数千个,每个包含一个小标题以了解这些小段的大小)然后发送这个较大的字符串,间隔为 100us 1000us 10ms 等等。在接收方,接收大字符串,并根据与其一起发送的大小标题一次删除一个较小的消息。这允许您潜在地批量发送数百万条消息(尽管std::string显然是一个糟糕的选择)而没有一次发送数百万个非常小的测量的开销。因此,您可以充分利用网络资源并提高吞吐量,还可以创建基本的FIFO行为。但是,您还会创建一个延迟以允许缓冲区填充,这意味着延迟会增加。

想象一下(再次,只是作为一个基本的例子):如果你花费半秒钟(包括字符串操作等)缓冲一百万条消息,这将导致一个更大的几兆字节的字符串。现代网络可以在剩下的半秒内轻松发送这个更大的字符串。每封邮件1000000us(1秒)/ 1000000条消息 1us ,对吗?错误 - 所有消息都有半秒延迟以允许队列填充,导致所有消息的延迟增加长达半秒。 ZeroMQ发送批次的速度比500ms每个ZeroMQ快得多,但这说明的延迟时间的增加仍然发生在ms,尽管它通常只有几个df = pd.read_csv(file, skiprows=[1,2], index_col=False)