Утечка памяти ядра EF в многопоточном консольном приложении

#c# #.net-core #memory-leaks #entity-framework-core #autofac

#c# #.net-ядро #утечки памяти #entity-framework-core #autofac

Вопрос:

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

Настройка заключается в следующем:

Я наследую от DbContext в MyContext

 public class MyContext : DbContext
{
    private readonly Assembly configurationAssembly;

    public MyContext(Assembly configurationAssembly, DbContextOptions dbContextOptions) : base(dbContextOptions)
    {
        this.configurationAssembly = configurationAssembly;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(this.configurationAssembly);
    }
}
  

У меня есть класс POCO:

 public class Job
{
    public int Id { get; set; }
    public string Applicationcode { get; set; }
}
  

И конфигурация EntityTypeConfiguration:

 public class JobConfig : IEntityTypeConfiguration<Job>
{
    public void Configure(EntityTypeBuilder<Job> builder)
    {
        builder.ToTable("Job_OP");
    }
}
  

MyContext использует эту конфигурацию в переопределении OnModelCreating (ApplyConfigurationFromAssembly)

Наконец, в программе у меня есть следующий код:

 class Program
{
    private static IContainer container;
    static void Main(string[] args)
    {
        var builder = new ContainerBuilder(); //Autofac dependency injection

        var dbContextOptionsBuilder = new DbContextOptionsBuilder().UseSqlServer("Data Source=localhost;Initial Catalog=TestEFThreads;Integrated Security=True");

        builder.Register(ctx => new MyContext(
            Assembly.Load("TestEFThreads"),
            dbContextOptionsBuilder.Options));
        //I tried to use different lifetime scopes here, but EF doesn't like using the same dbContext concurrently in different threads.
        //Currently each resolve instantiates a new MyContext

        container = builder.Build();

        var input = ShowMenu();
        var nrOfSimulatedMessages = 0;
        while (!string.Equals(input, "q", StringComparison.OrdinalIgnoreCase))
        {
            try
            {
                nrOfSimulatedMessages = Convert.ToInt32(input);
                PerformSimulation(maxThreads: 10, nrOfSimulatedMessages);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Incorrect input or exception, {e}");
            }
            input = ShowMenu().ToLower();
        }
    }

    static string ShowMenu()
    {
        Console.WriteLine("Enter number of simulated messages or q to quit");
        return Console.ReadLine();
    }

    private static void PerformSimulation(int maxThreads, int nrOfSimulatedMessages)
    {
        var messageCount = 0;

        var tasks = new Task[maxThreads];

        while (messageCount < nrOfSimulatedMessages)
        {
            for (int i = 0; i < maxThreads amp;amp; messageCount < nrOfSimulatedMessages; i  )
            {
                Console.WriteLine($"Creating thread {i} for message {messageCount}");
                tasks[i] = new Task(StartProcessing);
                tasks[i].Start();
                messageCount  ;
            }
            Console.WriteLine("Waiting for all threads to finish");
            Task.WaitAll(tasks.Where(a=>a !=null).ToArray());
        }
    }

    private static void StartProcessing()
    {
        using (var lifeTime = container.BeginLifetimeScope()) //does not help
        {
            var myContext = container.Resolve<MyContext>();
            var job = myContext.Set<Job>().SingleOrDefault(a => a.Id == 1);
            Console.WriteLine($"Found job with applicationCode {job.Applicationcode}");
            myContext.Dispose(); //memory leak even with dispose
        }
    }
}
  

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

Когда я запускаю его примерно для 50000 «сообщений», объем памяти продолжает увеличиваться, как видно из диагностики. Похоже, что это не сбор мусора.

Диагностика

У кого-нибудь есть идея о том, как исправить эту утечку памяти?

Обновление: похоже, это связано с внедрением зависимости Autofac. Если я просто использую новый MyContext iso, разрешающий это, использование памяти в порядке.

     private static void StartProcessing()
    {
        var myContext = new MyContext(Assembly.Load("TestEFThreads"), dbContextOptionsBuilder.Options);

        var job = myContext.Set<Job>().SingleOrDefault(a => a.Id == 1);
        Console.WriteLine($"Found job with applicationCode {job.Applicationcode}");
    }
  

Однако мне нужно использовать внедрение зависимостей.

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

1. Это не обязательно утечка памяти. Сбор мусора может подождать некоторое время, чтобы освободить память, если нет проблем с памятью со стороны других программ или системных процессов.

2. Привет, приложение в конечном итоге использует почти 4 ГБ памяти, поэтому я думаю, что это проблема. Также желтые маркеры в диагностике указывают на начало сборки мусора управляемой кучи, поэтому я бы ожидал, что он упадет туда.

3. Используете ли вы using инструкции? Я не вижу никаких using инструкций для ваших объектов DbContext. Единственное using утверждение, которое я вижу, относится к этому BeginLifetimeScope() вызову, и это связано с autofac. DbContext объекты имеют свое собственное управление временем жизни, и если вы не планируете найти способ изменить этот DbContext для какой-либо другой технологии, внедрение его кажется бессмысленным.

4. Кроме того, вы уверены, что возвращаемый объект job не равен null ? По замыслу autofac хранит ссылки на одноразовые объекты и при вызове консоли. В вашем коде произошел сбой Writeline, DbContext никогда не будет удален. Не могли бы вы попробовать еще раз с помощью инструкции using , как было предложено выше?

5. Это может дать некоторое представление: autofaccn.readthedocs.io/en/latest/lifetime/disposal.html

Ответ №1:

Кажется, что использование ExternallyOwned помогает.

     builder.Register(ctx => new MyContext(
        Assembly.Load("TestEFThreads"),
        dbContextOptionsBuilder.Options)).ExternallyOwned();
  

Нормальное потребление памяти