Error: 1040-08004: Too many connections
개발 서버로 운영하던 서버에 어느날부터 위와 같은 로그가 찍히기 시작했다. 비용 절감을 위해 AWS RDS 인스턴스 클래스를 하향 조정하면서 DB의 최대 커넥션 개수가 적어진 탓이었다. 기존에 안일하게 기본 설정으로 쓰고 있던 부분을 최적화하여 최소 사양에도 문제없이 서버를 운영하는 방법을 알아보자.
최대 커넥션 개수 확인하기
SHOW VARIABLES LIKE 'max_connections';
위 쿼리를 통해서 DB에서 허용하는 최대 커넥션 개수를 확인할 수 있다. 그런데 사실 쿼리를 날리기 위해서도 커넥션 연결이 최소 하나는 필요하다.
Too many connections
에러가 발생하고 있는 상황에서는 위 쿼리를 요청하기도 어려울 수 있는 것이다. 따라서 이럴 경우 커넥션을 물고 있는 애플리케이션을 잠시 종료하거나 임시로 RDS 인스턴스 클래스를 올리도록 하자. 인스턴스 클래스를 변경하면 변경이 완료되는 데에 10분정도 소요된다.

조회 시 위와 같은 결과를 확인할 수 있다. 기본 설정이 MySQL은 최대 60개, MariaDB는 최대 30개의 커넥션을 연결할 수 있도록 되어 있다. 둘은 동일하게
db.t3.micro
클래스이다.왜 최대 커넥션 개수가 다를까?
같은 인스턴스 클래스이기에 사양은 동일하다. 그런데 기본 최대 연결 수가 2배나 차이나는 이유는 무엇일까? 우선 최대 연결 수가 어떻게 결정되는 지 확인해보자.
// Default for all MariaDB and MySQL versions except for MariaDB version 10.5 and 10.6
{DBInstanceClassMemory/12582880} // MB/12
// Default for MariaDB version 10.5 and 10.6
LEAST({DBInstanceClassMemory/25165760},12000) // MB/25
MySQL과 MariaDB의 기본 설정은 위와 같다. DB 인스턴스 메모리를 12 혹은 25 나눈 값이 최대 연결 개수가 된다. 양쪽의
db.t3.micro
인스턴스 클래스의 메모리는 1GB로 동일하다. 그런데 MariaDB의 분모값이 MySQL의 약 2배 정도이기에 위와 같은 차이가 발생하는 것이다. 다른 데이터베이스 엔진의 기본값은 아래 링크에서 확인할 수 있다. 그런데 위 공식에 따른다 하더라도 실제로 계산해보면 약 85개(1024/12)로 DB에서 확인해본 60개와는 차이가 있다. 이에 대해 AWS는 다음과 같이 설명한다.
Because of memory reserved for the operating system and RDS management processes, this memory size is smaller than the value in gibibytes (GiB) shown in Hardware specifications for DB instance classes. When a MariaDB or MySQL DB instance is running on a small DB instance class, such as db.t3.micro or db.t3.small, the total memory available is low. For these DB instance classes, RDS reserves a significant portion of the available memory, which affects the valuemax_connections
. For example, the default maximum number of connections for a MySQL DB instance running on a db.t3.micro DB instance class is approximately 60. You can determine themax_connections
value for your DB MariaDB or MySQL DB instance by connecting to it and running the following SQL command:
총 메모리에서 운영체제와 RDS 관리 프로세스용으로 예약된 메모리로 인해 실제 기본 최대 연결 개수가 줄어든다는 것이다.
커넥션 개수 조정하기
그렇다면 이번에는 커넥션 개수를 조정해서
Too many connection
이슈를 해결해보도록 하자.RDS 최대 커넥션 개수 조정
기본적으로 RDS의 최대 커넥션 개수를 조정할 수 있다. 여기에는 임시로 변경하는 방법과 영구적으로 변경하는 방법이 있다.
- 임시로 변경
SET GLOBAL max_connections = <<vallue>>;
위와 같이 변경하면 RDS가 재부팅될 때 설정이 다시 변경되니 주의해야 한다.
- 영구적으로 변경

AWS RDS에서 기본 설정은 DB 인스턴스 파라미터 그룹에서 할 수 있다. 처음 RDS를 생성할 때 기본값으로 하고 넘어가는데, 여기서 파라미터 그룹을 따로 생성하여 설정하면 기본 설정 값을 변경 할 수 있다. 변경 후 DB를 재기동하면 변경된 값이 반영된다.

위는
default.mariadb 10.11
의 파라미터 그룹이다. AWS 설명에 의하면 10.5와 10.6 외에는 계산 방법이 MySQL과 같다고 하였으나 실제로 그렇지 않다는 것을 확인할 수 있다. 그런데 위에서도 살펴보았지만 최대 커넥션 수는 DB 인스턴스의 리소스를 고려하여 지정된 값이다. 이 값을 과하게 초과하여 설정할 경우, 메모리 부족으로 제대로 커넥션이 생성되지 않을 수 있다. 따라서 가급적 DB 인스턴스 기본 설정 값을 따르고 거기에 애플리케이션 커넥션을 맞추는 방법이 좋을 것 같다.
애플리케이션 커넥션 개수 조정
사내에서 사용하는 Spring Boot와 HikariCP 기준으로 살펴본다.
spring.datasource.hikari.maximum-pool-size: 5
HikariCP는 풀에 커넥션을 미리 만들어 놓는다. 따라서 풀 사이즈를 조정함으로 커넥션 개수를 조정할 수 있다. 여기서 유휴 커넥션 최소 값을 지정하는
minimum-idle
옵션도 있지만 최대 풀 사이즈와 동일하게 설정하는 것이 권장 사항이므로 따로 설정하지 않는다.maximum-pool-size
의 기본값은 10이며, RDS 최대 커넥션 개수와 RDS에 연결될 애플리케이션들의 개수 및 필요 커넥션 개수를 고려하여 지정하도록 한다.이때 로컬 환경에서 연결할 것도 고려해야 한다. 개발 DB를 로컬에서 연결하여 개발하는 경우가 많은데 로컬에서 애플리케이션을 실행할 때 연결 할 수 있도록 여유분을 남겨놓는다.
커넥션 개수를 몇 개로 해야 할까?
애플리케이션 동작 프로세스 상 필요한 커넥션 개수가 제한될 수도 있다. (특정 수의 작업을 한 번만 실행하는 스케줄 작업 등) 하지만 그외의 경우, 커넥션 개수를 늘리면 성능도 올라갈까? 그렇다면 제한된 리소스 내에서 최대의 커넥션 개수만 설정하면 될까?

커넥션 개수와 성능에 대해 테스트한 영상을 확인해보자.
단순히 연결 풀 크기를 줄이는 것만으로도 애플리케이션의 응답 시간이 ~100ms에서 ~2ms로 단축되어 성능이 50배 이상 향상되었다. 처음 생각과는 달리 커넥션 개수가 줄어들수록 성능이 올라간 것이다.
이는 멀티 스레딩이 이루어지는 원리를 생각해보면 알 수 있다. 멀티 스레딩은 스레드 여러 개가 동시에 일하는 것 같지만 스레드 수가 CPU 코어 수를 초과하면 코어는 빠르게 스레드를 바꿔가며 실행한다. 이렇게 컨텍스트 스위칭이 일어나는데 이 과정에서 속도가 느려지는 것이다.
물론 멀티 스레드로 처리하는 것이 더 빠를 때도 있다.
보통 데이터베이스의 주요 병목 지점은 CPU, 디스크, 네트워크이다. 특히 디스크가 HDD인 경우, 물리적인 한계로 인해 읽기/쓰기 시간이 오래 걸리고 스레드는 Blocking 된다. 이때 CPU가 다른 스레드를 실행하면 스레드 수가 코어 수보다 더 많아도 많은 작업을 실행할 수 있는 것이다.
하지만 이 역시 SSD를 사용할 경우, 읽기/쓰기 시간이 매우 적기 때문에 Blocking으로 인한 멀티 스레딩의 기회가 거의 없다. 따라서 스레드 수가 코어 수와 가까울 수록 더 나은 성능을 보여준다.

AWS RDS를 확인해보자. RDS는 스토리지 선택 시 인스턴스에 따라서 마그네틱 옵션을 선택할 수 있는 경우가 있다. 하지만 대부분의 경우에는 SSD를 사용할 것이다.
커넥션 수를 결정하는 데에 다음과 같은 공식이 있다.
A formula which has held up pretty well across a lot of benchmarks for years is that for optimal throughput the number of active connections should be somewhere near ((core_count * 2) + effective_spindle_count). Core count should not include HT threads, even if hyperthreading is enabled. Effective spindle count is zero if the active data set is fully cached, and approaches the actual number of spindles as the cache hit rate falls. ... There hasn't been any analysis so far regarding how well the formula works with SSDs.
connections = ((core_count * 2) + effective_spindle_count)
하지만 이 역시 SSD 환경에서는 확인되지 않았기에 테스트가 필요하다.
그럼 RDS의 코어 수를 확인해보자.
db.t3.micro
클래스의 vCPU는 2개이다. 물리적 CPU는 여기의 반이므로 실제 코어 수는 1개인 것이다. 그럼 위의 공식으로 생각해볼 때 풀 사이즈는 2~3개가 되며 SSD 환경임을 고려하면 그것보다 더 적게 설정해야 할 것이다.여기서 DB에 연결하는 서버가 1개 이상임을 고려하면 서버 1개당 할당할 커넥션은 1~2개 밖에 되지 않는다. 아무리 성능이 더 뛰어나다고 하더라도 슬로우 쿼리라도 하나 날린다면 바로 문제가 생길 것 같다. 이에 대해 HikariCP에서는 다음과 같은 해결책을 제시하고 있다.
Pool sizing is ultimately very specific to deployments.For example, systems with a mix of long running transactions and very short transactions are generally the most difficult to tune with any connection pool. In those cases, creating two pool instances can work well (eg. one for long-running jobs, another for "realtime" queries).
슬로우 쿼리용 풀을 따로 만들라는 것이다. 어쨌든 풀 사이즈는 생각보다 훨씬 작게 만드는 것이 좋다.
Share article