💬 Project

[이길어때] 우당탕탕 DB 레플리케이션 적용기

date
Jan 3, 2024
slug
yigil-db-replication
author
status
Public
tags
Java
postgreSQL
Docker
summary
이길어때 프로젝트를 진행하면서 DB 레플리케이션을 프로젝트에 적용한 경험을 기록합니다
type
Post
thumbnail
제목을 입력해주세요_-001 (12).png
category
💬 Project
updatedAt
Jan 3, 2024 09:35 AM
지난 번 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_containerMaster 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 을 활용하여 접속하였습니다.
notion image
해당 환경에서 모든 파일을 선택 후 삭제합니다. (꼭 컨테이너가 종료되어 있는지 확인 후 작업합니다)
이후 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 DBSlave 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를 통해 더 쉽고 간단하게 설정하는 방법에 대해서도 알아보도록 하겠습니다~ 😄