들어가며
사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 서비스를 개발하면서, 현재 서비스가 얼마나 많은 부하를 받아낼 수 있는가 알고 싶었습니다. 그 과정에서, 슬로우 쿼리가 나타났을 때, 모든 커넥션을 사용하고 있다면, 다른 요청들은 대기해야 하는 상황이 발생합니다. 그 과정에서, 사용자들에게 불편함을 줄 수 있는데, 그럼 커넥션 풀을 잘 활용하는 방법에 대해 알아야겠다고 생각했습니다. 이 글은 이동욱 님의 NodeJS와 PostgreSQL Connection Pool 글과 최범균 님의 초식 : DB 커넥션 풀 설정 영상을 참고했습니다.
커넥션 풀
커넥션이란, DB를 사용하기 위해 DB와 애플리케이션 간 통신을 할 수 있는 수단을 뜻합니다. DB 커넥션은 Database Driver와 Database 연결 정보를 담은 URL이 필요합니다. Pool은 이런 커넥션을 미리 생성해서 보관하고, 코드는 풀에서 커넥션을 가져와서 커넥션을 사용하고 사용이 끝나면 풀에 반환하는 식으로 동작합니다. 풀 안에 있는 커넥션을 유휴 커넥션이라 부르고, 커넥션에서 가져와서 사용 중인 커넥션을 in use 상태의 커넥션이라 합니다.
일반적인 데이터베이스를 연결해서 사용하는 것을 본다면 다음과 같습니다.
- 데이터베이스 드라이버를 사용하여 데이터베이스 커넥션 Open
- 데이터 읽기/쓰기를 위한 TCP 소켓 Open
- 소켓을 통한 데이터 읽기/쓰기
- 커넥션 Close
- 소켓 Close
데이터베이스 연결 작업은 상당히 많은 비용이 드는 일입니다. 그래서 가능한 직접적인 데이터베이스 연결 작업을 최소한으로 줄여야 합니다. 이때 기존 커넥션을 재사용할 수 있도록 구성만 한다면 막대한 비용이 드는 데이터베이스 수행 비용을 효과적으로 절감할 수 있으므로 데이터베이스 기반 애플리케이션의 전체 성능이 크게 향상됩니다.
결과적으로 커넥션 풀을 활용하면 응답 시간을 단축시킬 수 있고, 그 결과 처리량을 증가시킬 수 있습니다. 그리고 DB에 대한 커넥션 개수를 일정 수준으로 제한하기도 하는데, 이는 DB 포화를 방지하고 일관된 DB 성능을 유지시켜 줍니다. 하지만 이런 풀 설정을 제대로 설정하지 않으면 성능 문제를 유발할 수 있습니다. 그럼 테스트를 통해 커넥션 풀에 대해 알아보겠습니다.
실험 코드
아래와 같은 코드를 활용해서 환경을 구축했습니다.
const express = require('express');
const pg = require('pg');
const app = express()
const port = 3000
const client = new pg.Pool({
host: 'localhost',
user: 'test',
password: 'test',
database: 'test',
port: 5432,
max: 5,
})
client.connect(err => {
if (err) {
console.log('Failed to connect db ' + err)
} else {
console.log('Connect to db done!')
}
})
app.get('/test-timeout', async (req, res) => {
const start = new Date().getMilliseconds();
try {
await client.query('SELECT pg_sleep(3);');
const lag = new Date().getMilliseconds() - start;
console.log(`Lag: \t${lag} ms`);
} catch (e) {
const lag = new Date().getMilliseconds() - start;
console.log(`Lag: \t${lag} ms`);
console.error('pg error', e);
}
res.send('test-timeout!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
API 호출 시 강제로 3초의 쿼리를 발생시켰으며 그 후 실제 3초의 시간만 수행되었는지 로그로 출력했습니다. 여기서 핵심은 pg.Pool에서 max가 5라는 설정입니다. 이렇게 하면 최대 커넥션 생성 개수가 5로 설정됩니다. max의 기본 값은 10인데, 5으로 하면 어떻게 될까요? 이에 대해 알아보겠습니다.
테스트
일정 트래픽을 몰아주기 위해 Apache Bench를 통해 테스트를 진행했습니다. Mac을 활용하는 경우엔 ApacheBench가 기본적으로 설치되어 있는데, 설치 여부를 확인하기 위해서 ab -v로 확인해보면 됩니다. 그 후, 아래 명령어를 터미널에 입력합니다.
ab -n 100 -c 20 http://localhost:3000/test-timeout/
여기서 -n 100은 총 요청 횟수입니다. 요청 사용자들이 다 합쳐서 보낼 횟수입니다. -c 20은 총 요청자들의 수입니다. 20명의 동시 사용자가 요청합니다. 먼저 max 값이 10일 때와 5 일 때를 비교해보겠습니다.
왼쪽은 max 값이 10일 때이고, 오른쪽은 max 값이 5일 때입니다. 요청당 평균 시간인 Time per request의 첫 번째 값을 비교해보면, 7210에서 15616으로 두 배 가깝게 증가한 것을 볼 수 있습니다. 즉, max가 5일 때 평균 15초 정도 걸렸다는 것을 알 수 있고, 초당 요청 값인 Request per second 항목은 1초에 최대 요청 양을 이야기합니다. max가 5일 때 1.28인데, 이는 1초에 1건 정도 겨우 처리한다는 것을 의미합니다.
테스트를 통해 평균 15초의 응답 시간을 주었다는 것을 알 수 있었는데, 커넥션 개수가 최대 10개에서 5개로 이동했을 뿐인데, 성능 저하가 일어난 것을 확인할 수 있었습니다. 그럼 테스트에서는 100개의 동시 요청이 들어왔는데, 만약 동시 요청이 1000, 2000개씩 오면 다음과 같은 고민을 할 수 있습니다.
- "커넥션을 맺기 위한 대기시간이 수십 분이 걸리면 어떡하지?"
- 처리하지 못하고 대기하고 있는 수많은 요청들로 인해서 서버 부하와 함께 장애가 발생하는 것은 걱정이다.
- 그럴 바에는 "차라리 일정시간이상 대기하는 것들은 모두 다 취소" 시키고, 서버의 부하를 낮추는 방향을 선택하는 게 낫겠다.
위와 같은 고민이 들 때는 connectionTimeoutMillis 설정을 사용하면 됩니다.
const client = new pg.Pool({
...
connectionTimeoutMillis: 10000,
max: 5,
})
이렇게 설정하면 최대 커넥션 대기 시간은 10초로 설정됩니다. connectionTimeoutMillis의 기본값은 무제한이며 기본 단위는 ms입니다. 최대 커넥션 대기 시간을 10초로 설정하고, 똑같이 부하 테스트를 실행해봅니다. 그럼 아래와 같이 10초 이상 대기가 되면 바로 취소됩니다.
왼쪽은 커넥션 대기 시간을 설정하지 않은 값이고, 오른쪽은 커넥션 대기 시간을 10초로 설정했을 때의 값입니다. 보는 것과 같이, Time per request에서 평균 15초 걸리던 작업이 12초 걸리는 작업으로 개선되었습니다.
또한 10초 이상 대기가 되면 위와 같은 로그가 출력되면서 취소되는 것을 확인할 수 있습니다. 위와 같은 결과를 보고서, 커넥션 수를 많이 늘리면 늘릴수록 좋은 게 아닌가 생각이 듭니다. 이때 알아야 하는 것은, 많은 커넥션 수를 유지하려면 그만큼 DB의 메모리가 따라줘야 합니다. 그래서 AWS와 같은 클라우드에서 지원하는 매니지드 데이터베이스에서는 기본 설정으로 사양에 맞는 커넥션 설정이 되도록 합니다.
예를 들어 AWS RDS MySQL의 MaxConnection 계산식은 {DBInstanceClassMemory/12582880}입니다. 그럼,
- t2.micro RDS를 사용한다면
- t2.micro의 메모리는 512MB이니,
- max_connections 은 (512*1024*1024)/12582880 가 되어 40개의 MaxConnection RDS에서 설정됩니다.
즉, t2.micro 는 Node Application & 개별 DB Gui 도구까지 합쳐서 40개의 커넥션 이상은 동시에 연결될 수 없습니다.
그래서 이에 맞게 Node에서 Connection Option에서 적절한 수치의 connectionLimit을 설정해야만 합니다.
- 우리 팀에 몇 대의 서버에서 Node 애플리케이션을 실행하는지
- 서버 1대당 PM2 혹은 클러스터링으로 여러 Node를 같이 실행하는지
- 갑자기 트래픽이 몰려와 서버를 늘린다면 최대 몇 대까지 늘리는지
- 각자 개발 PC에서 접근하는 사람은 몇 명인지
등등을 정리해서 적절한 수치의 max 값을 설정해야 합니다.
RDS 옵션 설정하기
그럼, RDS를 사용하고 있을 때, 어떻게 커넥션 수를 관리할 수 있을까요? 이런 경우 보통 max_connection 값을 증가시켜 줍니다. max_connection 값은 수동으로 값을 지정할 수 있으니, 보통 성능 이상의 요청이 발생하면, DB 자체의 문제가 생길 수 있으므로 AWS에서는 각 인스턴스에 설정된 기본값을 사용하고 여유 있게 설정하는 것을 권장합니다.
RDS 인스턴스 타입에 따른 기본 max_connections 값은 다음과 같습니다.
t2.micro: 66
t2.small: 150
m3.medium: 296
t2.medium: 312
M3.large: 609
t2.large: 648
M4.large: 648
M3.xlarge: 1237
R3.large: 1258
M4.xlarge: 1320
M2.xlarge: 1412
M3.2xlarge: 2492
R3.xlarge: 2540
그럼, max_connection 값을 변경하고 싶다면, 어떻게 해야 할까요? DB 인스턴스에 연결된 파라미터 그룹을 수정해야 합니다. RDS를 생성해서 파라미터 그룹을 들어갑니다. 그 후 기본 파라미터 그룹의 max_connections 값을 콘솔에서 확인해보면 {DBInstanceClassMemory/12582880}으로 설정되어 있는 것을 확인할 수 있습니다.
마치며
앞으로도 팀의 발전을 돕는 개발자가 되기 위해 노력하려 합니다. 팀에 필요한 부분이 무엇일지 고민하면서, 팀에 도움이 된다면, 열심히 공부해서 실무에 적용할 수 있는 개발자가 되기 위해 노력하고 싶습니다. 팀의 성장에 기여할 수 있는 개발자가 되겠습니다.
참고 및 출처
'Project > 서버 개발' 카테고리의 다른 글
[Project] 프로젝트 삽질기18 (feat 쿼리 튜닝, 인덱스) (0) | 2022.04.11 |
---|---|
[Project] 프로젝트 삽질기17 (feat Table Scan 실행계획) (0) | 2022.04.09 |
[Project] 프로젝트 삽질기15 (feat TypeORM Query Timeout) (0) | 2022.04.08 |
[Project] 프로젝트 삽질기14 (feat 큐 모니터링) (0) | 2022.03.30 |
[Project] 프로젝트 삽질기13 (feat class-transformer) (0) | 2022.03.29 |