简体   繁体   English

C#控制台应用程序中HttpWebRequest的奇怪行为

[英]Strange behavior with HttpWebRequest in C# console application

I have a simple console application that sends a number of POST requests to a server and prints out the average successful requests per second. 我有一个简单的控制台应用程序,该应用程序将许多POST请求发送到服务器,并打印出每秒成功的平均请求数。 The application makes use of the HttpWebRequest class to do the actual message sending. 该应用程序利用HttpWebRequest类进行实际的消息发送。 It is multi-threaded and leverages the C# ThreadPool. 它是多线程的,并利用C#ThreadPool。

I have tested this application on a server that immediately sends a response back and one that actually does some work. 我已经在服务器上测试了该应用程序,该服务器可以立即发送回响应,并且该服务器实际上可以完成一些工作。 In the former scenario, a single instance of my application is able to achieve around 30k messages a second. 在前一种情况下,我的应用程序的单个实例每秒能够实现约30k消息。 In the later scenario around 12k messages a second. 在后面的场景中,每秒大约有12k条消息。

The strange behavior I am observing is that if I run multiple instances (2-4) of my application I achieve a greater accumulative throughput than if I were to run a single instance. 我观察到的奇怪行为是,如果我运行应用程序的多个实例(2-4个),则比起运行单个实例,可以获得更大的累积吞吐量。 Thus for the two scenarios described above I achieve an accumulated throughput of 40k and 20k respectively. 因此,对于上述两种情况,我分别达到40k和20k的累积吞吐量。

What I cannot figure out is why a single instance of my application cannot achieve the 20k throughput in the second scenario if a single instance of my application has been proven to achieve a throughput of 30k in the first scenario. 我无法弄清楚的是,如果事实证明我的应用程序的单个实例在第一种情况下可达到30k的吞吐量,为什么在第二种情况下我的应用程序的单个实例无法达到20k的吞吐量。

I have played around with the maxconnection parameter as well as the various threadpool parameters eg minthreads / maxthreads. 我玩过maxconnection参数以及各种线程池参数,例如minthreads / maxthreads。 I have also tried altering the implementation eg Parallel.For / Task.Run / ThreadPool.QueueUserWorkItem. 我也尝试过更改实现,例如Parallel.For / Task.Run / ThreadPool.QueueUserWorkItem。 I have even tested an asynchronous paradigm. 我什至已经测试了异步范例。 Unfortunately, all versions exhibit the same behavior. 不幸的是,所有版本都表现出相同的行为。

Any idea what could be going on? 知道会发生什么吗?

EDIT 编辑

The main loop looks as follows: 主循环如下所示:

Task.Run(() =>
            {
                while (true)
                {
                    _throttle.WaitOne();
                    ThreadPool.QueueUserWorkItem(SendStressMessage);
                }
            });

Ripping out the unecessary bits of SendStressMessage, looks like this: 剥去SendStressMessage不必要的部分,如下所示:

private static void SendStressMessage(Object state)
        {

            var message = _sampleMessages.ElementAt(_random.Next(_sampleMessages.Count));

            if (SendMessage(_stressuri, message))
            {
                Interlocked.Increment(ref _successfulMessages);
                _sucessfulMessageCounter.Increment();
                _QPSCounter.Increment();
            }
            else
            {
                Interlocked.Increment(ref _failedMessages);
                _failedMessageCounter.Increment();
            }
            // Check time of last QPS read out
            if (DateTime.UtcNow.Subtract(_lastPrint).TotalSeconds > _printDelay)
            {
                lock (_lock)
                {
                    // Check one last time while holding the lock
                    if (DateTime.UtcNow.Subtract(_lastPrint).TotalSeconds > _printDelay)
                    {
                        // Print current QPS and update last print / successful message count
                        Console.WriteLine("Current QPS: " + (int)((Thread.VolatileRead(ref _successfulMessages) - _lastSuccessfulMessages) / DateTime.UtcNow.Subtract(_lastPrint).TotalSeconds));
                        _lastSuccessfulMessages = _successfulMessages;
                        _lastPrint = DateTime.UtcNow;
                    }
                }
            }

            _throttle.Release();
        }

Finally, the sendmessage method looks as follows: 最后,sendmessage方法如下所示:

private static bool SendMessage(Uri uri, byte[] message)
    {

        HttpWebRequest request = null;
        HttpWebResponse response = null;

        try
        {
            request = WebRequest.CreateHttp(uri);
            request.Method = "POST";
            request.KeepAlive = true;
            request.Proxy = null;
            request.ContentLength = message.Length;

            // Write to request body
            using (Stream requestStream = request.GetRequestStream())
            {
                requestStream.Write(message, 0, message.Length);
            }

            // Try posting message
            response = (HttpWebResponse)request.GetResponse();

            // Check response status and increment accordingly
            if (response.StatusCode == HttpStatusCode.OK)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        catch
        {
            return false;
        }
        finally
        {
            // Dispose of response
            if (response != null)
            {
                response.Close();
                response.Dispose();
            }
        }
    }

If you notice above, there is a lock. 如果您在上面注意到,那儿有一把锁。 I have tried removing the lock and this does not affect performance. 我尝试删除锁定,但这不会影响性能。

UPDATE 更新

As stated above, I initially experienced poor performance when I rewrote the application to be asynchronous. 如上所述,当我将应用程序重写为异步时,最初遇到的性能很差。 I had not realized that the implementation was incorrect as I was in fact blocking on an IO call. 我没有意识到实现是不正确的,因为我实际上在阻塞IO调用。 As pointed out by @David d C e Freitas, there is an async version of GetRequestStream which I was not utilizing. 正如@David d C e Freitas指出的那样,我没有使用GetRequestStream的异步版本。 Finally, my initial implementation did not throttle the number of pending messages. 最后,我的初始实现并没有限制待处理消息的数量。 This greatly reduced performance as too many async handles were being created. 由于创建了太多异步句柄,因此大大降低了性能。

An outline of the final solution which does not exhibit the issues described above follows: 没有上述问题的最终解决方案的概要如下:

The main loop: 主循环:

Task.Run(async () =>
        {
            while (true)
            {
                await _throttle.WaitAsync();
                SendStressMessageAsync();
            }
        });

A stripped down version of SendStressMessageAsync() follows: 以下是SendStressMessageAsync()的精简版本:

private static async void SendStressMessageAsync()
    {

        var message = _sampleMessages.ElementAt(_random.Next(_sampleMessages.Count));

        if (await SendMessageAsync(_stressuri, message))
        {
            Interlocked.Increment(ref _successfulMessages);
            _sucessfulMessageCounter.Increment();
            _QPSCounter.Increment();
        }
        else
        {
            //Failed request - increment failed count
            Interlocked.Increment(ref _failedMessages);
            _failedMessageCounter.Increment();
        }
        // Check time of last QPS read out
        if (DateTime.UtcNow.Subtract(_lastPrint).TotalSeconds > _printDelay)
        {
            await _printlock.WaitAsync();
            // Check one last time while holding the lock
            if (DateTime.UtcNow.Subtract(_lastPrint).TotalSeconds > _printDelay)
            {
                // Print current QPS and update last print / successful message count
                Console.WriteLine("Current QPS: " + (int)(Interlocked.Read(ref _successfulMessages) / DateTime.UtcNow.Subtract(_startTime).TotalSeconds));
                _lastPrint = DateTime.UtcNow;
            }
            _printlock.Release();
        }

        _throttle.Release();
    }

Finally the async send message looks as follows: 最后,异步发送消息如下所示:

private static async Task<bool> SendMessageAsync(Uri uri, byte[] message)
    {

        HttpWebRequest request = null;
        HttpWebResponse response = null;

        try
        {
            // Create POST request to provided VIP / Port
            request = WebRequest.CreateHttp(uri);
            request.Method = "POST";
            request.KeepAlive = true;
            request.Proxy = null;
            request.ContentLength = message.Length;

            // Write to request body
            using (Stream requestStream = await request.GetRequestStreamAsync())
            {
                requestStream.Write(message, 0, message.Length);
            }

            // Try posting message
            response = (HttpWebResponse)await request.GetResponseAsync();

            // Check response status and increment accordingly
            if (response.StatusCode == HttpStatusCode.OK)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        catch
        {
            //Failed request - increment failed count
            return false;
        }
        finally
        {
            // Dispose of response
            if (response != null)
            {
                response.Close();
                response.Dispose();
            }
        }
    }

My best guess is you are simply not using your resources enough, until you run several instances. 我最好的猜测是,您只是在运行多个实例之前没有充分利用资源。 Using parallel or async doesn't magically speed up your application, you need to place them at the right place where there's a bottleneck. 使用并行或异步并不能神奇地加快您的应用程序的速度,您需要将它们放置在存在瓶颈的正确位置。

The specific problem is you probably used parallel, but you needed a bigger degree of parallelism because the nature of your operation isn't CPU-Intensive but I/O-Intensive. 特定的问题是您可能使用了并行,但是您需要更大程度的并行性,因为操作的本质不是CPU密集型的,而是I / O密集型的。 You can use both parallel and async together to fully utilize your resources. 您可以同时使用并行和异步来充分利用您的资源。

That's all I can do without any specific code. 这就是我不需要任何特定代码即可完成的全部工作。

The .NET framework limits the number of connections made to a particular host, using an application configuration item maxconnections . .NET框架使用应用程序配置项maxconnections限制与特定主机的连接数。 By default this limit is 2 connections per host. 默认情况下,此限制是每个主机2个连接。

What this means is that with the default settings only 2 requests can be active at any time. 这意味着在默认设置下,任何时候都只能激活2个请求。 If you initiate 10 requests at once only 2 of them will connect immediately. 如果您一次启动10个请求,则其中只有2个将立即连接。 The rest will spin until one of the available slots becomes free. 其余的将旋转,直到可用插槽之一可用。

Try modifying your App.config to include the following: 尝试修改您的App.config以包括以下内容:

<configuration>
    <system.net>
        <connectionManagement>
            <add address = "*" maxconnection = "10" />
        </connectionManagement>
    </system.net>
</configuration>

This will allow 10 connections per host, which should greatly improve your throughput. 这将允许每个主机10个连接,这将大大提高吞吐量。

It seems your question is: Why does a request that does nothing return up to 30kmsg/s and a request that does some work returns 12kmsg/s? 您的问题似乎是:为什么什么都不做的请求返回到30kmsg / s的速度,而有些工作的请求返回到12kmsg / s的速度呢?

Can you see how many requests are in a waiting state (in-flight) on your test application? 您可以在测试应用程序上看到有多少个请求处于等待状态(进行中)吗? ie how many have opened the socket and fully written the request out but haven't started receiving some data back. 即有多少人打开了套接字并完全写出了请求,但是还没有开始接收一些数据。 Perhaps your test application can only handle so many "in-progress" requests at a time before it slows down the pending queue. 也许您的测试应用程序一次只能处理如此多的“正在进行中”的请求,然后才减慢挂起的队列的速度。

So this question is about the server and not your test client application, as the server is what is determining how many requests it can handle per second more likely than your test application. 所以这个问题是关于服务器而不是您的测试客户端应用程序的,因为服务器决定了它每秒比测试应用程序更有可能处理多少个请求。 What is the server written in, what have you tweaked there? 服务器写的是什么,您在那里做了什么调整?

Update: 更新:

One thing to consider is that there are many synchronous operations happening behind the scenes that you aren't handling: 要考虑的一件事是,在您未处理的幕后发生了许多同步操作:

  1. Sync DNS lookup for the address. 同步DNS查找地址。 (Try using the IP) (尝试使用IP)
  2. The TCP connection setup/startup time (opening a connection). TCP连接建立/启动时间(打开连接)。
    • GetRequestStream can be done async, ie BeginGetRequestStream 可以异步完成GetRequestStream,即BeginGetRequestStream
    • GetResponse can be done async, ie BeginGetResponse or GetResponseAsync 可以异步完成GetResponse,即BeginGetResponse或GetResponseAsync

Blocking on any of these actions will limit your throughput in the single threaded instance. 阻止任何这些操作将限制您在单线程实例中的吞吐量。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM