들어가며
사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 서비스를 개발하면서, 현재 서비스가 얼마나 많은 부하를 받아낼 수 있는가 알고 싶었습니다. 그 과정에서, 슬로우 쿼리가 나타났을 때, 현재 서비스에서는 어떻게 슬로우 쿼리를 처리할 수 있는지 알고 싶었습니다. 만약 슬로우 쿼리가 나타났을 때 성능은 어떻게 변할 수 있으며, 슬로우 쿼리가 나타났을 때 어떻게 처리해야 하는지 알아본 과정에 대해 자세히 알아보겠습니다. 이 글은 이동욱 님의 NodeJS와 PostgreSQL Query Timeout 글을 참고했습니다.
슬로우 쿼리
slow query란 DBMS가 client로부터 요청받은 query를 수행할 때 일정 시간 이상 수행되지 못한 query를 뜻합니다. 길어야 1~2초 걸리는 DB 쿼리가 예상보다 오래 걸리는 경우를 뜻합니다. 만약 처리할 시간이 긴 쿼리가 여러 개 요청이 온다면, Connection Pool이 가득 차 더 이상 쿼리가 수행되지 못하거나, 데이터베이스에 장애가 날 수 있습니다. 그래서 적정시간 이상으로 쿼리가 수행되면 강제로 종료하고, 다시 요청하도록 해야 하는데, timeout을 어떻게 설정할 수 있는지 알아보겠습니다.
실험 코드
아래와 같은 코드를 활용해서 환경을 구축했습니다.
@Get('test-timeout')
async test() {
try {
await this.Service.test();
return ResponseEntity.OK_WITH('Query Timeout 테스트에 성공했습니다.');
} catch (error) {
this.logger.error(error);
return ResponseEntity.ERROR_WITH('Query Timeout 테스트에 실패했습니다.');
}
}
먼저 컨트롤러에서, test-timeout url에 Get 요청을 할 수 있도록 설정했습니다.
Service.ts
async test() {
const client = new pg.Pool({
host: process.env.DB_TEST_HOST,
port: Number(process.env.DB_TEST_PORT),
user: process.env.DB_TEST_USERNAME,
password: process.env.DB_TEST_PASSWORD,
database: process.env.DB_TEST_NAME,
min: 5,
max: 50,
});
const start = LocalDateTime.now().second();
try {
await client.query('SELECT pg_sleep(3)');
const lag = LocalDateTime.now().second();
console.log(`Lag: \t${lag - start} ms`);
} catch (error) {
console.error('pg error', error);
}
}
그 후 서비스에 test 메서드를 구축하여, postgreSQL Pool을 생성했습니다. API 호출 시 강제로 3초의 쿼리를 발생시켰으며 그 후 js-joda 라이브러리를 활용하여 초 시간을 구하는 로직을 start와 lag에서 활용하여 실제로 3초의 시간만 수행되었는지 로그로 출력했습니다.
테스트
일정 트래픽을 몰아주기 위해 Apache Bench를 통해 테스트를 진행했습니다. Mac을 활용하는 경우엔 ApacheBench가 기본적으로 설치되어 있는데, 설치 여부를 확인하기 위해서 ab -v로 확인해보면 됩니다. 그 후, 아래 명령어를 터미널에 입력합니다.
ab -n 100 -c 20 http://localhost:3000/test-timeout/
여기서 -n 100은 총 요청 횟수입니다. 요청 사용자들이 다 합쳐서 보낼 횟수입니다. -c 20은 총 요청자들의 수입니다. 20명의 동시 사용자가 요청합니다. 이렇게 요청하면 다음과 같은 결과를 얻을 수 있습니다.
Concurrency Level은 20명의 동시 사용자로, Complete requests는 총 100번을 호출했다는 것을 의미합니다. 그래서 총 18.224초가 걸렸다는 뜻이 되며, Requests per second의 경우 1초에 최대 요청 양을 이야기합니다. 현재 5.49인데, 이는 1초에 5건을 처리할 수 있다는 의미입니다. 또한 첫 번째 Time per request는 요청당 평균 시간으로, 이 요청은 3644ms 즉 평균 3.6초가 소요됐다는 뜻입니다.
총 100개의 요청을 보냈지만, Connection 수의 제한으로 초당 5건밖에 처리하지 못했는데, 만약 지연 쿼리가 3초가 아닌 그 이상 걸렸다면, 그 시간 동안 지정된 Max Connection이 가득 차 더 이상 Database에 쿼리를 날릴 수가 없는 상태가 됩니다. 이런 문제를 해결하려면 어떻게 해야 할까요?
timeout 설정하기
PostgreSQL에서는 QueryTimeout 옵션을 statement_timeout으로 설정할 수 있습니다. 해당 옵션을 지정하면 지정된 시간이 지나도록 끝나지 않은 쿼리는 강제 종료시킵니다. 만약 pg 모듈만 사용한다면, 아래와 같이 connection Option에서 설정할 수 있습니다.
const client = new pg.Pool({
statement_timeout: 1000,
})
만약 TypeORM을 사용한다면, 아래와 같이 extra 항목에 추가해서 설정할 수 있습니다.
return TypeOrmModule.forRoot({
maxQueryExecutionTime: dbEnv.connectionTimeout,
extra: {
statement_timeout: dbEnv.connectionTimeout,
...
},
...
});
여기서 주의할 점은 maxQueryExecutionTime은 타임아웃 설정이 아닙니다. 이 시간이 지나면 롱 쿼리 로그를 남기겠다는 옵션입니다. 타임아웃 설정은 extra.statement_timeout으로 해야 합니다.
위와 같이 설정하고 테스트를 진행하면 다음과 같이 1초만 지나도 바로 쿼리가 취소되는 것을 확인할 수 있습니다.
만약 TypeORM에서 timeout을 설정했다면 다음과 같은 로그를 볼 수 있습니다.
위와 같이 설정하면, 슬로우 쿼리로 인해 장애가 발생했을 때, 전체 서비스의 장애로 확장되는 것을 미리 방지할 수 있습니다.
마치며
앞으로도 팀의 발전을 돕는 개발자가 되기 위해 노력하려 합니다. 팀에 필요한 부분이 무엇일지 고민하면서, 팀에 도움이 된다면, 열심히 공부해서 실무에 적용할 수 있는 개발자가 되기 위해 노력하고 싶습니다. 팀의 성장에 기여할 수 있는 개발자가 되겠습니다.
참고 및 출처
'Project > 서버 개발' 카테고리의 다른 글
[Project] 프로젝트 삽질기17 (feat Table Scan 실행계획) (0) | 2022.04.09 |
---|---|
[Project] 프로젝트 삽질기16 (feat Connection Pool) (0) | 2022.04.08 |
[Project] 프로젝트 삽질기14 (feat 큐 모니터링) (0) | 2022.03.30 |
[Project] 프로젝트 삽질기13 (feat class-transformer) (0) | 2022.03.29 |
[Project] 프로젝트 삽질기12 (feat 직렬화) (0) | 2022.03.29 |