본문 바로가기

DB/AWS Aurora DB

[WIL] AWS RDS Aurora Cluster의 Read/Writer 연결 분리 이력 - 3가지

요약

  • 수동: 수동으로 DataSource 분기를 해서 사용하기
    • AbstractRoutingDataSource
    • LazyConnectionDataSourceProxy
  • 자동 (라이브러리에 맡기기)

0. MySQL Connector/J

1. AWS JDBC Driver for MySQL

Kotlin / Spring 기반으로 신규 서비스 프로젝트 코드를 구성할 때 MySQL Connector/J 대신 AWS JDBC Driver for MySQL를 사용했습니다. 왜냐하면 후자가 failover 전환을 빠르게 할 수 있는 기능을 제공했기 때문입니다.

1.1.0 부터 시작해서 1.1.14 버전까지 사용했습니다. 1.1.15까지 마지막으로 릴리스한 버전입니다.

  • software.aws.rds:aws-mysql-jdbc:1.1.0 → 1.1.1 → 1.1.2  1.1.7  1.1.9  1.1.12  1.1.14

 

다만 아쉬웠던 점은 드라이버 자체가 Read 인스턴스와 Writer 인스턴스로 구성된 RDS 클러스트에 대하여 읽기/쓰기 분리기능을 제공하지 않았던 부분이었습니다.

이 드라이버는 읽기-쓰기 분리를 지원하지 않습니다.

 

그래서 인프랩의 JDBC setReadOnly 호출 이슈 해결기에 나오는 것처럼 수동으로 DataSource 분기하도록 구성하여 사용하는 경우가 있었습니다.

더보기
수동으로 분리

2. MariaDB Java Client

하지만 수동 설정을 위해서는 Reader 및 Writer 인스턴스의 정보를 이용해서 DataSource에 제공을 해주어야 하는데 failover가 발생하면 각각의 인스턴스가 서로 바뀌어(swap)버릴 수 있어서 의도하지 않은 동작이 이루어질 수 있다는 생각을 했습니다.

 

따라서 Cluster 정보만 설정으로 가지고 있고 Reader와 Writer의 정보를 드라이버가 가져와서 Read/Write 분리를 하는 것이 좋을 것 같았습니다.

그래서 3.x 버전이 나왔지만 구 버전인 MariaDB Java Client를 이용해서 Read/Write 분리가 되도록 프로젝트 DataSource 구성을 해서 한동안 사용했습니다.

 

동작 원리와 관련해서는 Hocaron님의 [Aurora DB] Aurora DB 는 읽기 전용 커넥션일 때 알아서 라우팅을 해준다고? 라는 글에 소스코드 수준으로 적혀있으므로 바퀴의 재발명을 하지 않기 위해 이 글에서는 자세히 다루지는 않겠습니다.

 

다만 MariaDB 드라이버는 3.x 버전은 AWS Aurora 지원을 더 이상하지 않습니다.

1.2.0 ~ 2.7.12까지만 사용이 가능하다.

 

또한 MySQL 8.x 버전 부터는 애플리케이션 가동 때마다 아래와 같은 경고가 표시되어 눈에 거슬립니다.

HHH000511: The 8.0.32 version for [org.hibernate.dialect.MariaDBDialect] is no longer supported, hence certain features may not work properly. The minimum supported version is 10.4.0. Check the community dialects project for available legacy versions.

이 오래된 드라이버를 대체할 다른 드라이버를 물색하다가 AWS Advanced JDBC Wrapper가 있다는 것을 알게 되었습니다.

3. AWS Advanced JDBC Wrapper

 

1번에서 봤던 AWS JDBC Driver for MySQL가 2024년 7월 25일 프로젝트 종료가 되었습니다.


위에 적혀있듯이 이제는 AWS Advanced JDBC Wrapper를 사용하라고 권장하고 있습니다.

래퍼(wrapper)라는 이름에서 알 수 있듯이 기존의 JDBC 드라이버를 보완하여 Amazon Aurora와 같은 클러스터 데이터베이스의 기능들을 잘 활용하기 위한 기능을 제공합니다.

따라서 이 라이브러리는 부가적인 기능을 제공하는 라이브러리이고 연동할 JDBC드라이버가 필요합니다.

현재 제공하는 드라이버는 PostgreSQL JDBC Driver, MySQL JDBC Driver, MariaDB JDBC Driver 등 세 가지입니다.

 

저는 Amazon RDS의 8가지 주요 엔진 중 MySQL용 Amazon RDS를 사용하고 있었기에 MySQL JDBC Driver와 같이 연동을 했습니다.

의존성 설정(build.gradle.kts)

dependencies {
    // ..
    runtimeOnly("com.mysql:mysql-connector-j")
    runtimeOnly("software.amazon.jdbc:aws-advanced-jdbc-wrapper:2.5.3")
}

 

애플리케이션 설정(application.yml)

spring:
  datasource:
    driver-class-name: software.amazon.jdbc.Driver
    url: jdbc:aws-wrapper:mysql://db-dev.cluster-c7oooloigb4y.ap-northeast-2.rds.amazonaws.com:3306/enterprise?autoReconnect=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Seoul
    hikari:
      username: dbusername
      password: dbpassword
      data-source-properties:
        wrapperPlugins: readWriteSplitting,failover2,efm2

 

애플리케이션 설정에는 두 가지 부분을 수정을 해주어야 합니다.

1. url 수정

JDBC 문자열인 url 부분에 접두어에 "aws-wrapper:" 들어갑니다.

기존에 "jdbc:mysql://"이나 Maria Driver를 썼더라면 "jdbc:mysql:aurora://" 이었을 부분이 "jdbc:aws-wrapper:mysql://"로 바꾸어줍니다. 드라이버의 종류(dialect) 파악하는 부분에 보면 aws-wrapper 부분은 제외하고 매핑을 하고 있습니다.

2. 데이터소스 프로퍼티에 wrapperPlugins 추가

AWS Advanced JDBC Wrapper는 플러그인 방식을 통해 기능을 넣고 뺄 수 있습니다.
Read/Write 분리 기능도 플러그인을 통해 가능합니다.

  • 플러그인 이름: readWriteSplitting

스프링에서는 기본적으로 Hikari Connection Pool을 사용하고 있습니다. 데이터 소스 프로퍼티를 Hikari에 넘기기 위해서는 HikariConfig의 dataSourceProperties 속성을 이용할 수 있습니다.

 

위에서는 failover2,efm2도 같이 추가를 해주었습니다.
failover로 발생하는 다운타임을 최소화하기 위한 장점을 살리기 위해서 입니다.
현재 시점에는 failover와 failover2 두 가지가 있습니다.

v2가 향상된 성능과 더 낮은 리소스의 장점을 가지고 있고, 실 테스트 했을 때도 1~2초 정도 더 빨리 전환이 되는 것으로 측정되어 failover2을 사용하기로 결정했습니다.

 

efm2 부분은 Host Monitoring Plugin v2에서 사용할 수 있는 기능인 Enhanced Failure Monitoring (EFM)을 의미합니다. 데이터베이스 노드의 상태 또는 가용성을 주기적으로 확인하여 비정상으로 판단되면 연결을 중단시키는 역할을 합니다. 위의 설정에서는 구체적으로 명시를 했지만 드라이버 2.3.3부터 기본적으로 활성화가 되기 때문에 따로 적지 않아도 기본 플러그인으로 동작합니다.


 

Maria DB Driver vs. AWS Advanced JDBC Wrapper

Maria DB 드라이버랑 AWS Advanced JDBC 래퍼와 동작이 다른 것을 확인했다.

전자는 Writer / Reader 인스턴스가 애플리케이션 처음 가동시 동일한 연결을 요청함을 확인했다.

후자는 Writer는 maximum-pool-size 설정만큼 올라가고 Reader는 연결이 추가로 되지 않았다. 이후 readonly 요청이 들어오면 그때 연결이 되는 것으로 확인했다.

 

Connection pool은 연결에 드는 비용을 사전에 미리(eager) 지불하고 필요시 드는 비용을 낮추는 것이 목적인데, 이렇게 동작한다면 커넥션 풀의 장점이 상쇄되는 것 아닌가 싶었다.

 

추가로 Limitations when using Spring Boot/Framework 부분에 적혀있듯이 Internal connection pools을 사용하지 않을 경우 성능 저하가 발생한다고 적혀있었다.

The use of read/write splitting with the annotation @Transactional(readOnly = True) is only recommended for configurations using an internal connection pool. Using the annotation with any other configurations will cause a significant performance degradation.
@Transactional(readOnly = True) 애너테이션으로 읽기/쓰기의 분리를 사용할 경우 내부 커넥션풀을 사용하는 경우에만 권장합니다. 그 외의 설정으로 애너테이션을 사용시 심각한 성능 저하가 발생할 수 있습니다.

 

스프링 부트 애플리케이션이고 애너테이션 방식으로 읽기/쓰기 분리를 하고 있었기에 어느정도 성능하는지 궁금했다.

쿼리가 무거운 API를 샘플링하여 버전별로 확인해 보았다.

버전 평균(ms) 5% 제외한 평균 최소(ms) 최대(ms)
기존 (MariaDB Driver) 275.6621429 254.7192857 206.67 525.33
변경 (Adv. JDBC Wrapper + R/W split plug-in) 1907.885682 587.8068182 318.35 29000
  6.921101541 2.307665148 1.540378381 55.20339596

 

최대/최소 5%를 제외한 평균을 보았을 때 2.3배 느려짐을 확인할 수 있었다.

용어의 정리

  • external connection pools: 스프링 등에 의해 제공하는 드라이브 외부의 커넥션풀
  • internal connection pools: 드라이버의 내부에서 동작하는 커넥션풀

AWS Advanced JDBC Wrapper의 소스코드를 보니 내부 커넥션 풀도 Hikra connection pool을 사용하고 있었다.