Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 48 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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:
5 changes: 5 additions & 0 deletions mysql/init-master.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Master DB 초기화 스크립트
-- 복제용 사용자 생성
CREATE USER 'replica'@'%' IDENTIFIED WITH mysql_native_password BY '1234';
GRANT REPLICATION SLAVE ON *.* TO 'replica'@'%';
FLUSH PRIVILEGES;
4 changes: 4 additions & 0 deletions mysql/master-data-source.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-do-db=rabbit
4 changes: 4 additions & 0 deletions mysql/replica-data-source.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[mysqld]
server-id=2
log-bin=mysql-bin
read_only=1
109 changes: 109 additions & 0 deletions mysql/setup-replication.sh
Original file line number Diff line number Diff line change
@@ -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
87 changes: 87 additions & 0 deletions src/main/java/com/rabbitmqprac/config/DataSourceConfiguration.java
Original file line number Diff line number Diff line change
@@ -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<Object, Object> 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;
}
}
}
20 changes: 15 additions & 5 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -60,4 +70,4 @@ logging:
org.springframework.orm: TRACE
org.springframework.transaction: TRACE
com.zaxxer.hikari: TRACE
com.mysql.cj.jdbc: TRACE
com.mysql.cj.jdbc: TRACE
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Loading