[대용량 데이터 처리] Master-Slave db replication 설정 with docker 본문

백앤드 개발일지/데이터베이스

[대용량 데이터 처리] Master-Slave db replication 설정 with docker

giron 2021. 10. 21. 11:28
728x90

대용량 데이터 처리하는 방법에 여러 방법이 있다.

 

Load Balancer

  • Request를 연결된 서버들에게 나누어줌
  • 장애 발생시 해당 LB(Load Balancer)에게 할당된 IP를 다른 LB에게 넘겨줌

DBMS 2개(Master-Slave = Primary-Secondary)

  • primary(실제 서비스)
  • primary에서 장애 발생시 secondary가 primary로 되고, 장애가 해결되도 primary는 secondary 역할을 하게 된다.
  • primary(CUD), secondary(R)
  • 두대를 두고 primary의 데이터를 secondary로 계속 Replication을 통해 복제한다.

Object Storage Service (File-Server)

  • 파일을 저장할 서버를 둘 경우 총 3개의 File-server가 필요하다.
  • 3개를 사용할 시 Data Loss : 99.999%가 보장된다.
  • AWS S3, AZURE Blob, Gcp Google Storage에서 서비스를 제공한다.

하지만 데이터가 많아질 수록 DBMS의 TPS 한계가 온다.

TPS(Transaction Per Second)

  • 초당 몇개의 명령을 처리할 수 있는가
  • ex) MAX 1000 TPS = 1초에 1000개의 명령을 처리
  • CPU, 메모리, 디스크 등 하드웨어 적인 부분이 속도에 영향을 미친다.
  • 너무 많은 경우 오래걸리거나 장비가 고장날 수 있다.

위에 설명한 내용 이외에 더 다양한 방식이 있지만 이번에는 DB를 master(primary)와 slave(secondary)로 나눠보는 실습을 해보려고 간략히 적었다. 

왜? primary 혹은 master라고 두개로 불릴까?

- 원래는 master-slave구조로 불렸다고 한다. 하지만 흑인 문제와 관련되면서 master-slave에서 primary-secondary로 바뀐것으로 안다. *(깃에서 master branch가 디폴트였는데 main branch로 바뀐 이유도 위와 같다.)

Replication

 - 단방향인 Replication은 replication을 주는게 Master이고, 받는 게 Slave이다.

 - Slave는 Master와 동기화되지만, Master는 Slave와 동기화가 되지 않는다.

 - 즉, Master도 Slave와 동기화를 하고 싶다면 반대로도 replication을 걸어야 한다. 

스프링 부트와 Replication

  1. @Transactional(readOnly = true) 인 경우는 Slave DB 접근
  2. @Transactional(readOnly = false) 인 경우에는 Master DB 접근

위와 같은 방식으로 사용할 것입니다.

디렉토리 구조

왜?  docker-compose를 이용했는가?

- 두개의 dbms가 필요하고 이를 따로 따로 관리하기엔 불편하다. 또한 실제 실무에서는 여러대의 slave들이 필요하는데 이떄 각각 관리하면 불편하기 때문에 docker-compose명령어를 통해서 컨테이너들을 한 번에 관리하기 위해서 사용했다.

docker/Dockerfile

FROM --platform=linux/x86_64 mysql:8.0
ADD ./master/my.cnf /etc/mysql/my.cnf

간단합니다. 리눅스 환경에 mysql:8.0으로 master에 my.cnf파일을 컨테이너 속으로 넘겨주는 파일입니다.

slave에서도 위와 마찬가지로 slave에 .cnf파일을 컨테이너로 넘겨주면 됩니다.

docker/master/my.cnf

[mysqld]
log_bin = mysql-bin // <- MySQL(오라클의 redo로그와 유사) 로그생성 설정
server_id = 1  //<- 서버 고유아이디, Slave와 다르게 설정
expire_logs_days = 7                     // <- 로그 보관주기 설정(일)
default_authentication_plugin=mysql_native_password

docker/slave/my.cnf

[mysqld]
log_bin = mysql-bin
server_id = 11
relay_log = /var/lib/mysql/mysql-relay-bin
log_slave_updates = 1
read_only = 1
default_authentication_plugin=mysql_native_password

docker-compose.yml

version: "3"
services:
  db-master:
    build:
      context: ./
      dockerfile: master/Dockerfile
    restart: always
    environment:
      MYSQL_DATABASE: 'db'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: 'password'
      MYSQL_ROOT_PASSWORD: 'password'
    ports:
      - '3306:3306'
    # Where our data will be persisted
    volumes:
      - my-db-master:/var/lib/mysql
      - my-db-master:/var/lib/mysql-files
    networks:
      - net-mysql

  db-slave:
    build:
      context: ./
      dockerfile: slave/Dockerfile
    restart: always
    environment:
      MYSQL_DATABASE: 'db'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: 'password'
      MYSQL_ROOT_PASSWORD: 'password'
    ports:
      - '3307:3306'
    # Where our data will be persisted
    volumes:
      - my-db-slave:/var/lib/mysql
      - my-db-slave:/var/lib/mysql-files
    networks:
      - net-mysql

volumes:
  my-db-master:
  my-db-slave:

networks:
  net-mysql:
    driver: bridge

net-mysql이라는 network를 만들어 주고 master와 slave를 연결해줍니다.

 

docker-compose

도커를 이용해서 3306은 Master, 3307은 Slave에게 할당해줍니다.

DataSourceConfiguration

Data Source를 Bean으로 등록해줍니다.

package com.db.replication.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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.jdbc.datasource.LazyConnectionDataSourceProxy;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfiguration {

    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();
    }

    @Bean
    @DependsOn({MASTER_DATASOURCE, SLAVE_DATASOURCE})
    public DataSource routingDataSource(
            @Qualifier(MASTER_DATASOURCE) DataSource masterDataSource,
            @Qualifier(SLAVE_DATASOURCE) DataSource slaveDataSource) {
        RoutingDataSource routingDataSource = new RoutingDataSource();
        Map<Object, Object> datasource = new HashMap<>();
        datasource.put("master", masterDataSource);
        datasource.put("slave", slaveDataSource);
        routingDataSource.setTargetDataSources(datasource);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        return routingDataSource;
    }

    @Primary
    @Bean
    @DependsOn("routingDataSource")
    public LazyConnectionDataSourceProxy dataSource(DataSource routingDataSource){
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}
  1.  yml에서 설정한 값을 DataSource를 생성하는데 이용.

RoutingDataSource

package com.db.replication.config;

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



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


}
  1.  determineCurrentLookupKey() 메서드는 현재 조회 키를 반환받기 위해 구현해야 하는 추상 메서드입니다.
  2.  readOnly 속성을 구별하여 알맞은 key를 반환하게 합니다.

모든 설정은 끝났습니다. 이제 실행해봅시다!

실행

docker-compose up -d

Master db의 host주소를 찾기 위해 inspect로 찾아줍니다.

dokcer network ls
docker inspect {network의 ID}

찾은 master db의 주소를 기억해두고 slave로 접속합니다.

docker ps
docker exec -it {SLAVE_CONTAINER_ID} bash
mysql -u root -p

SLAVE Setting

stop slave;

CHANGE MASTER TO 
MASTER_HOST='{master network ip address}',  <- Master IP나 호스트명을 입력
MASTER_USER='root', 						<- Replication 아이디
MASTER_PASSWORD='password', 				<- Replication 패스워드
MASTER_LOG_FILE='mysql-bin.000001', 		<- Master의 SHOW MASTER STATUS; 결과화면의 로그파일명 입력
MASTER_LOG_POS=0, 							<- Master의 SHOW MASTER STATUS; 결과화면의 Position 입력
GET_MASTER_PUBLIC_KEY=1;

start slave;
show slave status\G

여기서 뒤에 net주소는 버리고 입력해줍니다.

show slave status\G

정상 작동

성공입니다.


해결

Worker 0 failed executing transaction 'ANONYMOUS' at ~~

 

위와 같이 나오면서 연결이 안될 때는 아래처럼 skip count를 해줍니다.

https://developpaper.com/three-parameter-analysis-of-mysql-replication/

 

Three parameter analysis of MySQL replication - Develop Paper

This Tuesday morning, I got up late and was late for work. It was… Not much nonsense. In yesterday’s article, we mentioned three parameters, namely: slave_ exec_ Mode parameter; sql_ slave_ skip_ Counter = n parameter; Slave skip errors = n parameter.

developpaper.com

SQL_ slave_ skip_ 값 뒤에 counter가 이벤트 수라고 했으므로 여기서는 이벤트를 건너뛰는 것과 같습니다. MySQL은 이벤트를 건너뛴 후에도 이벤트가 여전히 트랜잭션에 있으면 트랜잭션을 계속 건너뛸 것이라고 규정합니다.

set global sql_slave_skip_counter=1;

https://k3068.tistory.com/102

 

[Spring-boot] Master - Slave 구조에 따른 Read, Write 분기

서론 데이터베이스를 이용한다면 대부분 쓰기보다 읽기 의 행위가 더 많습니다. DB의 부하를 줄이기 위해 다음과 같이 Master - Slave 구조를 많이 사용하는데요. 이러한 구조를 가지고 있을 때 Transe

k3068.tistory.com

 

728x90
Comments