簡體   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