기술 블로그

MySQL Replication 구축

우 석 2024. 4. 12. 01:46

디렉토리 구조

 

 

Docker Compose를 사용하여 MySQL 데이터베이스 구성

// docker-compose.yml 

version: "1"
services:
  # main 데이터베이스 서비스 정의
  db-main:
    # Dockerfile을 사용하여 이미지 빌드
    build:
      context: ./
      dockerfile: main/Dockerfile
    # 컨테이너 재시작 설정
    restart: always
    # 플랫폼 설정
    platform: linux/x86_64
    # 환경 변수 설정
    environment:
      MYSQL_DATABASE: 'db'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: '1234'
      MYSQL_ROOT_PASSWORD: '1234'
    # MySQL 서버 설정
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    # 포트 포워딩 설정
    ports:
      - '3307:3306'
    # 컨테이너 이름 설정
    container_name: main-db
    # 볼륨 설정
    volumes:
      - my-db-main:/var/lib/mysql
      - my-db-main:/var/lib/mysql-files
    # 네트워크 설정
    networks:
      - net-mysql

  # replica 데이터베이스 서비스 정의
  db-replica:
    # Dockerfile을 사용하여 이미지 빌드
    build:
      context: ./
      dockerfile: replica/Dockerfile
    # 컨테이너 재시작 설정
    restart: always
    # 플랫폼 설정
    platform: linux/x86_64
    # 환경 변수 설정
    environment:
      MYSQL_DATABASE: 'db'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: '1234'
      MYSQL_ROOT_PASSWORD: '1234'
    # MySQL 서버 설정
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    # 포트 포워딩 설정
    ports:
      - '3308:3306'
    # 컨테이너 이름 설정
    container_name: replica-db
    # 볼륨 설정
    volumes:
      - my-db-replica:/var/lib/mysql
      - my-db-replica:/var/lib/mysql-files
    # 네트워크 설정
    networks:
      - net-mysql

# 볼륨 설정
volumes:
  my-db-main:
  my-db-replica:

# 네트워크 설정
networks:
  net-mysql:
    driver: bridge

 

 

Main DB 파일

// docker/main/Dockerfile
# MySQL 8.0 이미지를 기반으로 함
FROM mysql:8.0
# 로컬 파일 시스템에서 main 디렉토리에 있는 my.cnf 파일을 컨테이너 내부의 /etc/mysql/my.cnf 경로로 복사함
ADD ./main/my.cnf /etc/mysql/my.cnf

// docker/main/my.cnf
[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci
default-time-zone='+9:00'

# 바이너리 로그 활성화
log-bin = mysql-bin

# 서버 식별 ID 설정
server-id = 1

# 기본 인증 플러그인 설정
default_authentication_plugin=mysql_native_password

 

 

Replica DB 파일

// docker/replica/Dockerfile
# MySQL 8.0 이미지를 기반으로 함
FROM mysql:8.0
# 로컬 파일 시스템에서 replica 디렉토리에 있는 my.cnf 파일을 컨테이너 내부의 /etc/mysql/my.cnf 경로로 복사함
ADD ./replica/my.cnf /etc/mysql/my.cnf


// docker/replica/my.cnf
[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci
default-time-zone='+9:00'

# 바이너리 로그 활성화
log_bin = mysql-bin
# 서버 식별 ID 설정
server_id = 2

# MySQL 슬레이브 서버에서 사용되는 중계 로그의 저장 위치를 지정합니다.
relay_log = /var/lib/mysql/mysql-relay-bin

# 이 설정은 슬레이브 서버에서 발행된 변경 사항도 바이너리 로그에 기록하도록 허용합니다.
# 기본적으로, 슬레이브 서버에서 변경 사항은 바이너리 로그에 기록되지 않습니다.
log_slave_updates = 1

# 슬레이브 서버를 읽기 전용 모드로 설정합니다.
read_only = 1

# 기본 인증 플러그인 설정
default_authentication_plugin = mysql_native_password

 

 

application.yml

spring:
  datasource:
    main:
      hikari:
        jdbc-url: jdbc:mysql://localhost:3307/popcorntalk
        read-only: false
        username: root
        password: 1234
        driver-class-name: com.mysql.cj.jdbc.Driver
    replica:
      hikari:
        jdbc-url: jdbc:mysql://localhost:3308/popcorntalk
        read-only: true
        username: root
        password: 1234
        driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        highlight_sql: true
        dialect: org.hibernate.dialect.MySQLDialect

 

 

MySQL Replication 설정

 

1. compose 실행 → docker-compose up -d

2. 메인 DB의 IP Address 조회 → docker network ls

3. 메인 DB의 status 조회,  File과 Position 기록 → mysql-bin.000004, 157

 

 

4. 레플리카 DB 접속 -> 레플리카를 메인과 연결

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 서버의 현재 로그의 위치

 

 

6. 레플리카 DB 실행 → start replica;

7. replication 설정확인 → show replica status\G;

  • Replica_IO_Running: 이 변수는 메인 서버에서 바이너리 로그를 읽는 I/O 스레드가 실행 중인지 여부를 나타냅니다. 이 변수가 "YES"로 설정되면 I/O 스레드가 실행 중이고 메인 서버의 바이너리 로그에서 업데이트를 성공적으로 가져오는 것을 의미합니다.
  •  Replica _SQL_Running: 이 변수는 I/O 스레드가 바이너리 로그에서 검색한 이벤트를 실행하는 SQL 스레드가 실행 중인지 여부를 나타냅니다. 이 변수가 "YES"로 설정되면 SQL 스레드가 실행 중이고 복제본 데이터베이스에 이벤트가 성공적으로 적용되고 있음을 의미합니다.

 

 

DataSourceConfig

@Configuration
public class DataSourceConfig {

    // Main 데이터 소스 빈 이름
    public static final String MAIN_DATASOURCE = "mainDataSource";
    // Replica 데이터 소스 빈 이름
    public static final String REPLICA_DATASOURCE = "replicaDataSource";

    // Main 데이터 소스 빈 설정
    @Bean(MAIN_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource.main.hikari")
    public DataSource mainDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }

    // Replica 데이터 소스 빈 설정
    @Bean(REPLICA_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource.replica.hikari")
    public DataSource replicaDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }

    // 라우팅 데이터 소스 빈 설정
    @Bean
    @DependsOn({MAIN_DATASOURCE, REPLICA_DATASOURCE})
    public DataSource routingDataSource(
        @Qualifier(MAIN_DATASOURCE) DataSource mainDataSource,
        @Qualifier(REPLICA_DATASOURCE) DataSource replicaDataSource) {

        RoutingDataSource routingDatasource = new RoutingDataSource();

        // 데이터 소스 맵 구성
        Map<Object, Object> dataSourceMap = new HashMap<>() {
            {
                put("main", mainDataSource);
                put("replica", replicaDataSource);
            }
        };

        // 데이터 소스 맵 설정
        routingDatasource.setTargetDataSources(dataSourceMap);
        // 기본 데이터 소스는 Main으로 설정
        routingDatasource.setDefaultTargetDataSource(mainDataSource);

        return routingDatasource;
    }

    // 기본 데이터 소스 빈 설정
    @Primary
    @Bean
    @DependsOn("routingDataSource")
    public LazyConnectionDataSourceProxy dataSource(DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}

 

 

RoutingDataSource

@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {

    // 현재 사용할 데이터 소스 키를 결정하는 메서드
    @Override
    protected Object determineCurrentLookupKey() {
        // 현재 트랜잭션이 읽기 전용인지 확인하여 데이터 소스를 결정
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            log.info("replicaDB"); // replica DB 사용 확인 테스트
            return "replica"; // 복제 데이터 소스 선택
        }
        log.info("mainDB"); // main DB 사용 확인 테스트
        return "main"; // 메인 데이터 소스 선택
    }
}