목차
1. Master-Slave 구조란 ?
2. DB Replication으로 Master-Slave 구조를 사용할 때 장점과 단점
3. 스프링에서 설정하기
1. Master-Slave 구조란 ?
DB 시스템에서 사용되는 복제(Replication) 형태이다.
Master DB에서는 주로 쓰기 작업을 처리하고, Slave DB에서 읽기 작업들을 처리하여 부하를 분산하고 가용성을 높일 수 있다. 하지만 이렇게 작업을 나누면 DB들 간의 데이터 정합성 문제가 생길 수 있다.
데이터를 동기화 시키기 위해 MySQL에서는 기본적으로 비동기 복제 방식을 사용하고 있다.
Master DB에서 쓰기, 삭제, 수정 작업이 일어나서 변경이 생기면, 이를 비동기적으로 Binary Log에 기록하고 Master Thread가 Slave 쪽으로 전송한다. Slave는 데이터를 수신하여 I/O Thread가 Realy Log에 기록하고, SQL Thread가 Relay Log를 읽어서 데이터를 적용한다.
2. Master-Slave 구조를 사용할 때 장점과 단점
장점
- 부하 분산
트래픽이 몰렸을 때, 이를 대처하기 위해 애플리케이션 서버를 늘리는 방법을 선택할 수 있다. 하지만 서비스의 정보들은 DB에 저장되어 있기 때문에 DB 서버 또한 트래픽이 같이 몰릴 것이다.
애플리케이션 서버는 증설로 해결이 가능하지만, DB 서버는 동기화 때문에 애플리케이션 서버처럼 쉽게 증설할 수 없다. 또한 디스크 I/O 작업이 일어나기 때문에 애플리케이션 서버보다 훨씬 큰 병목 지점이 될 수 있다. 그렇기 때문에 Master-Slave 구조를 사용하여 Read/Wirte에 따른 요청을 분리함으로써 부하를 분산하여 트래픽이 몰렸을 때에 대응할 수 있다. - 데이터 복제 및 백업
Master DB에 장애가 생겨 Write 작업을 할 수 없거나 데이터가 사라지면, Slave DB 중 하나를 Master로 승격 시켜 장애에 빠르게 대응할 수 있다.
단점
- 동기화 문제
Master와 Slave 서버 간 데이터 동기화까지는 어느 정도 시간이 소요될 수 밖에 없다. 이 때 데이터의 정합성 문제가 발생할 수 있다. 그렇기 때문에, 실시간성이나 데이터 동기화가 중요한 요청에서는 Master 서버로 요청해야 한다.
3. Spring에서 Master-Slave DB 설정
application.properties(또는 application.yml) 에서 사용할 DB의 정보를 설정한다.
나는 AWS의 RDS 2개를 사용했다.
#master
spring.datasource.master.hikari.username=
spring.datasource.master.hikari.password=
spring.datasource.master.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.hikari.jdbc-url=jdbc:mysql://주소/스키마
#slave
spring.datasource.slave.hikari.username=
spring.datasource.slave.hikari.password=
spring.datasource.slave.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.hikari.jdbc-url=jdbc:mysql://주소/스키마
DataSourceConfiguration.class
@Configuration
public class DataSourceConfiguration {
static final String MASTER_DB = "MASTER_DB";
static final String SLAVE_DB = "SLAVE_DB";
@Bean(name = MASTER_DB)
@ConfigurationProperties(prefix = "spring.datasource.master.hikari")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(name = SLAVE_DB)
@ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@DependsOn({MASTER_DB, SLAVE_DB})
@Bean(name = "routingDataSource")
public DataSource routingDataSource(
@Qualifier(MASTER_DB) DataSource masterDataSource,
@Qualifier(SLAVE_DB) DataSource slaveDataSource) {
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? SLAVE_DB : MASTER_DB;
}
};
Map<Object, Object> datasourceMap = new HashMap();
datasourceMap.put(MASTER_DB, masterDataSource);
datasourceMap.put(SLAVE_DB, slaveDataSource);
Map<Object, Object> immutableDataSourceMap = Collections.unmodifiableMap(datasourceMap);
routingDataSource.setTargetDataSources(immutableDataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@DependsOn({"routingDataSource"})
@Bean
public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
write 쿼리는 master, read쿼리는 slave로 각각 요청해야 한다.
이를 위해 AbstractRoutingDataSource 클래스의 determineCurrentLookupKey()로 쿼리를 구분한다.
determineCurrentLookupKey()에서는 현재 트랜잭션이 read only일 경우 slave DB로 DataSource를 라우팅하고, 그게 아니라면 master DB로 라우팅한다.
DataSource를 LazyConnectionDataSouceProxy로 반환하는 이유는,
스프링은 기본적으로 트랜잭션을 시작하고 쿼리가 실행되기 전에 DataSource를 미리 정해놓는데, 이는 트랜잭션이 시작되면 같은 DataSource만을 사용하기 때문이다. 그래서 DataSource를 정하기 위해서는 쿼리를 실행할 때 정할 수 있도록 연결을 지연하도록 해야한다.
이제는 트랜잭션이 시작되고 DataSource가 정해지는 것이 아니라, @Transactional이 붙은 메소드에서 쿼리가 실제로 실행될 때 DataSource가 정해진다.
(@Transactional이 붙지 않은 메소드에서는 원래 쿼리가 실행될 때 정해짐)
JpaAuditingConfig.class
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("dataSource") DataSource dataSource) {
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(dataSource);
entityManagerFactory.setPackagesToScan("com.blocker.blocker_server");
entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter());
entityManagerFactory.setPersistenceUnitName("entityManager");
return entityManagerFactory;
}
private JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setShowSql(true);
hibernateJpaVendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL8Dialect");
return hibernateJpaVendorAdapter;
}
@Bean
public PlatformTransactionManager transactionManager (
@Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(entityManagerFactory.getObject());
return jpaTransactionManager;
}
}
JPA를 사용할 때, EntityManager를 따로 등록해줄 필요는 없다. 하지만 현재 어떤 DataSource를 사용해야 하는지 설정해줘야 하기 때문에, 직접 EntityManager를 Bean으로 등록해준다.
Write와 Read에 따라서 각각 처리되고 있는지 확인
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly())
log.info("SLAVE");
else
log.info("MASTER);
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? SLAVE_DB : MASTER_DB;
}
AbstractRoutingDataSource에서 @Transactional이 read only인지에 따라 로그도 같이 찍어보자.
Insert 쿼리를 실했을 때는 MASTER DB가 호출되고,
select 쿼리를 실행했을 때는 slave DB가 호출되었다.
4. 마무리
MySQL Replication으로 Master-Slave 구조를 사용하면, 부하가 분산으로 안정적인 서비스를 운영할 수 있고 DB에 장애가 생겼을 때도 좀 더 대응하기 수월하다. 하지만 실시간성과 데이터 정합성이 중요한 로직의 경우 master DB로 요청하는 방법 같은 애플리케이션 단에서의 설정 등을 적용하거나 다른 해결 방법을 고려해야 한다.
참고
http://cloudrain21.com/mysql-replication
MySQL - Replication 구조 - Rain.i
All about IT tech, especially database, cloud, linux, clustering.
cloudrain21.com
https://tjdrnr05571.tistory.com/14
[#8] Mysql Replication - Spring에서 Master/Slave 이중화 with Docker
이글에선 단일서버에서 Mysql Replication을 port를 나누어 하는 방법을 다룹니다. 목차 - 내 프로젝트에서 Mysql Replication을 사용해야 하는 이유 - Mysql Replication의 동작 원리 - Docker로 Mysql 컨테이너 두개
tjdrnr05571.tistory.com
'DB' 카테고리의 다른 글
(2) Mysql 실행 계획 분석 및 쿼리 튜닝 (0) | 2024.05.01 |
---|---|
(2) 스프링 Redisson 분산락 (0) | 2024.04.09 |
(1) 스프링 동시성 문제 (1) | 2024.04.04 |
auto_increment index 꼭 필요한가 ? (0) | 2023.07.28 |