diff --git a/docker-compose.yml b/docker-compose.yml index ca1f114..80999d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,16 +12,6 @@ services: environment: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest - - mongodb: - image: mongo:latest - container_name: mongodb - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: 1234 - MONGO_INITDB_DATABASE: rabbitmq - ports: - - "27017:27017" networks: - my_network @@ -34,16 +24,60 @@ services: networks: - my_network - mysql: + mysql_master: + container_name: rabbit-mysql-master image: mysql:8.0 - container_name: mysql environment: + MYSQL_DATABASE: rabbit + MYSQL_ROOT_HOST: '%' MYSQL_ROOT_PASSWORD: 1234 - MYSQL_DATABASE: rabbitmq ports: - "3306:3306" + volumes: + - ./mysql/master-data-source.cnf:/etc/mysql/conf.d/my.cnf + - ./mysql/init-master.sql:/docker-entrypoint-initdb.d/01-init-master.sql + networks: + - my_network + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p1234" ] + timeout: 20s + retries: 10 + + mysql_replica: + container_name: rabbit-mysql-replica + image: mysql:8.0 + environment: + MYSQL_DATABASE: rabbit + MYSQL_ROOT_HOST: '%' + MYSQL_ROOT_PASSWORD: 1234 + ports: + - "3307:3306" + volumes: + - ./mysql/replica-data-source.cnf:/etc/mysql/conf.d/my.cnf networks: - my_network + depends_on: + mysql_master: + condition: service_healthy + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p1234" ] + timeout: 20s + retries: 10 + + mysql_replication_setup: + image: mysql:8.0 + container_name: mysql_replication_setup + volumes: + - ./mysql/setup-replication.sh:/setup-replication.sh + command: [ "/bin/bash", "/setup-replication.sh" ] + networks: + - my_network + depends_on: + mysql_master: + condition: service_healthy + mysql_replica: + condition: service_healthy + restart: "no" nginx: image: nginx:latest @@ -55,7 +89,7 @@ services: - ./nginx/config/nginx.conf:/etc/nginx/conf.d/default.conf - ./nginx/ssl:/etc/nginx/ssl networks: - - my_network + - my_network networks: my_network: diff --git a/mysql/init-master.sql b/mysql/init-master.sql new file mode 100644 index 0000000..fdbf3d1 --- /dev/null +++ b/mysql/init-master.sql @@ -0,0 +1,5 @@ +-- Master DB 초기화 스크립트 +-- 복제용 사용자 생성 +CREATE USER 'replica'@'%' IDENTIFIED WITH mysql_native_password BY '1234'; +GRANT REPLICATION SLAVE ON *.* TO 'replica'@'%'; +FLUSH PRIVILEGES; diff --git a/mysql/master-data-source.cnf b/mysql/master-data-source.cnf new file mode 100644 index 0000000..2239e80 --- /dev/null +++ b/mysql/master-data-source.cnf @@ -0,0 +1,4 @@ +[mysqld] +server-id=1 +log-bin=mysql-bin +binlog-do-db=rabbit diff --git a/mysql/replica-data-source.cnf b/mysql/replica-data-source.cnf new file mode 100644 index 0000000..4331422 --- /dev/null +++ b/mysql/replica-data-source.cnf @@ -0,0 +1,4 @@ +[mysqld] +server-id=2 +log-bin=mysql-bin +read_only=1 diff --git a/mysql/setup-replication.sh b/mysql/setup-replication.sh new file mode 100755 index 0000000..12bb831 --- /dev/null +++ b/mysql/setup-replication.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# MySQL Master-Replica 복제 설정 자동화 스크립트 +set -e + +echo "MySQL Master-Replica 복제 설정을 시작합니다..." + +# Master DB가 완전히 시작될 때까지 대기 +echo "Master DB 연결 대기 중..." +until mysql -h rabbit-mysql-master -u root -p1234 -e "SELECT 1" > /dev/null 2>&1; do + echo "Master DB 연결 대기 중..." + sleep 3 +done + +# Replica DB가 완전히 시작될 때까지 대기 +echo "Replica DB 연결 대기 중..." +until mysql -h rabbit-mysql-replica -u root -p1234 -e "SELECT 1" > /dev/null 2>&1; do + echo "Replica DB 연결 대기 중..." + sleep 3 +done + +# 추가 안정화 대기 시간 +echo "DB 초기화 완료 대기 중..." +sleep 10 + +# Master DB에서 복제 사용자가 생성되었는지 확인 +echo "Master DB에서 복제 사용자 확인 중..." +REPLICA_USER_EXISTS=$(mysql -h rabbit-mysql-master -u root -p1234 -e "SELECT COUNT(*) FROM mysql.user WHERE user='replica';" 2>/dev/null | tail -n 1) +if [ "$REPLICA_USER_EXISTS" -eq 0 ]; then + echo "복제 사용자가 존재하지 않습니다. 생성 중..." + mysql -h rabbit-mysql-master -u root -p1234 << EOF +CREATE USER IF NOT EXISTS 'replica'@'%' IDENTIFIED WITH mysql_native_password BY '1234'; +GRANT REPLICATION SLAVE ON *.* TO 'replica'@'%'; +FLUSH PRIVILEGES; +EOF + echo "복제 사용자가 생성되었습니다." +fi + +# Master DB에서 바이너리 로그 상태 확인 +echo "Master DB에서 바이너리 로그 상태 확인 중..." +MASTER_STATUS=$(mysql -h rabbit-mysql-master -u root -p1234 -e "SHOW MASTER STATUS\G" 2>/dev/null) +MASTER_FILE=$(echo "$MASTER_STATUS" | grep "File:" | awk '{print $2}') +MASTER_POSITION=$(echo "$MASTER_STATUS" | grep "Position:" | awk '{print $2}') + +echo "Master File: $MASTER_FILE" +echo "Master Position: $MASTER_POSITION" + +if [ -z "$MASTER_FILE" ] || [ -z "$MASTER_POSITION" ]; then + echo "❌ Master 상태를 가져올 수 없습니다." + exit 1 +fi + +# 기존 복제 설정 정리 +echo "기존 복제 설정 정리 중..." +mysql -h rabbit-mysql-replica -u root -p1234 << EOF +STOP SLAVE; +RESET SLAVE ALL; +EOF + +# Replica DB에서 Master 설정 +echo "Replica DB에서 Master 연결 설정 중..." +mysql -h rabbit-mysql-replica -u root -p1234 << EOF +CHANGE MASTER TO + MASTER_HOST='rabbit-mysql-master', + MASTER_USER='replica', + MASTER_PASSWORD='1234', + MASTER_LOG_FILE='$MASTER_FILE', + MASTER_LOG_POS=$MASTER_POSITION, + MASTER_CONNECT_RETRY=10, + MASTER_RETRY_COUNT=3; +START SLAVE; +EOF + +# 복제 연결 대기 및 상태 확인 +echo "복제 연결 대기 중..." +for i in {1..30}; do + SLAVE_STATUS=$(mysql -h rabbit-mysql-replica -u root -p1234 -e "SHOW SLAVE STATUS\G" 2>/dev/null) + IO_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_IO_Running:" | awk '{print $2}') + SQL_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_SQL_Running:" | awk '{print $2}') + + echo "시도 $i/30 - IO Running: $IO_RUNNING, SQL Running: $SQL_RUNNING" + + if [ "$IO_RUNNING" = "Yes" ] && [ "$SQL_RUNNING" = "Yes" ]; then + echo "✅ MySQL Master-Replica 복제 설정이 성공적으로 완료되었습니다!" + + # 복제 상태 상세 정보 출력 + echo "=== 복제 상태 상세 정보 ===" + mysql -h rabbit-mysql-replica -u root -p1234 -e "SHOW SLAVE STATUS\G" | grep -E "(Slave_IO_Running|Slave_SQL_Running|Master_Host|Master_User|Read_Master_Log_Pos|Exec_Master_Log_Pos)" + exit 0 + elif [ "$IO_RUNNING" = "No" ]; then + echo "❌ IO 스레드 연결 실패. 오류 확인 중..." + LAST_IO_ERROR=$(echo "$SLAVE_STATUS" | grep "Last_IO_Error:" | cut -d':' -f2- | xargs) + if [ -n "$LAST_IO_ERROR" ]; then + echo "IO 오류: $LAST_IO_ERROR" + fi + break + fi + + sleep 2 +done + +echo "❌ 복제 설정에 문제가 발생했습니다." +echo "최종 상태: IO Running: $IO_RUNNING, SQL Running: $SQL_RUNNING" + +# 오류 정보 출력 +echo "=== 복제 오류 정보 ===" +mysql -h rabbit-mysql-replica -u root -p1234 -e "SHOW SLAVE STATUS\G" | grep -E "(Last_IO_Error|Last_SQL_Error)" + +exit 1 diff --git a/src/main/java/com/rabbitmqprac/config/DataSourceConfiguration.java b/src/main/java/com/rabbitmqprac/config/DataSourceConfiguration.java new file mode 100644 index 0000000..b382cad --- /dev/null +++ b/src/main/java/com/rabbitmqprac/config/DataSourceConfiguration.java @@ -0,0 +1,87 @@ +package com.rabbitmqprac.config; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.extern.slf4j.Slf4j; +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.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.sql.DataSource; +import java.util.HashMap; + +@Slf4j +@Profile("!test") +@Configuration +public class DataSourceConfiguration { + + private static final String MASTER_DATA_SOURCE = "MASTER"; + private static final String REPLICA_DATA_SOURCE = "REPLICA"; + + @Bean + @Qualifier(MASTER_DATA_SOURCE) + @ConfigurationProperties(prefix = "spring.datasource.master") + public DataSource masterDataSource() { + HikariDataSource dataSource = DataSourceBuilder + .create() + .type(HikariDataSource.class) + .build(); + dataSource.setPoolName(MASTER_DATA_SOURCE); + return dataSource; + } + + @Bean + @Qualifier(REPLICA_DATA_SOURCE) + @ConfigurationProperties(prefix = "spring.datasource.replica") + public DataSource replicaDataSource() { + HikariDataSource dataSource = DataSourceBuilder + .create() + .type(HikariDataSource.class) + .build(); + dataSource.setPoolName(REPLICA_DATA_SOURCE); + return dataSource; + } + + @Bean + public DataSource routingDataSource( + @Qualifier(MASTER_DATA_SOURCE) DataSource masterDataSource, + @Qualifier(REPLICA_DATA_SOURCE) DataSource replicaDataSource + ) { + RoutingDataSource routingDataSource = new RoutingDataSource(); + + HashMap dataSourceMap = new HashMap<>(); + dataSourceMap.put(MASTER_DATA_SOURCE, masterDataSource); + dataSourceMap.put(REPLICA_DATA_SOURCE, replicaDataSource); + + routingDataSource.setTargetDataSources(dataSourceMap); + routingDataSource.setDefaultTargetDataSource(masterDataSource); + + return routingDataSource; + } + + @Bean + @Primary + public DataSource dataSource( + @Qualifier(MASTER_DATA_SOURCE) DataSource masterDataSource, + @Qualifier(REPLICA_DATA_SOURCE) DataSource replicaDataSource + ) { + DataSource determinedDataSource = routingDataSource(masterDataSource, replicaDataSource); + return new LazyConnectionDataSourceProxy(determinedDataSource); + } + + @Slf4j + public static class RoutingDataSource extends AbstractRoutingDataSource { + @Override + protected Object determineCurrentLookupKey() { + String lookupKey = TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? REPLICA_DATA_SOURCE : MASTER_DATA_SOURCE; + log.debug("Current DataSource type: {}", lookupKey); + return lookupKey; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index afb5e81..adce61d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,14 +3,24 @@ spring: init: mode: always datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/rabbitmq?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: root - password: 1234 + master: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: jdbc:mysql://localhost:3306/rabbit?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: 1234 + replica: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: jdbc:mysql://localhost:3307/rabbit?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: 1234 jpa: hibernate: ddl-auto: update defer-datasource-initialization: true + database-platform: org.hibernate.dialect.MySQLDialect + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect data: mongodb: @@ -60,4 +70,4 @@ logging: org.springframework.orm: TRACE org.springframework.transaction: TRACE com.zaxxer.hikari: TRACE - com.mysql.cj.jdbc: TRACE \ No newline at end of file + com.mysql.cj.jdbc: TRACE diff --git a/src/test/java/com/rabbitmqprac/RabbitMqPracApplicationTests.java b/src/test/java/com/rabbitmqprac/RabbitMqPracApplicationTests.java index 975102b..68d1680 100644 --- a/src/test/java/com/rabbitmqprac/RabbitMqPracApplicationTests.java +++ b/src/test/java/com/rabbitmqprac/RabbitMqPracApplicationTests.java @@ -3,7 +3,9 @@ import com.rabbitmqprac.common.container.MySQLTestContainer; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") @SpringBootTest class RabbitMqPracApplicationTests extends MySQLTestContainer {