[V2] DB 마이그레이션 - 100-hours-a-week/KTB4-10th-Cloud GitHub Wiki

  • 앞선 마이그레이션 설계 문서와 이어서 작성 *

4.4. DB 정합성 검증

  • 체크섬 검증

    • 해시값 기반으로 데이터의 값들을 비교
    • V1 MySQL과 V2 RDS의 전체 데이터를 PK 기준으로 조회하고, 각 행의 SHA-256 해시값을 비교하여 데이터 내용의 일치 여부를 검증하는 방식
  • cdc검증

#RDS, V1 DB 데이터 건수 일치 확인
+------------------+-----------+
| table_name       | row_count |
+------------------+-----------+
| generation_jobs  |       106 |
| itinerary_items  |      1297 |
| tourism_contents |     48959 |
+------------------+-----------+
#전체 테이블
+--------------------------+------------+
| TABLE_NAME               | TABLE_ROWS |
+--------------------------+------------+
| auth_sessions            |        346 |
| content_images           |          0 |
| credit_transactions      |        200 |
| credit_wallets           |         60 |
| event_details            |       1000 |
| favorite_contents        |         18 |
| flyway_schema_history    |          7 |
| generation_jobs          |        104 |
| guidebooks               |         99 |
| itinerary_days           |        347 |
| itinerary_items          |       1205 |
| member_guidebooks        |         93 |
| member_preferences       |        431 |
| members                  |         60 |
| notifications            |         53 |
| regions                  |        246 |
| tour_api_region_mappings |        257 |
| tour_api_sync_runs       |          6 |
| tourism_contents         |      47714 |
| web_push_subscriptions   |          9 |
+--------------------------+------------+

  • 체크섬 결과

    (.venv) ubuntu@ip-10-0-0-210:~/db-validation$ python validate.py
    Connecting to V1 MySQL and V2 RDS...
    Found 20 tables
    
    [PASS] auth_sessions: 346 rows
    [PASS] content_images: 0 rows
    [PASS] credit_transactions: 203 rows
    [PASS] credit_wallets: 60 rows
    [PASS] event_details: 1007 rows
    [PASS] favorite_contents: 20 rows
    [PASS] flyway_schema_history: 7 rows
    [PASS] generation_jobs: 106 rows
    [PASS] guidebooks: 99 rows
    [PASS] itinerary_days: 367 rows
    [PASS] itinerary_items: 1297 rows
    [PASS] member_guidebooks: 99 rows
    [PASS] member_preferences: 447 rows
    [PASS] members: 60 rows
    [PASS] notifications: 55 rows
    [PASS] regions: 246 rows
    [PASS] tour_api_region_mappings: 257 rows
    [PASS] tour_api_sync_runs: 6 rows
    [PASS] tourism_contents: 48959 rows
    [PASS] web_push_subscriptions: 9 rows
    
    ========== RESULT ==========
    Total tables : 20
    Passed       : 20
    Failed       : 0
    ============================
    All checked rows have matching SHA-256 checksums.
    
    • CDC 검증

      # 테스트용 RDS에 업데이트 된 것 확인(1개 입력 후 테스트)
      +------+---------------------+----------+--------------+
      | id   | administrative_code | name     | region_level |
      +------+---------------------+----------+--------------+
      | 2080 | DMS_CDC_TEST_01     | DMS CDC  | PROVINCE     |
      +------+---------------------+----------+--------------+
      1 row in set (0.00 sec)
      
      #삭제 검증
      #해당 데이터 삭제 확인
      Empty set (0.00 sec)
      
      

4.5. 무중단 전환 방식 선정

  • V2 백엔드는 현재 구축 및 검증 중으로, DB 마이그레이션 작업과 설정,일정 충돌 가능성이 있음
    • 따라서 V2 백엔드를 마이그레이션 경로에서 제외하는 방식 검토
    • 기존의 v2 백엔드를 이용한 블루그린 방식 제외
  • 현재 운영중인 V1 백엔드와 동일한 임시 백엔드를 RDS에 연결하여 전환하는 방식 검토

4.5.1. 목표

  • 현재 운영중인 V1 백엔드의 DB를 Docker MySQL에서 RDS로 전환
  • 서비스 및 백엔드 중단 없이 진행
  • 요청 실패 없이 전환 중 잠깐의 대기만 허용
  • 백엔드 코드 수정 없이 진행
  • 구축 중인 V2 백엔드는 마이그레이션 경로에서 제외

4.5.2. 고려사항

  • 운영중인 백엔드가 중단되지 않아야 함
    • 백엔드 코드 수정 없이 진행해야 함
  • 기존 DB와 RDS에 동시 쓰기가 발생하면 안 됨
    • 덮어쓰기의 가능성 있음
  • 신규 요청을 차단하거나 오류로 반환하면 안 됨
    • 약간의 지연은 가능, 차단 및 오류는 무중단이 아님

4.5.3. 고려사항으로 소거

  • 백엔드 재시작: DB 연결 변경은 가능하지만 순간적인 서비스 중단 발생
  • DNS 변경: 이미 생성된 Hikari DB 연결에는 변경사항이 적용되지 않음
  • ProxySQL, HAProxy: 기존 연결과 트랜잭션을 RDS로 이동할 수 없어 동시 쓰기 위험 존재 (중간에 요청 보관은 가능)
  • V2 백엔드 전환: 현재 구축 중으로 DB 마이그레이션 작업과 충돌 가능
    • 별도로 분리 결정

⇒ DB 연결 계층이 아닌 HTTP 계층에서 요청을 먼저 받은 후 잠시 대기시키는 방식 선택

요청 대기 → 기존 트랜잭션 종료 → DMS 최종 동기화
→ RDS 백엔드 전환 → 대기 요청 처리

4.5.4. 요청 대기 계층 검토

  1. OpenResty: NGINX와 Lua를 사용해 요청 대기 및 대상 변경 가능

  2. NGINX+njs: NGINX와 JavaScript를 사용해 동일한 기능 구현 가능

    • 두 방식 모두 백엔드 코드 수정 없이 임시 컨테이너로 사용 가능

    ⇒ Lua보다 JavaScript 기반 설정이 익숙하고 관리하기 쉬워 NGINX+njs 선택

4.5.5. 최종 전환 구조

  • Backend A는 현재 운영중인 V1 백엔드
  • Backend B는 A와 동일한 이미지이며 DB 연결만 RDS로 변경
  1. 전환 전
  • NGINX+njs는 모든 요청을 Backend A로 전달
  • Backend A는 기존 서비스를 계속 운영
  • Backend B는 RDS에 연결한 상태로 대기
  • Backend B의 스케줄러 및 Flyway 비활성화
  • DMS를 통해 Docker MySQL 데이터를 RDS로 지속 복제
  • RDS 연결 및 조회 API 정상 여부 확인

2.전환 중

신규 요청 → NGINX+njs에서 잠시 대기

기존 요청 완료
→ Docker MySQL 쓰기 잠금
→ DMS 최종 동기화
→ 전체 데이터 검증
→ DMS 중지
→ NGINX 대상을 Backend B로 변경
→ 대기 요청 처리
  • NGINX+njs에서 모든 신규 요청을 잠시 대기
  • Backend A에서 실행 중인 요청과 트랜잭션 완료 확인
  • Docker MySQL에 쓰기 잠금 적용
  • DMS 지연 0 및 오류 여부 확인
  • 20개 테이블 전체 데이터 검증
  • DMS 중지 후 데이터 재검증
  • NGINX 대상을 Backend A에서 Backend B로 변경
  • 대기 요청과 신규 요청을 Backend B로 전달
  1. 전환 후
  • 모든 요청을 Backend B와 RDS에서 처리
  • 기존 Docker MySQL을 읽기 전용으로 변경
  • Backend A의 활성 요청이 없는 것을 확인한 후 종료
  • Backend B와 RDS 모니터링
  • 안정화 완료 후 CloudFront를 Backend B에 직접 연결
  • 임시 NGINX+njs 컨테이너 제거

최종 구조:

사용자 → CloudFront → V1 Backend B → RDS

최종 선택

  • 기존 V1과 동일한 임시 백엔드를 RDS에 연결
  • NGINX+njs에서 신규 요청을 잠시 대기
  • DMS 최종 동기화 및 데이터 검증 후 요청 대상 전환
  • 백엔드 코드 수정 및 서비스 중단 없이 DB 전환
  • 안정화 완료 후 NGINX+njs 제거
  • 임시 V1 백엔드를 새로운 운영 백엔드로 사용

한계

백엔드 코드 수정이 없으므로 요청 멱등성을 완전히 보장할 수 없음.

다음 상황에서는 요청 실패 또는 중복 가능성이 남음.

  • 요청 대기 중 NGINX 또는 서버 장애
  • CloudFront 및 클라이언트 timeout 초과
  • 사용자가 응답 전에 동일 요청을 다시 실행

위험을 줄이기 위해 다음을 적용함.

  • CloudFront Origin Response Timeout 확대
  • NGINX upstream 자동 재시도 비활성화
  • 낮은 트래픽 시간에 전환
  • 제한 시간 초과 시 기존 Backend A로 복귀
  • 전환 중 NGINX 재시작 및 설정 reload 금지
  • 실제 전환 전 요청 대기·해제 부하 테스트 수행

4.6. 마이그레이션 진행 내용

1. 작업 목표

현재 운영 중인 V1 백엔드의 DB를 기존 MySQL에서 Amazon RDS MySQL로 변경

마이그레이션 중 다음 조건을 유지

  • V2 백엔드 사용하지 않음
  • V1 백엔드 코드 수정하지 않음
  • 읽기 요청은 중단하지 않음
  • 쓰기 요청은 전환 시간 동안 잠시 대기
  • 기존 DB와 RDS에 동시 쓰기하지 않음
  • 데이터 정합성 검증 후 전환

2. 기존 구조

사용자
  ↓
CloudFront
  ↓
V1 백엔드
  ↓
기존 MySQL

기존 V1 백엔드는 기존 MySQL에 읽기와 쓰기를 모두 수행하고 있었음.

3. 마이그레이션 구조

기존 V1 백엔드 이미지와 동일한 이미지를 사용하여 RDS 전용 백엔드를 추가함.

기존 V1 백엔드      → 기존 MySQL
RDS용 V1 백엔드     → Amazon RDS MySQL

CloudFront와 백엔드 사이에는 마이그레이션용 NGINX+njs 게이트를 추가함

사용자
  ↓
CloudFront
  ↓
NGINX+njs 게이트
  ├─ SOURCE → 기존 V1 백엔드
  ├─ WRITE_HOLD → 읽기는 SOURCE, 쓰기는 대기
  └─ TARGET → RDS용 V1 백엔드

5. 읽기·쓰기 요청 분류

일반적인 조회 요청은 전환 중에도 기존 백엔드로 계속 전달

GET
HEAD
OPTIONS

데이터를 변경할 수 있는 요청은 NGINX에서 잠시 대기

POST
PUT
PATCH
DELETE

OAuth 요청은 GET이지만 회원 및 인증 세션 데이터가 변경될 수 있으므로 쓰기 요청으로 분류

/api/v1/auth/oauth/**

따라서 전환 중 사용자 화면 조회는 계속 가능하고 저장,수정,삭제,로그인 요청만 잠시 대기

6. 데이터 복제

AWS DMS를 사용하여 기존 MySQL 데이터를 RDS로 복제

기존 MySQL
   ↓ 전체 적재 + CDC
AWS DMS
   ↓
Amazon RDS MySQL

확인 결과는 다음과 같음

  • 대상 테이블: 20개
  • 적재 완료: 20개
  • 테이블 오류: 0개
  • CDC 지연: 약 0~4초
  • DMS 실패 메시지: 없음

이 단계에서는 운영 쓰기가 기존 MySQL에만 발생

7. 전환 전 확인

전환 전 다음 내용을 확인함

  • NGINX 상태: SOURCE
  • 기존 V1 백엔드: 정상 실행
  • RDS 연결: 정상
  • DMS 상태: 실행 중
  • DMS 테이블 오류: 0개
  • 처리 중인 가이드북 생성 작업: 0개
  • 기존 DB와 RDS 사전 정합성 검증: 20/20 PASS

8. 쓰기 요청 대기

NGINX를 WRITE_HOLD_TO_TARGET 상태로 변경

읽기 요청
사용자 → NGINX → 기존 V1 백엔드 → 기존 MySQL

쓰기 요청
사용자 → NGINX → 대기

읽기 요청은 계속 처리하고 신규 쓰기 요청만 NGINX에서 대기

기존 백엔드에 이미 전달된 쓰기 요청이 모두 끝나 active_writes=0이 된 것을 확인

9. 기존 DB 쓰기 고정

기존 MySQL에 전역 읽기 잠금을 적용함(일반 select는 가능)

FLUSH TABLES WITH READ LOCK;

확인한 마지막 binlog 위치는 다음과 같음

binlog.000003:1471787

처리 중인 생성 작업도 다시 확인하여 새로운 작업 없음을 먼저 확인

PROCESSING=0

이후부터 기존 DB에는 새로운 쓰기가 발생하지 않음. 기존 백엔드는 읽기 요청만 계속 처리

10. 최종 데이터 동기화

기존 DB의 쓰기가 멈춘 상태에서 DMS가 마지막 변경분을 RDS에 반영하도록 대기

마지막 반영 후 DMS를 정상 중지

DMS 상태: stopped
중지 사유: NORMAL
실패 메시지: 없음

11. 최종 정합성 검증

DMS 중지 후 기존 MySQL과 RDS의 전체 데이터를 다시 비교

검증 방식은 다음과 같음(기존 검증 방식과 동일)

  • 전체 테이블 행 개수 비교
  • 주요 컬럼 데이터 비교
  • SHA-256 체크섬 비교

검증 결과:

전체 테이블: 20
성공: 20
실패: 0

주요 데이터 수:

tourism_contents: 48,960
itinerary_items: 1,297
guidebooks: 99
generation_jobs: 106
members: 60

12. 기존 DB 읽기 전용 고정

최종 검증 후 기존 DB를 읽기 전용으로 고정 (일반 select는 가능)

event_scheduler = OFF
read_only = ON
super_read_only = ON

이를 통해 기존 DB에 실수로 쓰기가 발생하는 것을 방지

13. RDS 백엔드 실행

RDS 연결 설정을 가진 동일한 V1 백엔드를 실행

kgb-backend-rds
  ↓
kgb-prod-db.cn4y6ewqqvwj.ap-northeast-2.rds.amazonaws.com:3306

확인 결과:

  • 애플리케이션 시작: 정상
  • RDS 연결: 정상
  • Flyway 검증: 7개 마이그레이션 정상
  • 추가 스키마 변경: 없음
  • Health Check: UP
  • 백엔드 시작 시간: 약 18.5초
    • RDS 백엔드가 준비되는 동안에도 읽기 요청은 기존 백엔드에서 계속 처리함.

14. TARGET 전환

RDS 백엔드의 Health Check가 통과한 후 NGINX 상태를 TARGET으로 변경

사용자
  ↓
CloudFront
  ↓
NGINX
  ↓ TARGET
RDS용 V1 백엔드
  ↓
Amazon RDS MySQL

전환과 동시에 대기 중이던 쓰기 요청을 RDS 백엔드로 전달

기존 DB에는 쓰기를 보내지 않았으므로 기존 DB와 RDS에 동시 쓰기가 발생하지 않음

전환 완료 후 대기했던 요청은 RDS 백엔드에서 처리됨.

15. 전환 후 상태

NGINX             → TARGET
RDS 백엔드         → 실행 중, Health UP
Amazon RDS         → 운영 DB
기존 V1 백엔드     → 종료
기존 MySQL         → 읽기 전용
AWS DMS            → 정상 중지
V2 백엔드          → 사용하지 않음

최종 운영 구조는 다음과 같음.

사용자
  ↓
CloudFront
  ↓
NGINX
  ↓
RDS용 V1 백엔드
  ↓
Amazon RDS MySQL

16. 결론

V1 백엔드 코드를 수정하지 않고 NGINX+njs와 동일한 V1 백엔드 이미지를 이용해 기존 MySQL에서 Amazon RDS MySQL로 전환

전환 중 읽기 요청은 기존 백엔드에서 계속 처리하고 쓰기 요청만 잠시 대기시킴. 최종 데이터 정합성 검증과 RDS 백엔드 Health Check가 완료된 후 대기 중인 쓰기를 RDS로 전달

이를 통해 다음 조건을 충족

  • 읽기 서비스 중단 없음
  • 쓰기 요청 유실 없음
  • 기존 DB와 RDS의 동시 쓰기 없음
  • 백엔드 코드 변경 없음
  • V2 백엔드와 작업 충돌 없음
  • 전체 테이블 데이터 정합성 확보