OutOfMemoryException 由 TaskScheduler 在高并发异步 asp.net 核心应用程序中提



在托管在 AWS ECS FARGATE(docker)上的 dotnet core 2.2 REST 服务中,我经常(每 30-60 分钟)有一个实例崩溃并出现System.OutOfMemoryException,即使 ECS 报告的最大内存使用率为 11%(总计 16GB)。崩溃始终来自任务计划程序(下面的堆栈跟踪)。它只发生在生产中。

我正在寻求有关如何解决此问题的建议。(编辑:我不认为这实际上是一个内存不足的问题,除非Thread:StartInternal()可以突然使用16GB的90%,速度比AWS监控工具注册它的速度快

)该应用程序在 Windows 10 上本地运行,我还尝试通过维持 100 个并发请求在单独的 ECS 集群(我们的测试集群)上重现,但没有运气。 服务的一个终结点接收 99%+ 的请求。基本操作为:

  • 尝试使用async/await在MongoDB数据库中查找一些文档(基于输入)
  • 从 WCF 提取数据(同步,见下文)
  • 对于某些结果,使用外部 URL 获取数据(有时很慢),System.New.WebRequest使用async/await
  • 返回结果

WCF 服务称为同步,因为我们使用的是 WCF 之上的客户端库,这不是异步安全的。但是,结果在MemoryCache中存储 1 分钟,并且到期时重新获取使用 AsyncEx.AsyncMonitor 进行保护,因此只允许一个调用方更新缓存,如下所示:

using( await _monitor.EnterAsync( ) )
{
if( !Cache.TryGetValue( "UserLookup", out LookupUsers lookupUsers ) )
{
lookupUsers = await GetCachedUsers( ssoToken );
Cache.Set( "UserLookup", lookupUsers, TimeSpan.FromMinutes( 1 ) );
}
return lookupUsers;
}

GetCachedUsers()这样做:

var users = await Task.Run( ( ) => client.Proxy.ListUsers( new ListUsersInput { } ) );

并且在超时或其他问题的情况下返回默认值。

操作的入口点如下:

[Route( "get-content" )]
[HttpPost]
public async Task<RemoteGetContentResult> GetContent( [FromBody]RemoteGetContentInput input )
{
// input validation
var c = Interlocked.Increment( ref _concurrency );
try
{
// log value of _concurrency
return await _provider.GetContentExAsync( input );
}
finally
{
Interlocked.Decrement( ref _concurrency );
}
}

记录的并发级别通常为 10-30,但可以达到 100(当有许多外部 http 提取时)。

以下是我在 AWS ECS 日志中看到的堆栈跟踪:

2019-07-10T06:22:39.554Z Unhandled Exception: System.Threading.Tasks.TaskSchedulerException: An exception was thrown by a TaskScheduler. ---> System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
2019-07-10T06:22:39.554Z    at System.Threading.Thread.StartInternal()
2019-07-10T06:22:39.554Z    at System.Threading.Tasks.Task.ScheduleAndStart(Boolean needsProtection)
2019-07-10T06:22:39.554Z    --- End of inner exception stack trace ---
2019-07-10T06:22:39.554Z    at System.Threading.Tasks.Task.ScheduleAndStart(Boolean needsProtection)
2019-07-10T06:22:39.554Z    at System.Threading.Tasks.Task.InternalStartNew(Task creatingTask, Delegate action, Object state, CancellationToken cancellationToken, TaskScheduler scheduler, TaskCreationOptions options, InternalTaskOptions internalOptions)
2019-07-10T06:22:39.554Z    at System.Runtime.IOThreadScheduler.ScheduleCallbackHelper(SendOrPostCallback callback, Object state)
2019-07-10T06:22:39.554Z    at System.Runtime.IOThreadScheduler.ScheduleCallbackNoFlow(SendOrPostCallback callback, Object state)
2019-07-10T06:22:39.554Z    at System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter.System.Runtime.CompilerServices.IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox box)
2019-07-10T06:22:39.554Z    at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AwaitUnsafeOnCompleted[TAwaiter,TStateMachine](TAwaiter& awaiter, TStateMachine& stateMachine)
2019-07-10T06:22:39.554Z --- End of stack trace from previous location where exception was thrown ---
2019-07-10T06:22:39.554Z    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
2019-07-10T06:22:39.554Z --- End of stack trace from previous location where exception was thrown ---
2019-07-10T06:22:39.554Z    at System.Threading.ThreadPoolWorkQueue.Dispatch()

更新: 我每 5 秒添加一些关于该过程的额外日志记录。在18:30:16.741Z,它记录了:

2019-07-10T18:30:16.741Z concurrency:   4 proc thread cnt:   29 avail worker threads: 32,766 avail compl port threads:  1,000 ws: 1,733,996,544 peak ws:      0

所以 16GB 中的 ~1.7GB 工作集。(由于某种原因,峰值 WS 始终为 0,但我看到的最大值是 2,053,316,608 字节)。 4 秒后,它引发 OOM 异常:

2019-07-10T18:30:20.630Z Unhandled Exception: System.Threading.Tasks.TaskSchedulerException: An exception was thrown by a TaskScheduler. ---> System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.

事实证明,我们正在使用一个库,该库使用HttpClient而不处理它,从而泄漏套接字。

我们已经在 Windows 上使用这个库一段时间了,但显然套接字最终被终结器关闭,但在 Linux 上没有。

我最终在一台普通的 Linux 机器上运行了该应用程序,从而更容易监控操作系统。事实证明,这个命令

$ lsof -p <PID>

像这样返回数千行

dotnet  15613 ec2-user  215u     sock                0,8      0t0  4968805 protocol: TCP
dotnet  15613 ec2-user  219u     sock                0,8      0t0  4968844 protocol: TCP
dotnet  15613 ec2-user  220u     sock                0,8      0t0  4968236 protocol: TCP
dotnet  15613 ec2-user  221u     sock                0,8      0t0  4968247 protocol: TCP
...

HttpClient用法转换为单例解决了该问题。

最新更新