필터#VM×

동일한 부하 스크립트를 온프레미스와 AWS 환경에 각각 테스트 해 보았다.

온프레미스(AsIs) vs AWS(ToBe) 같은 k6 스크립트, 같은 부하를 온프레미스 단일 VM과 AWS ECS Fargate에 그대로 가해서, "정말 클라우드가 버티는가"를 숫자로 확인한 기록. 결론부터: 온프레미스는 평상시 부하(VU 100)에서조차 이미 마비 상태였고, 클라우드는 42만 건이 넘는 요청을 에러 0%로 처리했다. 들어가며 부하테스트를 하면 보통 "얼마나 버티는지"를 확인한다고 생각한다. 그런데 이번 테스트의 결론은 조금 달랐다. 버티고 말고를 따질 필요도 없이, 온프레미스는 평상시 트래픽 수준에서조차 이미 죽어있었다. 같은 k6 시나리오(Baseline·Stress·Spike)를 온프레미스 단일 VM과 AWS ECS Fargate에 똑같이 쐈다. 온프는 세 시나리오 전부 p95가 44초 근처로 수렴하며 마비됐고, 클라우드는 태스크를 1 → 4개로 자동 확장하며 요청을 전부 받아냈다. 그리고 이 과정에서 클라우드 쪽에 잠복해 있던 버그 2건(Redis 역직렬화, RDS ReplicaLag)까지 실제로 찾아내서 고쳤다. 이 글의 한 문장 요약은 이거다 — 느려서 무너진 게 아니라, 캐시도 없고 수평 확장도 안 되는 구조라서 무너졌다. 한눈에 보는 결론 같은 부하·같은 스크립트(k6loadv3.js)를 온프레미스 단일 VM과 AWS ECS Fargate에 그대로 가했다. 결과는 정반대였다. 시나리오 온프레미스 (AsIs) AWS 클라우드 (ToBe) Baseline (100 RPS) p95 43.99s p95 31ms (워밍업 후) Stress (→2000 RPS) p95 45.02s, 처리량 60 → 33 RPS 역행, 유실 93% p95 2.2s, 에러 0%, 태스크 1 → 4 자동 확장, 427,421건 무장애 Spike (→2500 RPS) DB CPU 100% 2회 재포화, 첫 타임아웃 3건 단일 태스크 큐잉 흡수, 에러 0%, 종료 후 1분 내 복구 공통 병목 DB CPU 포화 + HikariCP pending 169 (App CPU 1320% 유휴) 병목 없음 — RDS CPU 한 자릿수, 태스크 수평 확장 온프레미스 단일 VM은 평시 부하조차 p95 44초로 사실상 응답 불가였고, 부하를 올릴수록 처리량이 오히려 꺾이는 구조적 천장(DB CPU 포화 + 커넥션 풀 고갈)을 보였다. 동일 부하에서 AWS는 에러 0%로 흡수하며 태스크를 1 → 4로 자동 확장했다. 이 격차가 VMware → AWS 마이그레이션의 정량적 당위성이다. 1. 목적과 테스트 설계 마이그레이션의 당위성을 "느낌"이 아니라 AsIs/ToBe 수치 대비로 증명하는 것이 목적이다. 멘토 조언("부하 테스트는 AsIs/ToBe 대비가 정석, 기준은 동시 접속자 수")에 따라 ① 온프레미스 현 구성의 한계점과 병목 특정 ② AWS 구성의 탄력성(오토스케일 실작동·무장애 흡수) ③ 장애·복구 거동을 MTTR·SLO·BCP·RCA 관점으로 해석한 운영 설계 근거 확보를 목표로 잡았다. 시나리오 3종 (공통 스크립트) 시나리오 목적 부하 패턴 baseline 평상시 기준선 100 RPS 일정 (RATEMULT=0.5) stress 한계점 탐색 100 → 2000 RPS 계단식 11단계 spike 순간 폭증 내성 순간 최대 2500 RPS 급증 → 급감 공통 파라미터는 RATEMULT=0.5, MAXHANDLE=100(반복 조회 농가 100명 = 캐시 효과 측정 범위). 엔드포인트는 공개 프로필 60% + health 40%(+ 일부 calendar), 부하 기준은 동시 접속자(VU)로 현실적 동시 농부 100200명을 가정했다. 환경 비교 항목 온프레미스 (AsIs) AWS 클라우드 (ToBe) 컴퓨팅 VMware VM, App 2vCPU/2GB 고정 1대 ECS Fargate, 오토스케일 1 → N (CPU 70%, 3분 평가) DB PostgreSQL VM 2vCPU/2GB RDS (연결 한도 225, MultiAZ) 캐시 없음 ElastiCache Redis 입구 / 부하생성 WAS 직접(192.168.x) / 노트북 k6 + Tailscale ALB(HTTPS) / 배스천 EC2(t3.small) 측정 한계 (정직한 기록): 두 환경 모두 k6에서 "Insufficient VUs, reached 2000" 경고가 떴다. 부하 생성기(클라이언트) 자체가 상한이었다는 뜻이며, droppediterations의 일부는 클라이언트 도착률 한계다. 그러나 서버 거동 차이는 명확하다. 온프는 달성 처리량이 수십 RPS로 붕괴하며 p95 44초(서버 포화)인 반면, 클라우드는 같은 조건에서 427,421건을 에러 0%로 처리하고 태스크를 확장했다. 비교는 유효하며, 클라우드의 진짜 한계는 클라이언트에 막혀 미측정인 만큼 더 높다. 2. AsIs — 온프레미스 (단일 VM) Baseline — 평시 부하에서 이미 "마비" 100 RPS 평상시 부하인데도 p95 43.99초(기준 0.8초의 약 55배), 달성 처리량은 목표의 60%(max 60.5 / mean 35.3 RPS)에 그쳤다. 에러는 0%지만 응답이 44초 걸리는 것은 사실상 장애다. 온프 Baseline · 자원포화 — DB CPU max 72.1%로 위험선(70%) 돌파, App(WAS) CPU는 20.4%로 유휴. 병목은 명확히 DB 레이어. 온프 Baseline · 커넥션풀 — HikariCP pending 169(풀 30 전부 점유·idle 0). 응답 지연의 직접 원인. JVM Heap 32.6%로 앱은 여유. 총 9,515건 처리 중 dropped 5,087건(약 35% 유실). Stress — DB CPU 100% 천장에 고정 100 → 2000 RPS로 올리자 DB CPU가 100% 완전 포화(mean 75.8%)됐다. 흡수할 추가 용량이 없어 달성 처리량이 오히려 60→33 RPS로 역행하고, 목표 부하의 93%(480,047건)가 유실됐다. p95 45.02초. HikariCP pending 169는 baseline과 동일하게 고정됐다. 온프 Stress · 자원포화 — DB CPU가 100%에 평탄하게 붙어 정체. WAS CPU는 13.2%. "더 줘도 DB가 먼저 천장을 친다"의 가장 명확한 증거. 온프 Stress · 부하 — 달성 요청률 max 32.9 / mean 12.2 RPS. baseline(60.5)보다 낮다. 서버 포화로 처리량이 역행하는 구간. Spike — 폭증 흡수 불가 + 첫 타임아웃 순간 폭증(목표 2500 RPS)에서 DB CPU가 "두 개의 산" 모양으로 두 차례 100% 재포화됐다(mean 66.2%). 폭증을 흡수할 버퍼가 없어 처음으로 타임아웃 실패 3건(60초 초과)이 발생했다. p95 44.88초, max 1분 0초. 커넥션풀은 세 시나리오 모두 동일하게 pending 169에서 막혔다. 온프 Spike · 자원포화 — DB CPU "두 개의 산": 1차 상승 → 중간 저점(36%) → 2차 100% 재포화. 충격 후 회복 전에 재포화. 온프 Spike · 지연·에러 — 폭증 구간(15:4915:50)에 4xx 401 스파이크, k6 로그에 request timeout 3건. 온프에서 처음 발생한 실질 실패. 종합 — "죽지는 않지만 마비" 10분 이상 고부하에도 전 인스턴스는 UP(Healthy 4/0)을 유지했다. 문제는 "죽음"이 아니라 "마비"다 — 살아는 있으나 p95 44초로 응답 불가다. 일관된 천장은 ① p95 4445초 ② DB CPU 72 → 100% 포화 vs App CPU 1320% 유휴 ③ HikariCP pending 169 고정 ④ 유실 35% → 93% → 91% ⑤ 부하를 올려도 처리량 역행으로 나타났다. 왜 이렇게 무너졌나 — 인과관계 5단계 App CPU는 노는데 p95 44초인 현상을 인과로 풀면 이렇다. 1. 근본 원인 = 캐시 부재 + 단일 DB 2vCPU. Redis가 없어 모든 읽기가 DB로 직행한다. 프로필·캘린더 조회가 매번 복수 테이블 JOIN으로 떨어지고, DB는 2vCPU뿐이라 연산이 곧 천장이 된다. 2. App CPU가 노는데 마비인 이유 — 톰캣/HikariCP 스레드가 DB 응답을 블로킹 대기(I/O wait)로 멈춰 있다. "일하는 중"이 아니라 "기다리는 중"이라 CPU를 거의 안 쓴다. 그래서 App을 증설해도 소용없다. 3. pending 169의 이유 — 풀 30개가 전부 느린 쿼리에 묶이면 새 요청이 대기 큐에 쌓인다. 동시 처리량 = 도착률 × 응답시간(Little's law)인데 응답시간(44초)이 폭발해 풀 처리율을 초과하며 169까지 적체됐다. 풀을 키워도 근본(느린 DB)이 그대로면 pending 위치만 바뀐다. 4. 처리량 역행의 이유 — DB CPU가 100%에 닿은 뒤엔 동시 쿼리들이 같은 자원(CPU·락·버퍼)을 두고 경쟁해 개별 쿼리가 더 느려진다. 전형적 혼잡 붕괴(congestion collapse)다. 5. 세 시나리오 p95가 모두 4445초인 이유 — 부하 방식과 무관하게 같은 평형점(풀 포화 + DB 100%)에 수렴하기 때문. 한계가 부하 패턴이 아니라 구조(용량)로 정해진다는 뜻이다. 요약하면 온프의 문제는 "느려서"가 아니라 구조가 한 곳(단일 DB)에서 직렬화되기 때문이다. 캐시가 그 직렬 구간을 우회시키고 수평 확장이 용량을 늘리는데, 온프엔 그 두 레이어가 없다. 3. ToBe — AWS 클라우드 (ECS Fargate) Baseline & RCA — Redis 역직렬화 버그 발견·수정 초기 baseline에서 응답은 빨랐으나(p95 65ms) apperrors가 59.67%에 달했다. 관측 → 로그 → 코드를 추적해 원인을 규명했다. 증상은 "첫 요청 200 → 두 번째부터 500 → TTL(10분) 만료 후 다시 200"이라는 간헐 장애·자가복구 패턴이었다. 원인은 RedisConfig의 GenericJackson2JsonRedisSerializer + DefaultTyping.NONFINAL 조합이 final 타입(Java record)에는 타입 정보를 붙이지 않는 비대칭이었다. record DTO 캐시가 쓰기는 {본문}, 읽기는 [타입, {본문}](WRAPPERARRAY)을 기대해, 캐시 HIT마다 역직렬화 예외 → 500이 발생했다. nonfinal 타입(crops)이 멀쩡했던 점이 이 가설을 뒷받침했다. 조치는 단순 버그 수정에 그치지 않고 재발 방지·장애 내성까지 설계했다. 직렬화 정상화 — DefaultTyping.NONFINAL → EVERYTHING으로 record/final 루트에도 타입 정보를 부여해 쓰기·읽기 라운드트립을 복원. 캐시 키 버전화 — CacheNames.VERSION(v2) 도입으로 직렬화 포맷이 또 바뀌어도 옛 키가 자동 격리돼 롤링 배포 중 포맷 혼재를 차단. 캐시 장애 내성 — CacheErrorHandler로 역직렬화 오류를 캐시 미스로 강등 + 깨진 키 evict → DB 폴백, Redis 일시 장애에도 500 대신 서비스가 지속되도록 설계. 변경 파일은 RedisConfig.java·CacheNames.java 2개이며, 코드 수정 후 에러는 59.67% → 0%로 떨어졌다. 클라우드 Baseline(버그) — ALB Target 5xx(앱 500) 8.94K 폭증. 캐시 히트 시점마다 500. 캐시가 독이 된 상태. 클라우드 Baseline(버그) — 현재 캐시 히트율 0%(빨강). 역직렬화 실패로 캐시가 무력화됨. 클라우드 Baseline(수정 후) — ALB 5xx 전부 0, 응답은 콜드스타트 첫 1점(p99 7.9s)만 튀고 14:21부터 평탄(p50 7.56ms). 버그 해소 입증. 클라우드 Baseline(수정 후) 요약 — 에러율 0%, Running 태스크 1(100 RPS는 CPU 13%로 오토스케일 트리거 미달 — 설계대로), RDS 연결 32. 이 과정에서 부하 시험이 운영 전 잠복 버그를 드러냈고, 관측 → 로그 기반 원인 규명 → 코드 수정 → 재현 시험으로 검증 루프를 완성했다. Stress — 오토스케일 1 → 4, 2000 RPS 에러 0% 흡수 CPU 평균 70%를 3분 연속 돌파하자 CloudWatch 알람이 발동해 desired 태스크가 1 → 2(14:53)→3(14:55) → 4(14:57)로 단계 증설됐다. 전 구간 apperrors 0% / httpreqfailed 0%(0/427,421건), ALB 5xx 0이었다. 클라우드 Stress · 가용성·오토스케일 — ECS 태스크 Running/Desired 1 → 2 → 3 → 4 단계 증설. 탄력 확장의 직접 증거. 클라우드 Stress · 자원포화 — ECS CPU avg 99.8% / max 100%(mean 70.1%)로 70·80% 선 돌파 → 스케일아웃 트리거. RDS CPU는 낮음. 클라우드 Stress · 요약 — 에러율 0%, Running 태스크 4, RDS 연결 122/225(한도 내 여유). RequestCount는 peak 66.6K/분(mean 20.4K)으로 온프와 자릿수가 다른 처리량. 지연 SLO 관점에서는 p95 2.18s로 "p95 클라우드 Spike · 자원포화 — ECS CPU 15:36 29% → 15:37 avg 84.9%/max 99.9% → 15:38 20% 급락(고점 약 1분). 짧아서 오토스케일 평가창(3분) 미충족. 클라우드 Spike · 지연·에러 — 응답시간 15:36 피크(p99 9.52s) 후 15:38 0으로 감쇠, ALB 5xx 전부 0. 에러 없이 지연으로 흡수 후 즉시 회복. 클라우드 Spike · 캐시 — 현재 캐시 히트율 100%(초록). Redis가 DB를 보호. RDS CPU 피크도 35%에 그침. 에러율은 평탄 0%(max 0.0346%). 교훈: 짧은 스파이크는 반응형 오토스케일(3분 지연)로 못 막는다. 기존 용량이 큐잉으로 버티되 지연이 오르므로, 최소 태스크 버퍼 + 요청 큐/타임아웃 설계가 필요하다(개선 백로그). 4. AsIs vs ToBe 직접 비교 시나리오 / 지표 온프레미스 (AsIs) AWS 클라우드 (ToBe) Baseline p95 43.99s 31ms (워밍업 후) Stress p95 / 에러 45.02s / 0% (마비) 2.18s / 0% Stress 처리량 거동 60 → 33 RPS 역행 정상 흡수, 427,421건 무장애 Stress 확장 불가 (단일 VM 고정) 태스크 1 → 4 자동 확장 Stress 유실 93% (480,047건) 0% 실패 (도착률은 클라이언트 한계) Spike 거동 DB CPU 100% 2회 재포화, 타임아웃 3건 단일 태스크 큐잉, 에러 0% 흡수 Spike 복구(MTTR) 자력 회복 느림·재포화 종료 후 1분 내 복귀 병목 자원 DB CPU 100% + HikariCP pending 169 없음 (RDS CPU 한 자릿수, 연결 122/225) 5. 멘토링 지표로 본 해석 (MTTR·SLO·BCP·RCA) MTTR(평균 복구 시간): 클라우드 spike는 충격 종료 후 1분 내 p50 ms 복귀로 빠른 자력 회복을 보였다. 온프는 회복 전 재포화로 MTTR이 길고 불안정하다. "120초"는 일반값일 뿐, Farmily 기준 자체 복구 목표(SLO)를 정해 알람·헬스체크 기준으로 삼는다. SLO: 무결성 SLO(에러율2000 RPS) 재측정 인프라 관측 SLO 공식화, 알람 기준(5xx·DB CPU·HikariCP·ReplicaLag) 정비 모니터링 결론 부하 시험은 마이그레이션의 당위성을 숫자로 확정했다. 온프레미스 단일 VM은 평상시 부하에서 이미 p95 44초로 마비했고, 그 원인(DB CPU 포화 + HikariCP 풀 고갈, 캐시·수평확장 부재)은 튜닝이 아니라 구조의 문제였다. 동일 부하에서 AWS는 에러 0%로 흡수하며 태스크를 자동 확장했고, 시험 과정에서 잠복 버그(Redis 역직렬화)와 ReplicaLag 이상까지 사전에 제거했다. 남은 과제(지연 SLO 튜닝·스파이크 버퍼)는 클라우드 위에서 푸는 운영 최적화 영역이며, 온프에서는 애초에 도달할 수 없는 단계다. App CPU가 20%로 놀고 있다고 "여유 있다"는 뜻은 아니다. 기다리는 중인지, 일하는 중인지부터 구분해야 한다. 온프레미스가 무너진 건 느려서가 아니라, 캐시도 없고 수평으로 확장할 수도 없는 구조였기 때문이다. 그 구조를 그대로 두고 서버 스펙만 올렸다면, 아마 같은 자리에서 또 무너졌을 것이다. 그림 출처: Grafana 대시보드 스크린샷 (온프 = Prometheus exporters, 클라우드 = CloudWatch 데이터소스)

Aug 18, 2026AWS
동일한 부하 스크립트를 온프레미스와 AWS 환경에 각각 테스트 해 보았다.