이길어때가 더 궁금하다면?
2024.02.06 - [회고/프로젝트 🖥️] - [이길어때] 목차
지난 번 postgreSQL
을 적용한 경험에 이어서 DB 레플리케이션
을 적용한 경험을 공유하고자 합니다.
우선 저희 프로젝트는 기존의 DB 를 한대만 운영했기 때문에, DB의 의존성이 너무나 심하게 걸려있는? 그런 형태였습니다.
예를 들어, 멀티 모듈로 구성하여 각 서비스의 결합도를 아무리 낮추었다고 한들,
데이터베이스에 문제가 생긴 경우에는 결국 모든 서비스가 마비가 됩니다.
이러한 단점을 상쇄하기 위해서 DB 레플리케이션을 적용하기로 하였습니다.
참고로 이 글에서는 Docker / postgreSQL 환경에서의 물리적 복제(replication) 을 다룹니다. 클라우드 환경에서의 RDS를 활용한 레플리케이션은 추후에 다른 글에서 작성하도록 하겠습니다 😊
Slave DB 구성하기
기존의 DB는 구성되어 있다고 가정하고 글을 작성하도록 하겠습니다. Master DB 구성에 대해서는 아래 글을 통해서 확인해주시면 됩니당 :)
2024.02.07 - [회고/프로젝트 🖥️] - [이길어때] 위치 정보 관리를 위한 PostGIS 적용기
docker run -d --name slave_container_name -e POSTGRES_PASSWORD=password -p 5433:5432 -v postgres_data:/var/lib/postgresql/data postgres
먼저 docker
환경에서 Slave
컨테이너를 생성합니다.
이 때 컨테이너의 이름은 slave_conatiner_name
(Master DB 의 컨테이너 명과 달라야합니다!)
루트 사용자의 비밀번호는 password 로 설정됩니다.
그리고 기존의 DB 컨테이너는 호스트의 5432 포트를 컨테이너의 5432 포트를 연결했었는데요, 이미 5432 포트는 사용 중이므로, 5433번 포트에 연결합니다.
이후, 해당 컨테이너가 동작하지 않을 때에도 해당 파일시스템에 접근할 수 있도록 볼륨 마운트를 진행해줍니다.
postgreSQL의 데이터 파일은 /var/lib/postgresql/data
하위에 저장되므로 해당 경로를 마운트하였습니다.
컨테이너 IP 확인하기
이후 Master DB의 컨테이너 IP와 Slave DB 의 컨테이너 IP를 확인해야 합니다.
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAdress}}{{end}}' container_name_or_id
해당 명령어를 사용하면 docker 컨테이너의 ip를 확인할 수 있습니다. 마지막의 container_name_or_id
에는 컨테이너의 이름이나 아이디를 입력하면 됩니다.
두 컨테이너의 IP를 모두 확인하였다면, 다음으로 넘어갑니다.
Master DB 설정하기
docker exec -it masterdb_container bash
먼저 컨테이너 환경에 접속하기 위해 해당 명령어를 통해 접속합니다. 이때 masterdb_container
는 Master DB 의 컨테이너 이름입니다.
apt-get update && apt-get install nano
그리고 설정파일 변경을 위해 파일 편집기를 설치합니다. 편의상 저는 nano
를 설치했는데 vi
와 같은 편집기를 설치해도 무관합니다.
nano /var/lib/postgresql/data/postgresql.conf
설치한 편집기로 해당 경로에 있는 postgresql.conf
파일을 열어서 수정합니다.
해당 파일을 열면 많은 내용이 있는데 거의다 #이 붙어있는 주석입니다.
필요한 설정들만 찾아서 주석 해제 및 값을 변경해주면 됩니다.
listen_addresses = '*' # 모든 연결을 허용합니다
port = 5432 # 포트번호를 작성합니다
max_connections = 100 # 최대 연결 수를 제한합니다 (충분한 값으로 설정)
shared_buffers = 256MB # 컨테이너 메모리의 1/4 정도로 설정합니다
work_mem = 4MB
maintenance_work_mem = 64MB
dynamic_shared_memory_type = posix
wal_level = replica # WAL 레벨을 설정합니다
max_wal_size = 1GB
min_wal_size = 80MB
archive_mode = on # WAL 파일을 아카이빙 합니다
archive_command = 'cp %p /archive/%f' # WAL 파일의 경로
max_wal_senders = 3 # Slave DB의 수 +1로 설정합니다
wal_keep_size = 1024
logging_collector = on
log_directory = pg_log
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_timezone = 'Etc/UTC'
해당 설정을 참고해서 설정 해주고 archive_command
에 작성한 경로를 컨테이너 내에서 생성해야 합니다.
mkdir /archive
이후 Master DB 의 컨테이너를 한 번 재실행 해줍니다.
만약 문제가 발생한다면 log_directory
의 값을 참고하여 로그 파일을 확인하면 됩니다.
위와 같이 설정한 경우, var/lib/postgresql/data/pg_log
하위에서 로그를 확인할 수 있습니다.
Master DB 가 정상적으로 실행 되었다면 DB에 접속해서 레플리케이션을 위한 사용자를 생성해야 합니다.
CREATE USER replica_user REPLICATION LOGIN CONNECTION LIMIT 2 ENCRYPTED PASSWORD '1234';
이 쿼리를 실행하면, 레플리케이션 권한을 가진 새 사용자를 생성합니다.
이때 이름은 replica_user 비밀번호는 1234 로 임의로 설정하였으므로, 본인의 입맛에 맞게 설정하면 됩니다.
이제 또 다시 docker 컨테이너 내에 접속합니다
nano /var/lib/postgresql/data/pg_hba.conf
편집기로 새로운 설정 파일인 pg_hba.conf
파일을 수정합니다.
host replication replica_user slave_ip/32 trust
파일에 접속해서 해당 한 줄을 추가해줍니다. 이때 replica_user 에는 위에서 생성한 레플리케이션 권한이 있는 DB 사용자명을 입력하고, slave_ip 는 위에서 확인한 Slave DB 컨테이너의 IP를 입력합니다.
이렇게 하면, Master DB 의 설정은 모두 끝나게 됩니다.
Slave DB 설정하기
docker exec -it slave_container bash
Slave DB 컨테이너에 해당 명령어를 통해 접속합니다. slave_container
는 해당 컨테이너의 이름입니다.
apt-get update
apt-get install postgis postgresql-16-postgis-3
apt-get install nano
Slave DB 컨테이너에 기존 Master DB 에 설치되어 있었던 확장기능과 아까 설치했던 편집기를 설치합니다.
이후 Master DB 를 종료합니다. 그리고 아까 생성했던 볼륨에 접속합니다. 저의 경우는 Docker Desktop
을 활용하여 접속하였습니다.
해당 환경에서 모든 파일을 선택 후 삭제합니다. (꼭 컨테이너가 종료되어 있는지 확인 후 작업합니다)
이후 MasterDB 컨테이너의 데이터 파일을 로컬 환경으로 복제해옵니다.
docker cp master-container:var/lib/postgresql/data [로컬 경로]
해당 방법을 통해 [로컬 경로]로 master-container 라는 이름의 컨테이너의 PostgreSQL data 파일을 가져옵니다.
이후 해당 파일들을 SlaveDB 컨테이너로 옮깁니다.
docker cp [로컬 경로] slave-container:var/lib/postgresql
해당 방법을 통해 [로컬 경로] (Master DB 에서 받아온 데이터 폴더) 를 slave-container 라는 이름의 컨테이너의 Data 파일을 붙여넣습니다.
이후, Slave DB 의 환경에 접속합니다. (위에 쓰였던 명령어를 통해서)
이후 설정 파일을 수정합니다.
nano /var/lib/postgresql/data/postgresql.conf
아까 Master DB 에서 했던 것 처럼 Slave DB 의 설정도 변경해줍니다.
이때 위 단계를 거쳐왔으면 Master DB 의 설정이 그대로 Slave DB 에 적용되어 있으므로 일 부분만 수정해주면 됩니다.
primary_conninfo = 'host=master_ip port=5432 user=replica_user password=1234 sslmode=prefer'
hot_standby = on
해당 설정을 추가로 변경하면 됩니다.
이때 master_ip 에는 Master DB 컨테이너의 IP 주소를, replica_user 에는 아까 생성했던 레플리케이션 권한을 받은 DB 유저 이름을 1234 에는 유저 생성 시 작성했던 비밀번호를 넣으면 됩니다.
모든 설정을 마쳤다면 저장 후 Slave DB 의 컨테이너를 재시작 합니다.
이러면 Slave DB 의 설정도 모두 마치게 됩니다.
설정 완료되었는지 확인하기
먼저 Master DB에서 해당 쿼리를 작성합니다.
SELECT * FROM pg_stat_replication;
해당 쿼리의 결과 Row 가 반환된다면 레플리케이션을 위해 Slave DB 가 접속을 하였음을 의미합니다.
Slave DB 에서는 해당 쿼리를 작성합니다.
SELECT * FROM pg_stat_wal_receiver;
해당 쿼리의 결과도 잘 반환되었다면, 설정이 잘 완료되어 잘 연결되었음을 확인할 수 있습니다.
만약 결괏값이 확인이 안될 경우, 각 컨테이너의 로그를 확인하여 설정을 조절하면 됩니다.
SpringBoot 환경 설정하기
웹 애플리케이션 설정의 경우 해당 게시글을 참고하였으니 확인해주시길 바랍니다.
https://wave1994.tistory.com/177
먼저 application.yml
파일을 수정해야 합니다.
spring:
datasource:
master:
hikari:
driver-class-name: org.postgresql.Driver
jdbc-url: [master-db의 주소]
read-only: false
username: [master-db의 Username]
password: [master-db의 password]
slave:
hikari:
driver-class-name: org.postgresql.Driver
jdbc-url: [slave-db의 주소]
read-only: true
username: [slave-db의 Username]
password: [slave-db의 password]
이런식으로 수정을하면 SpringBoot의 자동 설정 구성이 datasource 설정을 인식하지 못하므로 수동으로 해주어야 합니다.
MasterDataSourceConfig.java
@Configuration
public class MasterDataSourceConfig {
@Primary
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master.hikari")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
}
SlaveDataSourceConfig.java
@Configuration
public class SlaveDataSourceConfig {
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourcebuilder.create()
.type(HikariDataSource.class)
.build();
}
}
해당 방법을 통해 각 DataSource 객체를 선언하고 @Primary 어노테이션을 통해 우선순위를 설정합니다.
DataSourceType.java
public enum DataSourceType {
Master, Slave
}
Master DB 와 Slave DB 를 Enum 타입의 객체의 속성으로서 관리합니다.
ReplicationRoutingDataSource.java
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ?
DataSourceType.Slave : DataSourceType.Master;
}
}
해당 코드와 같이 AbstractRoutingDataSource 를 상속받는 클래스를 만들어서 서비스 레이어의 트랜잭션이 read-only 속성인지 에 따라 Slave DB 로의 요청인지 아니면 Master DB 로의 요청인지를 구분합니다.
RoutingDataSourceConfig.java
@Configuration
public class RoutingDataSourceConfig {
@Bean(name = "routingDataSource")
public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();
Map<Object, Object> dataSourceMap = Map.of(
DataSourceType.Master, masterDataSource,
DataSourceType.Slave, slaveDataSource
);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@Bean(name = "dataSource")
public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
이렇게하면 최종적으로 Transaction 설정에 따라 DataSource를 유동적으로 선택할 수 있게 되었습니다.
이렇게 물리적 복제를 통한 레플리케이션 방법에 대해 알아보았습니다.
다음 번에는 RDS를 통해 더 쉽고 간단하게 설정하는 방법에 대해서도 알아보도록 하겠습니다~ 😄
'회고 > 프로젝트 🖥️' 카테고리의 다른 글
[이길어때] Redis 캐싱으로 API 성능 개선하기 (0) | 2024.04.01 |
---|---|
[이길어때] SSE 방식의 실시간 알림 구현하기 (0) | 2024.03.19 |
[이길어때] 이벤트 기반으로 파일 업로드 기능 구현하기 (0) | 2024.02.07 |
[이길어때] 전략 패턴으로 확장성있게 소셜 로그인 설계하기 (0) | 2024.02.07 |
[이길어때] 위치 정보 관리를 위한 PostGIS 적용기 (0) | 2024.02.07 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆