리플리케이션 실습

2025. 6. 29. 19:24·CS/데이터베이스

지난 포스팅에 이어 오늘은 리플리케이션 실습을 진행해보기로 한다.

1. Replication with Docker

간단한 실습을 위해 docker를 통해 replication을 구성해보기로 한다.

 

 

docker-compose.yml

다음과 같이 master 노드 1개와 slave 노드 2개로 구성한다.

services:
  mysql-master:
    image: mysql:8.0
    container_name: mysql-master
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: test
      MYSQL_USER: user
      MYSQL_PASSWORD: 1234
    ports:
      - "3307:3306"
    volumes:
      - ./config/master.cnf:/etc/mysql/conf.d/master.cnf:ro
      - ./config/master-init.sql:/docker-entrypoint-initdb.d/master-init.sql:ro
    networks:
      - mysql-network

  mysql-slave1:
    image: mysql:8.0
    container_name: mysql-slave1
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: test
      MYSQL_USER: user
      MYSQL_PASSWORD: 1234
    ports:
      - "3308:3306"
    volumes:
      - ./config/slave1.cnf:/etc/mysql/conf.d/slave1.cnf:ro
    networks:
      - mysql-network

  mysql-slave2:
    image: mysql:8.0
    container_name: mysql-slave2
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: test
      MYSQL_USER: user
      MYSQL_PASSWORD: 1234
    ports:
      - "3309:3306"
    volumes:
      - ./config/slave2.cnf:/etc/mysql/conf.d/slave1.cnf:ro
    networks:
      - mysql-network

networks:
  mysql-network:
    driver: bridge

 

master.cnf

마스터 노드에 사용되는 mysql 설정 파일이다.

binlog 포맷은 ROW로 지정하였고, test db의 트랜잭션만 바이너리 로그에 기록하기로 한다.

[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
binlog-do-db = test
bind-address = 0.0.0.0

 

master-init.sql

다음은 마스터 노드에 실행될 리플리케이션을 위한 전용 사용자 계정 생성 및 권한 부여 SQL 명령어다.

컨테이너가 시작될 때 자동으로 수행될 예정이다.

CREATE USER IF NOT EXISTS 'replicator'@'%' IDENTIFIED WITH mysql_native_password BY 'replicator_password';
GRANT REPLICATION SLAVE ON *.* TO 'replicator'@'%';
FLUSH PRIVILEGES;

 

slave.cnf

이어서 슬레이브 노드에 사용되는 설정 파일이다.

read-only 옵션을 통해 읽기만 허용되게 한다.

[mysqld]
server-id = 2
relay-log = mysql-relay-bin
read-only = 1
bind-address = 0.0.0.0

 

설정 파일에서 replication 구성에 포함된 서버들은 각자 고유한 server-id를 갖도록 해줘야 한다!

 

replication_setup.sh

마지막으로 docker-compose 이후 MySQL Master-Slave 복제 환경을 자동으로 구축하는 셸 스크립트이다.

# 1. Docker Compose 시작
echo "Docker Compose를 시작합니다..."
cd docker
docker-compose up -d

# 2. Master가 준비될 때까지 대기
echo "Master 서버가 준비될 때까지 대기 중..."
sleep 20

# 2. Master 상태 확인 및 로그 파일과 위치 추출
echo "Master 상태를 확인합니다..."
MASTER_STATUS=$(docker exec mysql-master mysql -uroot -p1234 -e "SHOW MASTER STATUS\G")
LOG_FILE=$(echo "$MASTER_STATUS" | grep "File:" | awk '{print $2}')
LOG_POS=$(echo "$MASTER_STATUS" | grep "Position:" | awk '{print $2}')

echo "$MASTER_STATUS"
echo "Master Log File: $LOG_FILE"
echo "Master Log Position: $LOG_POS"

# 4. Slave 복제 설정
echo "Slave1 복제를 설정합니다..."
docker exec mysql-slave1 mysql -uroot -p1234 -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
  MASTER_HOST='mysql-master',
  MASTER_USER='replicator',
  MASTER_PASSWORD='replicator_password',
  MASTER_LOG_FILE='$LOG_FILE',
  MASTER_LOG_POS=$LOG_POS;
START SLAVE;
"

echo "Slave2 복제를 설정합니다..."
docker exec mysql-slave2 mysql -uroot -p1234 -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
  MASTER_HOST='mysql-master',
  MASTER_USER='replicator',
  MASTER_PASSWORD='replicator_password',
  MASTER_LOG_FILE='$LOG_FILE',
  MASTER_LOG_POS=$LOG_POS;
START SLAVE;
"

# 5. 복제 상태 확인
sleep 5
echo "복제 상태를 확인합니다..."
echo "=== Slave1 상태 ==="
docker exec mysql-slave1 mysql -uroot -p1234 -e "SHOW SLAVE STATUS\G" | grep -E "(Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master|Last_Error)"

echo "=== Slave2 상태 ==="
docker exec mysql-slave2 mysql -uroot -p1234 -e "SHOW SLAVE STATUS\G" | grep -E "(Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master|Last_Error)"

 

테스트

 

제대로 세팅이 완료되었는지 확인을 위해 master 노드에 order 테이블을 만들고 레코드 한 개를 insert 해보도록 한다.

 

 

다음으로 slave 노드에서 order 테이블의 레코드를 확인해보도록 하자.

 

 

다음과 같이 master 노드에서 삽입한 레코드가 정상적으로 slave 노드에도 저장된 것을 확인할 수 있다.

 


2. Spring 실습

이제 Spring에서 replication 구성이 완료된 DB 노드들을 데이터 소스로 등록하고 이용해보도록 하자!

 

application.yml

spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://localhost:3307/test?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
      username: user
      password: 1234
      driver-class-name: com.mysql.cj.jdbc.Driver
    slaves:
      slave1:
        name: slave-1
        jdbc-url: jdbc:mysql://localhost:3308/test?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
        username: user
        password: 1234
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave2:
        name: slave-2
        jdbc-url: jdbc:mysql://localhost:3309/test?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
        username: user
        password: 1234
        driver-class-name: com.mysql.cj.jdbc.Driver

 

DataSourceConfig

여러 데이터베이스의 연결을 설정하려면 기본 DataSource 설정을 비활성화하고 다음과 같이 커스텀 설정을 해줘야한다. master 노드와 salve 노드 2대를 map에 저장하고 타겟 데이터소스로 등록해주었다.

 

@RequiredArgsConstructor
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@Configuration
public class DataSourceConfig {

    private final DataSourceProperties dataSourceProperties;

    @Bean
    public DataSource routingDataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>();

        DataSourceProperties.Master masterProperty = dataSourceProperties.getMaster();
        DataSource masterDataSource = createDataSource(
                masterProperty.getJdbcUrl(),
                masterProperty.getUsername(),
                masterProperty.getPassword(),
                masterProperty.getDriverClassName()
        );
        dataSourceMap.put("master", masterDataSource);

        dataSourceProperties.getSlaves()
                .forEach((key, value) -> dataSourceMap.put(value.getName(), createDataSource(
                        value.getJdbcUrl(),
                        value.getUsername(),
                        value.getPassword(),
                        value.getDriverClassName()
                )));

        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        return routingDataSource;
    }

    public DataSource createDataSource(String url, String username, String password, String driverClassName) {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .url(url)
                .username(username)
                .password(password)
                .driverClassName(driverClassName)
                .build();
    }

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

 

ReplicationRoutingDataSource

다음으로 읽기/쓰기 요청에 따라 적절한 데이터베이스로 라우팅시킬 수 있는 설정이다.

설정에 따라 CUD 요청은 master 노드로, READ 요청은 slave 노드로 라우팅 된다.

@Slf4j
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    private CircularList<String> slaveDataSourceNameList;

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);

        slaveDataSourceNameList = new CircularList<>(
                targetDataSources.keySet()
                        .stream()
                        .map(Object::toString)
                        .filter(string -> string.contains("slave"))
                        .collect(Collectors.toList())
        );
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceKey;

        // 읽기 전용 트랜잭션인 경우 슬레이브 DB로 라우팅
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            dataSourceKey = slaveDataSourceNameList.getOne();
            log.info("[READ] Routing to: {}", dataSourceKey);
        } else {
            dataSourceKey = "master";
            log.info("[WRITE] Routing to: {}", dataSourceKey);
        }

        return dataSourceKey;
    }
}

 

 

읽기 요청의 경우 순환 리스트를 통해 라운드 로빈 방식으로 slave 노드들에 보내지게 된다.

 

OrderService

@Transaction 어노테이션에 readOnly=true 설정을 하면 읽기 전용 트랜잭션으로 지정되고, 이에 따라 slave 노드로 요청이 도달하게 된다. 

@RequiredArgsConstructor
@Service
public class OrderService {

    private static final Random RANDOM = new Random();

    private final OrderRepository orderRepository;

    @Transactional
    public Order createOrder() {
        BigDecimal randomValue = BigDecimal.valueOf(RANDOM.nextInt(10001))
                .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
        Order order = new Order(randomValue);
        return orderRepository.save(order);
    }

    @Transactional(readOnly = true)
    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }

}

 

테스트

이제 API 요청을 통해 쓰기 요청과 읽기 요청이 어떻게 처리되는지 확인해 보자.

 

 

 

 

'CS > 데이터베이스' 카테고리의 다른 글

Materialized View  (3) 2025.08.03
리플리케이션  (0) 2025.06.29
유일키 생성 전략  (1) 2025.06.15
샤딩 실습 (ShardingSphere)  (0) 2025.06.15
파티셔닝과 샤딩  (0) 2025.06.15
'CS/데이터베이스' 카테고리의 다른 글
  • Materialized View
  • 리플리케이션
  • 유일키 생성 전략
  • 샤딩 실습 (ShardingSphere)
nicky777
nicky777
  • nicky777
    Nicky Dev
    nicky777
  • 전체
    오늘
    어제
    • 분류 전체보기 (19)
      • Project (9)
        • 티켓핑 (9)
      • TroubleShooting (3)
      • Programming (0)
        • Java (0)
        • Spring (0)
      • CS (7)
        • 데이터베이스 (6)
        • 네트워크 (1)
        • 운영체제 (0)
        • 자료구조 (0)
      • 회고 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Contact
  • 인기 글

  • 태그

    유일키 생성 전략
    HTTP
    materialized view
    리플리케이션
    샤딩
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
nicky777
리플리케이션 실습
상단으로

티스토리툴바