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

时间:2014-01-22 01:06:13

标签: c# .net multithreading httpwebrequest threadpool

我有一个简单的控制台应用程序,它向服务器发送大量POST请求,并打印出每秒平均成功的请求数。该应用程序使用HttpWebRequest类来执行实际的消息发送。它是多线程的,并利用C#ThreadPool。

我已经在服务器上测试了这个应用程序,该服务器立即发回响应,并且实际上做了一些工作。在前一个场景中,我的应用程序的单个实例每秒能够实现大约30k个消息。在后面的场景中,每秒大约有12k条消息。

我观察到的奇怪行为是,如果我运行我的应用程序的多个实例(2-4),我实现了比运行单个实例更大的累积吞吐量。因此,对于上述两种情况,我分别实现了40k和20k的累积吞吐量。

我无法弄清楚的是,如果我的应用程序的单个实例已被证明在第一个场景中实现了30k的吞吐量,那么我的应用程序的单个实例无法在第二个场景中实现20k吞吐量。

我玩过maxconnection参数以及各种线程池参数,例如minthreads / maxthreads。我也尝试改变实施例如Parallel.For / Task.Run / ThreadPool.QueueUserWorkItem。我甚至测试了一个异步范例。不幸的是,所有版本都表现出相同的行为。

知道会发生什么事吗?

修改

主循环如下所示:

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

删除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();
        }

最后,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();
            }
        }
    }

如果您注意到上述情况,则会有锁定。我试过删除锁,这不会影响性能。

更新

如上所述,当我将应用程序重写为异步时,我最初表现不佳。我没有意识到实现是不正确的,因为我实际上阻止了IO调用。正如@David d C e Freitas指出的那样,我没有使用GetRequestStream的异步版本。最后,我的初始实现没有限制挂起消息的数量。由于创建了太多的异步句柄,这大大降低了性能。

最终解决方案的概要没有表现出上述问题如下:

主循环:

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

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();
    }

最后,异步发送消息如下所示:

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();
            }
        }
    }

3 个答案:

答案 0 :(得分:1)

我最好的猜测是,在您运行多个实例之前,您根本就没有充分利用您的资源。使用并行或异步不会神奇地加速您的应用程序,您需要将它们放置在存在瓶颈的正确位置。

具体问题是您可能使用了并行,但是您需要更大程度的并行性,因为您的操作性质不是CPU密集型,而是I / O密集型。 您可以同时使用parallel和async来充分利用资源。

在没有任何特定代码的情况下,我就能做到这一点。

答案 1 :(得分:1)

.NET框架使用应用程序配置项maxconnections限制对特定主机的连接数。默认情况下,此限制为每个主机2个连接。

这意味着使用默认设置时,任何时候只能激活2个请求。如果您一次发起10个请求,其中只有2个会立即连接。其余的将旋转,直到其中一个可用的插槽空闲。

尝试修改App.config以包含以下内容:

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

这将允许每个主机10个连接,这将大大提高您的吞吐量。

答案 2 :(得分:1)

看来你的问题是: 为什么没有任何操作的请求返回到30kmsg / s并且执行某些操作的请求返回12kmsg / s?

您是否可以在测试应用中看到有多少请求处于等待状态(正在进行中)?即有多少人已打开套接字并完全写出请求但尚未开始接收某些数据。 也许您的测试应用程序在减慢挂起队列之前一次只能处理这么多“正在进行”的请求。

所以这个问题是关于服务器而不是你的测试客户端应用程序,因为服务器正在确定它每秒可以处理的请求数量多于你的测试应用程序。什么是写入的服务器,你在那里调整了什么?

更新

要考虑的一件事是,在您未处理的幕后发生了许多同步操作:

  1. 同步地址的DNS查找。 (尝试使用IP)
  2. TCP连接设置/启动时间(打开连接)。
    • GetRequestStream可以异步完成,即BeginGetRequestStream
    • GetResponse可以异步完成,即BeginGetResponse或GetResponseAsync
  3. 阻止任何这些操作都会限制单线程实例中的吞吐量。