필터#RDS×

AWS DMS로 온프레미스 PostgreSQL을 RDS로, pgvector 타입까지 무손실 마이그레이션하기

들어가며.. 온프레미스 PostgreSQL을 AWS RDS로 옮겨야 했다. 그냥 옮기는 거라면 어렵지 않았겠지만, Farmily DB에는 AI 콘텐츠 생성용 RAG 테이블에 pgvector 확장의 커스텀 타입 vector(1024) 컬럼이 있었다. 일반적인 DMS 방식은 표준 SQL 타입만 변환하기 때문에 이런 확장 타입을 그대로 옮기지 못한다. 그래서 AWS DMS의 동종(Homogeneous) 마이그레이션으로 Full Load + CDC 방식을 선택했고, 그 과정에서 서로 다른 원인으로 7번 막혔다. 이 글은 마이그레이션 과정에서 실제로 부딪힌 문제와 거기서 얻은 인사이트, 그리고 최종 검증 결과를 정리한다. 1. 왜 동종(Homogeneous) 마이그레이션인가 일반(이종) DMS의 타입 매핑 엔진은 표준 SQL 타입만 변환한다. vector(1024) 같은 확장 커스텀 타입은 매핑 대상에 없어 그대로 보존되지 않는다. 동종 마이그레이션은 다르다. 내부적으로 PostgreSQL 네이티브 도구(pgdump/pgrestore + 논리 복제)를 사용하기 때문에, 확장과 커스텀 타입을 원본 그대로 옮길 수 있다. 소스와 타깃이 같은 엔진(PostgreSQL)일 때만 쓸 수 있는 방식이라 이번 케이스에 정확히 맞았다. 온프레미스 PostgreSQL 15 (pgvector 0.8.2) ↓ DMS 동종 마이그레이션 (Full Load + CDC) AWS RDS PostgreSQL 18 (pgvector 0.8.1) 2. 왜 Tailscale 릴레이가 필요했나 온프레미스 DB는 VMware HostOnly 네트워크(192.168.30.0/24)에 있어 AWS에서 직접 접근할 수 없었다. DB를 인터넷에 직접 노출하는 방식은 보안상 제외했다. 대신 Tailscale(WireGuard 오버레이 VPN)로 온프레미스와 AWS를 같은 사설 오버레이 네트워크로 묶고, VPC 안의 릴레이 EC2가 socat으로 5432 포트를 온프레미스 DB로 포워딩하는 구조를 만들었다. DMS는 릴레이 EC2의 사설 IP로 접속하면 온프레미스 DB에 도달한다. [온프레미스 dbserver] [AWS prodvpc] PostgreSQL 15 릴레이 EC2(farmilybastion) RDS PostgreSQL 18 192.168.30.10 ──Tailscale──► 10.1.1.64 (socat :5432) ──► prodrds (VMnet HostOnly) WireGuard Tailscale IP 100.106.105.53 (DMS/RDS가 접속) 이 "릴레이 경유" 구조가 나중에 다룰 7번째 이슈(CDC subscription)의 원인이 된다. 3. Full Load + CDC — 무중단 전환의 기반 Full Load로 기존 데이터를 통째로 복사하고, 이후 발생하는 변경(INSERT/UPDATE/DELETE)을 CDC(Change Data Capture)로 실시간 반영한다. 두 단계를 함께 쓰면 데이터를 옮기는 동안에도 소스를 계속 운영할 수 있어, 무중단 전환(cutover)의 기반이 된다. DMS는 Start 시 연결 → 논리 복제 검사 → 스키마 복원 → Full Load → CDC 순서로 진행한다. 이 다섯 단계 각각에서 서로 다른 원인으로 총 7번 차단됐다. 4. 7개의 차단 — 트러블슈팅 여정 에러 메시지가 가리키는 단계를 단서로 원인을 좁혀가며 하나씩 해소했다. 요약하면 이렇다. 단계 증상 원인 유형 조치 1 타깃 연결 password authentication failed for user "admin" 시크릿 username 오타 admin → farmilyadmin 2 타깃 연결 no pghba.conf entry ... no encryption SSL 모드 불일치 none → require 3 논리 복제 검사 Logical replication is not enabled for TARGET RDS 기본값 미설정 파라미터그룹 rds.logicalreplication=1 + 재부팅 4 소스 연결 password authentication failed for user "dmsuser" DB 비번 ↔ 시크릿 불일치 비번 변경 후 시크릿 동기화 5 CDC publication must be superuser to create FOR ALL TABLES publication 권한 부족 소스 접속 계정을 슈퍼유저(postgres)로 전환 6 스키마 복원 role "farmily" does not exist 소유권 롤 부재 타깃에 동일 이름 롤 생성(이름표용) 7 CDC subscription could not connect to the publisher ... Connection timed out RDS→릴레이 경로 미허용 릴레이 SG에 5432 ← RDS SG 인바운드 추가 에러 메시지는 어느 계층이 막혔는지를 알려준다 일곱 번 막히면서 알게 된 건, 에러 메시지의 종류가 곧 "어느 계층에서 막혔는지"를 알려준다는 점이다. password authentication failed → 비밀번호 단계까지 도달했다는 뜻. 즉 네트워크는 이미 뚫려 있고, 자격 증명만 틀렸다. no pghba.conf entry ... no encryption → 매칭되는 접속 규칙이 없다는 뜻. SSL 모드나 대역이 안 맞는다. Connection timed out → 네트워크 자체가 안 뚫렸다는 뜻. 보안 그룹이나 라우팅을 봐야 한다. 이 구분 덕분에 매번 처음부터 전체를 의심하지 않고, 에러 메시지 하나로 확인할 범위를 곧바로 좁힐 수 있었다. 가장 비자명했던 문제 — CDC는 양방향을 요구한다 7개 중 가장 오래 걸린 건 5와 7이었고, 둘은 같은 뿌리를 공유한다. 처음 세운 가정은 "DMS만 릴레이에 붙으면 된다"는 단방향 모델이었다. 하지만 동종 마이그레이션의 CDC는 그렇게 동작하지 않았다. publication(5): 소스에서 변경을 내보내려면 FOR ALL TABLES publication을 생성해야 하는데, 이건 슈퍼유저 권한이 필요하다. 처음 만든 dmsuser는 SELECT + REPLICATION 권한만 있는 일반 계정이라 거부됐다. subscription(7): 타깃(RDS)이 publication을 구독하려면, RDS가 직접 릴레이 EC2에 접속해야 한다. 지금까지 열어둔 경로는 "DMS → 릴레이"뿐이었고, "RDS → 릴레이" 경로는 열려 있지 않았다. DMS → 릴레이(10.1.1.64) ✅ (열려 있었음) RDS → 릴레이(10.1.1.64) ❌ → ✅ (추가로 열어야 했음) 정리하면, publication은 소스 측 슈퍼유저 권한을, subscription은 타깃→소스 직접 연결을 요구한다. "DMS가 릴레이에 붙는 것"과 "RDS가 릴레이에 붙는 것"은 별개의 경로이고, CDC를 쓰려면 둘 다 필요하다는 걸 두 번의 차단을 겪고 나서야 이해했다. 5 Whys — 7을 파고들면 단계 질문 → 답 Why 1 왜 막혔나 → RDS가 릴레이(10.1.1.64:5432)에 직접 접속을 시도했으나 timeout Why 2 왜 timeout이 났나 → 릴레이 SG가 RDS SG로부터의 5432 인바운드를 허용하지 않았음 Why 3 왜 막지 못했나 → 네트워크 경로를 "DMS → 릴레이" 단방향으로만 설계함 Why 4 왜 사전에 못 찾았나 → 연결 검증을 psql(베스천 경로)로만 했고, RDS→릴레이 경로는 검증 항목에 없었음 Why 5 (근본) 동종 마이그레이션 CDC의 데이터 흐름(타깃이 소스에 직접 subscribe)에 대한 이해 부족 시크릿이 실제 접속 주체를 결정한다 또 하나 명확히 알게 된 것: DB 쪽에서 비밀번호를 바꾸고 pghba.conf를 열어도, Secrets Manager의 username이 여전히 예전 계정을 가리키면 DMS는 계속 그 계정으로 접속을 시도한다. 계정을 전환하는 실제 스위치는 DB 설정이 아니라 시크릿이었다. 5. 최종 성과 및 검증 7개 이슈를 모두 해소한 뒤 재시작해서, 아래 결과로 완주했다. 지표 결과 로드된 테이블 24개 (오류 0) 경과 시간 약 16분 31초 CDC 지연 시간 0ms 정합성 (소스=타깃 행 수) 전 테이블 일치 vector 타입 보존 vector(1024) 보존 (확장 0.8.2 → 0.8.1) 데이터 정합성 — 소스와 타깃에서 동일한 행 수 비교 쿼리를 실행해 확인했다. users 501,015건, notificationsettings 501,010건을 포함해 검증한 6개 테이블 전부 소스=타깃으로 일치했다. 총 약 540만 행(약 1GB) 규모였다. 타입·스키마 보존 — 이번 마이그레이션의 핵심 목적이었던 부분이다. 타깃 RDS에서 \d recipeembeddings를 확인한 결과, embedding 컬럼이 vector(1024)로 보존됐고 PRIMARY KEY·UNIQUE 제약·IDENTITY 시퀀스·인덱스 4개·text[] 배열 타입까지 전부 그대로 옮겨졌다. pgvector 확장 버전이 소스(0.8.2)와 타깃(0.8.1)이 달랐음에도 타입과 차원이 정확히 일치함을 확인했다. recipeembeddings.embedding 값은 현재 전부 NULL이다. 이번 검증의 대상은 "벡터 데이터 이전"이 아니라 vector(1024) 타입(스키마)·차원·인덱스·제약의 보존이라는 점을 분명히 해둔다. 6. 범위 — 여기까지 했고, 여기부터는 안 했다 이번 작업은 DMS Full Load + CDC로 데이터를 이전하고 정합성·타입 보존을 검증하는 단계까지다. 애플리케이션(WAS)의 DB 연결을 RDS로 바꿔 실제 트래픽을 넘기는 cutover(앱 전환)는 포함하지 않았다. 검증이 끝난 뒤에는 환경을 정리했다 — DMS 삭제, 그리고 CDC가 생성한 replication slot을 온프레미스에서 수동 삭제했다(DMS를 지워도 slot은 자동으로 없어지지 않는다. 방치하면 WAL이 계속 쌓여 디스크를 위협한다). 그래서 지금 시점에는 소스↔타깃 실시간 동기화가 끊긴 상태이고, 실제로 cutover를 하려면 CDC 동기화를 다시 구성해야 한다. "검증"과 "앱 전환"은 다른 단계라는 걸 분명히 해두고 싶었다. 7. 얻은 교훈 잘된 점 에러 메시지의 종류로 막힌 계층을 빠르게 좁힌 것, 그리고 이슈를 하나 해결할 때마다 바로 psql로 재검증한 뒤 다음 단계로 넘어간 것. 덕분에 "고친 줄 알았는데 사실 안 고쳐진" 상태로 다음 단계에 진입하는 일이 없었다. 개선할 점 사전 점검 체크리스트가 있었다면 1·3·6은 애초에 마주치지 않았을 문제였다. 그리고 CDC를 양방향 구조로 이해하지 못한 채 네트워크·권한을 단방향으로만 설계한 게 5·7의 공통 원인이었다 — 이건 사전 지식으로 막을 수 있는 문제였다. 다른 마이그레이션에도 적용할 수 있는 인사이트 동종 마이그레이션 CDC는 publication(소스, 슈퍼유저)과 subscription(타깃→소스 직접 연결)이라는 양방향 권한·네트워크를 요구한다. 릴레이 경유 구조라면 양쪽 방향 모두 SG를 열어야 한다. RDS는 논리 복제가 기본적으로 꺼져 있다. Full Load + CDC를 쓰려면 소스뿐 아니라 타깃에도 rds.logicalreplication=1(Static, 재부팅 필요)을 설정해야 한다. 실제 접속 계정을 바꾸는 스위치는 DB 설정이 아니라 Secrets Manager의 값이다. 메이저 버전 차이(PG15→18)나 확장 버전 차이(pgvector 0.8.2→0.8.1)가 있어도, 타입 수준 검증을 직접 하면 무손실 이전을 확인할 수 있다. 마치며 일반 DMS로는 vector 커스텀 타입을 지킬 수 없었기 때문에 동종 마이그레이션을 선택했고, 그 선택이 맞았다는 걸 타입 검증으로 확인했다. 하지만 그 과정이 매끄럽지는 않았다 — 인증, SSL, 논리 복제, 권한, 스키마, 네트워크까지 다섯 단계 전부에서 한 번씩 막혔다. 가장 크게 배운 건 기술적인 해결책 하나가 아니라, CDC라는 메커니즘 자체를 양방향으로 이해해야 한다는 것이었다. "소스에서 타깃으로 데이터가 흐른다"는 그림만 그리고 설계하면, 실제로는 타깃이 소스에 다시 연결하러 오는 순간 막힌다. 이번 7번의 차단 중 절반 이상이 결국 이 오해에서 비롯됐다.

Aug 11, 2026AWS
AWS DMS로 온프레미스 PostgreSQL을 RDS로, pgvector 타입까지 무손실 마이그레이션하기

Multi-AZ RDS failover 테스트 검증기 (feat.AWS FIS)

MultiAZ RDS failover 테스트 검증기 (feat.AWS FIS) AWS FIS로 운영 DB에 강제 failover를 일으켜, "MultiAZ가 정말 장애를 견디는가"를 숫자로 확인한 기록. 결론부터: DB는 15초 만에 살아났는데, 앱은 15분 동안 못 붙었다. 들어가며 RDS를 MultiAZ로 켜두면 한 가용영역(AZ)이 죽어도 다른 AZ가 받아준다고 한다. 콘솔에서 체크박스 하나 켜는 걸로 "장애 대응 완료"라고 믿기 쉽다. 그런데 정말 그런가? 켜 두는 것과 실제로 견디는 것은 다른 이야기다. 그래서 직접 깨뜨려보기로 했다. AWS FIS(Fault Injection Simulator)로 운영 중인 RDS Primary를 강제로 재부팅시켜 failover를 일으키고, 그 과정을 Grafana로 초 단위로 관측했다. 두 번에 걸쳐 진행했는데, 1차에서 예상 못 한 게 터졌고 2차에서 그 원인을 끝까지 파고들었다. 이 글의 한 문장 요약은 이거다 — DB 가용성과 서비스 가용성은 같은 숫자가 아니다. 0. 먼저 개념부터 본문에 계속 나오는 용어 다섯 개만 잡고 가자. FIS (Fault Injection Simulator) — AWS의 장애 주입 도구. "이 자원에 이런 장애를 일으켜라"를 템플릿으로 정의해 일부러 장애를 내고 시스템 반응을 관측한다. 카오스 엔지니어링의 실행 도구다. MultiAZ Failover — RDS를 두 AZ에 Primary·Standby로 이중화한 구성. Primary가 죽으면 다른 AZ의 Standby가 새 Primary로 승격된다. 평소 동기 복제라 데이터 손실이 없다. RTO (Recovery Time Objective) — 장애부터 복구까지 허용하는 목표 시간. 어느 계층을 기준으로 재느냐(DB냐 앱이냐)에 따라 값이 완전히 달라진다 — 이 글의 핵심. HikariCP (커넥션 풀) — Spring Boot 기본 DB 커넥션 풀. 요청마다 연결을 새로 만들지 않고 미리 만든 연결을 재사용한다. failover로 Primary가 바뀌면 풀에 있던 연결은 옛 Primary를 바라보는 죽은 연결이 된다. 이걸 얼마나 빨리 버리고 새로 만드냐가 앱 복구 속도를 가른다. warm pool — 방금 새 연결로 통째로 갈려서 전부 "신선한" 상태의 풀. 반대는 오래 묵은 연결로 찬 cold pool. (뒤에서 함정으로 다시 등장한다.) 1. 무엇을 테스트했나 구성 내용 대상 DB prodrds · PostgreSQL 18.3 · db.t3.small · MultiAZ = true 앱 계층 ECS · Spring Boot · HikariCP 트래픽 ALB → Target Group → ECS 안전장치 Stop Condition = CloudWatch 5xx 알람 관측 Grafana(AMG) + RDS Recent events 합격 기준은 세 가지로 잡았다. 1. DB RTO Primary(2c)가 재부팅되면 Standby(2a)가 새 Primary로 승격된다. 문제는 오른쪽 — DB는 2a로 옮겨갔는데 앱의 커넥션 풀은 여전히 옛 Primary(2c)를 붙들고 있다. 2. 1차: 일단은 평온했다, 5xx가 터지기 전까진 FIS 액션 aws:rds:rebootdbinstances에 forceFailover=true를 줘서 Primary를 강제로 재부팅했다. "Primary가 갑자기 죽는" 상황을 인위적으로 만든 것이다. 시작 직전 대시보드는 모든 게 정상이었다. DB 커넥션 10개 안정, 5xx 0건, p99 10.7ms. 그림 1. 1차 — 시작 전 정상 기준선. DB 커넥션 10 · 정상 타깃 1 · 앱 5xx 0 · p99 10.7ms · DB CPU 3.99%. 이후 그림과 비교할 'before' 상태. 그림 2. 1차 — 장애 직전 상세. 요청 수·활성 커넥션·ECS 태스크 running 1 · Failover 5xx 누적 0. 아직 모든 지표 정상. 그런데 failover를 건 직후, 5xx가 폭증하기 시작했다. 그림 3. 1차 — 장애 정점. 요청 1.5K 스파이크, Failover 5xx 누적 280건(대부분 앱 target 5xx). DB와 ECS는 멀쩡히 살아있는데(태스크 running 1) 앱이 DB에 못 붙어 5xx가 터졌다. 이 한 장이 'DB 가용 ≠ 서비스 가용'을 그대로 보여준다. 이상했다. DB CPU도, 메모리도, ECS 태스크도 전부 건강했다. 그런데 앱은 5xx를 쏟아냈다. 전환 구간을 들여다보니 DB 커넥션이 10에서 5로 빠지고 p99가 396ms까지 치솟았다. 그림 4. 1차 — 전환 중. DB 커넥션 10→5 감소, 읽기/쓰기 지연 스파이크와 함께 p99가 396ms(p50/p99 패널은 2초까지)로 상승. 앱이 옛 Primary의 죽은 커넥션을 붙들고 새 Primary로 못 옮겨가는 구간. 그리고 한참 뒤에야 앱이 회복됐다. 그림 5. 1차 — 종료 후 회복. DB 커넥션 0→10 복귀, p99가 30초(커넥션 대기 천장)를 찍었다가 5.02ms로 정상화. 5xx 막대는 16:0316:04에 잡힌다. DB는 진작(15:50) 복구됐는데 앱 재연결은 16:05. 체감 RTO ≈ 15분의 끝. 정리하면 1차의 숫자는 이렇게 갈렸다. 지표 값 DB RTO (인프라) 약 1분 — 왜 1차가 실패했나? failover 직후 RDS가 Standby를 다시 동기화(재동기화)하는 시간을 기다리지 않고 곧바로 다시 실행했기 때문이다. 이게 2차에서 검증할 가설이 됐다. 3. 2차: 이번엔 기다렸다, 그리고 회복을 관측했다 2차는 단 하나만 바꿨다 — failover 후 RDS 동기화가 완료될 때까지 기다렸다가 다음 회차를 실행했다. 설정은 한 줄도 안 건드렸다. 그렇게 18:45 / 18:56 / 19:16, 세 번 깨끗하게 때렸다. 구분 1차 2차 실행 방식 동기화 완료 전 곧바로 재실행 동기화 완료까지 대기 후 실행 실행 횟수 2회 3회 결과 실패 — 전환 도중 앱 오류 폭증 정상 — 매 회차 깨끗하게 전환·관측 매 회차 깨끗하게 전환됐다. DB 연결 수 그래프를 보면 failover 시점에 커넥션이 잠깐 푹 꺼졌다가 다시 차오르는 게 그대로 보인다. 그림 6. 2차 1회차(18:45) — p99 2.21ms · DB CPU 5.02% · 정상 타깃 1. DB 연결 30 → failover 시점 일시 감소 → 30 복귀. 동기화 완료를 기다린 뒤 실행하니 깨끗이 전환됐다. 그림 7. 2차 2회차(18:56) — p99 2.39ms · DB CPU 5.41%. 같은 패턴이지만 앱 커넥션 풀 회복이 약 2분으로 빨라졌다. 그림 8. 2차 3회차(19:16) — p99 2.62ms. 읽기/쓰기 지연 스파이크가 짧게 여러 번 났다 회복. 요청 700 스파이크도 잠깐 보인다 — 요청 스레드가 죽은 커넥션을 즉시 일괄 폐기해 앱 회복이 약 20초까지 빨라졌다. DB 자체 복구는 RDS 이벤트 로그로 실측했다. 그림 9. 2차 — RDS Recent events. 18:45·18:56·19:16 세 번 모두 MultiAZ failover started → DB instance restarted → failover completed. DB 자체 복구 15초·전체 3045초의 실측 근거. (위 6/11 23:03·23:07은 정기 백업.) [2차 — RDS MultiAZ failover events] 1회차 18:45:02 started → 18:45:18 restarted → 18:45:31 completed (AZ 2c→2a) 2회차 18:56:17 started → 18:56:32 restarted → 18:57:01 completed (AZ 2a→2c) 3회차 19:16:27 started → 19:16:42 restarted → 19:17:01 completed (AZ 2c→2a) 3회 측정을 표로 모으면 이렇다. 지표 1회차 2회차 3회차 평균 DB 자체 복구 16초 15초 15초 15.3초 전체 완료 30초 45초 35초 36.7초 앱 커넥션 풀 복구 5분 2분 20초 — ECS 태스크 교체 없음 없음 없음 — 여기서 눈에 띄는 줄이 앱 커넥션 풀 복구: 5분 → 2분 → 20초다. 설정은 동일했는데 왜 점점 빨라졌을까? 4. 핵심 분석 — DB는 15초, 앱은 15분, 그 사이 14분의 사각 같은 failover 한 번을 두 계층이 전혀 다른 시간으로 겪었다. 그림으로 보면 이렇다. 시간축 0 15초 15분 │ │ │ DB 계층 ──────● (복구 15초) │ 앱 계층 ──────○━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● failover ↑ DB는 살아있으나 앱이 못 붙는 구간 ≈ 14분 ↑ 앱 복구 '가용성'을 DB 한 층으로만 재면 저 가운데 14분이 통째로 가려진다. 사용자가 실제로 겪는 시간은 가장 느린 계층, 즉 앱이 결정한다. 왜 앱이 늦게 붙었나 — HikariCP의 죽은 커넥션 failover로 Primary가 바뀌면, 풀 안의 연결들은 전부 옛 Primary를 향하는 죽은 연결이 된다. 문제는 이 죽은 연결을 언제 감지해서 새 연결로 갈아끼우느냐다. 1회차 (5분) — 죽은 커넥션 감지 후, housekeeper 스레드가 10초에 1개씩 검증·폐기했다. 풀 20개를 다 갈아끼우는 데 5분. 로그엔 'Thread starvation or clock leap detected'가 동반됐다. 2회차 (2분) — 같은 패턴이지만, 직전에 한 번 갈린 풀이라 수명 짧은 연결이 섞여 더 빨리 교체됐다. 3회차 (20초) — 요청 스레드가 즉시 감지해 0.5초 안에 풀 전체를 일괄 폐기했다(housekeeper 10초 대기 없이). 단, 직후 새 연결 생성이 몰리며 'request timed out after 30000ms'가 짧게 났다 정상화됐다. 회차가 빨라진 건 '개선'이 아니다 — warm pool 함정 여기가 제일 중요하다. 5분 → 20초는 내가 뭘 고쳐서가 아니다. 한 번 failover를 겪으면 풀이 통째로 새 연결로 갈린다. 그 직후 회차는 "방금 만들어 수명이 거의 0인" 신선한 연결만 남은 상태(warm pool)가 되고, 그 덕에 우연히 빨라진 것뿐이다. 문제는 실제 운영 failover는 며칠몇 주 멀쩡히 돌던 cold pool에서 딱 한 번 터진다는 것. 즉 현실은 늘 '1회차(5분)' 상황이지 2·3회차(빠름)가 아니다. 첫 회차가 5분이나 걸린 직접 원인은 maxLifetime 기본값이 30분이라 housekeeper가 죽은 연결을 천천히 교체했기 때문이다. 참고로 "warm pool"은 EC2 Auto Scaling의 Warm Pools와는 완전히 다른 개념이다. 여기선 "커넥션 풀이 한 번 갈려서 신선해진 상태"를 가리킨다. 5. 판정과 개선 과제 AWS 기대치 대비로 보면 인프라는 전부 통과, 앱만 숙제로 남았다. 지표 결과 AWS 기대치 판정 :: DB 자체 복구 1516초 30초 이내 양호 전체 Failover 완료 3045초 60초 이내 양호 데이터 손실 없음 (동기복제) 무손실 양호 ECS 태스크 생존 전 회차 교체 없음 — 양호 앱 에러 구간 최대 5분 · 체감 15분(1차) — 개선 필요 개선 1순위는 HikariCP 튜닝이다. 조치 기대 효과 :: 1 maxlifetime = 60000 (60초) 커넥션 수명 단축 → failover 시 빠른 교체 2 connectiontimeout = 5000 (5초) 새 연결 실패 시 빠른 에러 반환 3 keepalivetime = 30000 (30초) 주기적 alive 체크 → 죽은 연결 조기 발견 4 validationtimeout = 3000 (3초) validate 빠른 실패 처리 5 ECS min 태스크 2개 이상 1개가 죽어도 다른 태스크가 커버 그 외에 1차에서 도출한 개선들: Deep health check — ALB 헬스체크가 DB 연결까지 점검하게. Healthy=1 맹점을 없애 죽은 경로로 트래픽 보내는 걸 차단. 알람 기준 보강 — Healthy 수가 아니라 5xx·DB 활성 커넥션 수 기준으로. FIS 템플릿에 aws:fis:wait 추가 — reboot는 13초 fireandexit라 정작 장애 구간엔 실험이 끝나 Stop Condition이 무력화된다. wait로 실험 수명을 복구 구간까지 늘려야 한다. 마치며 이 실험의 목적은 "MultiAZ가 장애를 견디는가"를 추측이 아니라 숫자로 확인하는 것이었고, 결과는 두 계층으로 뚜렷이 갈렸다. 검증된 것 — MultiAZ failover 메커니즘 자체는 정상이다. DB 복구 1516초, 데이터 무손실, AZ 자동 전환, ECS 태스크 생존. 인프라 회복력은 합격. 발견한 것 — 서비스 RTO를 좌우하는 건 DB가 아니라 앱이었다. HikariCP가 죽은 연결을 늦게 교체해 1차 체감 복구가 15분, 5xx 280건. 그 와중에 ALB 헬스체크는 DB 단절을 못 봤다. 운영 교훈 — failover 직후 RDS 재동기화가 끝나기 전에 재실행하면 안 된다(1차 실패 원인). 그리고 풀 회복이 빨라 보인 건 warm pool 효과일 뿐, 실제 운영은 늘 '5분' 상황이라 튜닝이 전제다. 가장 크게 남은 한 줄은 이거다. 서비스 가용성은 가장 빠른 계층이 아니라 가장 느린 계층이 결정한다. DB가 15초 만에 살아나도 앱이 14분을 못 붙으면, 사용자에게 그 서비스는 14분간 죽어 있던 것이다. 'MultiAZ를 켜 두는 것'과 '장애를 실제로 견디는 것'의 차이가 바로 여기에 있다. 이게 카오스 엔지니어링을 직접 해보기 전엔 안 보이던 사각지대였다. 체크박스를 켜는 것과, 깨뜨려서 숫자를 보는 것은 정말로 달랐다.

Jun 18, 2026AWS
Multi-AZ RDS failover 테스트 검증기 (feat.AWS FIS)