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를 사용하는 방법을 알고싶습니다.
DBContext를 직접 GetRequiredService()로 IoC 컨테이너에서 직접 접근해서 인스턴스를 가져오지 말고
의존성 주입을 통해 가져와 사용하시면 됩니다.
기본적으로 DBContext는 컨테이너에서 수명주기 관리 방식이 ‘Scoped’ 방식으로 요청시 새로운 인스턴스를 생성해서 반환 합니다.
해당 스레드내에서 계속해서 인스턴스가 생성되어 처리 되서 비효율적으로 처리 되었던 것 같습니다.
의존성 주입으로 인스턴스를 받아 처리하시면 자연스럽게 하나의 DBContext로 처리 될 것 입니다. (상위 호출자에서 스레드로 반복 생성하지 않는 이상…)
따라서 직접 GetRequiredService() 호출로 DBContext 인스턴스를 가져오지 않고 의존성 주입을 사용하면 될 것이고,
기본적으로 DBContext는 멀티 스레드 안전하지 않습니다. 이 역시도 의존성 주입을 사용하면 해결 되는 문제 입니다.
의존성 주입 사용 없이 처리 한다면 직접 lock 처리를 하여 임계구역을 정해 처리하던지, 불필요해 보이지만 굳이 DBContext의 IoC 컨테이너 수명 주기를 싱글턴으로 관리하면 가능할지 모르겠습니다…
참고로 말씀 하신 현상은 메모리 누수 현상은 아니고 계속해서 인스턴스가 생성되어 메모리가 증가 되는 현상으로 기본적으로 참조 없이 스코프를 벗어나면 언젠가는 GC에 의해 인스턴스가 제거 되어 메모리가 반환 되게 됩니다.
이는 코드에서 의도적으로 계속해서 인스턴스를 생성 했기 때문에 당연한 현상입니다.
메모리 누수는 보통 의도치 않은 잘못된 코드로 '메모리가 회수 되지 않고, 비정상적’으로 게속해서 증가 되는 현상을 말합니다.
await using var scope = _serviceScopeFactory.CreateAsyncScope();
var config = scope.ServiceProvider.GetRequiredService<ConsumerConfig>();
var consumer = new ConsumerBuilder<string, string>(config).Build();
consumer.Subscribe(_kafkaTopicType.Name);
컨테이너에서 수명을 관리하는 인스턴스를 받은 경우 직접 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))