Repository navigation
docs(decisions): DB 페일오버 때 앱 전환 방법 — 측정 전 후보 정리 #844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # DB 페일오버 때 앱이 새 DB 로 넘어가는 방법 — 측정 전 후보 정리 | ||
|
|
||
| 작성일: 2026-11-08 | ||
| 상태: 🟢 **잴 것·rig 세부 확정.** 2026-11-08 사용자 confirm — 후보 A + B, 흉내 K1 + K2. 운영 채택 문서가 아니다. | ||
| 연관: [`./rpo-rto-targets.md`](./rpo-rto-targets.md) §5 구성 3 · §7 · §8 R1 · | ||
| [`../../loadtest/results/replica-promotion-2026-11-08/README.md`](../../loadtest/results/replica-promotion-2026-11-08/README.md) (DB 쪽 승격 0.25~8초) · | ||
| [`./replication-lag-and-semisync.md`](./replication-lag-and-semisync.md) | ||
|
|
||
| --- | ||
|
|
||
| ## 0. 왜 이 문서인가 | ||
|
|
||
| 11-08 승격 실험은 **DB 가 다시 쓰기를 받기까지**만 쟀다(밀림 없으면 약 0.25초, 약 21초 밀리면 약 7.8초). | ||
| 사용자가 겪는 시간은 그보다 길다: | ||
|
|
||
| ``` | ||
| 원본 DB 죽음 ─▶ ① 알아챔 ─▶ ② 승격 결정 ─▶ ③ 승격 (잼: 0.25~8초) ─▶ ④ 앱 연결을 새 DB 로 ─▶ ⑤ 앱이 실제로 다시 씀 | ||
| ``` | ||
|
|
||
| ④·⑤ 가 비어 있다. 그리고 승격 실험은 원본·리플리카가 **한 박스**라 «못 받은 데이터» 가 0 이었다 — 두 박스에서 얼마를 잃는지도 비어 있다. | ||
|
|
||
| 🔴 **범위 메모**: `rpo-rto-targets.md` §7 은 «자동 페일오버 — 리플리카(3) 채택 전엔 의미 없다» 로 안 하기로 했고, | ||
| 리플리카는 보류 상태다. 이 문서는 **재는 것**이지 페일오버를 운영에 넣자는 게 아니다. 측정 결과는 Q5(리플리카를 둘지)의 근거로만 쓴다. | ||
|
|
||
| ## 1. 지금 앱이 DB 에 붙는 방식 (바꿀 대상) | ||
|
|
||
| - `application.yml:43` — `jdbc:mysql://${DB_HOST:shadowfit-mysql}:3306/...` — 호스트 이름 하나. 운영 compose 는 `DB_HOST: shadowfit-mysql`(`docker-compose.prod.yml:95`) | ||
| - HikariCP `maximum-pool-size: 15` · `connection-timeout: 30000`(30초). `max-lifetime` 은 기본값 | ||
| - Connector/J `connectTimeout`·`socketTimeout` 은 **지정 안 함**(기본값) | ||
|
|
||
| → 지금은 DB 주소가 바뀌면 **설정을 고쳐 앱을 다시 띄우는 것 말고는 방법이 없다.** | ||
|
|
||
| ## 2. 후보 — 앱 연결을 새 DB 로 바꾸는 방법 (④) | ||
|
|
||
| | # | 방법 | 앱 쪽 변경 | 전환 때 하는 일 | 예상되는 함정 (재서 확인할 것) | 운영 부담 | | ||
| |---|---|---|---|---|---| | ||
| | **A** | **주소 바꾸고 앱 재시작** | 없음 | `DB_HOST` 를 새 DB 로 바꾸고 백엔드 컨테이너 재시작 | 재시작 동안 API 전체가 멈춘다. 그 시간 = Spring 기동 시간(안 쟀다) | 없음. 사람이 해야 함 | | ||
| | **B** | **DNS 이름 바꾸기** (예: Route 53 사설 영역 `db.internal`) | `DB_HOST` 를 DNS 이름으로 | 레코드를 새 DB IP 로 | **JVM 이 DNS 결과를 캐시한다** — 레코드 TTL 과 JVM 캐시가 겹쳐 «바꿨는데 옛 주소로 붙는» 시간이 생긴다. 이 길이가 측정 대상 | Route 53 사설 영역 1개 | | ||
| | **C** | **사설 IP 옮기기** (EC2 보조 사설 IP 를 새 DB 로 재할당) | `DB_HOST` 를 그 IP 로 | `assign-private-ip-addresses --allow-reassignment` | DNS 캐시는 없다. 대신 **AWS 전용**이고 같은 서브넷(같은 AZ)이어야 한다 — 다른 AZ 로 못 넘긴다 | IAM 권한 · 같은 AZ 제약 | | ||
| | **D** | **프록시** (ProxySQL · MySQL Router) | `DB_HOST` 를 프록시로 | 프록시가 백엔드를 바꿈 (자동 가능) | 프록시 자체가 새 단일 장애점. 홉 하나 추가 | 프록시 서버·설정 상시. 🔴 `db-portfolio-roadmap.md:200` 이 «read replica/CQRS 풀구현» 을 오버엔지니어링으로 닫았다(읽기 라우팅 ProxySQL 도 같은 이유로 안 함 — `replication-lag-and-semisync.md:548`) | | ||
| | **E** | **JDBC 다중 호스트 URL** (`jdbc:mysql://원본,리플리카/...`) | URL 한 줄 | 없음 — 드라이버가 첫 호스트 실패 시 다음으로 | Connector/J 는 넘어간 연결을 **기본적으로 읽기 전용**으로 연다고 알고 있다(`failOverReadOnly`, 확인 필요). 원본이 살아나면 그쪽으로 되돌아가는 문제(갈라진 두 DB 에 쓰기) | 없음. 대신 위험이 앱 설정 안에 숨는다 | | ||
| | F | 매니지드 (RDS Multi-AZ) | — | — | — | 🚫 범위 밖 — `rpo-rto-targets.md` §7 «매니지드는 다른 판단» | | ||
|
|
||
| ## 3. 장애를 어떻게 흉내 내나 — 이게 결과를 가장 크게 바꾼다 | ||
|
|
||
| | 흉내 | 앱이 보는 것 | 비고 | | ||
| |---|---|---| | ||
| | **K1. 프로세스 죽이기** (`docker kill`) | OS 가 살아 있어 연결이 **바로 끊겼다는 응답(RST)** 을 받는다 → 빨리 실패 | 11-08 승격 실험이 쓴 방식. 실제 호스트 상실보다 낙관적 | | ||
| | **K2. 네트워크 끊기** (보안 그룹·iptables 로 원본으로 가는 패킷 버리기) | **아무 응답도 없다** → 열린 연결의 쿼리가 타임아웃까지 매달린다. `socketTimeout` 이 기본값(무제한)이면 TCP 재전송 한도까지 갈 수 있다(추정, 확인 필요). 새 연결은 Hikari `connection-timeout` 30초에 걸린다 | 호스트 상실·AZ 장애에 더 가깝다 | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| → **같은 후보라도 K1 에선 몇 초, K2 에선 수십 초~분일 수 있다**(추정). 그래서 K2 를 빼면 «앱 전환 N초» 를 인용할 수 없다고 본다. | ||
| K2 에서 시간이 길게 나오면, 그 다음 질문은 «`socketTimeout`·`connectTimeout` 을 얼마로 둘까» 인데 — 그 값은 이 측정이 근거가 된다(지금 정하지 않는다). | ||
|
|
||
| ## 4. 무엇을 재나 (후보와 무관하게 공통) | ||
|
|
||
| - **무대**: EC2 3대 — 원본 DB · 리플리카 DB · 백엔드 + 부하 발생기. 원본·리플리카를 **다른 박스**로 나눠 «못 받아서 잃은 양» 을 같이 잰다 | ||
| - **부하**: 운동 기록 저장 같은 쓰기 API 를 일정 속도로 계속 부른다(문서 §8-1 가정 피크 — 초당 45행). 요청마다 «보낸 시각 · 응답 시각 · 성공 여부» 를 남긴다 | ||
| - **결과 칸**: 죽인 시각 → 첫 실패 · 마지막 실패 · **첫 성공까지 걸린 시간**, 그 사이 실패 요청 수, 매달린 요청의 최대 응답 시간, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 성공 응답을 받았는데 새 DB 에 없는 쓰기 수(= 잃은 양) | ||
| - **①② (알아채기·결정)는 고정값으로 뺀다** — 사람이 하면 사람 시간이고, 자동으로 하면 감시 주기다. 이 실험은 «승격 명령을 내린 뒤» 부터를 잰다. | ||
| 안 하면 결과가 «사람이 몇 초 만에 봤나» 에 묻힌다 | ||
|
Comment on lines
+60
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - 반복: 후보 × 흉내마다 버림판 1 + 본판 3 이상, 순서는 섞는다 | ||
|
|
||
| ## 5. 추천 (Claude) — 결정은 사용자 | ||
|
|
||
| - **A + B, 흉내는 K1·K2 둘 다.** | ||
| - A 는 이 규모에서 **실제로 하게 될 방법**이라 기준선으로 필요하다. 남은 빈칸이 «Spring 재기동 시간» 하나라 싸다 | ||
| - B 는 업계에서 가장 흔한 방식(매니지드 DB 도 DNS 를 바꾼다)이고, **JVM DNS 캐시라는 앱 쪽 함정**이 결과에 드러난다 — 면접에서 «페일오버 어떻게 하나요» 에 «DNS 를 바꾸면 끝» 이 아니라 그 뒤 몇 초가 어디서 나오는지 말할 수 있다 | ||
| - K2 는 §3 이유로 빼면 안 된다 | ||
| - C 는 같은 AZ 제약 때문에 AZ 장애엔 못 쓴다 — 추천 안 함. D 는 로드맵이 닫은 방향과 부딪힌다. E 는 읽기 전용·되돌아감 위험을 앱 설정 안에 숨겨서, 재더라도 «이래서 안 쓴다» 를 보이는 용도다 | ||
| - 규모: 2 후보 × 2 흉내 × 4판 = 16판. EC2 3대로 한 라운드(시간은 rig 를 만든 뒤 로컬 스모크로 추정) | ||
|
|
||
| ## 6. 결정할 것 | ||
|
|
||
| - [x] **Q1. 잴 후보** → 🟢 **A + B** (2026-11-08 사용자 confirm) | ||
| - [x] **Q2. 장애 흉내** → 🟢 **K1 + K2** (2026-11-08 사용자 confirm) | ||
| - [ ] **Q3. 범위** — §0 메모대로 «측정만, 운영 채택 아님» 으로 둘지. 명시 답은 아직 없다 — 그동안 `rpo-rto-targets.md` §7 «자동 페일오버 안 함» 은 그대로 둔다 | ||
|
|
||
| ### 6-1. rig 세부 — ✅ 2026-11-08 사용자 confirm (R-a 앱 박스 DNS · R-b·R-c 추천대로) | ||
|
|
||
| | | 후보 | 추천 | 걸리는 것 | | ||
| |---|---|---|---| | ||
| | R-a | B 의 DNS 를 무엇으로 | Route 53 사설 영역 → 🟢 **채택은 앱 박스 DNS(dnsmasq)** | 측정용 IAM 사용자에 Route 53 권한이 **없다**(11-08 `AccessDenied` 확인) — 사용자가 콘솔에서 붙여야 한다. 대안은 앱 박스에 작은 DNS 를 띄우는 것(권한 불필요, 레코드 전파 시간은 빠져 낙관적) | | ||
| | R-b | 앱의 DB 타임아웃 | 🟢 지금 설정 그대로(기본값) | 시간 제한 넣은 팔을 더하면 판이 2배이고, 넣을 값의 근거가 아직 없다 | | ||
| | R-c | 요청 간격 | 🟢 초당 10번 | 가정 피크(세션 시작 초당 0.075번)면 13초에 한 번이라 초 단위 변화를 못 잡는다. 10번은 부하가 아니라 **시간 해상도 0.1초**에서 나온 값이다 | | ||
|
|
||
| ## 결정 로그 | ||
|
|
||
| - 2026-11-08: 후보 초안. 11-08 승격 실험이 DB 쪽(0.25~8초)만 쟀고, 앱 전환·두 박스 유실이 비어 있어서 정리했다. 후보 A~F · 흉내 K1/K2 · 공통 측정 칸 · 추천(A + B, K1 + K2). 결정은 미결. | ||
| - 2026-11-08: 🟢 **Q1 A + B · Q2 K1 + K2 채택** (사용자 confirm «추천대로»). Q3 는 명시 답 없음. rig 세부 셋(§6-1 R-a~R-c)은 미정 — 특히 R-a 는 IAM 권한이 필요하다. | ||
| - 2026-11-08: 🟢 **rig 세부 확정** (사용자 confirm) — R-a 는 **앱 박스에 DNS 를 직접 띄운다**(Route 53 아님 → 레코드 전파 시간이 빠져 | ||
| 실제보다 낙관적이라는 것을 결과에 적는다), R-b 기본 타임아웃 그대로, R-c 초당 10번. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: Shadowfit/init
Length of output: 6909
Script executed:
Repository: Shadowfit/init
Length of output: 24200
Web query:
Docker Compose documentation docker compose restart changes compose.yml environment variables not reflected and docker compose up recreates containers configuration changedResult:
후보 A에서 백엔드 컨테이너를 재생성하도록 명시해 주세요.
Compose 파일의
DB_HOST를 바꾼 뒤 기존 컨테이너를 재시작하면 변경값이 적용되지 않습니다. 백엔드는 기존 DB로 연결을 시도하므로 새 DB로의 복구를 측정할 수 없습니다. 변경된 설정으로 컨테이너를 재생성하도록 절차를 수정해 주세요.