TaskScheduler在高度并发的异步asp.net核心应用程序中出现OutOfMemoryException

时间:2019-07-10 12:33:01

标签: linux amazon-web-services docker .net-core async-await

在托管于AWS ECS FARGATE(码头工人)的dotnet core 2.2 REST服务中,即使ECS报告最大内存使用量为11%,我也经常(每30-60分钟)使实例崩溃System.OutOfMemoryException(超出16GB)。崩溃总是来自TaskScheduler(下面的堆栈跟踪)。它只会在生产中发生。

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

该应用程序可在Windows 10上本地运行,并且我还尝试通过维持100个并发请求在一个单独的ECS群集(我们的测试群集)上进行复制,但是没有运气。 服务的一个端点接收99%以上的请求。基本操作是:

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

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。 (由于某种原因,Peak 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.

1 个答案:

答案 0 :(得分:0)

原来,我们使用的是一个使用HttpClient的库而没有对其进行处理,从而导致套接字泄漏。

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

我终于在常规Linux机器上运行了该应用程序,从而使监视OS更加容易。原来,该命令

$ 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用法转换为单例即可解决此问题。