AWS RDS Aurora MySQL Writer/ReadOnly 인스턴스 분기처리 테스트

2022. 10. 7. 18:26·AWS

 AWS RDS Aurora Mysql (1 writer +1 reader) 을 사용하는데 writer instance에서 cpu 사용률이 99% 를 보이며 매우 느리게 (죽지는 않음) 처리되는 현상에 대한 해결책을 검토하였다.

 

일단 ReadOnly 인스턴스의 경우에는 Writer가 99%인 상황에서도 0에 수렴하는 사용률(실제로 사용이 안되고 있으나 내부적으로 replication 은 동작)이 보였기 때문에 서비스단에서 CUD에 대한 처리는 Writer, R은 Reader에서 처리하는 부하분산에 대한 검토 요청이 있었다.

 

검토에 앞서 RDS Aurora MySQL 클러스터의 아키텍처를 보면 (1 writer + 1 reader 기준)

서로 다른 가용영역에 writer 와 reader 인스턴스가 생성이 되고 writer에서 reader로 데이터복제가 지속되고 있다가, writer가 down 되었을 경우 60~120초 사이에 기존의 reader가 writer로 승격이 되고 down 되었던 기존 writer는 다시 올라올때 reader로 변경이되는 구조이다. 

즉 부하의 로드밸런싱 보다는 failover에 초점이 맞춰져 있는 아키라 볼수 있다. 

 

하지만 AWS에 비싼 비용을 지불하고 있기도 하고, (동일 스펙에서) 최대 5배의 성능을 보일수 있다는 RDS 소개 문구가 현재 기준에서의 최적화에 대한 검토로 이어지게 된것 이었다. 

 

이에 우선 검토한 대상은 아래 블로그를 토대로 한 Mariadb connector/J와 AWS MySQL JDBC의 사용가능여부였는데

https://linux.systemv.pe.kr/aws-aurora-endpoint-and-was-connection-pool/
 

AWS Aurora Endpoint and WAS Connection Pool - Voyager of Linux

AWS Aurora Endpoint 와 WAS Connection Pool 를 위한 JDBC 드라이버, FailOver 시 고려해야할 사항에 대해서 포스팅 해보았습니다. Aurora를 위한 드라이버로 MariaDB Connector/j 를 사용하시면 기본설정만으로도 충

linux.systemv.pe.kr

https://hoing.io/archives/1285

 

AWS JDBC Driver for Amazon Aurora MySQL

안녕하세요 이번 포스팅에서는 Amazon AWS 에서 새롭게 출시한 Java의 MySQL 접속 드라이버인 AWS JDBC Driver 에 대해서 확인 해보려고 합니다. 드라이버의 특징AWS JDBC Driver 는 MySQL 8.0.28 community driver 를

hoing.io

위 블로그에도 나와있지만 두 개 모두 어떠한 문제들이 있어 적용이 불가한 것으로 결론 지었다.

1. mariadb 는 공식 드라이버가 아니고 3.x 버전부터는 aurora 에 대한지원이 되지 않는다.

오픈소스라서 db 패치등을 꼭 해야되는 상황( AWS는 기본적으로 수동패치이나 간혹 이러한 패치들이 쌓이면 지정된 날짜까지 패치를 진행하도록 하고 안할 경우 AWS에서 자체적으로 패치를 해버리는 상황)에서 문제가 발생하면 오픈 소스 드라이버에서는 대응방안을 마련하기가 어려운 부분이 있다. 

2. aws-mysql-jdbc 에서는 현재는 read/write split을 공식 지원하고 있지 않다. 

pull request로 해당 plugin이 올라와 있는 상태이나 반영 시점은 알수없으니 적합하지 않았다.

https://github.com/awslabs/aws-mysql-jdbc/pull/272

 

Add Read-Write Splitting Plugin by congoamz · Pull Request #272 · awslabs/aws-mysql-jdbc

Summary Implement Read-Write Splitting Plugin Additional Reviewers @sergiyv-bitquill @karenc-bq

github.com

 

이에 제 3안으로 hikari master와 slave를 통한 dynamic routing datasource 를 구현하여 테스트 하였는데

(구현은 아래 블로그 참조)

https://cheese10yun.github.io/spring-transaction/

 

Spring 레플리케이션 트랜잭션 처리 방식 - Yun Blog | 기술 블로그

Spring 레플리케이션 트랜잭션 처리 방식 - Yun Blog | 기술 블로그

cheese10yun.github.io

분산은 되었으나 또 예상치 못한 문제가 발견되었다.

1. @Transactional 패키지

https://interconnection.tistory.com/123

 

어떤 @Transactional을 사용해야 할까?

애플리케이션을 개발하다 보면, 보통 @Transactional을 사용해서 Transaction을 사용합니다. 관습적으로 사용하다 보니, 내부적으로 어떻게 돌아가는지 원리에 대해서만 관심을 가졌습니다. 하지만 여

interconnection.tistory.com

위 블로그에서도 중요한 부분은 이부분이다.

  • @javax.transaction.Transactional은 TransactionType, rollback 정도로 구현되어있습니다.
  • @org.springframework.transaction.annotation.Transactional은propagation, isolation, timeout, readOnly, rollback으로 구성되어있습니다.

분산처리를 위해서는 readOnly 어트리뷰트를 사용해야 하는데 javax.transaction.Transactional에서는 사용이 되지 않아 

전혀 분산처리가 되지 않고 에러도 나지 않아서 찾는데 오래걸렸다. 

변경이후 분산은 되었지만 또 다시

JPA Listener에서 @PostPersist 등의 처리를 하고 있었는데 함께 사용할때 NullPointer가 발생했다.

(원인을 찾는 중에 처리 방향이 바뀌어 확실친 않지만 javax 와 혼용 사용으로 인한것이지 않을까 생각만 해본다)

read와 writer가 명확하게 구분이 되는 application이 아닐경우 나오는 문제가 여기서 보인다.

1. 수정/개발 공수증가

aop를 구현하거나 @Transactional(readOnly=true) 에 대한 추가 및 config 구성은 기본이고 

이를 적용한 이후 발생하는 문제들에 대해 또다시 원인파악을 해서 수정해야하는 공수가 들어간다는것

2. 부하분산은 되었지만 안정성이 보장이 되지도 않는다.

만약 read 또는 writer 가 down되었을 경우 정상적인 동작이 될까?

 

 

결론

reader인스턴스의 replica를 추가하여 3중화를 구성더라도 writer 1개, reader 1~2개의 환경에서 최악의상황을 가정하여 1개 인스턴스만 남더라도 시스템이 안정적으로 구동될 수 있는 환경을 만드는 것으로 방향이 선회 되었다.

scale up을 통해 writer/reader 의 리소스를 두배로 늘리고 다시 검토하는 것으로 이번 검토를 마치게 되었다.

 

++ 이하는 hikari master/slave config 

// DataSourceConfig.java

package com.split.common.context;

import java.util.HashMap;
import java.util.Map;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import com.zaxxer.hikari.HikariDataSource;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class DataSourceConfig {

    private final Environment env;
    
    public static final String MASTER_DATASOURCE = "masterDataSource";
    public static final String SLAVE_DATASOURCE = "slaveDataSource";

    @Bean(MASTER_DATASOURCE) 
    @ConfigurationProperties(prefix = "spring.datasource.master.hikari") // (1)
    public DataSource masterDataSource() {
        return DataSourceBuilder.create() 
            .type(HikariDataSource.class) 
            .build();
    }

    @Bean(SLAVE_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }

    @DependsOn({MASTER_DATASOURCE, SLAVE_DATASOURCE})
    @Bean
    public DataSource routingDataSource(
        @Qualifier(MASTER_DATASOURCE) DataSource master,
        @Qualifier(SLAVE_DATASOURCE) DataSource slave) {
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<>();

        dataSourceMap.put("master", master);
        dataSourceMap.put("slave", slave);

        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(master);

        return routingDataSource;
    }

    @DependsOn({"routingDataSource"})
    @Bean
    public DataSource dataSource(DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.split");

        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.physical_naming_strategy",
            SpringPhysicalNamingStrategy.class.getName());
        properties.put("hibernate.implicit_naming_strategy",
            SpringImplicitNamingStrategy.class.getName());
        properties.put("hibernate.hbm2ddl.auto", env.getProperty("spring.jpa.hibernate.ddl-auto"));
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Primary
    @Bean
    public PlatformTransactionManager transactionManager(
        EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);

        return transactionManager;
    }
}
//DynamicRoutingDataSource.java

package com.split.common.context;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;


public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() { // (1)
        return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master"; //(2)
    }
}
// application.yml

spring:
  profiles: default
  main:
    allow-circular-references: true
    allow-bean-definition-overriding: true
  datasource:
    master:
      hikari: 
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:33060/split?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&enabledTLSProtocols=TLSv1.2
        username: split
        password: splitPwd99!
        read-only: false
        maximum-pool-size: 100
        connection-timeout: 50000
        connection-init-sql: SELECT 1
        validation-timeout: 5000
        minimum-idle: 50
        idle-timeout: 600000
        max-lifetime: 1800000 #30min
    slave:   
      hikari:
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:33061/split?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&enabledTLSProtocols=TLSv1.2
        username: split
        password: splitPwd99!
        read-only: true
        maximum-pool-size: 100
        connection-timeout: 50000
        connection-init-sql: SELECT 1
        validation-timeout: 5000
        minimum-idle: 50
        idle-timeout: 600000
        max-lifetime: 1800000 #30min
  jpa:
    database: mysql
    generate-ddl: true
    show-sql: true
    properties:
      hibernate:
        show_sql: false
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
        naming:
          physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    hibernate:
      ddl-auto: none
      use-new-id-generator-mappings: false

 

추가

aws-mysql-jdbc를 사용하게 되면서 failover 시간(정확하게는 failover 이후 DB 복구까지의 시간)에 대한 검증을 해보았다.

@Scheduled로 1초마다 db에 시간을 기록하는 잡을 돌린 상태에서 writer인스턴스를 재기동하여 측정하였더니

어느 블로그에서 10초이내에 대한이야기도 하는것 같았는데, 직접 테스트 해보니 약 20초정도의 시간이 걸렸다.

이것도 실제 운영환경에서는 어떨런지 모르겠으나, 일단 공식적으로 60~120초 이내 정상복구이니 (저번 테스트에서는 100초) 방어적으로 이것을 기준으로 하는것이 좋겠다.

 

//pom.xml
<dependency>
    <groupId>software.aws.rds</groupId>
    <artifactId>aws-mysql-jdbc</artifactId>
    <version>1.1.1</version>
</dependency>

//application.yml
spring:
  profiles: prd
  main:
    allow-circular-references: true
    allow-bean-definition-overriding: true
  datasource:
    driver-class-name: software.aws.rds.jdbc.mysql.Driver
    url: jdbc:mysql:aws://${MYSQL_URL}/test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&enabledTLSProtocols=TLSv1.2&allowMultiQueries=true
    username: ${MYSQL_USERNAME}
    password: ${MYSQL_PASSWORD}
  jpa:
    database: mysql
    generate-ddl: true
    show-sql: true
    properties:
      hibernate:
        show_sql: false
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
        naming:
          physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    hibernate:
      ddl-auto: update
      use-new-id-generator-mappings: false
  mvc:
    hiddenmethod:
      filter:
        enabled: true
728x90

'AWS' 카테고리의 다른 글

DOP-C02 다이어그램3  (0) 2025.02.24
DOP-C02 다이어그램 2  (0) 2025.02.23
DOP-C02 다이어그램1  (0) 2025.02.23
AWS TechCamp - 1 일차 오전  (0) 2024.11.27
AWS EKS 에 CloudWatchAgent 배포  (3) 2022.09.21
'AWS' 카테고리의 다른 글
  • DOP-C02 다이어그램 2
  • DOP-C02 다이어그램1
  • AWS TechCamp - 1 일차 오전
  • AWS EKS 에 CloudWatchAgent 배포
yunapapa
yunapapa
working on the cloud
    250x250
  • yunapapa
    supermoon
    yunapapa
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • 개발 (20)
        • java (17)
        • web (2)
        • MSX (1)
        • Go (0)
      • CloudNative (50)
        • App Definition & Developeme.. (17)
        • Orchestration & Management (4)
        • Runtime (3)
        • Provisioning (7)
        • Observability & Analysis (14)
        • event review (5)
      • AWS (7)
      • 환경관련 (17)
      • 취미생활 (0)
        • 맛집 (0)
        • 게임 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • CNCF Past Events
    • Kubernetes Korea Group
  • 공지사항

  • 인기 글

  • 태그

    Java
    티스토리챌린지
    istio
    devops
    gitlab
    kubernetes
    dop-c02
    APM
    Pinpoint
    OpenShift
    springboot
    helm
    k8s
    오블완
    AWS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
yunapapa
AWS RDS Aurora MySQL Writer/ReadOnly 인스턴스 분기처리 테스트
상단으로

티스토리툴바