DbContext 메모리 누수 관련

Task.Run으로 약 20여개의 스레드에서 DB 관련 처리를 하고 있습니다.
주로 Update와 Insert 작업이 각 스레드별로 동일 DB에 처리하고 있습니다.
DB 작업은 정상적으로 잘 되고 있습니다.
각 스레드에서 DB 작업을 하기 위해 DbContext를 다음과 같이 생성하여 사용하고 있습니다.

using (var myDbContext = _ServiceProviderScope.GetRequiredService<MyDbContext>())
{
}

_ServiceProviderScope는 다음과 같이 생성자에서 생성하였습니다.

public MyClass(IServiceProvider serviceProvider)
	{
		_ServiceProviderScope = _ServiceProvider.CreateScope().ServiceProvider;

이렇게 작업을 하니 프로세스의 메모리가 무제한으로 늘어납니다.
dotnet-dump로 확인해보니 MyDbContext가 무제한으로 생성되고 있었습니다.

그래서 다음과 같이 DbContext 가져오는 부분을 변경했습니다.

using (var myDbContext = new MyDbContext(_DbOption))
{
}

위와 같이 변경하니 메모리 누수 문제가 해결되었습니다.

DI로 DbContext를 생성하면 메모리 누수가 발생하는 원인과 멀티스레드에서 안전하게 DbContext를 사용하는 방법을 알고싶습니다.

3 Likes

생성하신 Scope에 대한 Dispose를 하지 않아서 아닐까요?

3 Likes

DBContext를 직접 GetRequiredService()로 IoC 컨테이너에서 직접 접근해서 인스턴스를 가져오지 말고

의존성 주입을 통해 가져와 사용하시면 됩니다.

기본적으로 DBContext는 컨테이너에서 수명주기 관리 방식이 ‘Scoped’ 방식으로 요청시 새로운 인스턴스를 생성해서 반환 합니다.


해당 스레드내에서 계속해서 인스턴스가 생성되어 처리 되서 비효율적으로 처리 되었던 것 같습니다.

의존성 주입으로 인스턴스를 받아 처리하시면 자연스럽게 하나의 DBContext로 처리 될 것 입니다. (상위 호출자에서 스레드로 반복 생성하지 않는 이상…)

따라서 직접 GetRequiredService() 호출로 DBContext 인스턴스를 가져오지 않고 의존성 주입을 사용하면 될 것이고,

기본적으로 DBContext는 멀티 스레드 안전하지 않습니다.
이 역시도 의존성 주입을 사용하면 해결 되는 문제 입니다.
의존성 주입 사용 없이 처리 한다면 직접 lock 처리를 하여 임계구역을 정해 처리하던지, 불필요해 보이지만 굳이 DBContext의 IoC 컨테이너 수명 주기를 싱글턴으로 관리하면 가능할지 모르겠습니다…

참고로 말씀 하신 현상은 메모리 누수 현상은 아니고 계속해서 인스턴스가 생성되어 메모리가 증가 되는 현상으로 기본적으로 참조 없이 스코프를 벗어나면 언젠가는 GC에 의해 인스턴스가 제거 되어 메모리가 반환 되게 됩니다.

이는 코드에서 의도적으로 계속해서 인스턴스를 생성 했기 때문에 당연한 현상입니다.
메모리 누수는 보통 의도치 않은 잘못된 코드로 '메모리가 회수 되지 않고, 비정상적’으로 게속해서 증가 되는 현상을 말합니다.

4 Likes

답변 감사합니다.

using을 사용했기 때문에 GetRequiredService로 가져온 DbContext는 언젠가는 GC에 의해 인스턴스가 제거되어 메모리가 반환될거라고 예상했는데, PC 메모리(192GB)가 꽉차서 PC가 죽을때까지도 반환되지 않습니다.

혹시 의존성 주입을 사용하여 DBContext를 가져오려면 어떻게 해야 하는지 설명 부탁드려도 될지요?

멀티스레드에서 동시에 Update 및 Insert 하기 위하여 DBContext를 가져오는 가장 효율적인 방법은 무엇일까요? (당연히 Update, Insert 시에는 Transaction 처리를 합니다.)

1 Like

개발 환경에 대해 자세한 내용이 없어 기본적으로 asp net 계열 (.netframework 아닌) 에서
AddDbContext 로 DbContext를 등록하면 의존성 주입으로 사용 할 수 있습니다.

builder.Services.AddDbContext<MyDbContext>(~~~~
private readonly MyDbContext _context;
public MyClass(MyDbContext context) {
    _context = context;
}

첫번째 답변에서 말씀 드렸듯이 정상적인 상황에선 의존성 주입 사용으로 해결 됩니다.
DbContext를 DI해서 사용하면 Http 요청 컨텍스트에서 각 인스턴스별로 처리가 됩니다.

DBMS 입장에서 동시에 UPDATE / INSERT 되는 것은 일관성, 무결성으로 보장이 되고, 잘못된 데이터가 만들어 진다면 그것은 테이블이 잘못 설계된것입니다.

2 Likes

그대로 사용하고, _DbOption 를 서비스에 의존하면 좋을 듯 합니다.

        await using var scope = _serviceScopeFactory.CreateAsyncScope();
        
        var config = scope.ServiceProvider.GetRequiredService<ConsumerConfig>();
        var consumer = new ConsumerBuilder<string, string>(config).Build();
        consumer.Subscribe(_kafkaTopicType.Name);
1 Like

컨테이너에서 수명을 관리하는 인스턴스를 받은 경우 직접 Dispose 하지 않아야 합니다.
MyDbContext 를 Transient 로 등록한 경우 IDisposable 이기 때문에 컨테이너 또는 상위 범위에서 수명을 관리하기 위해 등록됩니다. 프로바이더에서 인스턴스를 가져올 때마다 인스턴스가 생성되며 싱글턴(프로바이더)이나 범위의 수명을 따라 해제되기 때문에 메모리 누수가 발생할 수 있습니다.

using (var myDbContext = _ServiceProviderScope.GetRequiredService())

다음 코드는 생성자에서 스코프를 계속 생성하게 됩니다.
스코프는 Dispose 해야 합니다. 범위에서 관리하는 인스턴스들의 관리되지 않는 메모리를 해제합니다. 생성자가 아닌 메서드에 포함하시고 CreateScope()의 결과 scope 참조에 using 을 붙여주세요.

public MyClass(IServiceProvider serviceProvider)
{
_ServiceProviderScope = _ServiceProvider.CreateScope().ServiceProvider;

ASP.NET Core에서 AddDbContext 로 등록하면 Http 요청에 맞게 스코프로 등록됩니다.
쿼리 병렬 실행 또는 멀티 스레드에서 실행은 스레드별로 스코프를 생성하거나 마지막에 작성하신 옵션을 이용한 DbContext 인스턴스 생성을 사용하는 것이 좋아보입니다.

using (var myDbContext = new MyDbContext(_DbOption))

종속성 주입 지침
DbContext 수명
DbContext 스레딩 문제 방지