필터#ECS×

동일한 부하 스크립트를 온프레미스와 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 환경에 각각 테스트 해 보았다.

ECS vs Docker Swarm 컨테이너 오케스트레이션 비교 프로젝트

ECS vs Docker Swarm 컨테이너 오케스트레이션 비교 프로젝트 "성능 비교에서 운영 철학 비교로 — 실패가 아닌 발견의 과정" 프로젝트 개요 항목 내용 프로젝트 유형 인프라 비교 실험 기간 2026년 4월 목표 동일 EC2 환경에서 Docker Swarm과 Amazon ECS의 성능·운영 특성 비교 핵심 기술 Docker Swarm, Amazon ECS(EC2 Launch Type), AWS ALB, Prometheus, Grafana, k6 인프라 EC2 t3.small × 2대, ALB, VPC(퍼블릭 서브넷 단일 구성) 초기 목표 CPU 부하 상황에서의 오토스케일링 반응 속도 비교 컨테이너 강제 종료 후 복구 시간(Recovery Time) 측정 동일 애플리케이션 · 동일 부하 시나리오 기반의 공정한 비교 시스템 구성 공통 환경 EC2 t3.small × 2대 (각 2GB RAM) ├── 애플리케이션 컨테이너 (Spring Boot /env, /stress, /health) ├── cAdvisor (컨테이너 메트릭 수집) └── nodeexporter (시스템 메트릭 수집) 모니터링: Prometheus + Grafana 부하 생성: k6 (rampingarrivalrate 방식) 로그 수집: Python autoscaletest.py (2초 간격 컨테이너 수 추적) Docker Swarm 구성 Manager Node (클러스터 제어 + ingress 담당) Worker Node 1 ─┐ Worker Node 2 ─┘ → 앱 컨테이너 실행 (2 replicas) 네트워크: bridge / overlay 스케줄링: 로컬 Besteffort 방식 Amazon ECS 구성 ECS Cluster ├── EC2 인스턴스 1 (ECS Agent + 앱 Task + cAdvisor) └── EC2 인스턴스 2 (ECS Agent + 앱 Task + cAdvisor) Task 스펙: 0.25 vCPU / 0.5GB → 0.5 vCPU / 0.5GB (진행과정에서 변경) 네트워크: bridge → awsvpc 전환 ALB + Target Group(IP 타입) + Health Check(/health) 오토스케일링: CloudWatch CPU 70% 초과 시 Task 증가 비교 공정성 가설 및 검증 가설 — "Swarm 3대 vs ECS 2대가 공정한 비교다" 두 플랫폼을 같은 인스턴스 수(예: 둘 다 2대)로 맞추는 것이 얼핏 공정해 보이지만, 실제 애플리케이션을 처리하는 데이터 플레인 자원을 기준으로 보면 다르다. Docker Swarm은 Manager 노드가 클러스터 제어(오케스트레이션) 및 ingress 트래픽을 담당하므로, 앱 컨테이너는 Worker 2대에만 배치된다. Amazon ECS는 ECS의 Control Plane이 AWS 관리 영역에 존재하므로, 별도의 Manager 인스턴스가 불필요하다. EC2 2대 모두 앱 Task를 실행하는 워커 역할을 한다. 따라서 공정한 비교 기준은 "앱을 실제로 처리하는 인스턴스 수" 이며, 이 관점에서: Swarm: Manager 1대 + Worker 2대 (총 3대) ↔ ECS: EC2 2대 (총 2대) 양쪽 모두 앱 처리 인스턴스는 2대로 동일하다. 하지만 이 가설이 성립하려면 한 가지를 검증해야 했다. "Swarm Manager가 앱 컨테이너를 실행하지 않고, 실제 처리 부하는 Worker 2대가 전담하는가?" 검증 — 노드별 리소스 사용 현황 측정 Grafana + nodeexporter로 유휴 상태에서 각 노드의 리소스 사용량을 측정했다. 노드 역할 CPU Busy Sys Load RAM 사용량 비고 :::::: Swarm Manager 3.1% 12.5% 25.3% CPU/Load 가장 높음 → 제어 부담 Swarm Worker 1 2.5% 28.5% 33.1% Sys Load 매우 높음 → 실제 연산 수행 Swarm Worker 2 2.5% 4.5% 34.0% 메모리 점유 높음 → 앱 컨테이너 실행 ECSClu 1 2.0% 1.0% 34.9% 안정적 유휴 상태 ECSClu 2 2.1% 2.5% 34.9% ECSClu 1과 거의 동일 패턴 검증 결과 — 가설 입증 측정 결과는 가설을 명확히 지지한다. Manager: RAM 25.3%로 Worker(3334%)보다 유의미하게 낮고, Sys Load도 낮다. 앱 컨테이너를 실행하지 않고 클러스터 제어 부담만 지고 있음이 확인됐다. Worker 1·2: RAM 3334% 점유 및 높은 Sys Load → 실제 앱 컨테이너들이 Worker 노드에 배치되어 자원을 점유하고 있음. ECSClu 1·2: RAM 34.9%로 Swarm Worker와 유사한 수준 → 동일한 앱 처리 부담을 지고 있음. 결론: "Swarm 3대(Manager + Worker 2) vs ECS 2대"는 공정한 비교다. 앱을 실제로 처리하는 데이터 플레인은 양쪽 모두 2대이며, ECS는 관리형 서비스이므로 Manager 인스턴스 비용 없이 동일한 처리 용량을 확보할 수 있다. 실험 1 — 컨테이너 장애 복구 테스트 테스트 방법 runrecoverytest.py 스크립트를 각 노드에 대해 실행해 측정을 자동화했다. SSH로 원격 접속하여 docker kill로 컨테이너를 강제 종료하고, Prometheus API를 5초 간격으로 폴링해 컨테이너 수가 다시 EXPECTEDREPLICAS(1) 이상으로 복구되는 시점을 감지한다. [측정 흐름] kill 명령 실행 (killepoch 기록) → 5초 간격 Prometheus 폴링 (containerstarttimeseconds 기반) → 새 컨테이너의 starttime이 killepoch 이후임을 확인 → 복구 시각 기록 (recoveryseconds = recoveredepoch killepoch) → 결과를 recoverysummary.csv / recoveryraw.csv 에 저장 Prometheus 쿼리 구조 Swarm: containerlabelcomdockerswarmservicename="myapp" 으로 서비스 단위 집계 ECS: name="ecstoy." 정규식으로 Task 컨테이너 집계 복구 판단: max(containerstarttimeseconds{...}) 값이 kill 시점 이후로 갱신되었는지 확인 Docker Swarm Test ※ 그라파나 대시보드의 시간축은 UTC 기준으로 표시되며, 실제 테스트 실행 시각(KST, UTC+9)과 9시간 차이가 있습니다. ECS Test ※ 그라파나 대시보드의 시간축은 UTC 기준으로 표시되며, 실제 테스트 실행 시각(KST, UTC+9)과 9시간 차이가 있습니다. 전체 측정 결과 (recoverysummary.csv) 총 8회 테스트 수행 (Swarm 4회, ECS 4회). 테스트 ID 플랫폼 kill 시각 복구 시각 복구 시간(초) 결과 :: swarmworker120260406 Swarm 14:32:27 14:32:46 19.08초 ✅ success swarmworker220260406 Swarm 15:34:42 15:35:01 19.19초 ✅ success ecs120260407 ECS 02:42:49 — timeout ❌ fail ecs220260407 ECS 03:19:07 03:30:25 677.63초 (약 11분) ✅ success swarmworker120260411 Swarm 13:50:25 13:50:46 21.20초 ✅ success swarmworker220260411 Swarm 13:54:44 13:55:05 21.21초 ✅ success ecs120260411 ECS 13:56:26 14:13:54 1047.99초 (약 17분) ✅ success ecs220260411 ECS 14:17:25 14:40:39 1394.02초 (약 23분) ✅ success 플랫폼별 요약 구분 측정 횟수 평균 복구 시간 범위 성공률 ::::: Docker Swarm 4회 약 20초 19.08 21.21초 4/4 (100%) Amazon ECS 4회 약 1,040초 (약 17분) 677초 1,394초 (1회 timeout 제외) 3/4 (75%) Swarm은 4회 모두 1921초로 일관된 복구를 보인 반면, ECS는 677초1,394초로 회차마다 편차가 크고, 1회는 180초 timeout 내에 복구 자체를 실패했다. ECS timeout 케이스(ecs120260407)는 스크립트 제한(180초) 때문에 실패로 기록됐지만, 이후 테스트에서 같은 노드가 1,047초 만에 복구된 것으로 보아 실제 복구는 이뤄졌으나 측정 범위를 벗어난 것으로 판단된다. 원인 분석 (ECS 복구 지연) ECS의 복구 지연은 단순한 성능 문제가 아니라 구조적 설계 차이에서 비롯된다. ① ALB Health Check 대기 (가장 큰 원인) 체크 주기: 기본 30초 정상 임계 횟수: 기본 5회 최소 소요: 30초 × 5회 = 150초(2분 30초) ② Deregistration Delay (Connection Draining) 기존 컨테이너 등록 해제 대기: 기본 300초(5분) 이론 합계: 5분 + 2분 30초 ≈ 7분 30초 실측값(677초1,394초)은 이 이론치보다도 훨씬 큰데, 이는 테스트 당시 EC2 자원 여유 부족으로 새 Task 생성 자체가 지연됐기 때문으로 추정된다. ③ ECS Agent 폴링 주기 EC2 내 ECS Agent는 실시간 Push가 아닌 주기적 Poll 방식으로 AWS Control Plane과 통신 Swarm 대비 수초수십초 추가 지연 발생 ④ ECS 회차별 편차의 원인 실험 당시 EC2 t3.small(2GB)에 ECS Agent, cAdvisor, nodeexporter가 상주 중이었고, 새 Task 생성 시 메모리 여유가 부족한 경우 배치 자체가 지연됐다. 이는 ECS의 Resource reservation 기반 스케줄링 특성상, 자원이 확보되지 않으면 배치를 아예 거부하는 구조에서 비롯된다. 최적화 가능 방향 설정 항목 기본값 최적화 값 효과 Deregistration Delay 300초 30초 이하 대기 시간 대폭 단축 Health Check Interval 30초 10초 확인 주기 단축 Healthy Threshold 5회 2회 통과 시간 20초로 단축 EC2 메모리 여유 확보 Task 0.25vCPU/0.5GB 인스턴스 업그레이드 or Task 스펙 조정 Task 배치 지연 해소 실험 2 — CPU 부하 오토스케일링 테스트 부하 시나리오 (k6 stresstest.js) 시나리오: rampingarrivalrate 워밍업(30→2분) → CPU 상승(60→3분) → 70% 근접(80→3분) → 유지(80, 5분) → 추가 확장(100, 3분) → 유지(100, 5분) 오토스케일링 트리거: CPU Utilization 70% 초과 Docker Swarm 오토스케일링 결과 이벤트 시각 내용 ScaleOut 10:56:15 2 → 4 컨테이너 증가 Stabilization 10:56:15 이후 count=4로 안정 유지 ScaleIn 11:00:15 4 → 2 컨테이너 감소 SelfHealing 실험 중 1→0→1 컨테이너 재생성 확인 Amazon ECS 오토스케일링 결과 및 이슈 ECS에서는 의도한 ScaleOut 대신 SelfHealing(Task 교체) 현상이 반복됐다. 증상 /stress 요청으로 CPU 100% → /health 지연 → ALB Health Check 실패 ECS가 Unhealthy Task를 종료 후 새 Task 생성 (Desired Count 유지) container=error 로그 반복 확인 원인 부하가 한쪽 컨테이너에 집중 → 전체 평균 CPU가 낮아 오토스케일링 미충족 bridge 모드 고정 hostPort(80) → 동일 인스턴스에 복수 Task 배치 불가 조치 및 awsvpc 전환 후 네트워크 모드: bridge → awsvpc 전환 Target Group 타입: instance → IP 타입 재생성 부하 방식: constantarrivalrate → rampingarrivalrate로 변경 전환 후 2→3 증가는 확인됐으나 Task 1개가 종료되는 현상 재발. 실험 시간 제약으로 완전한 안정화 달성 전 종료. 주요 트러블슈팅 기록 T1. Bridge 모드 고정 hostPort Scaleout 실패 내용 증상 Desired Count 34로 변경해도 Running Task는 2개에서 증가 안 함 원인 hostPort=80 고정 시 EC2 한 대에 동일 포트 컨테이너 2개 배치 불가 조치 hostPort=0(동적 포트) 변경 → 이후 awsvpc + IP Target Group으로 전환 T2. Target Group 설정 오류 → Health Check 실패 → SelfHealing 오해 발생 흐름 bridge 모드에서 hostPort 고정 → Scaleout 시도 시 포트 충돌로 신규 Task 배치 실패 → awsvpc 전환 결정 → 기존 instance 타입 Target Group을 ip 타입으로 변경 시도 → AWS 정책상 불가(immutable) → Target Group 신규 생성했으나 포트 설정 미스 → ALB가 Task를 Unhealthy로 판정 → ECS가 Unhealthy Task 종료 후 새 Task 생성 반복 → Task가 죽었다 살아나는 걸 보고 Scaleout으로 오해 → 실제로는 SelfHealing(장애 교체) 이었음 내용 증상 Scaleout을 기대했으나 기존 Task가 종료되고 새 Task가 생성되는 패턴 반복 근본 원인 instance 타입 Target Group → awsvpc 전환 후 ip 타입으로 재생성 필요. AWS ALB Target Type은 생성 후 변경 불가(immutable)이므로 신규 생성했으나 포트 설정 불일치로 Health Check 계속 실패 조치 Target Group ip 타입으로 재생성 (port 8080, /health) → ALB Security Group에서 8080 인바운드 허용 확인 → /stress?timeout=10 → timeout=1로 부하 완화 T3. Grafana 컨테이너 수 과다 표시 내용 증상 ECS Running Task 2개인데 Grafana에서 45개로 표시 원인 awsvpc 모드에서 Task마다 internalecspause 컨테이너 자동 생성 조치 PromQL에 containerlabelcomamazonawsecscontainername="toysb" 필터 추가 비교 분석 핵심 지표 비교 항목 Docker Swarm Amazon ECS 설계 목적 경량 오케스트레이션 클라우드 운영 플랫폼 복구 시간 약 21초 약 7분 (기본 설정) 스케일링 반응 즉시 (로컬 스케줄러) 수십초수분 (CloudWatch 기반) 스케줄링 방식 Besteffort (공격적) Resource reservation (보수적) 오버커밋 가능 거의 불가 네트워크 오버헤드 낮음 (bridge/overlay) 높음 (ENI, ALB, Target Group) 운영 복잡도 낮음 높음 안정성 상대적으로 낮음 높음 모니터링 통합 직접 구성 필요 CloudWatch 기본 연동 왜 ECS가 느린가 (구조적 이해) [Docker Swarm 복구 흐름] 컨테이너 종료 감지 (Docker Engine 내장) → 즉시 재스케줄링 (로컬 의사결정) → 컨테이너 Running → 즉시 트래픽 수신 ≈ 수초20초 [Amazon ECS 복구 흐름] Docker Engine 장애 감지 → ECS Agent 전달 → AWS Control Plane 보고 (네트워크 왕복) → 새 Task 생성 → ENI 할당 → ALB Target Group 등록 → Health Check 대기 (30초 × 5회 = 150초) → Deregistration Delay 대기 (300초) ≈ 7분+ ECS의 느림은 "안전한 트래픽 전환을 보장하는 설계"의 결과다. 💡 프로젝트를 통해 얻은 핵심 인사이트 1. 동일 환경 비교의 어려움 — ECS는 불리한 조건에서 측정됐다 ECS를 Swarm 수준에 맞추기 위해 세팅 초기에 Task 리소스를 0.25 vCPU/0.5GB로 제한하자 컨테이너 불안정, 메모리 부족, GC 문제가 발생했다. "비교를 공정하게 만들려다 ECS의 장점을 모두 제거하는" 역설을 경험했다. 이번 실험에서 측정된 ECS의 느린 복구 시간(677초1,394초)은 ECS 자체의 한계가 아니라 실험 조건의 한계다. [이번 실험의 ECS 조건 — 불리한 환경] EC2 t3.small (2GB) 위에 ECS Agent + cAdvisor + nodeexporter 상주 → Task 리소스를 0.25vCPU/0.5GB로 억지로 제한 → 메모리 여유 부족으로 새 Task 배치 자체가 지연 → Swarm 비교를 위해 ECS 본래 설계 방식을 벗어난 구성 [ECS 본래 용도로 사용했다면] Fargate 사용 → EC2 관리 불필요, AWS가 미리 준비한 자원 풀에서 즉시 컨테이너 주입 → Task 배치 지연 없음 → Health Check 설정 튜닝 (Interval 10초, Threshold 2회) 시 복구 시간 20초대도 가능 즉, "ECS가 느리다"가 아니라 "EC2 위에 억지로 올린 ECS가 느렸다" 는 것이 정확한 표현이다. Fargate로 ECS 본래 용도대로 운영했다면 복구 속도 자체는 Swarm과 크게 다르지 않을 수 있다. 2. 비교 관점의 전환 Before After 단순 성능 비교 (속도) 운영 전략 비교 (적합한 환경의 차이) "ECS가 느리다" "EC2 위에 억지로 올린 ECS가 느렸다" "Swarm이 더 좋다" "온프레미스엔 Swarm, 클라우드엔 ECS(Fargate)" 3. 실무 관점에서의 선택 기준 ECS가 Swarm보다 낫다거나, 기업이 Swarm을 버리고 ECS를 선택한다는 식의 결론은 정확하지 않다. 실제로는 인프라 환경(온프레미스 vs 클라우드 온디맨드) 에 따라 각각이 적합한 상황이 다르다. 상황 적합한 선택 이유 온프레미스 / 자체 서버 보유 Docker Swarm 클라우드 벤더 종속 없이 직접 소유한 서버에서 경량 오케스트레이션 운영 가능. AWS API 호출 비용·지연 없음. AWS 클라우드 온디맨드 Amazon ECS ALB·CloudWatch·IAM·VPC 등 AWS 에코시스템과 완전 통합. 인프라를 직접 소유하지 않고 사용한 만큼만 과금. 클라우드지만 벤더 독립이 중요 Docker Swarm 멀티클라우드 또는 이식성이 중요한 환경에서는 Swarm이 유리. 서버리스 지향, 인프라 관리 최소화 ECS (Fargate) EC2 자체를 관리할 필요 없이 컨테이너 단위로 과금·운영 가능. 이 프로젝트를 통해 깨달은 핵심은, "어느 쪽이 더 좋은가"가 아니라 "우리 인프라 환경이 온프레미스냐 클라우드냐에 따라 자연스럽게 선택이 갈린다" 는 점이다. 온프레미스 서버를 보유한 조직이 굳이 ECS를 쓸 이유가 없고, AWS 위에서 운영하는 조직이 굳이 Swarm을 직접 관리할 이유도 없다. 4. 트러블슈팅 역량 단순 기능 구현을 넘어, 네트워크 모드 선택이 확장성에 미치는 영향, ALB Health Check가 복구 시간을 결정하는 메커니즘, ECS Agent 폴링 방식의 한계 등 인프라 내부 동작 원리를 실험을 통해 직접 검증했다. 기술 스택 구분 기술 컨테이너 오케스트레이션 Docker Swarm, Amazon ECS (EC2 Launch Type) 애플리케이션 Spring Boot (Java) 인프라 AWS EC2 (t3.small), ALB, VPC, Target Group 모니터링 Prometheus, Grafana, cAdvisor, nodeexporter 부하 테스트 k6 (rampingarrivalrate), Python (autoscaletest.py) 네트워크 bridge, overlay, awsvpc, ENI 회고 및 결론 본 프로젝트는 "완벽한 비교 실험"보다 더 가치 있는 것을 남겼다. 단순히 수치를 비교하는 과정에서, 두 시스템이 서로 다른 문제를 풀기 위해 설계됐다는 점을 발견했다. ECS를 Swarm처럼 다운그레이드해서 비교하는 것 자체가 잘못된 접근이었음을 실험 도중 깨달았고, 그 깨달음 자체가 이 프로젝트의 핵심 성과다. Docker Swarm은 빠르고 단순한 오케스트레이터, Amazon ECS는 느리지만 안정적인 클라우드 운영 플랫폼이다.

May 11, 2026오케스트레이션
ECS vs Docker Swarm 컨테이너 오케스트레이션 비교 프로젝트