안녕하세요.
현재 ASP.NET CORE(.net 8)를 통해서 REST API를 만들어 보는 연습을 하고 있습니다. 그리고 DB는 Docker로 PostgreSQL을 띄웠고, EF Core(ver. 8.0.7)를 통해 DB에 접근하고 있습니다.
그런데 연습 과정에서 두 가지 문제를 발견했습니다.
문제 1)
서버 실행 후 첫 번째로 실행되는 쿼리가 매우 느립니다. 하지만 그 다음 두 번째 쿼리부터는 정상적인(빠른) 속도를 보여줍니다.
[첫 번째 쿼리 실행 - 응답까지 약 8.8s]
[두 번째 쿼리 실행 - 응답까지 약 11ms]
개발을 하다보면 서버를 내렸다 올렸다 반복하게 되는데, 이럴때마다 첫 번째 쿼리는 8초정도 기다린 후 응답을 받으니 꽤나 답답했습니다.
문제 2)
첫 번째 쿼리 수행 후 다음 쿼리부터는 정상적인 속도를 보여줬으나, (서버를 내리지 않고 가만히 있다가) 5분 후에 다시 쿼리를 보내보면 또다시 8초를 기다린 후에 응답을 받을 수 있었습니다.
DB도 확인해보면서 몇 번 테스트를 해보니, ASP.NET Core와 연결된 (IDLE 상태의) 커넥션이 5분 후에 사라지는 것을 보았습니다.
(위 그림에선 pid 6157이 5분후에 사라짐)
커넥션이 사라진 후 쿼리를 날릴 때마다 느린 응답 속도를 보여줬습니다.
NPGSQL(ADO.NET Data Provider for PostgreSQL) 공식문서를 확인해보니 Pooling 관련 Default 옵션이 다음과 같았습니다. (참고: Connection String Parameters | Npgsql Documentation)
- Minimum Pool Size : 0
- Connection Idle Lifetime : 300
즉, 최소로 유지하고자 하는 Connection이 0개이므로 Idle 상태의 Connection은 5분 후면 자동으로 제거되고, 이후의 쿼리는 새로운 Connection을 생성하게 되면서 느린 응답 속도를 보여줬던게 아닌가 싶습니다.
저는 위와 같은 두 가지 문제를 발견하고 다음과 같이 조치를 취해봤습니다.
문제 2에 대한 조치)
// Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseNpgsql(builder.Configuration.GetConnectionString("ShirtStoreManagement"))
);
{
"ConnectionStrings": {
"ShirtStoreManagement": "Host=localhost;Port=5432;Database=ShirtStoreManagement;Username=postgres;Password=postgres;Include Error Detail=true;Minimum Pool Size=5;Keepalive=30"
},
:
:
}
위와 같이 Connection Pool의 최소 사이즈를 5로 설정하고, KeepAlive 옵션을 통해 Idel 상태의 Connection을 계속 유지할 수 있도록 했습니다.
이처럼 설정을 하니 5분, 10분이 지난 후에 쿼리를 다시 날려도, 가지고 있는 connection을 바로 사용하기 때문에 빠른 응답 속도를 받아 볼 수 있었습니다.
또한 다른 방법으로는, 위처럼 Connection String에 추가적인 옵션을 추가하지 않고, AddDbContextPool을 사용하는 것만으로도 두 번째 문제는 해결이 될 수 있었습니다.
builder.Services.AddDbContextPool<ApplicationDbContext>(
options => options.UseNpgsql(builder.Configuration.GetConnectionString("ShirtStoreManagement"))
);
문제 1에 대한 조치)
첫 번째 문제는 서버가 실행될 때 생성된 Connection이 없기 때문에 첫 번째 쿼리가 느린 것이라고 판단을 해서, 다음과 같은 생각으로 조치를 취해봤습니다.
“서버가 실행되는 과정에서 미리 쿼리를 날려 Connection을 생성해보자”
// Program.cs
using Microsoft.EntityFrameworkCore;
using WebAPIDemo.Data;
using WebAPIDemo.MyCompiledModels;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContextPool<ApplicationDbContext>(
options => options.UseNpgsql(builder.Configuration.GetConnectionString("ShirtStoreManagement"))
);
builder.Services.AddControllers();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
try
{
// Execute "SELECT 1;" to initialize the database connection
dbContext.Database.ExecuteSqlRaw("SELECT 1;");
Console.WriteLine("Database connection initialized successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error during database initialization: {ex.Message}");
throw;
}
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Program.cs 중간에 있는 using 구문에서 “SELECT 1;” 쿼리를 날려 DB와의 Connection을 미리 생성하도록 해봤습니다.
비록 서버가 실행되는데 걸리는 시간이 약간 더 추가됐지만, 서버가 열린 후 첫 번째 DB 쿼리 수행에서 빠른 응답 속도(ms 단위)를 얻을 수 있었습니다.