繁体   English   中英

System.TimeoutException:使用CompositeServerSelector选择服务器30000ms后发生超时

[英]System.TimeoutException: A timeout occured after 30000ms selecting a server using CompositeServerSelector

我有以下申请:

应用程序部署图

A - 应用程序A是使用在.net 4.5中编译的c#mongodriver 2.2.4在IIS 7.5中托管的.net wcf服务

B - 应用程序B是使用在.net 3.5中编译的mongodriver 1.11的Windows服务应用程序

两种服务都相似,服务B维护用于遗留系统,服务A正在发展。

两个应用程序都托管在相同的服 (Windows Standard 2008 R2)这些应用程序已经运行了超过1年,但是自2016年6月24日应用程序A(WCF)在打开与Mongo Server的新连接时开始出现奇怪的行为:

 > System.TimeoutException: A timeout occured after 30000ms selecting a > server using CompositeServerSelector{ Selectors = > ReadPreferenceServerSelector{ ReadPreference = { Mode = Primary, > TagSets = [] } }, LatencyLimitingServerSelector{ AllowedLatencyRange = > 00:00:00.0150000 } }. Client view of cluster state is { ClusterId : > "1", ConnectionMode : "ReplicaSet", Type : "ReplicaSet", State : > "Disconnected", Servers : [{ ServerId: "{ ClusterId : 1, EndPoint : > "Unspecified/mongodb-log-act01:27017" }", EndPoint: > "Unspecified/mongodb-log-act01:27017", State: "Disconnected", Type: > "Unknown" }] }. at > MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task > completedTask) at > MongoDB.Driver.Core.Clusters.Cluster.<WaitForDescriptionChangedAsync>d__44.MoveNext() > --- End of stack trace from previous location where exception was thrown --- at > System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at > System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task > task) at > MongoDB.Driver.Core.Clusters.Cluster.<SelectServerAsync>d__37.MoveNext() > --- End of stack trace from previous location where exception was thrown --- at > System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at > System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task > task) at > System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() > at > MongoDB.Driver.Core.Bindings.ReadPreferenceBinding.<GetReadChannelSourceAsync>d__8.MoveNext() > --- End of stack trace from previous location where exception was thrown --- at > System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at > System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task > task) at > System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() > at > MongoDB.Driver.Core.Operations.FindOperation`1.<ExecuteAsync>d__107.MoveNext() > --- End of stack trace from previous location where exception was thrown --- at > System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at > System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task > task) at > MongoDB.Driver.OperationExecutor.<ExecuteReadOperationAsync>d__1`1.MoveNext() > --- End of stack trace from previous location where exception was thrown --- at > System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at > System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task > task) at > MongoDB.Driver.MongoCollectionImpl`1.<ExecuteReadOperationAsync>d__59`1.MoveNext() > --- End of stack trace from previous location where exception was thrown --- at > System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at > System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task > task) at > System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() > at > MongoDB.Driver.IAsyncCursorSourceExtensions.<ToListAsync>d__16`1.MoveNext() > --- End of stack trace from previous location where exception was thrown --- at > System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at > System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task > task) at > Liberty.LogService.Implementation.LogManagerService.<Inicializar>d__0.MoveNext() 

这个错误不是一成不变的,有时没有理由发生。 但是服务B继续工作,如果尝试从我的桌面连接到mongo我可以做到,那么如果服务器完全可访问,mongo驱动程序如何引发与连接问题相关的异常?

最后一次尝试已迁移到最后一个驱动程序版本。 当这个问题开始时,我使用的是驱动程序2.0.1

我感谢任何帮助

这是与任务库相关的非常棘手的问题。 简而言之,创建和调度的任务太多,因此MongoDB的驱动程序正在等待的任务之一无法完成。 我花了很长时间才意识到这不是一个僵局,虽然它看起来像是。

以下是重现的步骤:

  1. 下载MongoDB的CSharp驱动程序的源代码。
  2. 打开该解决方案并在其中创建一个控制台项目并引用该驱动程序项目。
  3. 在Main函数中,创建一个System.Threading.Timer,它将按时调用TestTask。 将计时器设置为立即启动一次。 最后,添加一个Console.Read()。
  4. 在TestTask中,使用for循环通过调用Task.Factory.StartNew(DoOneThing)来创建300个任务。 将所有这些任务添加到列表中并使用Task.WaitAll等待所有这些任务完成。
  5. 在DoOneThing函数中,创建一个MongoClient并进行一些简单的查询。
  6. 现在运行它。

这将在你提到的同一个地方失败: MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

如果你设置了一些断点,你就会知道WaitForDescriptionChangedHelper创建了一个超时任务。 然后,它等待任何一个DescriptionUpdate任务或超时任务完成。 但是,DescriptionUpdate永远不会发生,但为什么呢?

现在,回到我的例子,有一个有趣的部分:我开始了一个计时器。 如果直接调用TestTask,它将运行没有任何问题。 通过将它们与Visual Studio的“任务”窗口进行比较,您会注意到计时器版本将创建比非计时器版本更多的任务。 让我稍后解释这一部分。 还有另一个重要的区别。 您需要在Cluster.cs添加调试行:

    protected void UpdateClusterDescription(ClusterDescription newClusterDescription)
    {
        ClusterDescription oldClusterDescription = null;
        TaskCompletionSource<bool> oldDescriptionChangedTaskCompletionSource = null;

        Console.WriteLine($"Before UpdateClusterDescription {_descriptionChangedTaskCompletionSource?.Task.Id}, {_descriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
        lock (_descriptionLock)
        {
            oldClusterDescription = _description;
            _description = newClusterDescription;

            oldDescriptionChangedTaskCompletionSource = _descriptionChangedTaskCompletionSource;
            _descriptionChangedTaskCompletionSource = new TaskCompletionSource<bool>();
        }

        OnDescriptionChanged(oldClusterDescription, newClusterDescription);
        Console.WriteLine($"Setting UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
        oldDescriptionChangedTaskCompletionSource.TrySetResult(true);
        Console.WriteLine($"Set UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
    }

    private void WaitForDescriptionChanged(IServerSelector selector, ClusterDescription description, Task descriptionChangedTask, TimeSpan timeout, CancellationToken cancellationToken)
    {
        using (var helper = new WaitForDescriptionChangedHelper(this, selector, description, descriptionChangedTask, timeout, cancellationToken))
        {
            Console.WriteLine($"Waiting {descriptionChangedTask?.Id}, {descriptionChangedTask?.GetHashCode().ToString("F8")}");
            var index = Task.WaitAny(helper.Tasks);
            helper.HandleCompletedTask(helper.Tasks[index]);
        }
    }

通过添加这些行,您还会发现非计时器版本将更新两次,但计时器版本将只更新一次。 第二个来自ServerMonitor.cs中的“MonitorServerAsync”。 事实证明,在计时器版本中,MontiorServerAsync已经执行,但在它通过ServerMonitor.HeartbeatAsync,BinaryConnection.OpenAsync,BinaryConnection.OpenHelperAsync和TcpStreamFactory.CreateStreamAsync之后,它最终到达了TcpStreamFactory.ResolveEndPointsAsync。 坏事发生在这里: Dns.GetHostAddressesAsync 这个永远不会被执行。 如果您稍微修改代码并将其转换为:

    var task = Dns.GetHostAddressesAsync(dnsInitial.Host).ConfigureAwait(false);

    return (await task)
        .Select(x => new IPEndPoint(x, dnsInitial.Port))
        .OrderBy(x => x, new PreferredAddressFamilyComparer(preferred))
        .ToArray();

您将能够找到任务ID。 通过查看Visual Studio的“任务”窗口,很明显前面有大约300个任务。 只有几个正在执行但被阻止。 如果在DoOneThing函数中添加Console.Writeline,您将看到任务调度程序几乎同时启动其中几个,但随后,它会减慢到每秒一个左右。 因此,这意味着,在解决dns的任务开始运行之前,您需要等待大约300秒。 这就是它超过30秒超时的原因。

现在,如果您没有做疯狂的事情,这里有一个快速解决方案:

Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);

这将强制ThreadPoolScheduler立即启动一个线程,而不是在创建一个新线程之前等待一秒。

但是,如果你像我一样疯狂的事情,这将无法奏效。 让我们将for循环从300更改为30000,即使此解决方案也可能失败。 原因是它创建了太多线程。 这是资源和时间的消耗。 它可能会开始启动GC流程。 总之,它可能无法在时间用完之前完成所有这些线程的创建。

完美的方法是停止创建大量任务并使用默认调度程序来安排它们。 您可以尝试创建工作项并将其放入ConcurrentQueue,然后创建多个线程作为工作者来使用项目。

但是,如果您不想过多地更改原始结构,可以尝试以下方法:

创建从TaskScheduler派生的ThrottledTaskScheduler。

  1. 此ThrottledTaskScheduler接受TaskScheduler作为将运行实际任务的底层TaskScheduler。
  2. 将任务转储到基础调度程序,但如果超出限制,则将其放入队列中。
  3. 如果任何任务完成,请检查队列并尝试将其转储到限制内的基础调度程序中。
  4. 使用以下代码启动所有这些疯狂的新任务:

·

var taskScheduler = new ThrottledTaskScheduler(
    TaskScheduler.Default,
    128,
    TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler,
    logger
    );
var taskFactory = new TaskFactory(taskScheduler);
for (var i = 0; i < 30000; i++)
{
    tasks.Add(taskFactory.StartNew(DoOneThing))
}
Task.WaitAll(tasks.ToArray());

您可以将System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler作为参考。 它比我们需要的要复杂一点。 这是出于其他目的。 因此,不要担心与ConcurrentExclusiveSchedulerPair类中的函数来回传递的那些部分。 但是,您无法直接使用它,因为它在创建包装任务时未传递TaskCreationOptions.LongRunning。

这个对我有用。 祝好运!

PS:在计时器版本中有很多任务的原因可能是在TaskScheduler.TryExecuteTaskInline中。 如果它位于创建ThreadPool的主线程中,它将能够执行某些任务而不将它们放入队列中。

暂无
暂无

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

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