CS/데이터베이스

DB Server의 CPU 사용률이 높은 상황 - 3

잼문 2025. 5. 26. 00:26

커넥션 수의 과다로 인한 성능 저하와 그 원인

MySQL은 기본적으로 Thread per Connection 모델을 사용합니다. 이 말은 하나의 커넥션이 생성될 때마다 별도의 스레드가 생성된다는 뜻입니다.

실제 요청을 처리하는 스레드는 별도의 Thread Pool이 아니라, 커넥션마다 1:1로 매핑된 독립 스레드를 의미합니다. 따라서 애플리케이션이나 사용자로부터 동시에 많은 커넥션이 유입된다면, 그 수만큼 스레드가 생성되며 시스템 리소스 소모가 눈에 띄게 증가하게 됩니다.

커넥션 수가 과도해진다면?

가장 먼저 발생하는 문제는 스레드 생성 및 관리 비용입니다. 수백 수천개의 커넥션이 들어오면 동일한 수의 스레드가 생성됩니다. 이는 OS 차원에서 모두 스케줄링 대상입니다. 이로 인해 CPU는 모든 스레드를 조금씩 번갈아가며 실행해야합니다.

이 과정에서 컨텍스트 스위칭(Context Switching)이 빈번하게 발생합니다. 컨텍스트 스위칭은 단순히 스레드를 교체하는 작업이 아닌 CPU의 레지스터 상태 저장과 복원, 캐시 플러시 등을 포함합니다. 이 때문에 상당한 오버헤드가 동반됩니다.

특히 CPU 캐시는 스레드 전환 시 대부분 무효화되므로, 동일한 연산을 하더라도 캐시 미스로 인해서 처리 효율이 급격히 저하되며, 이로 인해 CPU 사용률이 급증하게 됩니다.

스케줄링뿐만 아니라, 큐잉 현상도 심각한 병목의 원인입니다.

MySQL에서 각 커넥션은 쿼리를 실행하는 동안 특정 자원인 테이블, 인덱스, row에 락을 획득하게 됩니다. 사실 MySQL에서는 동시성을 높이기 위해 단일 row에만 락을 걸긴 합니다. 특정 상황에서 락 범위를 넓힐 수는 있습니다. 그런데 동일한 자원에 여러 커넥션이 동시에 접근하게 되면 락 경합(Lock Contention)이 발생하고, 락을 얻지 못한 나머지 커넥션들은 대기 상태로 전환됩니다.

이때 대기 중인 스레드들은 내부적으로 Mutex, Semaphore, Read-Write Lock을 활용해 큐에 진입하게 됩니다. 그 이후 해당 락이 해제될 때까지 기다립니다.

예를 들어, 1000개의 커넥션 중 50개가 동시에 같은 행에 대해 Update를 시도한다고 가정한다면, 이 경우 InnoDB의 row-level locking 메커니즘이 작동하면서 단 하나의 커넥션만 락을 획득하고 나머지 49개는 대기 큐로 들어갑니다.

이 과정에서 락 대기중인 스레드는 spin lock, sleep, wake-up등의 과정을 거칩니다. 이는 모두 CPU의 개입이 필요한 작업이며 불필요한 리소스 소모로 이어집니다. 락 대기가 늘어날수록 처리량은 급격히 떨어지고,응답 시간도 비례해 증가합니다.

또한, 커넥션이 많다는 것은?

단순히 스레드가 많다는 의미만은 아닙니다. 실제 실행하는 스레드가 많다는 것이고 쿼리가 많다는 것입니다. DB 내부에서 락 경합이 더 빈번하게 일어나고, 결국 전체 시스템의 처리량인 TPS도 감소하게 될 것입니다.

해당 문제를 해결하기 위해선?

이런 문제를 해결하기 위해 가장 먼저 고려할 것은 애플리케이션 레벨에서 커넥션 수를 적절히 제어하는 것입니다. 예를 들어 Node.js에서 mysql2나 mysql 모듈을 사용할 경우, Pool을 설정해 커넥션을 재사용하는 것입니다.

커넥션 풀은 애플리케이션이 필요 이상으로 커네션을 생성하지 못하게 하며, 동일한 커넥션을 여러 요청에서 재사용할 수 있게 합니다. 이 방식만으로도 커넥션 수를 수십에서 수백개 수준으로 제한시켜, Thread per Connection으로 인한 오버헤드를 대폭 줄일 수 있습니다.

MySQL 8.0 이상에서는 Thread Pool Plugin을 활용하는 것도 효과적이다. 이 플러그인을 사용하면 더 이상 커넥션 수만큼 스레드가 생성되지 않고, 하나의 스레드 풀이 여러 커넥션 요청을 스케줄링하여 처리하게 된다. 이 방식은 실제 처리 가능한 스레드 수를 줄이고 스레드 전환 오버헤드를 완화해 준다. 단, 이 기능은 기본적으로 비활성화되어 있으며, Enterprise Edition에서는 기본 지원되나 Community Edition에서는 직접 빌드하거나 외부 플러그인을 사용해야 할 수 있다.

또한 MySQL 설정 중 max_connections 값을 현실적인 범위 내에서 보수적으로 설정하는 것도 중요하다. 가능한 동시 커넥션 수를 무제한으로 열어두면, 스레드가 폭발적으로 증가하고 CPU가 더는 감당할 수 없을 만큼의 부하가 걸릴 수 있다. 실제 시스템의 처리 능력에 기반하여 커넥션 수를 제한하고, 초과하는 요청은 Queue나 Circuit Breaker 방식으로 제어하는 것이 바람직하다.

이 외에도 동시에 실행되는 쿼리 중 처리 시간이 오래 걸리는 쿼리가 있다면, 이들은 스레드를 장시간 점유하게 되므로 전체 처리 성능을 더욱 저하시킨다. 이런 경우 Slow Query Log를 통해 오래 걸리는 쿼리를 식별하고, 이들을 우선적으로 튜닝하여 실행 시간을 단축시켜야 한다. 튜닝이 어려운 쿼리는 비동기 처리나 배치 전환을 고려해볼 수도 있을 것입니다.