Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DB Replication 설정하기 #399

Closed
wannte opened this issue Sep 24, 2021 · 17 comments · Fixed by #426
Closed

DB Replication 설정하기 #399

wannte opened this issue Sep 24, 2021 · 17 comments · Fixed by #426
Assignees
Labels
BE 개선 New feature or request

Comments

@wannte
Copy link
Contributor

wannte commented Sep 24, 2021

이슈 설명

DB 가용성 증대와 단일 장애점 해소를 위한 Replication 도입
replication

@wannte wannte added 개선 New feature or request BE labels Sep 24, 2021
@wannte wannte changed the title DB Relication 설정하기 DB Replication 설정하기 Sep 27, 2021
@knae11
Copy link
Collaborator

knae11 commented Oct 1, 2021

PrimaryDB

  • /etc/mysql/my.cnf에서 replication 관련 설정
[mariadb]
server_id              = 1
log_bin                = /var/log/mysql/mysql-bin.log
expire_logs_days        = 10
log-basename            = primary1
binlog-format           = mixed
  • mysql 재시작

DB 들어가서 확인

MariaDB [(none)]> show variables like 'server_id';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id     | 1     |
+---------------+-------+
1 row in set (0.001 sec)
MariaDB [(none)]> FLUSH TABLES WITH READ LOCK;
MariaDB [(none)]> show master status;
+---------------------+----------+--------------+------------------+
| File                | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+---------------------+----------+--------------+------------------+
| primary1-bin.000005 |     4344 |              |                  |
+---------------------+----------+--------------+------------------+
1 row in set (0.000 sec)
  • dump 파일을 생성해서 replica DB에 보내주어야 함 (이 경우 replica DB와 연결되어 있으면 연결이 물려있어서 dump가 생성되지 않음)
  • unlock tables;

Replica DB

  • 새로운 EC2에 생성(db 설치하고 ip를 열어주는 설정도 해줌)
  • replica DB는 server-id 100번대 부터 진행 (팀내 설정)
CHANGE MASTER TO
  MASTER_HOST='master.domain.com',
  MASTER_USER='replication_user',
  MASTER_PASSWORD='bigs3cret',
  MASTER_PORT=3306,
  MASTER_LOG_FILE='master1-bin.000096',
  MASTER_LOG_POS=568,
  MASTER_CONNECT_RETRY=10;
  • CHANGE MASTER TO MASTER_USE_GTID = slave_pos
  • START SLAVE;

참고

@sjpark-dev
Copy link
Collaborator

sjpark-dev commented Oct 4, 2021

Spring Boot에서 Master-Slave 설정하기

기본적으로 Spring Boot는 하나의 DataSource만 등록되어 있는데,
설정을 통해 Master-Slave DataSource를 등록할 수 있다.
분기 처리는 @Transactional 또는 Spring AOP를 활용하는 방식이 있는데,
우리는 @Transactional 방식을 채택했다.
@Transactional일 때 Master DataSource를 사용하고, @Transactional(readOnly = true)일 때 Slave DataSource를 사용한다.

application.properties

spring.datasource.master.jdbc-url=jdbc:mariadb://{master ip:port}/gpu_is_mine?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.master.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.master.username=root
spring.datasource.master.password=password

spring.datasource.slave.jdbc-url=jdbc:mariadb://{slave ip:port}/gpu_is_mine?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.slave.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.slave.username=root
spring.datasource.slave.password=password

ReplicationRoutingDataSource.java

public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "slave" : "master";
    }
}

DataSourceConfig.java

@Configuration(proxyBeanMethods=flase_
public class DataSourceConfig {

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


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

    @Bean
    public DataSource routingDataSource() {
        var routingDataSource = new ReplicationRoutingDataSource();

        var dataSourceMap = new HashMap<>();

        DataSource slaveDataSource = slaveDataSource();
        DataSource masterDataSource = masterDataSource();

        dataSourceMap.put("master", masterDataSource);
        dataSourceMap.put("slave", slaveDataSource);
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }

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

참고 블로그 에서는

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.mudchobo.example.masterslave"})

의 많은 설정을 추가해주었지만, Auto-Configuration의 설정 부분을 고려하여 불필요하다 판단하였습니다. (@ConditionalOnMissingBean의 특징을 활용하여, 우리가 정의한 DataSource Bean이 있을 때는 기존의 설정말고 우리의 방식을 이용한다.)

기본적으로 @Autowired의 경우 type에 따라 Bean을 주입하는데, 여러 종류의 DataSource Bean이 등록되기 때문에, @Primary 어노테이션을 활용하여 진행했다.

Qualifier를 활용한 다른 방식

이 경우에는 직접 springContext의 bean을 가져오는 방식. CGLIB proxy생성이 필요 없기 때문에, 성능상의 이점을 위해 proxyBeanMethods=false 처리해준다. (실제 많은 많은 spring의 auto-configuration에서는 proxyBeanMethods=false로 설정되어있다.)

@Configuration(proxyBeanMethods = false)
public class DataSourceConfig {

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


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

    @Bean
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        var routingDataSource = new ReplicationRoutingDataSource();

        var dataSourceMap = new HashMap<>();

        dataSourceMap.put("master", masterDataSource);
        dataSourceMap.put("slave", slaveDataSource);
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }

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

참고

참고 블로그

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

Screenshot from 2021-10-06 17-45-33
Screenshot from 2021-10-06 18-05-51

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

WARN 21-10-06 20:59:48 [AnnotationConfigServletWebServerApplicationContext:591] - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Unsatisfied dependency expressed through method 'flywayInitializer' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flyway' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.flywaydb.core.Flyway]: Factory method 'flyway' threw exception; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSource' defined in class path resource [mine/is/gpu/config/DataSourceConfig.class]: Unsatisfied dependency expressed through method 'dataSource' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'routingDataSource' defined in class path resource [mine/is/gpu/config/DataSourceConfig.class]: Unsatisfied dependency expressed through method 'routingDataSource' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'masterDataSource' defined in class path resource [mine/is/gpu/config/DataSourceConfig.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'dataSource': Requested bean is currently in creation: Is there an unresolvable circular reference?

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

@Profile("prod|was1|was2")
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DataSourceConfig {

@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) 의 옵션을 넣어주면, 순환 참조 문제가 해결이 됨.

보다 이를 구조적으로 이해했으면, 최선의 방법을 사용했겠지만, 지금의 수준에서는 Circular 의존을 명확하게 이해하지는 못하겠음.

참고 자료

Remember a Spring Boot multi-data source circular reference problem
의존성을 가진 다중 DataSource의 순환 참조 오류 분석

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

실제 dev 서버로 올리고 작업했을 때, insert, select query 모두 slave db를 찌르는 문제 발생.

  • slave가 master보다 더 많은 data가 있는 상황 -> 해결 필요
  • 문제 이유 확인을 위해 debug 레벨의 로깅(dev), ReplicationRoutingDataSource부분 로깅 진행

@sjpark-dev
Copy link
Collaborator

순환 의존을 막기 위해서 @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) 이 설정이 필요했군요.

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
    private static final Logger logger = LoggerFactory.getLogger(ReplicationRoutingDataSource.class);
    public static final String DATASOURCE_KEY_MASTER = "master";
    public static final String DATASOURCE_KEY_SLAVE = "slave";

    @Override
    protected Object determineCurrentLookupKey() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();

        logger.info("This Transaction is " + isReadOnly);

        if (isReadOnly) {
            return DATASOURCE_KEY_SLAVE;
        }
        return DATASOURCE_KEY_MASTER;
    }
}

로깅을 진행했을 때,
Screenshot from 2021-10-06 22-58-58
의 요청에도 ReadOnly = true 로 로깅이 남는 상황 발생

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

Screenshot from 2021-10-06 23-05-19

POST https://dev-api.gpuismine.com/api/members
에 경우에는, 정상적으로 ReadOnly = false 로 작동

Job을 Post하는 부분의 Transaction이 ReadOnly지 확인 필요

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

Screenshot from 2021-10-07 08-44-06

하나의 api콜이 여러개의 @Transactional의 Service 메소드로 구성된 경우, 어떻게 로직이 처리되는 지 확인 필요

memberService.checkMemberOfServer(member.getId(), jobRequest.getGpuServerId()); 부분에서 ReadOnly=true 처리되는 데, 이런 경우 slave DB를 접근하는 것으로 보임

@sjpark-dev
Copy link
Collaborator

혹시 @EnableTransactionManagement 이 설정과 관련이 있을까요?

@wannte
Copy link
Contributor Author

wannte commented Oct 6, 2021

혹시 @EnableTransactionManagement 이 설정과 관련이 있을까요?

이 부분도 혹시 몰라서 지금은 넣은 상태입니다! 근데, 안돼..

featrue/db-replication 브랜치에서 확인 가능합니다!

@wannte
Copy link
Contributor Author

wannte commented Oct 8, 2021

Screenshot from 2021-10-08 16-13-43
OpenEntityManagerViewInterceptor 는 jpa.open-in-view가 true로 설정되기 때문에 Bean으로 등록된다. 이를 False로 설정하게 되면, 하나의 @Transactional별로 요청을 진행하게 된다.

Screenshot from 2021-10-08 17-04-09
Transactional의 readOnly 설정에 따라 master, slave db로 연결을 진행하며,

show global status like "Com_select"; , show global status like "Com_insert"; 를 mysql에서 select, insert query를 확인할 수 있었다.

JPA의 default 세팅으로는 하나의 응답이나, View의 렌더링되기 까지, 영속성 컨텍스트가 유지된다.

api별로 Master, Slave 의 요청을 결정하는 것 vs Transaction 별로 Master, Slave의 요청을 결정하는 것.

우선 지금의 경우 Transaction 별로 진행. 만약 1번의 방법을 사용한다면 어떻게 방식으로 사용할 수 있을까 의문이다
후자의 경우, Transaction의 범위를 명확하게 결정해야 할 것.

Open Session In View
조영호님 OSIV 정리 글

@wannte
Copy link
Contributor Author

wannte commented Oct 12, 2021

Screenshot from 2021-10-12 15-31-01

SHOW SLAVE STATUS\G; 명령어 결과
db sync가 맞고 있지 않던 상황을 해결해야함.

[master-db] mysqldump --single-transaction -master-data -u amdin -p gpu_is_mine > masterdump.sql
[slave-db] sudo mysql < masterdump.sql (admin 유저에 권한을 부여해도 되지만, sudo로 data를 넣어둠

정상적으로 master에 쓰기작업이 이뤄지면 slave-db에 적용되는 부분 확인
Screenshot from 2021-10-12 15-44-01

@knae11
Copy link
Collaborator

knae11 commented Oct 13, 2021

DB 쿼리 이력 갯수 확인

show global status like "Com_select";
show global status like "Com_insert";

@wannte
Copy link
Contributor Author

wannte commented Oct 13, 2021

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
public class DataSourceConfig {
    private final JpaProperties jpaProperties;

    public DataSourceConfig(JpaProperties jpaProperties) {
        this.jpaProperties = jpaProperties;
    }


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

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

    @Bean
    public DataSource routingDataSource() {
        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();

        HashMap<Object, Object> sources = new HashMap<>();
        DataSource masterDataSource = masterDataSource();
        sources.put(DATASOURCE_KEY_MASTER, masterDataSource);
        sources.put(DATASOURCE_KEY_SLAVE, slaveDataSource());


        routingDataSource.setTargetDataSources(sources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }

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

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        EntityManagerFactoryBuilder entityManagerFactoryBuilder = createEntityManagerFactoryBuilder(jpaProperties);
        return entityManagerFactoryBuilder.dataSource(dataSource()).packages("com.example.dbreplication").build();
    }

    private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) {
        AbstractJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        return new EntityManagerFactoryBuilder(vendorAdapter, jpaProperties.getProperties(), null);
    }

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

왜 이런 방식으로 했을 때에는, OpenEntityManagerInViewInterceptor Bean이 생성되지 않을까?가 계속 의문이였다.

그건 바로 아래의 @ConditionalOnSingleCandidate(DataSource.class) 설정때문이였다.

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HibernateProperties.class)
@ConditionalOnSingleCandidate(DataSource.class)
class HibernateJpaConfiguration extends JpaBaseConfiguration {
}

DataSource의 SinglaeCandidate으로 정의될 때에만, HibernateJpaConfiguration의 autoConfiguration 설정이 실행된다. 즉, DataSource의 Candidate가 설정되지 않은 위의 설정은 HibernateJpaConfiguration가 아예 실행되지 않는다. -> 이 부모클래스인 JpaBaseConfiguration의 설정들도 모두 실행되지 않는다. OpenEntityManagerInViewInterceptor 또한 설정이되지 않는 것이다!

Clarify the scope of DataSourceInitializer

@wannte
Copy link
Contributor Author

wannte commented Oct 13, 2021

Provide support for auto-configuring multiple datasources
multi-datasource에 대해서는 아직 편한 solution이 나오지 않은 것으로 보인다.

Flyway circle에 관한 생각

DataSourceInitializerInvoker는 dataSource를 필요로함.
flyway -> dataSource -> routingDataSource -> masterDataSource -> DataSourceInitializerInvoker는 다시 dataSource를 필요로함. (이상하게도 환경에 따라 조금 다른 것 같다. 다른 프로젝트에서 재현해보려했지만, 진행되지 않는다. 이 글 에서는 다른 의존성의 여부에 따라 bean등록 순서가 바뀔 수 있고, 그 방식에 영향을 받는다고 한다.

@FlywayDataSource의 방식을 통해 Flyway에 적용되는 DataBase를 지정해줄 수도 있다. Master와 Slave의 migration을 동시에 지원해야하는지는 의문이든다. Master만 Migration 처리를 해주면, Slave에 해당 옵션이 반영되는지 확인해봐야 한다.

-> Master에 적용하면 Slave에도 반영되는 부분 확인. 하지만, 안정적인 시스템을 위해서는 Application이 뜨는 시점에서 모든 DB에 migration validate를 진행해도 좋을 듯 하다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BE 개선 New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants