일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 트랜잭션
- JPA
- 세션
- JUnit5
- Docker
- HTTP
- 프리코스
- 우아한세미나
- yml
- 우테코
- 레벨2
- 백준
- REDIS
- 서블릿
- 스프링부트
- 자바
- 코드리뷰
- Level2
- MSA
- AWS
- AOP
- CircuitBreaker
- Paging
- 프로그래머스
- 스프링 부트
- mock
- 미션
- Spring Batch
- 우아한테크코스
- 의존성
- Today
- Total
늘
[대용량 데이터 처리] Master-Slave db replication 설정 with docker 본문
대용량 데이터 처리하는 방법에 여러 방법이 있다.
Load Balancer
- Request를 연결된 서버들에게 나누어줌
- 장애 발생시 해당 LB(Load Balancer)에게 할당된 IP를 다른 LB에게 넘겨줌
DBMS 2개(Master-Slave = Primary-Secondary)
- primary(실제 서비스)
- primary에서 장애 발생시 secondary가 primary로 되고, 장애가 해결되도 primary는 secondary 역할을 하게 된다.
- primary(CUD), secondary(R)
- 두대를 두고 primary의 데이터를 secondary로 계속 Replication을 통해 복제한다.
Object Storage Service (File-Server)
- 파일을 저장할 서버를 둘 경우 총 3개의 File-server가 필요하다.
- 3개를 사용할 시 Data Loss : 99.999%가 보장된다.
- AWS S3, AZURE Blob, Gcp Google Storage에서 서비스를 제공한다.
하지만 데이터가 많아질 수록 DBMS의 TPS 한계가 온다.
TPS(Transaction Per Second)
- 초당 몇개의 명령을 처리할 수 있는가
- ex) MAX 1000 TPS = 1초에 1000개의 명령을 처리
- CPU, 메모리, 디스크 등 하드웨어 적인 부분이 속도에 영향을 미친다.
- 너무 많은 경우 오래걸리거나 장비가 고장날 수 있다.
위에 설명한 내용 이외에 더 다양한 방식이 있지만 이번에는 DB를 master(primary)와 slave(secondary)로 나눠보는 실습을 해보려고 간략히 적었다.
왜? primary 혹은 master라고 두개로 불릴까?
- 원래는 master-slave구조로 불렸다고 한다. 하지만 흑인 문제와 관련되면서 master-slave에서 primary-secondary로 바뀐것으로 안다. *(깃에서 master branch가 디폴트였는데 main branch로 바뀐 이유도 위와 같다.)
Replication
- 단방향인 Replication은 replication을 주는게 Master이고, 받는 게 Slave이다.
- Slave는 Master와 동기화되지만, Master는 Slave와 동기화가 되지 않는다.
- 즉, Master도 Slave와 동기화를 하고 싶다면 반대로도 replication을 걸어야 한다.
스프링 부트와 Replication
- @Transactional(readOnly = true) 인 경우는 Slave DB 접근
- @Transactional(readOnly = false) 인 경우에는 Master DB 접근
위와 같은 방식으로 사용할 것입니다.
왜? docker-compose를 이용했는가?
- 두개의 dbms가 필요하고 이를 따로 따로 관리하기엔 불편하다. 또한 실제 실무에서는 여러대의 slave들이 필요하는데 이떄 각각 관리하면 불편하기 때문에 docker-compose명령어를 통해서 컨테이너들을 한 번에 관리하기 위해서 사용했다.
docker/Dockerfile
FROM --platform=linux/x86_64 mysql:8.0
ADD ./master/my.cnf /etc/mysql/my.cnf
간단합니다. 리눅스 환경에 mysql:8.0으로 master에 my.cnf파일을 컨테이너 속으로 넘겨주는 파일입니다.
slave에서도 위와 마찬가지로 slave에 .cnf파일을 컨테이너로 넘겨주면 됩니다.
docker/master/my.cnf
[mysqld]
log_bin = mysql-bin // <- MySQL(오라클의 redo로그와 유사) 로그생성 설정
server_id = 1 //<- 서버 고유아이디, Slave와 다르게 설정
expire_logs_days = 7 // <- 로그 보관주기 설정(일)
default_authentication_plugin=mysql_native_password
docker/slave/my.cnf
[mysqld]
log_bin = mysql-bin
server_id = 11
relay_log = /var/lib/mysql/mysql-relay-bin
log_slave_updates = 1
read_only = 1
default_authentication_plugin=mysql_native_password
docker-compose.yml
version: "3"
services:
db-master:
build:
context: ./
dockerfile: master/Dockerfile
restart: always
environment:
MYSQL_DATABASE: 'db'
MYSQL_USER: 'user'
MYSQL_PASSWORD: 'password'
MYSQL_ROOT_PASSWORD: 'password'
ports:
- '3306:3306'
# Where our data will be persisted
volumes:
- my-db-master:/var/lib/mysql
- my-db-master:/var/lib/mysql-files
networks:
- net-mysql
db-slave:
build:
context: ./
dockerfile: slave/Dockerfile
restart: always
environment:
MYSQL_DATABASE: 'db'
MYSQL_USER: 'user'
MYSQL_PASSWORD: 'password'
MYSQL_ROOT_PASSWORD: 'password'
ports:
- '3307:3306'
# Where our data will be persisted
volumes:
- my-db-slave:/var/lib/mysql
- my-db-slave:/var/lib/mysql-files
networks:
- net-mysql
volumes:
my-db-master:
my-db-slave:
networks:
net-mysql:
driver: bridge
net-mysql이라는 network를 만들어 주고 master와 slave를 연결해줍니다.
도커를 이용해서 3306은 Master, 3307은 Slave에게 할당해줍니다.
DataSourceConfiguration
Data Source를 Bean으로 등록해줍니다.
package com.db.replication.config;
import com.zaxxer.hikari.HikariDataSource;
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.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfiguration {
public static final String MASTER_DATASOURCE = "masterDataSource";
public static final String SLAVE_DATASOURCE = "slaveDataSource";
@Bean(MASTER_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.master.hikari") // (1)
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(SLAVE_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@DependsOn({MASTER_DATASOURCE, SLAVE_DATASOURCE})
public DataSource routingDataSource(
@Qualifier(MASTER_DATASOURCE) DataSource masterDataSource,
@Qualifier(SLAVE_DATASOURCE) DataSource slaveDataSource) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> datasource = new HashMap<>();
datasource.put("master", masterDataSource);
datasource.put("slave", slaveDataSource);
routingDataSource.setTargetDataSources(datasource);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@Primary
@Bean
@DependsOn("routingDataSource")
public LazyConnectionDataSourceProxy dataSource(DataSource routingDataSource){
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
- yml에서 설정한 값을 DataSource를 생성하는데 이용.
RoutingDataSource
package com.db.replication.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() { // (1)
return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master"; //(2)
}
}
- determineCurrentLookupKey() 메서드는 현재 조회 키를 반환받기 위해 구현해야 하는 추상 메서드입니다.
- readOnly 속성을 구별하여 알맞은 key를 반환하게 합니다.
모든 설정은 끝났습니다. 이제 실행해봅시다!
실행
docker-compose up -d
Master db의 host주소를 찾기 위해 inspect로 찾아줍니다.
dokcer network ls
docker inspect {network의 ID}
찾은 master db의 주소를 기억해두고 slave로 접속합니다.
docker ps
docker exec -it {SLAVE_CONTAINER_ID} bash
mysql -u root -p
SLAVE Setting
stop slave;
CHANGE MASTER TO
MASTER_HOST='{master network ip address}', <- Master IP나 호스트명을 입력
MASTER_USER='root', <- Replication 아이디
MASTER_PASSWORD='password', <- Replication 패스워드
MASTER_LOG_FILE='mysql-bin.000001', <- Master의 SHOW MASTER STATUS; 결과화면의 로그파일명 입력
MASTER_LOG_POS=0, <- Master의 SHOW MASTER STATUS; 결과화면의 Position 입력
GET_MASTER_PUBLIC_KEY=1;
start slave;
show slave status\G
여기서 뒤에 net주소는 버리고 입력해줍니다.
show slave status\G
성공입니다.
해결
Worker 0 failed executing transaction 'ANONYMOUS' at ~~
위와 같이 나오면서 연결이 안될 때는 아래처럼 skip count를 해줍니다.
https://developpaper.com/three-parameter-analysis-of-mysql-replication/
SQL_ slave_ skip_ 값 뒤에 counter가 이벤트 수라고 했으므로 여기서는 이벤트를 건너뛰는 것과 같습니다. MySQL은 이벤트를 건너뛴 후에도 이벤트가 여전히 트랜잭션에 있으면 트랜잭션을 계속 건너뛸 것이라고 규정합니다.
set global sql_slave_skip_counter=1;
'백앤드 개발일지 > 데이터베이스' 카테고리의 다른 글
[postgreSQL] order by와 limit이 걸린 슬로우 쿼리 해결 방법 (0) | 2024.05.25 |
---|---|
[데이터베이스] B+Tree, 해싱, 인덱싱 정리 (0) | 2021.11.10 |
식별자와 비식별자 (0) | 2021.08.11 |
데이터베이스 관리 시스템(DBMS) & 튜닝 (0) | 2021.07.23 |