使用循环发送的大批量HttpClient异步请求未完成

6

我认为我已经成功地制作了一个测试,可以重复地显示这个问题,至少在我的系统上是这样的。 这个问题与使用 HttpClient 访问错误的端点(不存在的端点,目标宕机)有关。

问题在于完成任务的数量总是比总数少,通常少了几个。我不介意请求无法工作,但这会导致应用程序在等待结果时挂起。

我从下面的测试代码中获得以下结果:

已用时间:237.2009884 秒。 批处理数组中的任务数量:8000 完成的任务数:7993

如果我将批处理大小设置为 8 而不是 8000,则会完成。对于 8000,它在 WhenAll 上卡住了。

我想知道其他人是否得到了相同的结果,我是否做错了什么,以及这是否似乎是一个错误。

using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace CustomArrayTesting
{

    /// <summary>
    /// Problem: a large batch of async http requests is done in a loop using HttpClient, and a few of them never complete
    /// </summary>
    class ProgramTestHttpClient
    {
        static readonly int batchSize = 8000; //large batch size brings about the problem

        static readonly Uri Target = new Uri("http://localhost:8080/BadAddress");

        static TimeSpan httpClientTimeout = TimeSpan.FromSeconds(3);  // short Timeout seems to bring about the problem.

        /// <summary>
        /// Sends off a bunch of async httpRequests using a loop, and then waits for the batch of requests to finish.
        /// I installed asp.net web api client libraries Nuget package.
        /// </summary>
        static void Main(String[] args)
        {
            httpClient.Timeout = httpClientTimeout; 

            stopWatch = new Stopwatch();
            stopWatch.Start();


            // this timer updates the screen with the number of completed tasks in the batch (See timerAction method bellow Main)
            TimerCallback _timerAction = timerAction;
            TimerCallback _resetTimer = ResetTimer;
            TimerCallback _timerCallback = _timerAction + _resetTimer;

            timer = new Timer(_timerCallback, null, TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
            //

            for (int i = 0; i < batchSize; i++)
            {
                Task<HttpResponseMessage> _response = httpClient.PostAsJsonAsync<Object>(Target, new Object());//WatchRequestBody()

                Batch[i] = _response;
            }

            try
            {
                Task.WhenAll(Batch).Wait();
            }
            catch (Exception ex)
            {

            }

            timer.Dispose();
            timerAction(null);
            stopWatch.Stop();


            Console.WriteLine("Done");
            Console.ReadLine();
        }

        static readonly TimeSpan timerRepeat = TimeSpan.FromSeconds(1);

        static readonly HttpClient httpClient = new HttpClient();

        static Stopwatch stopWatch;

        static System.Threading.Timer timer;

        static readonly Task[] Batch = new Task[batchSize];

        static void timerAction(Object state)
        {
            Console.Clear();
            Console.WriteLine("Elapsed: {0} seconds.", stopWatch.Elapsed.TotalSeconds);
            var _tasks = from _task in Batch where _task != null select _task;
            int _tasksCount = _tasks.Count();

            var _completedTasks = from __task in _tasks where __task.IsCompleted select __task;
            int _completedTasksCount = _completedTasks.Count();

            Console.WriteLine("Tasks in batch array: {0}       Completed Tasks : {1} ", _tasksCount, _completedTasksCount);

        }

        static void ResetTimer(Object state)
        {
            timer.Change(timerRepeat, Timeout.InfiniteTimeSpan);
        }
    }
}

有时它只是在完成之前崩溃并出现“访问冲突未处理异常”。调用堆栈只显示:
>   mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode = 1225, uint numBytes = 0, System.Threading.NativeOverlapped* pOVERLAP = 0x08b38b98) 
    [Native to Managed Transition]  
    kernel32.dll!@BaseThreadInitThunk@12()  
    ntdll.dll!___RtlUserThreadStart@8()     
    ntdll.dll!__RtlUserThreadStart@8()  

大多数情况下,它不会崩溃,只是永远等待whenall。无论如何,对于每个请求,都会出现以下第一次机会异常:

A first chance exception of type 'System.Net.Sockets.SocketException' occurred in System.dll
A first chance exception of type 'System.Net.WebException' occurred in System.dll
A first chance exception of type 'System.AggregateException' occurred in mscorlib.dll
A first chance exception of type 'System.ObjectDisposedException' occurred in System.dll

我调试程序时遇到了对象已释放异常,得到了以下的调用栈信息:
>   System.dll!System.Net.Sockets.NetworkStream.UnsafeBeginWrite(byte[] buffer, int offset, int size, System.AsyncCallback callback, object state) + 0x136 bytes    
    System.dll!System.Net.PooledStream.UnsafeBeginWrite(byte[] buffer, int offset, int size, System.AsyncCallback callback, object state) + 0x19 bytes  
    System.dll!System.Net.ConnectStream.WriteHeaders(bool async = true) + 0x105 bytes   
    System.dll!System.Net.HttpWebRequest.EndSubmitRequest() + 0x8a bytes    
    System.dll!System.Net.HttpWebRequest.SetRequestSubmitDone(System.Net.ConnectStream submitStream) + 0x11d bytes  
    System.dll!System.Net.Connection.CompleteConnection(bool async, System.Net.HttpWebRequest request = {System.Net.HttpWebRequest}) + 0x16c bytes  
    System.dll!System.Net.Connection.CompleteConnectionWrapper(object request, object state) + 0x4e bytes   
    System.dll!System.Net.PooledStream.ConnectionCallback(object owningObject, System.Exception e, System.Net.Sockets.Socket socket, System.Net.IPAddress address) + 0xf0 bytes 
    System.dll!System.Net.ServicePoint.ConnectSocketCallback(System.IAsyncResult asyncResult) + 0xe6 bytes  
    System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken) + 0x65 bytes    
    System.dll!System.Net.ContextAwareResult.Complete(System.IntPtr userToken) + 0x92 bytes 
    System.dll!System.Net.LazyAsyncResult.ProtectedInvokeCallback(object result, System.IntPtr userToken) + 0xa6 bytes  
    System.dll!System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped) + 0x98 bytes 
    mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) + 0x6e bytes    
    [Native to Managed Transition]

异常信息是:
{"Cannot access a disposed object.\r\nObject name: 'System.Net.Sockets.NetworkStream'."}    System.Exception {System.ObjectDisposedException}

注意与我很少遇到的未处理访问冲突异常之间的关系。

因此,似乎 HttpClient 在目标不可用时不够健壮。顺便说一下,这是在 Windows 7 32 上进行的。


1
这在我的机器上运行需要相当长的时间(892秒),但所有8000个任务都完成了。也许你的临时端口不足了? - Stephen Cleary
感谢您运行它。也许那与此有关。弄清楚可能超出了我的能力范围。重启后我会再次运行它。 - Elliot
大批量的问题仍然存在。我使用了来自Technet的脚本检查了短暂端口情况,看起来很好,没有使用太多端口。有时会出现未处理的错误。我正在更新问题并提供更多信息。 - Elliot
你到底为什么想要运行8000个并行任务? - Aaronaught
1
8000个任务不算什么;我测试了能够处理数十万个(无实际操作)任务的任务并行库,它在不占用过多内存的情况下完美运行。对于网络请求,如果每个请求需要1秒钟,我可能希望并行地进行8000个请求,以避免花费8000秒的时间。 - Elliot
显示剩余2条评论
3个回答

3
我使用反射查看了HttpClient的源代码。就同步执行的部分(启动时)而言,据我观察,似乎没有对返回的任务应用任何超时。有一些超时实现会调用HttpWebRequest对象上的Abort()方法,但是他们好像在异步函数的这一侧遗漏了任何超时取消或使返回的任务发生故障的操作。或许在回调这一侧有一些解决方法,但有时候回调可能会“消失”,导致返回的Task永远无法完成。
我发布了一个问题,询问如何为任何任务添加超时,一个回答者给出了这个非常好的解决方案(作为扩展方法在此处):
public static Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout)
{
    var delay = task.ContinueWith(t => t.Result
        , new CancellationTokenSource(timeout).Token);
    return Task.WhenAny(task, delay).Unwrap();
}

因此,像这样调用 HttpClient 应该可以避免任何“任务失败”永远不会结束的情况:
Task<HttpResponseMessage> _response = httpClient.PostAsJsonAsync<Object>(Target, new Object()).WithTimeout<HttpResponseMessage>(httpClient.Timeout);

以下是我认为可以减少请求丢失的两个因素: 1. 将超时时间从3秒增加到30秒,使得我在发布这个问题时所写的程序中所有任务都能完成。 2. 增加允许的并发连接数量,例如使用System.Net.ServicePointManager.DefaultConnectionLimit = 100;


2
当我在谷歌上寻找解决类似WCF问题的方法时,我遇到了这个问题。那一系列的异常情况与我看到的完全相同。最终,通过大量的调查,我发现了HttpWebRequest中HttpClient使用的一个bug。HttpWebRequest进入了一个糟糕的状态,并且只发送HTTP标头。然后它等待永远不会被发送的响应。
我已经在Microsoft Connect上提交了一个请求单,可以在此处找到:https://connect.microsoft.com/VisualStudio/feedback/details/1805955/async-post-httpwebrequest-hangs-when-a-socketexception-occurs-during-setsocketoption 具体内容请看请求单,但是需要从HttpWebRequest异步POST到非本地主机。我在Windows 7中的.NET 4.5和4.6上重现了这个问题。在测试中,引发SocketException的失败的SetSocketOption调用仅在Windows 7上失败。
对于我们来说,UseNagleAlgorithm设置导致了SetSocketOption调用,但我们无法避免它,因为WCF关闭UseNagleAlgorithm,你无法停止它。在WCF中,它似乎是一个超时调用。显然,这并不是很好,因为我们要花60秒等待什么都没有。

我有一个类似的错误。我有一个ASP.NET Web API,它以异步方式向另一个服务进行HTTP POST请求。但是偶尔会出现“卡住”的情况,直到我重新启动应用程序池,所有http请求才能正常执行。我使用了await client.PostAsJsonAsync() - Zapnologica

1

你的异常信息可能在WhenAll任务中丢失了。不要使用它,尝试以下方法:

Task aggregateTask = Task.Factory.ContinueWhenAll(
    Batch,
    TaskExtrasExtensions.PropagateExceptions,
    TaskContinuationOptions.ExecuteSynchronously);

aggregateTask.Wait();

这里使用了Parallel Extensions Extras示例代码中的PropagateExceptions扩展方法,以确保批量操作中任务的异常信息不会丢失:
/// <summary>Propagates any exceptions that occurred on the specified tasks.</summary>
/// <param name="tasks">The Task instances whose exceptions are to be propagated.</param>
public static void PropagateExceptions(this Task [] tasks)
{
    if (tasks == null) throw new ArgumentNullException("tasks");
    if (tasks.Any(t => t == null)) throw new ArgumentException("tasks");
    if (tasks.Any(t => !t.IsCompleted)) throw new InvalidOperationException("A task has not completed.");
    Task.WaitAll(tasks);
}

谢谢。另外,我刚刚发现如何停止在第一次异常时,所以我正在更新我的问题。 - Elliot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接