회사의 모든 데이터베이스를 CNPG로 일원화하고자 하는 정책에 따라, 현재 운영 중인 Azure PostgreSQL 서비스를 자체 관리형 CloudNativePG(CNPG) 클러스터로 마이그레이션해야 하는 상황이 되었다.

 

만약 운영 서비스와 연결되지 않은 데이터베이스였다면 단순히 dump & restore 방식으로 쉽게 해결할 수 있었겠지만, 실제 서비스가 연결되어 있어 다른 접근 전략이 필요했다.

 

팀 내에서도 실시간으로 운영 중인 데이터베이스를 마이그레이션해본 경험이 없었기 때문에 팀원들과 충분히 논의하며 마이그레이션 계획을 수립했다.

 

검토 결과 두 가지 선택지가 있었다.

  • 무중단 마이그레이션 (복제 지연 위험 존재)
  • 다운타임 허용 마이그레이션 (데이터 정합성 보장)

우리 서비스의 특성상 교육 시간대만 피하고 사전 공지를 한다면 짧은 다운타임이 발생해도 비즈니스에 큰 영향이 없었기 때문에, 데이터 정합성을 우선시하는 방향으로 결정했다.

마이그레이션 순서

다운타임 허용 마이그레이션 순서는 아래와 같이 정리했다

  • DB 실시간 복제 실행 → 기존 앱 중단 → 데이터 정합성 확인 → 새로운 앱 배포

마이그레이션 방식 선택

마이그레이션 방식으로는 CDC와 Logical Replication 두 가지를 검토했다.

두 방식 모두 내부적으로 WAL(Write Ahead Logging)을 사용한다는 공통점이 있지만, CDC는 Kafka와 같은 추가 이벤트 스트림 컴포넌트가 필요하다는 차이점이 있다.

  • CDC: Source DB → WAL → Debezium → Kafka → Target(PostgreSQL, Elasticsearch 등)
  • Logical Replication: Source DB → WAL → Target DB

이번 케이스에서는 Debezium, Kafka 등의 추가 컴포넌트 설정이 불필요하고, 동일한 PostgreSQL 간의 단순 마이그레이션이므로 Logical Replication을 선택했다.

도구 선택

Logical Replication 구현을 위해 별도의 서드파티 도구 없이 PostgreSQL 내장 기능만으로 동작하는 pgcopydb(https://github.com/dimitri/pgcopydb)를 선택했다.


테스트 환경 세팅

가장 먼저 개발서버에서 사전 테스트를 진행하기 위해 운영환경과 동일한 환경을 세팅하였다

docker compose로 진행했으며, source/target DB에 대해 postgresql.conf를 적절히 작성하였다

version: '3'
services:
    postgres_source:
        image: postgres:13
        environment:
            - POSTGRES_DB=source-db
            - POSTGRES_USER=hanbin
            - POSTGRES_PASSWORD=thisispassword
        volumes:
            - postgres_13-data:/var/lib/postgresql/data
            - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
        ports:
            - "127.0.0.1:5432:5432"
        shm_size: 1g
        command: [
            "-c", "config_file=/etc/postgresql/postgresql.conf",
         ]
    postgres_target:
        image: postgres:16
        environment:
            - POSTGRES_DB=target-db
            - POSTGRES_USER=hanbin
            - POSTGRES_PASSWORD=thisispassword
        volumes:
            - postgres_16-data:/var/lib/postgresql/data
            - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
        ports:
            - "127.0.0.1:5433:5432"
        shm_size: 1g
        command: [
            "-c", "config_file=/etc/postgresql/postgresql.conf",
        ]
volumes:
    postgres_source-data:
        driver: local
    postgres_target-data:
        driver: local

 


테스트 순서

자세한 순서는 아래와 같다

1. Bastion VM pgcopydb 설치 및 연결 테스트
2. Source DB유저에게 모든 sequence 권한 부여
3. Target DB유저에게 임시로 superuser 권한 부여
4. DB 동기화
5. 복제 상황 모니터링
6. 복제 지연 최소화 대기 및 데이터 검증
7. api, worker, cron pod 제거
8. 복제 지연 없음 확인
9. pgcopydb 프로세스 종료
10. replication slot, origin 삭제
11. target DB유저 superuser 권한 제거
12. api, worker, cron 재배포

순서대로 차근차근 진행해보자

1. Bastion VM pgcopydb 설치 및 연결 테스트
sudo apt update
sudo apt install pgcopydb

mkdir -p pgcopydb-migration
cd pgcopydb-migration

export PGCOPYDB_SOURCE_PGURI="postgres://source-db:thisispassword@127.0.0.1:5432/source-db"
export PGCOPYDB_TARGET_PGURI="postgres://target-db:thisispassword@127.0.0.1:5433/target-db"

pgcopydb ping --source "$PGCOPYDB_SOURCE_PGURI" --target "$PGCOPYDB_TARGET_PGURI"
# 기대 출력
## INFO Successfully could connect to source database
## INFO Successfully could connect to target database
2. Source DB유저에게 모든 sequence 권한 부여
-- source-db 유저에게 모든 sequence 권한 부여
GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA public TO "source-db";
3. Target DB유저에게 임시로 superuser 권한 부여
-- 임시로 superuser 권한 부여
ALTER USER "target-db" WITH SUPERUSER;
4. DB 동기화
nohup pgcopydb clone --follow \
  --source "$PGCOPYDB_SOURCE_PGURI" \
  --target "$PGCOPYDB_TARGET_PGURI" \
  --no-owner \
  --no-acl \
  --table-jobs 1 \
  --index-jobs 1 \
  --verbose \
  --dir . \
  > ./pgcopydb-migration.log 2>&1 &

# 프로세스 ID 저장
echo $! > ./pgcopydb-migration.pid

# 실시간 로그 확인
tail -f ./pgcopydb-migration.log

# 진행 상황 확인
./replication_monitoring.sh
6. 복제 지연 최소화 대기 및 데이터 검증
복제지연 확인
SELECT
    slot_name,
    active,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) as lag_size,
    pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) as lag_bytes
FROM pg_replication_slots
WHERE slot_name = 'pgcopydb';

-- Result --
 slot_name | active | lag_size | lag_bytes
-----------+--------+----------+-----------
 pgcopydb  | t      | 541 kB   |    553920
(1 row)
데이터 일관성 검증
pgcopydb compare schema --verbose
pgcopydb compare data --verbose

약 541KB 만큼의 복제 지연이 있다는 의미이다.

# 데이터 정합성 확인
pgcopydb compare data

# Output
16:11:44.270 162208 INFO   Running pgcopydb version 0.17-1.pgdg22.04+1 from "/usr/bin/pgcopydb"
16:11:44.357 162208 INFO   Using work dir "/tmp/pgcopydb"
16:11:44.358 162208 INFO   SOURCE: Connecting to "postgres://target-db@127.0.0.1:5432/target-db?keepalives=1&keepalives_idle=10&keepalives_interval=10&keepalives_count=60"
16:11:44.361 162208 INFO   Re-using catalog caches
16:11:44.365 162208 INFO   Starting 4 table compare processes
                    Table Name | ! |                      Source Checksum |                      Target Checksum
-------------------------------+---+--------------------------------------+-------------------------------------
                 public.user |   |  9136aaf4-0f32-cd2b-850f-49270ff7c03 |  9136aaf4-0f32-cd2b-850f-49270ff7c03

만약 두 데이터가 다른 경우 Checksum이 다르고, 두번째 컬럼에 !가 나타난다

7. api, worker, cron pod 제거

사용중인 CD tool에서 직접 내렸다

8. 복제 지연 없음 확인

6번 과정에서 진행한 작업을 반복하여 확인한다

9. replication slot, origin 삭제
-- SOURCE DB에서
SELECT pg_drop_replication_slot('pgcopydb');

-- TARGET DB에서
SELECT pg_replication_origin_drop('pgcopydb');
10. target DB유저 superuser 권한 제거
-- superuser 권한 제거
ALTER USER "target-db" WITH NOSUPERUSER;
11. api, worker, cron 재배포

사용중인 CD tool에서 직접 다시 올렸다

 


실제로 진행할때는 개발환경에서 권한과 postgresql 버전 등등을 정확히 동일하게 세팅 하지 않았어서 운영환경에 반영할때 잡음이 많았다.

좀더 꼼꼼하게 개발환경을 세팅했으면 좋았을텐데 아쉬웠다.

 

팀 내에서 경험이 없던 작업이라 문서화에 신경을 많이 썼는데, 문서화가 제일 피곤했다

 

데이터 정합성과 다운타임을 모두 잡는 해결책이 있을지 궁금해졌다.

'DB' 카테고리의 다른 글

[PostgreSQL] Lock에 대해  (0) 2025.11.02
[Elasticsearch] 유저가 원하는 검색결과 보여주기 (analyzer)  (1) 2025.07.31
[DB] MongoDB 장점  (0) 2022.06.01
[DB] MongoDB 자료구조  (0) 2022.06.01
[DB] Lock 이해하기  (0) 2022.05.20

+ Recent posts