본문 바로가기
문제 해결, 기술 비교/개인프로젝트(북클럽)

Mysql Replication 구성하기

코동이 2022. 6. 29.

개요


 이번 시간은 Mysql Replication 개념을 정리하고 읽기와 쓰기 작업에 Replication 구성 및 적용 과정을 알아보겠습니다.

 

복제(Replication)


 복제(Replication)는 1개 이상의 레플리카(replica) 저장소가 소스 저장소와 동기화를 자동으로 유지하는 과정입니다.
(기존의 일반적으로 사용하였던 master-slave라는 용어를 source-replica로 대체하는 추세입니다. 하지만 일부 문서를 하면서 전자 용어를 사용하는 부분도 있습니다.)

아래는 1개의 Master 저장소와 복제한 Slave 저장소가 있습니다.

 

MySQL Replica



기본적으로 복제는 비동기 방식입니다. 설정에 따라서 전체가 아닌 부분 데이터를 복제할 수 있습니다.

 

복제의 장점



1. 스케일 아웃

성능을 향상하기 위해 여러 개의 복제본에 분산을 부하할 수 있습니다.
모든 쓰기와 수정 작업은 마스터에서 진행합니다. 그러나 읽기는 레플리카에서 할 수 있습니다.
여러 개의 레플리카를 추가함으로 읽기 속도를 올림과 동시에 쓰기의 성능을 향상할 수 있습니다.


2. 데이터 보호

레플리카는 복제 과정을 중단할 수 있기 때문에, 원본 데이터를 손상시키지 않고 레플리카에서 백업 서비스를 가동할 수 있습니다.


3. 분석 

원본 데이터에 실시간 데이터가 생성되고, 원본 데이터에 성능 이슈없이 레플리카에서 분석을 할 수 있습니다


4. 원거리 데이터 분산

원본데이터에 접근하지 않고도,  원격 사이트에서 사용 할 로컬 데이터 복사본을 생성할 수 있습니다.



 

MySQL 복제 원리


아래는 MySQL의 Master-Slave 복제 원리입니다.

 

 

MySQL 복제 방식


 MySQL에서 복제는 바이너리 로그 파일에 데이터에 대한 모든 변경사항을 기록합니다.

 

1. 레플리카가 초기화가 된다면, 2개의 쓰레드 작업을 생성합니다.

2. 하나는 I/O 쓰레드인데 원본 인스턴스에 연결하고 한 줄씩 바이너리 로그를 읽습니다. 그리고 레플리카 서버의 Relay 로그에 해당 내용들을 복사합니다.

3. 두번째 쓰레드는 SQL 쓰레드로, relay 로그를 읽고 레플리카 인스턴스에 최대한 빠르게 적용합니다.

 

 

MySQL 셋팅하기


MySQL에서 복제를 어떻게 하는지 docker를 이용해 실습해 보겠습니다.

 

 

docker를 이용해 mysql 이미지를 pull 합니다.

$ docker pull mysql

 

 

docker images로 image를 확인합니다.

 

 

image를 기반으로 master, slave를 docker로 실행합니다.

$ docker run -p 3306 --name mysql-master -e MYSQL_ROOT_PASSWORD=1234 -d docker.io/mysql

 

 

그 결과 mysql-master라는 이름으로, 3306 포트에 mysql이 실행됩니다.

 

 

docker의 exec 명령어로 mysql 내부로 접속합니다.

$ docker exec -it mysql-master /bin/bash

 

 

master 설정을 위해 파일을 열어야 하는데, vim 이 없으므로 아래 명령어 순차적으로 설치합니다.

$ apt-get update
$ apt-get install -y vim

 

 

vi 명령어로 /etc/mysql/my.cnf 파일을 열고, 아래와 같이 2줄을 추가합니다.

log-bin=mysql-bin  
server-id=1

 

my.cnf

 

 

  • log-bin

업데이트되는 모든 쿼리들이 Binary log 파일에 기록됩니다. 기본적으로 Binary log 파일은 MySQL의 data directory인 /var/lib/mysql/ 에 호스트명-bin.000001, 호스트명-bin.000002 형태로 생성됩니다.
 이때, log-bin 설정을 변경하면 Binary log 파일의 경로와 파일명의 접두어를 변경할 수 있습니다. log-bin=mysql이라 설정하면 mysql-bin.000001, mysql-bin.000002 이름으로로 Binary log 파일이 생성됩니다.

 

 

 

  • server-id

설정에서 서버를 식별하기 위한 고유 ID값입니다. master, slave 각각 다르게 설정해야 합니다.

 

 

 

도커를 재시작하여 설정 변경을 적용합니다.

$ docker restart mysql-master

 

 

 

도커 내부에 접속해서 제대로 설정이 되었는지 확인합니다.

$ docker exec -it mysql-master /bin/bash
$ mysql -u root -p 
mysql> SHOW MASTER STATUS\G

 

아래 사진에서 File 은 현재 바이너리 로그 파일명이고, Position 은 현재 로그의 위치를 나타냅니다.

 

 

 

master DB에 계정 생성하기


master DB에서 사용할 계정을 생성합니다. 이 계정이 slave DB에서 복제할 예정입니다.

bookroot라는 계정을 새로 생성하고, 모든 ip에 대해서 권한을 열어줍니다.

$ CREATE USER 'bookroot'@'%' IDENTIFIED BY '1234';

//sha256_password
$ ALTER USER 'bookroot'@'%' IDENTIFIED WITH mysql_native_password BY '1234';

$ GRANT REPLICATION SLAVE ON *.* TO 'bookroot'@'%';

$ FLUSH PRIVILEGES;

 

아래는 실습 화면입니다.

 

 

 

mysql의 USER 테이블을 확인하면 생성한 bookroot가 있습니다. Host는 전체 대상입니다.

$ SELECT User, Host FROM mysql.user;

 

 

User bookroot는 Host %입니다. 즉, 어떤 호스트도 접근이 가능합니다.

 

 

 

DB 생성하고 dump 만들기


DB는 bookclub 사용자를 생성해서 사용하겠습니다.

 

 

 

bookclub에서 testtable 테이블을 만들고, "test row"라는 데이터를 넣습니다.

 

 

 

docker로 다시 master DB에 접속하고, dump를 합니다.

$ docker exec -it mysql-master /bin/bash

$ mysqldump -u root -p bookclub > dump.sql

 

 

mysql 접속을 종료하고, 로컬 환경에 dump.sql을 가져옵니다.

$ docker cp mysql-master:dump.sql .

$ cat dump.sql

 

아래와 같이 dump 파일의 내용을 알 수 있으며, testtable 정보가 있습니다.

 

 

 

 

slave DB 계정 생성하기


slave DB를 생성하기 위해 도커를 이용해 새로운 MySQL를 생성합니다.

docker run -p 3306 --name mysql-slave -e MYSQL_ROOT_PASSWORD=1234 --link mysql-master -d docker.io/mysql

 

--link mysql-master는 mysql-master 컨테이너 이름을 Host 주소로 사용하기 위해서 사용한 옵션입니다. 

 

 

이제 master, slave 2개의 DB가 실행됨을 확인할 수 있습니다.

 

 

mysql에 접속해 vim을 설치합니다.

$ docker exec -it mysql-slave /bin/bash

$ apt-get update; apt-get install vim -y

 

 

/etc/mysql/my.cnf에 접속해 log-bin과 server-id를 추가합니다.

server-id의 경우, 각각 달라야 하기 때문에 이번에는 2번으로 설정합니다.

log-bin=mysql-bin  
server-id=2

 

 

 

 

 

도커의 mysql-slave를 재시작합니다.

docker restart mysql-slave

 

 

로컬 환경에 있는 dump 파일을 mysql-slave에 복사합니다.

또한, slave DB에 접속해, bookclub 저장소를 생성하고 dump 파일을 적용합니다.

$ docker cp dump.sql mysql-slave:.
$ docker exec -it mysql-slave /bin/bash

$ mysql -u root -p
mysql> CREATE DATABASE bookclub;

mysql> exit

$ mysql -u root -p bookclub < dump.sql

 

 

slave DB에 접속해서 bookclub을 확인하면, master DB에서 생성한 testtable과 데이터가 잘 복제되어 있습니다.

 

 

master-mysql에 다시 접속해서 SHOW MASTER STATUS\G를 확인하면, 처음에 비해 DB 테이블과 쿼리들이 추가됐으므로 이전보다 Position이 증가했습니다.

 

 

 

slave를 master와 연결하기


다시 mysql-slave에 접속해, MASTER를 mysql-master로 변경하고 SLAVE를 시작합니다.

$ docker exec -it mysql-slave /bin/bash

$ mysql -u root -p 

mysql> CHANGE MASTER TO MASTER_HOST='mysql-master', MASTER_USER='bookroot', MASTER_PASSWORD='1234', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=1721;

mysql> START SLAVE;

 

 

  • 명령어 설명

MASTER_HOST : master 서버의 호스트명
MASTER_USER : master 서버의 mysql에서 REPLICATION SLAVE 권한을 가진 User 계정의 이름
MASTER_PASSWORD : master 서버의 mysql에서 REPLICATION SLAVE 권한을 가진 User 계정의 비밀번호
MASTER_LOG_FILE : master 서버의 바이너리 로그 파일명
MASTER_LOG_POS : master 서버의 현재 로그의 위치

 

 

 

 

slave에서 연결정보를 조회해 보면, 아래와 같이 mysql-master와 연결된 정보가 나옵니다.

mysql> SHOW SLAVE STATUS\G

 

 

 

replica 테스트해 보기


mysql-master에 'test row, ', 'text second', 'text third' 3개의 데이터를 insert 하고 mysql-slave에서 조회해 봅니다. master와 똑같이 복사된 것을 확인할 수 있습니다.

 

 

mysql-slave db

 

 

 

Spring Boot에서 MySQL Replica 사용하기


 Spring Boot에서 MySQL Replica를 이용해 트랜잭션에서 읽기 전용은 slave DB에서 처리하고, 쓰기는 master DB에서 처리하도록 설정해 보겠습니다. 이를 위해서 master, slave DB 중 어느 DB를 선택하는지 설정하는 AbstractRoutingDataSource와 읽기 전용 트랜잭션에 slave DB 가 커넥션 되도록 하는  LazyConnectionDataSourceProxy을 사용합니다.

 

 

AbstractRoutingDataSource


 AbstractRoutingDataSource는 spring-jdbc 모듈에 포함되어 있는 클래스로, 여러 데이터소스를 등록하고 특정 상황에 원하는 데이터소스를 사용할 수 있도록 추상화한 클래스입니다.

 

 룩업 키(lookup key)에 따라서 여러 개의 데이터소스 중에 특정 하나로 연결을 하는 추상화된 데이터소스 구현체입니다. 일반적으로, 쓰레드 바인딩된 트랜잭션 컨텍스트(context)로 결정됩니다.

 

@Slf4j
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
	@Override
	protected Object determineCurrentLookupKey() {
		boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
		log.info("current dataSourceType is ReadOnly ? : {}", isReadOnly);
		return isReadOnly ? "read" : "write";
	}
}

 

determineCurrentLookupKey() 메서드를 재정의하여, 읽기 전용일 경우 slave를, 아닌 경우 master를 반환하도록 합니다.

 

 

LazyConnectionDataSourceProxy


 스프링은 트랜잭션 "시작" 시 커넥션의 실제 사용여부와 무관하게 커넥션을 확보합니다. 그로 인해 트랜잭션 시작 후 커넥션과 무관한 다른 작업으로 많은 시간이 지체되면 그 시간 동안 해당 트랜잭션의 커넥션은 사용불가 상태가 됩니다. 따라서, 데이터소스에 커넥션 풀이 부족할 수도 있습니다. 혹은 캐시를 사용하여 커넥션이 전혀 필요 없는 상태에서는 아예 커넥션을 맺지 않고 지나갈 수도 있습니다. LazyConnectionDataSourceProxy를 사용하면 트랜잭션이 시작시 실제로 커넥션이 필요한 경우에만 데이터소스에서 커넥션을 반환합니다.

 

 데이터소스 프록시를 사용하면 실제로 필요하지 않는 경우 JDBC 연결을 하지 않습니다. JDBC 트랜잭션 통제는 풀 연결 없어도 가능하며, db를 사용하지 않고도 가능합니다; JDBC 명령문이 만들어졌을 때 느리게 수행됩니다.

 

JDBC 늦은 연결은 Hibernate와 read-only 작업에 추천합니다, (특히 2차 캐싱으로 문제를 해결하는 경우)

 

해당 클래스가 필요한 이유는 Spring의 @Transactional 처리과정 때문입니다.

 

TransactionManager 선별 -> DataSource에서 Connection 획득 -> Transaction 동기화(Synchronization)

 

 트랜잭션 동기화를 마친 뒤 ReplicationRoutingDataSource에서 커넥션을 획득해야 하지만 트랜잭션 시작 시 커넥션을 획득하기 때문에 읽기 전용과 쓰기 전용 트랜잭션을 구분할 수 없습니다.

 

 따라서, ReplicationRoutingDataSourceLazyConnectionDataSourceProxy로 감싸면, 문제를 해결할 수 있습니다.

 

TransactionManager 선별 -> LazyConnectionDataSourceProxy에서 Connection Proxy 객체 획득

-> Transaction 동기화(Synchronization) -> 실제 쿼리 호출 시에 ReplicationRoutingDataSource.getConnection() / determineCurrentLookupKey() 호출

 

 

먼저 master와 slave의 데이터소스를 만들어야 합니다. 커넥션 풀은 Hikari를 사용하도록 합니다.

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

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

 

DataSourceBuilder@ConfigurationProperties를 사용해, application.yml에 설정된 내용으로 데이터 소스를 빈으로 등록합니다. 데이터 소스는 실제 물리 DB에 연결하는 팩토리이며, 커넥션을 얻는 역할을 합니다. 좀 더 자세한 설명은 다음 글을 참고하시면 됩니다. ( 참고 )

 

 

 빈으로 만든 2개의 데이터 소스를 ReplicationRoutingDataSource에 설정합니다.

@Bean
public DataSource routingDataSource(@Qualifier("writeDataSource") DataSource writeDataSource,
                           @Qualifier("readDataSource") DataSource readDataSource) {
   ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();

   Map<Object, Object> dataSourceMap = new HashMap<Object, Object>();
   dataSourceMap.put("write", writeDataSource);
   dataSourceMap.put("read", readDataSource);
   routingDataSource.setTargetDataSources(dataSourceMap);
   routingDataSource.setDefaultTargetDataSource(writeDataSource);

   return routingDataSource;
}

 

디폴트로는 writeDataSource를, 타깃으로는 dataSourceMap으로 설정하고 반환합니다.

 

 

ReplicationRoutingDataSource 정보를 LazyConnectionDataSourceProxy에 넣어, 트랜잭션이 동기화된 이후에 실제로 커넥션을 얻도록 합니다.

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

 

 

routingDataSource의 빈 이름이 DataSource가 아니라면 TransactionManager에 DataSource를 새로 지정해주어야 합니다. 트랜잭션 매니저 중에서 DataSourceTransactionManagerroutingLazyDataSource라는 데이터소스를 관리하도록 설정하여 해당 데이터소스 기준으로 트랜잭션이 관리되도록 합니다.

@Bean
public PlatformTransactionManager transactionManager(
@Qualifier("routingLazyDataSource") DataSource dataSource) {
   DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
   transactionManager.setDataSource(dataSource);
   return transactionManager;
}

 

 


* 참고

Chapter 17 Replication
What is MySQL replication?
MySQL - Replication

 

반응형