본문으로 바로가기

[Springboot] MySQL Replication

category Programming/Spring 2021. 7. 11. 00:36

MySQL Replication

우아한테크캠프Pro 2기 마지막 주차의 마지막 미션은 데이터 베이스 이중화. 정말 벌써 마지막 주차의 마지막 미션을 진행하게 되었다,,, 감격,,,
마지막 미션인 Replication 은 복제라는 뜻으로 Master 라는 DB와 동일한 환경의 동일한 구성으로 Slave DB를 만들어 주는 것이다. nginx 와 같이 웹 어플리케이션의 부하를 막기 위해 로드밸런싱 역할을 수행하면 웹 어플리케이션 자체의 부하는 막을 수 있지만 DB의 부하도 분산을 해주어야 CRUD 의 기능을 원활히 수행할 수 있도록 해준다
보통 등록/삭제/수정은 Master db를 조회는 Slave db를 접속하여 분산 시킨다
MySQL에서 지원하는 Replication 기능을 이용하여 분산을 해주었다.

먼저 Master DB를 설정해준다.

  • 주의할 점은 Master DB를 먼저 설정한 후에 Slave DB를 설정해 주어야 한다,
$ docker run --name mysql-master -p 13306:3306 -v ~/mysql/master:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=master -d mysql

$ docker exec -it mysql-master /bin/bash  
$ mysql -u root -p  
mysql> CREATE USER 'replication\_user'@'%' IDENTIFIED WITH mysql\_native\_password by 'replication\_pw';  
mysql> GRANT REPLICATION SLAVE ON _._ TO 'replication\_user'@'%';

mysql> SHOW MASTER STATUS\\G  
************\*\*\************* 1. row ************\*\*\*************  
File: binlog.000002  
Position: 683  
Binlog\_Do\_DB:  
Binlog\_Ignore\_DB:  
Executed\_Gtid\_Set:  
1 row in set (0.00 sec)

다음 Slave DB를 설정해준다.

$ docker run --name mysql-slave -p 13307:3306 -v ~/mysql/slave:/etc/mysql/conf.d -e MYSQL\_ROOT\_PASSWORD=slave -d mysql

$ docker exec -it mysql-slave /bin/bash  
$ mysql -u root -p

mysql> SET GLOBAL server\_id = 2;  
mysql> CHANGE MASTER TO MASTER\_HOST='172.17.0.1', MASTER\_PORT = 13306, MASTER\_USER='replication\_user', MASTER\_PASSWORD='replication\_pw', MASTER\_LOG\_FILE='binlog.000002', MASTER\_LOG\_POS=683;

mysql> START SLAVE;  
mysql> SHOW SLAVE STATUS\\G  
...  
Slave\_IO\_Running: Yes  
Slave\_SQL\_Running: Yes
  • SHOW SLAVE STATUS\G 실행 시 에 볼 수 있는 것처럼 Running이 Yes 상태로 있어야 정상적으로 적용이 된다. Connecting 이나 다른 상태를 보여주면 실행이 아직 안된 걸로 볼 수 있다.
다음 Master db로 접속해서 Create Schema test_schema 를 생성하고 Slave db 로 접속하여 show databases를 실행하면 동일하게 schema가 설정된 것을 볼 수 있다.
  • application.properties
spring.datasource.hikari.master.username=root  
spring.datasource.hikari.master.password=master  
spring.datasource.hikari.master.jdbc-url=jdbc:mysql://ip:13306/test\_schema?useSSL=false&useUnicode=yes&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true

spring.datasource.hikari.slave.username=root  
spring.datasource.hikari.slave.password=slave  
spring.datasource.hikari.slave.jdbc-url=jdbc:mysql://ip:13307/test\_schema?useSSL=false&useUnicode=yes&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
  • ReplicationRoutingDataSource.java
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {  
  public static final String DATASOURCE\_KEY\_MASTER = "master";  
  public static final String DATASOURCE\_KEY\_SLAVE = "slave";

  @Override
  protected Object determineCurrentLookupKey() {
      boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
      return (isReadOnly)
          ? DATASOURCE_KEY_SLAVE
          : DATASOURCE_KEY_MASTER;
  }
}
  • DataBaseConfig.java
@Configuration  
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})  
@EnableTransactionManagement  
@EnableJpaRepositories(basePackages = {"nextstep.subway"})  
class DataBaseConfig {
  @Bean
  @ConfigurationProperties(prefix = "spring.datasource.hikari.master")
  public DataSource masterDataSource() {
      return DataSourceBuilder.create().type(HikariDataSource.class).build();
  }

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

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

      HashMap<Object, Object> sources = new HashMap<>();
      sources.put(DATASOURCE_KEY_MASTER, master);
      sources.put(DATASOURCE_KEY_SLAVE, slave);

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

      return routingDataSource;
  }

  @Primary
  @Bean
  public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
      return new LazyConnectionDataSourceProxy(routingDataSource);
  }
}
마지막으로 Transaction이 발생하는 service 메서드에 @Transactional(readOnly = true) annotation을 추가하면 ReplicationRoutingDataSource.javadetermineCurrentLookupKey 메서드에 의해 단순 select는 slave db를 나머지 수정/삭제/등록에 대한 메서드는 Master db를 향할면서 db 이중화의 기능을 수행할 것이다

마치며

웹어플리케이션의 부하를 줄일 수 있는 방법이 정말 여러가지라는 것을 배웠고 그에 따라 천차만별의 성능을 낸다는 것도,, 평소에 DB 이중화를 알고는 있었는데 실제로 눈으로 DB가 어떻게 백업이되고 복제가 되는지 확인할 수 있었다. 그리고 Master dbms 변경 시 즉각 Slave dbms에 반영이 되는 것도 신기,,
나는 flyway를 통해 Master dbms에 ddl를 수행하였는데 이 ddl도 Slave에 바로 복제가 되는 것을 확인할 수 있었다!