Как предотвратить исчерпание доступных объединенных соединений в моем DbContext при использовании в ServiceProvider?

#c# #multithreading #azure #dependency-injection

#c# #многопоточность #azure #внедрение зависимостей

Вопрос:

Я написал консольное приложение aspnet core, которое использует ядро Entity Framework Core с SQL Server. У приложения есть несколько клиентов очереди служебной шины Azure, которые, как я полагаю, каждый запускают обработчик получателя сообщений в своих собственных потоках — так что, я полагаю, это потоковое приложение в этой сторонней библиотеке служебной шины.

Чтобы избежать настройки и создания SQL-подключений в каждом клиенте очереди служебной шины, я использую внедрение зависимостей с помощью a Microsoft.Extensions.DependencyInjection.ServiceCollection . Затем я передаю результат ServiceProvider в каждый клиент очереди служебной шины.

Моя проблема в том, что со временем моя программа перестает работать, потому что в пуле подключений SQL заканчиваются доступные соединения. Это похоже на утечку памяти, но это утечка соединения SQL, потому что соединения никогда не возвращаются обратно в пул, я полагаю.

Я немного изучил это, и, насколько я понимаю, соединение должно просто вернуться в пул, когда оно выходит за рамки. Я также читал, что потоковая передача может вызывать эти проблемы, поэтому я поднял клиент очереди служебной шины и тот факт, что я считаю, что обработчик сообщений запускается из потока.

Я уверен, что это что-то простое, что я упускаю из виду, но я не смог это исправить, попробовав несколько разных подходов. Я вставлю свой код ниже, а затем объясню некоторые вещи, которые я пробовал.

Итак, вот код, который настраивает и создает ServiceProvider

 private ServiceProvider BuildServiceProvider()
{
    var services = new ServiceCollection();

    services.AddDbContext<WibWorkspaceContext>(ConfigureDbContext, ServiceLifetime.Transient);

    return services.BuildServiceProvider();
}
  

И затем вот ConfigureDbContext метод, который использует приведенный выше код…

 private void ConfigureDbContext(DbContextOptionsBuilder options)
{
    var migrationsAssembly = typeof(WibWorkspaceContext).GetTypeInfo().Assembly.GetName().Name;
    var sqlConnectionString = ConfigurationManager.ConnectionStrings["Sql"];

    options
        .UseLazyLoadingProxies()
        .UseSqlServer(sqlConnectionString.ConnectionString, x => x.MigrationsAssembly(migrationsAssembly));
}
  

И я передаю это ServiceProvider в каждый обработчик сообщений служебной шины, к которому я обращаюсь GetRequiredService . Итак, я действительно считаю, что мой вызов GetRequiredService происходит в потоке, созданном клиентом служебной шины. Ниже приведен некоторый сокращенный код этого…

 private async Task ProcessMessagesAsync(Message message, CancellationToken token)
{
    var sql = m_services.GetRequiredService<WibWorkspaceContext>();

    ...
}
  

Внутри ProcessMessagesAsync я использую DbContext , но я никогда не делаю ничего, что связано с его удалением или чем-либо еще. Я просто разрешаю ему выходить за рамки, и все.

Итак, это мой код. Эта программа будет работать нормально, пока не начнет заканчиваться SQL-соединение. В этот момент он начинает выдавать следующую ошибку и трассировку стека…

2020-10-02 23:07:55.1660 Истек тайм-аут ОШИБКИ. Период ожидания, прошедший до получения соединения из пула. Возможно, это произошло из-за того, что использовались все объединенные соединения и был достигнут максимальный размер пула.

 2020-10-02 23:07:15.3764 ERROR System.InvalidOperationException: Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  This may have occurred because all pooled connections were in use and max pool size was reached.
   at Microsoft.Data.Common.ADP.ExceptionWithStackTrace(Exception e)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenDbConnectionAsync(Boolean errorsExpected, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenDbConnectionAsync(Boolean errorsExpected, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenAsync(CancellationToken cancellationToken, Boolean errorsExpected)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(DbContext _, Boolean result, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
   at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
   at WhatInBoxWorkerService.Queues.CreateRelativityPackageQueueClient.ProcessMessagesAsync(Message message, CancellationToken token) in C:UsersrcoleProjectsWhatInBoxsrcApplicationsWhatInBoxWorkerServiceQueuesCreateRelativityPackageQueueClient.cs:line 203
   at Microsoft.Azure.ServiceBus.MessageReceivePump.MessageDispatchTask(Message message)
  

И, как и следовало ожидать, программа начинает аварийно завершать работу, потому что никакие соединения не могут быть извлечены из пула подключений.

Одна вещь, которую я пробовал, но, похоже, не имела никакого значения, заключалась в использовании AddDbContextPool вместо AddDbContext .

Я что-то забыл сделать?

Комментарии:

1. Так много вещей, которые могут быть, не видя всего кода… Но первое, что приходит на ум, это спросить, как долго объект, в который вы вводите dbcontext, работает, включая его кэширование в памяти или сеансе и т. Д… Соединение будет оставаться открытым до тех пор, пока родительский класс не будет удален…

Ответ №1:

Я подозреваю, что здесь есть проблема с вашей настройкой внедрения зависимостей:

 var sql = m_services.GetRequiredService<WibWorkspaceContext>();
  

Если это m_services корневой контейнер, который вы никогда не утилизируете и основываете на своем комментарии

В ProcessMessagesAsync я использую DbContext, но я никогда не делаю ничего, что связано с его удалением или чем-либо еще. Я просто разрешаю ему выходить за рамки, и все.

тогда вы можете в конечном итоге получить все WibWorkspaceContext s, которые никогда не будут удалены.

Почему?

Я предполагаю, что вы используете Microsoft extensions DI. Службы Transient / Scoped удаляются только тогда, когда их область действия заканчивается. Если вы разрешите их из корневого контейнера, они не будут удалены до тех пор, пока контейнер не будет удален. Вот почему не рекомендуется разрешать переходные IDisposable процессы из корневого контейнера.

Чтобы исправить это, вам может понадобиться настраиваемая область:

 using(var scope = m_services.CreateScope())
{
    var sql = scope.ServiceProvider.GetRequiredService<WibWorkspaceContext>();

} // <--- Your DB context will be disposed here