문제 상황
회사에서 MySQL의 Too many connections 문제가 발생했다.
확인해 보니 MySQL 내부에 Sleep 상태의 connection이 과도하게 쌓여 있었고,
결국 DB가 허용 가능한 최대 connection 수를 초과하면서 장애가 발생한 것으로 보였다.
그렇다면 왜 이런 문제가 발생할까?
이를 이해하려면 먼저 DB Connection이 무엇인지, 그리고 DBCP(Database Connection Pool) 이 왜 필요한지를 이해해야 한다.
DB Connection이란?
애플리케이션과 DB 서버는 네트워크를 통해 통신한다.
예를 들어:
- 애플리케이션이 SQL 요청 전송
- DB 서버가 SQL 실행
- 결과 응답 반환
이 과정을 반복하게 된다.
이때 MySQL은 TCP 기반으로 동작한다.
즉, 애플리케이션과 DB 서버는 단순히 데이터를 보내는 것이 아니라 TCP Connection 을 맺고 통신한다.
DB Connection 생성 비용
TCP는 연결 지향 프로토콜이다.
따라서 통신 전에 반드시 연결 과정이 필요하다.
연결 생성: 3-Way Handshake
TCP 연결 시:
- SYN
- SYN + ACK
- ACK
과정을 거쳐 연결이 생성된다.
이를 3-Way Handshake라고 한다.
연결 종료: 4-Way Handshake
연결 종료 시에는:
- FIN
- ACK
- FIN
- ACK
과정을 거친다.
이를 4-Way Handshake라고 한다.
문제점
만약 SQL 요청마다:
- TCP 연결 생성
- SQL 실행
- TCP 연결 종료
를 반복한다면 어떻게 될까?
매 요청마다 Handshake 비용이 발생하게 되고,
트래픽이 많아질수록 성능 저하가 커지게 된다.
즉, DB 작업 자체보다도 connection 생성/종료 비용이 무시할 수 없게 된다.
DBCP(Database Connection Pool)
이 문제를 해결하기 위해 등장한 것이 DBCP(Database Connection Pool)이다.
DBCP는 애플리케이션이 시작될 때 미리 여러 개의 DB Connection을 생성해 두고, 이를 Pool에 보관하여 재사용하는 방식이다.
즉:
요청 발생
→ Pool에서 Connection 가져오기
→ SQL 실행
→ Connection 반환
형태로 동작한다.
핵심은 매번 새로운 TCP 연결을 만드는 것이 아니라, 기존 connection을 재사용한다는 점이다.
Connection close()의 진짜 의미
여기서 중요한 점이 있다.
DBCP 환경에서 connection.close()는 실제 TCP 연결 종료가 아니다.
실제로는:
Pool에 Connection 반환(return)
의 의미를 가진다.
즉:
- close() 호출 → Pool로 반환
- 실제 TCP 연결 유지 → 다음 요청에서 재사용
이 된다.
반환하지 않으면 발생하는 문제
만약 application에서:
- connection 반환 누락
- 예외 발생 후 close() 미호출
- 비정상 종료
등이 발생하면 어떻게 될까?
Pool의 connection이 계속 점유된 상태로 남게 된다.
그러면:
새로운 요청
→ Pool에 사용 가능한 Connection 없음
→ 새로운 Connection 생성 시도
→ MySQL max_connections 초과
→ Too many connections 발생
문제가 발생할 수 있다.
Sleep 상태가 많으면 무조건 문제일까?
반드시 그렇지는 않다.
MySQL에서 Sleep 상태는 일반적으로:
현재 쿼리를 수행하지 않고 대기 중인 idle connection
을 의미한다.
DBCP는 재사용을 위해 idle connection을 유지하기 때문에,
일정 수준의 Sleep connection은 정상이다.
다만 아래 상황에서는 문제가 된다.
- connection 반환 누락
- pool 크기 과다 설정
- 트래픽 급증
- 장시간 idle connection 유지
이런 상황이 겹치면 Sleep connection이 비정상적으로 증가하게 된다.
MySQL 주요 설정
max_connections
MySQL이 동시에 허용할 수 있는 최대 connection 수이다.
예를 들어:
max_connections = 600
이라면 최대 100개의 client 연결만 허용한다.
실제 운영 상황 예시
A 서버만 있을 때
A 서버
→ Connection 100개 사용
문제 없음.
B 서버 증설
트래픽 증가로 인해 서버를 추가했다고 가정하자.
A 서버 → 100개 사용
B 서버 → 추가 요청 발생
하지만 이미 MySQL의 최대 connection 수가 가득 찬 상태라면:
Too many connections
오류가 발생하게 된다.
즉, 서버를 늘려도 DB의 connection 제한은 그대로이기 때문에 병목이 발생할 수 있다.
wait_timeout
wait_timeout은 일정 시간 동안 아무 요청이 없는 idle connection(Sleep 상태)을 MySQL이 언제 끊을지를 결정하는 값이다.
예를 들어:
wait_timeout=60
이라면 60초 동안 사용되지 않은 connection은 MySQL이 종료한다.
주의할 점
만약 DBCP가 connection을 계속 재사용하려고 하는데,
MySQL이 먼저 connection을 끊어버리면 어떻게 될까?
예를 들어:
59초 동안 idle
→ 갑자기 요청 발생
→ DB는 이미 60초 시점에 connection 종료
→ application 사용 시 exception 발생
문제가 발생할 수 있다.
DBCP 주요 설정
(HikariCP 기준)
minimumIdle
Pool에서 유지할 최소 idle connection 수
즉:
항상 최소 몇 개는 대기 상태로 유지할 것인가
를 의미한다.
maximumPoolSize
Pool이 가질 수 있는 최대 connection 수
여기에는:
- idle
- active
모두 포함된다.
즉:
idle + active <= maximumPoolSize
이다.
maximumPoolSize를 크게 하면 좋은 걸까?
반드시 그렇지는 않다.
connection 수가 너무 많아지면:
- DB Thread 증가
- Context Switching 증가
- Lock 경합 증가
- 메모리 사용량 증가
문제가 발생할 수 있다.
즉, 무조건 크게 설정하는 것이 아니라 적절한 값을 찾는 것이 중요하다.
minimumIdle과 maximumPoolSize
일반적으로:
minimumIdle = maximumPoolSize
형태를 많이 권장한다.
트래픽 변화에 따라 connection을 생성/삭제하는 비용을 줄일 수 있기 때문이다.
다만 시스템 특성에 따라 다르게 설정하기도 한다.
maxLifetime
Pool에서 connection을 재사용할 수 있는 최대 시간
중요한 점은:
DB가 connection을 끊기 전에
Pool이 먼저 안전하게 교체
하도록 설정하는 것이다.
따라서 일반적으로:
maxLifetime < wait_timeout
으로 설정한다.
connectionTimeout
Pool에서 connection을 얻기 위해 기다리는 최대 시간
예를 들어:
connectionTimeout = 3000ms
이라면:
- 3초 동안 connection 획득 대기
- 실패 시 exception 발생
한다.
적절한 Connection 수 찾기
정답은 없다.
결국 시스템 부하 테스트와 모니터링을 통해 찾는 수밖에 없다.
일반적인 튜닝 방법
1. 모니터링 환경 구축
확인해야 할 것:
- DB CPU
- DB Memory
- 서버 Thread 수
- DBCP 상태
- Active / Idle Connection 수
2. 부하 테스트 진행
예:
- nGrinder
- JMeter
등을 사용한다.
3. 핵심 지표 확인
대표적으로:
- TPS
- RPS(Request Per Second)
- 평균 응답 시간(avg response time)
등을 확인한다.
4. 병목 지점 확인
Connection 수를 늘렸을 때:
- DB CPU 급증
- 응답 시간 증가
- lock 증가
등이 발생하는지 확인해야 한다.
참고
3-way handshake
SYN 전송: Client -> Server 연갈하고 싶어요
SYN + ACK 전송: Server -> Client 요청 잘 받았고, 나도 연결 가능해요
ACK 전송: 좋아요, 이제 연결합시다
4-way handshake
FIN 전송: Clinet -> Server 난 이제 보낼 거 없습니다.
ACK 전송: Server -> Client 알겠어요
FIN 전송: SERVER -> CLIEN 나도 이제 끝났습니다.
ACK 전송: Clinet -> Server 확인했습니다 연결 종료