我有以下申请:
application deployment diagram
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
我感谢任何帮助
答案 0 :(得分:2)
这是与任务库相关的非常棘手的问题。简而言之,创建和安排了太多任务,因此MongoDB驱动程序正在等待的任务之一将无法完成。我花了很长时间才意识到,虽然看起来确实不是死锁。
这是复制步骤:
这将在您提到的同一位置失败: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,您将看到任务调度程序几乎同时启动了其中几个任务,但随后速度降低到每秒一左右。因此,这意味着您需要等待300秒钟左右才能解决dns的任务开始运行。这就是为什么它超过30秒超时的原因。
现在,如果您不做疯狂的事情,这是一个快速解决方案:
Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);
这将迫使ThreadPoolScheduler立即启动一个线程,而不是在创建新线程之前等待一秒钟。
但是,如果您正在做像我这样疯狂的事情,这将不起作用。让我们将for循环从300更改为30000,即使此解决方案也可能失败。原因是它创建了太多线程。这是资源和时间的浪费。而且它可能会开始启动GC流程。总之,在时间用完之前,它可能无法完成所有这些线程的创建。
理想的方法是停止创建许多任务,并使用默认的调度程序来调度它们。您可以尝试创建工作项目并将其放入ConcurrentQueue,然后创建多个线程作为工作人员来使用这些项目。
但是,如果您不想太多改变原始结构,可以尝试以下方法:
创建一个从TaskScheduler派生的ThrottledTaskScheduler。
·
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。
对我有用。祝你好运!
P.S .: 计时器版本中有很多任务的原因可能是在TaskScheduler.TryExecuteTaskInline内部。如果它在创建ThreadPool的主线程中,则可以执行某些任务而无需将其放入队列。