💬 Project
[이길어때] 우당탕탕 DB 레플리케이션 적용기
지난 번
postgreSQL
을 적용한 경험에 이어서 DB 레플리케이션
을 적용한 경험을 공유하고자 합니다.우선 저희 프로젝트는 기존의
DB
를 한대만 운영했기 때문에, DB
의 의존성이 너무나 심하게 걸려있는? 그런 형태였습니다.예를 들어, 멀티 모듈로 구성하여 각 서비스의 결합도를 아무리 낮추었다고 한들,
데이터베이스에 문제가 생긴 경우에는 결국 모든 서비스가 마비가 됩니다.
이러한 단점을 상쇄하기 위해서
DB 레플리케이션
을 적용하기로 하였습니다.참고로 이 글에서는 Docker / postgreSQL 환경에서의 물리적 복제(replication) 을 다룹니다.
클라우드 환경에서의 RDS를 활용한 레플리케이션은 추후에 다른 글에서 작성하도록 하겠습니다 😊
Slave DB 구성하기
기존의 DB는 구성되어 있다고 가정하고 글을 작성하도록 하겠습니다. Master DB를 구성한 경험은 이 글 에서 확인해주시면 됩니다 🙂
기존 DB는 지난번 설정과 같이 실행하도록 하겠습니다.
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}}{{.IPAddress}}{{end}}' container_name_or_id
해당 명령어를 사용하면
docker
컨테이너의 ip를 확인할 수 있습니다. 마지막의 container_name_or_id
에는 컨테이너의 이름이나 아이디를 입력하면 됩니다.두 컨테이너의 IP를 모두 확인하였다면, 다음으로 넘어갑니다.
Master DB 설정하기
docker exec -it masterdb_conatiner 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 환경 설정하기
웹 애플리케이션 설정의 경우 해당 게시글을 참고하였으니 확인해주시길 바랍니다.
먼저
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를 통해 더 쉽고 간단하게 설정하는 방법에 대해서도 알아보도록 하겠습니다~ 😄