[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. 요청 대기 계층 검토
-
OpenResty: NGINX와 Lua를 사용해 요청 대기 및 대상 변경 가능
-
NGINX+njs: NGINX와 JavaScript를 사용해 동일한 기능 구현 가능
- 두 방식 모두 백엔드 코드 수정 없이 임시 컨테이너로 사용 가능
⇒ Lua보다 JavaScript 기반 설정이 익숙하고 관리하기 쉬워 NGINX+njs 선택
4.5.5. 최종 전환 구조
- Backend A는 현재 운영중인 V1 백엔드
- Backend B는 A와 동일한 이미지이며 DB 연결만 RDS로 변경
- 전환 전
- 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로 전달
- 전환 후
- 모든 요청을 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 백엔드와 작업 충돌 없음
- 전체 테이블 데이터 정합성 확보