繁体   English   中英

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

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

我有一个简单的控制台应用程序,该应用程序将许多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();
            }
        }
    }

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

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

这就是我不需要任何特定代码即可完成的全部工作。

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

这意味着在默认设置下,任何时候都只能激活2个请求。 如果您一次启动10个请求,则其中只有2个将立即连接。 其余的将旋转,直到可用插槽之一可用。

尝试修改您的App.config以包括以下内容:

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

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

您的问题似乎是:为什么什么都不做的请求返回到30kmsg / s的速度,而有些工作的请求返回到12kmsg / s的速度呢?

您可以在测试应用程序上看到有多少个请求处于等待状态(进行中)吗? 即有多少人打开了套接字并完全写出了请求,但是还没有开始接收一些数据。 也许您的测试应用程序一次只能处理如此多的“正在进行中”的请求,然后才减慢挂起的队列的速度。

所以这个问题是关于服务器而不是您的测试客户端应用程序的,因为服务器决定了它每秒比测试应用程序更有可能处理多少个请求。 服务器写的是什么,您在那里做了什么调整?

更新:

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

  1. 同步DNS查找地址。 (尝试使用IP)
  2. TCP连接建立/启动时间(打开连接)。
    • 可以异步完成GetRequestStream,即BeginGetRequestStream
    • 可以异步完成GetResponse,即BeginGetResponse或GetResponseAsync

阻止任何这些操作将限制您在单线程实例中的吞吐量。

暂无
暂无

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

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