Mysql Replication, DB 부하 분산 본문
Mysql db replication
DB를 백업하기 위해서 replication 을 사용한다. 백업을 위함도 클 것이고, 쿼리 부하 분산, 지역화의 이점도 얻을 수 있을 것이다. 이 글에선 아래 네가지 키워드를 다룬다.
- Mysql이 제공하는 Replication 방식과 각 장단점
- 비동기 복제의 정합성 문제
- Spring boot에서 읽기와 쓰기 Datasource 분기하기
복제 종류
Mysql 의 복제 방식은 '동기', '비동기', '반동기'가 대표적이다.
동기 방식은 한 노드에 요청된 쿼리를 트랜잭션 내에서 다른 노드들에 전달하고 모든 노드들이 동기화되면 트랜잭션의 결과를 반환한다. 마스터(또는 첫 요청을 받은 노드)가 다른 노드들에 쿼리를 전달하는 식으로 진행된다. 모든 노드에서 동일한 요청을 수행한 후 결과를 반환하기에 모든 노드에 데이터 정합성이 보장된다는 장점이 있으나, 그렇기 때문에 애플리케이션 쪽에선 DB 처리 속도가 느려지고 트랜잭션의 생명주기가 길어진다는 단점이 있다. 또 노드 중 하나라도 에러나 다른 상태를 갖게된다면 모든 노드가 결국 요청을 못 다루는 상황이 생기게 된다.
비동기 방식은 한 노드에 요청된 쿼리를 다른 노드에서 읽어서 처리한다. Mysql 의 경우 수행된 쿼리나 변경 값을 로그로 (바이너리 로그) 저장하고 master thread 에서 , slave 에선 그 내용을 기록해두었다가 (릴레이로그) slave thread 로 읽어 본인의 data 에도 이를 반영하는 식으로 진행된다. 애플리케이션 입장에선 Master 노드의 처리만 기다리면 되기 때문에 속도에서 이점이 있으나, slave 에서 sync 처리를 반영하는 사이 시간 동안 데이터 정합성이 깨지는 단점이 있다.
반동기 방식은 이 둘을 섞는다. Master 가 요청된 요청을 Binary log 에 저장하고 slave 는 이 log 를 읽고 처리가 완료되면 master node 에 ACK 로 알린다. Master 는 한 개 이상의 ACK 를 확인하면 그제서야 트랜잭션을 커밋한다. 이렇게 하면 모든 Node 를 기다리지 않되 한 개의 slave 는 데이터 정합성을 유지한다는 것이 보장되게 된다. 그렇지만 여전히 비동기 방식보다 느리고, slave 의 상태나 네트워크에 영향을 받는다는 단점이 있다.
비동기식 복제의 정합성 문제
비동기식 복제는 동기화를 위한 시간이 필요하기에, 데이터 정합성 문제있는 엔티티인지 여부에 대한 고민이 필요하다. 그리고 주로 발생하는 동기화가 오래 걸리는 이유는 아래와 같았다.
1. 읽기 요청 과다로 Sync를 위한 시간이 없는 것은 아닌지 : slave db에 읽기 요청이 워낙 많아 처리 성능 자체가 떨어지고 sync 도 밀리는 경우이다. 이런 경우 slave db 를 scale up 하거나, slave db 자체를 증설하여 요청을 분산 할 수 있겠다.
2. Sync를 위한 스레드 부족은 아닌지 : mater db 의 쓰기량에 비해 slave db sync 속도가 낮은 경우이다. Mysql 의 기본 옵션은 replication 을 위해 싱글 스레드만을 사용했다. master db 는 여러 세션에서 처리 결과를 만들고 쿼리 내용 또는 결과를 log 에 기록하는데 복제를 위한 thread 는 단 한개 뿐이라 쓰기가 많아진다면, slave db 가 많아진다면 정합성 문제는 불가피했다. Mysql 5.6 부터 추가된 Multi-Threaded Replication Slaves 을 사용해서 복제를 위한 스레드 (SQL thread, worker)를 증설하는 것이 도움이 된다. [mysql-mysql-5-6-ga-replication-enhancements]
3. Binary log Sync 자체가 느린건 아닌지 : slave db 에서 Sync 를 처리하는 시간 자체가 느린 경우를 말한다. 그 시간 동안 정합성 문제가 생긴다. Mysql 의 binary log 을 이용한 동기화엔 여러 방식이 있다. (Binary Format) 그 중 Statement Based Replication 는 master 에서 처리한 쿼리를 그래도 slave 에서 실행하는 방식이고 그 경우 쿼리 시간 이상은 반드시 정합성 문제가 생긴다. 쿼리 속도 자체를 낮추거나 Row Based Replication 으로 바꿔 master 에서 쿼리 실행으로 변경된 row 를 sync 하는 것으로 쿼리 시간 대신 변경 반영 시간으로 정합성 문제의 시간을 바꿀 수 있다.
Spring boot에서 읽기와 쓰기 Datasource 분기하기
Spring 에서 두 개의 db를 db source 로 설정하고 transactional 의 readOnly 옵션으로 쿼리가 수행될 target source 를 분기한다. 읽기 작업은 SLAVE DB 에서, 쓰기 작업은 MASTER DB 에서 처리한다.
1. DB source 를 정의한다.
연결할 DB 정보를 설정한다.
# application.properties
spring.datasource.master.username={USER_NAME}
spring.datasource.master.password={PASSWORD}
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.jdbc-url= jdbc:mysql://{SOURCE_URL}/{DB}
spring.datasource.slave.username={USER_NAME}
spring.datasource.slave.password={PASSWORD}
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.jdbc-url= jdbc:mysql://{SOURCE_URL}/{DB}
# DataSourceConfig
@Bean(value = DB_SOURCE_BEAN_ALIAS_MASTER)
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(value = DB_SOURCE_BEAN_ALIAS_SLAVE)
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
2. DB 타입을 정의하고 ReadOnly 여부에 따라 사용할 DB source 를 반환한다.
enum DBType {
MASTER,
SLAVE
}
determineCurrentLookupKey() 를 재정의하여 쿼리를 전송할 DB source 를 나타낼 lookup key 를 반환할 수 있다.
class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
var isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if (isReadOnly) {
return SLAVE;
}
return MASTER;
}
}
TransactionSynchronizationManager.isCurrentTransactionReadOnly() 를 사용하면 현재 transaction 의 readOnly 옵션 여부를 확인할 수 있다. readOnly 면 SLAVE type 을 키로, 그렇지 않으면 MASTER type 을 키로 반환할 것이다.
3. RoutingDataSource 를 정의하여 DB type 과 DB source 를 mapping 한다.
lookup 키로 반환 받은 DB type 만으론 어떤 type 에 어떤 DB source 가 매핑 되었는지 알 수 없다. RoutingDataSource 를 정의하고 { DB type : DB source } 를 mapping 하여 type 별 target DB source 를 지정한다.
@Bean
public DataSource routingDataSource(
@Qualifier(DB_SOURCE_BEAN_ALIAS_MASTER) DataSource masterDataSource,
@Qualifier(DB_SOURCE_BEAN_ALIAS_SLAVE) DataSource slaveDataSource
) {
var routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(masterDataSource);
routingDataSource.setTargetDataSources(
Map.of(
MASTER, masterDataSource,
SLAVE, slaveDataSource
)
);
return routingDataSource;
}
4. LazyConnectionDataSourceProxy 를 앞서 설정한 routingDataSource 와 함께 생성한다.
Spring은 default 로 Transactional 이 시작되는 시점부터 DataSource 를 미리 정해둔다. 지금 상황처럼 Master, Slave 로 나누거나, 쿼리 동작에 따라 DB1, DB2 로 나누는 등 transaction 진입 이후에 DB source 분기가 필요한 상황에서 이미 transaction 이 시작된 상황에서 DB source 가 결정 되었기에 이후 분기가 불가능하다.
@Bean
@Primary
public DataSource dataSource() {
var determinedDataSource = routingDataSource(
masterDataSource(),
slaveDataSource()
);
return new LazyConnectionDataSourceProxy(determinedDataSource);
}
LazyConnectionDataSourceProxy 를 사용하여 DB source 결정을 transaction 진입 이후로 미뤄 쿼리가 실행될 때 DB source 를 결정할 수 있도록 한다.
5. Test code - Routing data source 의 db source 분기를 확인한다.
@TestPropertySource(locations = "/application-dev.properties")
@SpringBootTest
class DataSourceConfigTest {
private final RoutingDataSource routingDataSource;
public DataSourceConfigTest(@Autowired RoutingDataSource routingDataSource) {
this.routingDataSource = routingDataSource;
}
@Transactional
@DisplayName("Transactional readonly = false 인 경우 master db에 쿼리된다.")
@Test
void masterDataSource() {
Object currentLookupKey = routingDataSource.determineCurrentLookupKey();
assertEquals(DBType.MASTER, currentLookupKey);
}
@Transactional(readOnly = true)
@DisplayName("Transactional readonly = true 인 경우 slave db에 쿼리된다.")
@Test
void slaveDataSource() {
Object currentLookupKey = routingDataSource.determineCurrentLookupKey();
assertEquals(DBType.SLAVE, currentLookupKey);
}
}
'Architecture > Infrastructure' 카테고리의 다른 글
Nginx 요청 호출 수 제한과 접근 가능 IP 제한 (0) | 2023.12.17 |
---|---|
Mysql Multi source replication, 백업 데이터 중앙화 (0) | 2023.11.18 |
EKS Endpoint access 를 private 으로 (0) | 2023.08.30 |
EKS 모니터링, Cloudwatch 세팅부터 slack 알람까지 (0) | 2023.06.06 |
Kubernetes apt repository updated : NO_PUBKEY B53DC80D13EDEF05 (0) | 2023.06.06 |