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

Ric*_*des 5 .net c# wcf mongodb mongodb-.net-driver

我有以下应用程序:

应用部署图

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

B - 应用程序 B 是使用 mongodriver 1.11 在 .net 3.5 中编译的 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()
Run Code Online (Sandbox Code Playgroud)

此错误不是恒定的,有时会无缘无故地发生。但是服务 B 继续工作,如果尝试从我的桌面连接到 mongo,我可以做到,那么如果服务器完全可访问,mongo 驱动程序如何引发与连接问题相关的异常?

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

我感谢任何帮助

小智 3

这是一个与任务库相关的非常棘手的问题。简而言之,创建和计划的任务太多,以至于 MongoDB 驱动程序正在等待的任务之一将无法完成。我花了很长时间才意识到这并不是一个僵局,尽管看起来是这样。

\n\n

这是重现的步骤:

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

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

\n\n

如果放置一些断点,您将知道 WaitForDescriptionChangedHelper 创建了一个超时任务。然后,它等待 DescriptionUpdate 任务或超时任务之一完成。然而,DescriptionUpdate 从未发生,但为什么呢?

\n\n

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

\n\n
    protected void UpdateClusterDescription(ClusterDescription newClusterDescription)\n    {\n        ClusterDescription oldClusterDescription = null;\n        TaskCompletionSource<bool> oldDescriptionChangedTaskCompletionSource = null;\n\n        Console.WriteLine($"Before UpdateClusterDescription {_descriptionChangedTaskCompletionSource?.Task.Id}, {_descriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");\n        lock (_descriptionLock)\n        {\n            oldClusterDescription = _description;\n            _description = newClusterDescription;\n\n            oldDescriptionChangedTaskCompletionSource = _descriptionChangedTaskCompletionSource;\n            _descriptionChangedTaskCompletionSource = new TaskCompletionSource<bool>();\n        }\n\n        OnDescriptionChanged(oldClusterDescription, newClusterDescription);\n        Console.WriteLine($"Setting UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");\n        oldDescriptionChangedTaskCompletionSource.TrySetResult(true);\n        Console.WriteLine($"Set UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");\n    }\n\n    private void WaitForDescriptionChanged(IServerSelector selector, ClusterDescription description, Task descriptionChangedTask, TimeSpan timeout, CancellationToken cancellationToken)\n    {\n        using (var helper = new WaitForDescriptionChangedHelper(this, selector, description, descriptionChangedTask, timeout, cancellationToken))\n        {\n            Console.WriteLine($"Waiting {descriptionChangedTask?.Id}, {descriptionChangedTask?.GetHashCode().ToString("F8")}");\n            var index = Task.WaitAny(helper.Tasks);\n            helper.HandleCompletedTask(helper.Tasks[index]);\n        }\n    }\n
Run Code Online (Sandbox Code Playgroud)\n\n

通过添加这些行,您还会发现非定时器版本将更新两次,而定时器版本只会更新一次。第二个来自 ServerMonitor.cs 中的“MonitorServerAsync”。原来,在定时器版本中,MontiorServerAsync被执行了,但是经过ServerMonitor.HeartbeatAsync、BinaryConnection.OpenAsync、BinaryConnection.OpenHelperAsync和TcpStreamFactory.CreateStreamAsync之后,最终到达了TcpStreamFactory.ResolveEndPointsAsync。坏事发生在这里:Dns.GetHostAddressesAsync。这个人永远不会被处决。如果你稍微修改一下代码,将其变成:

\n\n
    var task = Dns.GetHostAddressesAsync(dnsInitial.Host).ConfigureAwait(false);\n\n    return (await task)\n        .Select(x => new IPEndPoint(x, dnsInitial.Port))\n        .OrderBy(x => x, new PreferredAddressFamilyComparer(preferred))\n        .ToArray();\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n
Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

然而,如果你像我一样做了非常疯狂的事情,这将不起作用。让我们将 for 循环从 300 次改为 30000 次,即使这个解决方案也可能会失败。原因是它创建了太多线程。这是耗费资源和时间的。它可能会开始启动 GC 进程。总之,它可能无法在时间耗尽之前完成所有这些线程的创建。

\n\n

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

\n\n

不过,如果你不想过多改变原有的结构,可以尝试以下方式:

\n\n

创建从 TaskScheduler 派生的 ThrottledTaskScheduler。

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

\xc2\xb7

\n\n
var taskScheduler = new ThrottledTaskScheduler(\n    TaskScheduler.Default,\n    128,\n    TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler,\n    logger\n    );\nvar taskFactory = new TaskFactory(taskScheduler);\nfor (var i = 0; i < 30000; i++)\n{\n    tasks.Add(taskFactory.StartNew(DoOneThing))\n}\nTask.WaitAll(tasks.ToArray());\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

这个对我有用。祝你好运!

\n\n

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

\n

  • 嘿,我知道已经过去一年半了,但是有可能建议一种方法来解决这个问题吗?答案仅显示如何重现它。谢谢。 (2认同)
  • 我也想知道答案。 (2认同)