이코에코(Eco²)/Event Streams & Scaling

이코에코(Eco²) Streams & Scaling for SSE #10: Scan API 부하 테스트 (1)

mango_fr 2025. 12. 28. 17:06

 

테스트 시나리오 (K6)

┌─────────────────────────────────────────────────────────────────────────────┐
│                         VU n Load Test Timeline                            │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   VU                                                                        │
│    n ├──────────────────────────────────────────────────────┤               │
│      │              ↑ 2분 유지                              │               │
│   25 │             ╱                                        ╲               │
│      │            ╱                                          ╲              │
│    0 ├───────────╱                                            ╲─────────    │
│      0s        30s                                        2m30s  3m         │
│      └─ ramp-up ┘└───────── steady state ─────────────────┘└─ ramp-down     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
  1. VU: 동시접속자, 각 VU는 Scan API 요청 후 task_id 수령
  2. Cycle: POST api/v1/scan: task_id 수령- > GET result/task_id polling으로 결과 조회 -> 결과 수령 후 재요청
  3. ramp-up: 30초동안 최대 VU까지 점진적 도달
  4. Steady: 2분간 최대 VU 유지
  5. ramp-donw: 30초동안 Max VU -> 0 VU까지 점진적 감소

VU50 테스트 결과

https://snapshots.raintank.io/dashboard/snapshot/ZdMIItsw6ZRFhXKLO1M1BJDW14QkHC4o

 

Grafana

If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m

snapshots.raintank.io

 
 

완료율 99.70% (674/685) ✅ 우수, 실제 성공률 (vision->rule->answer->reward->done)
보상 수령률 99.26% (671/685) ✅ 우수
Scan API p95 93ms ✅ 빠름
완료 시간 p95 17.7s ⚠️ 추론 시간 포함 (GPT 5.1 x2)
Polling 요청 3,419건 약 5회/job

성과

  1. 100% 성공률 - Scan API 안정성 검증 (Event Bus 전, 75-85%)
  2. 99.7% 완료율 - Worker 파이프라인 안정적
  3. 99.3% 보상률 - Reward 로직 정상 동작
  4. 10건 미완료 - 3분 테스트 종료로 인한 interrupted iterations (정상)

VU 200 테스트 (Success Rate 99+%)

https://snapshots.raintank.io/dashboard/snapshot/Xbr36P3VSZhqvE8RHv2vlpgNm8lHcwSs

 

Grafana

If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m

snapshots.raintank.io

 

테스트 환경

  • 시간: 2025-12-28 20:41 ~ 20:46 KST (약 5분)
  • VUs: 200 (동시 사용자)
  • 테스트 방식
    • POST → Polling 방식
    • E2E Latency: POST  ~ done 인까지 전체 시간 측정
    • Completion Rate: status === 'completed'인 job 수 / 성공한 scan 요청 수

핵심 지표

성공률 99.8% (1,646/1,649) ✅ 우수
완료율 99.8% (1,645/1,646) ✅ 우수
보상 수령률 98.9% (1,627건) ✅ 우수
Scan API p95 83ms ✅ 빠름
완료 시간 p95 33.16s ✅ 정상
E2E Latency p95 33.21s ✅ 정상
처리량 370 req/m ✅ 양호

Redis Streams 상태

Stream Length (scan:events:*):
  scan:events:0: 10,006
  scan:events:1: 10,002
  scan:events:2: 10,005
  scan:events:3: 10,005

Pending Messages: 0

4개 샤드에 균등 분배 (±0.04% 편차). Pending 0 = 모든 메시지 ACK 완료.

KEDA Scaling 상태

scan-worker 1 3

리소스 사용량

RabbitMQ 적체 없음
k8s-worker-ai CPU 20%
k8s-worker-ai Memory 71% (2,658Mi)

250 VU 부하테스트 결과

https://snapshots.raintank.io/dashboard/snapshot/g3Qa0xMImwNmNuzQTXGW6C6I5VBopY5J

 

Grafana

If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m

snapshots.raintank.io

테스트 환경

  • 시간: 2025-12-28 21:15:18 ~ 21:19:18 KST (약 4분)
  • VUs: 250 (동시 사용자)
  • 테스트 방식:
    • POST → Polling 방식
    • E2E Latency: POST  ~ done 인까지 전체 시간 측정
    • Completion Rate: status === 'completed'인 job 수 / 성공한 scan 요청 수

핵심 지표

성공률 99.9% (1,753/1,754) ✅ 즉시 응답 후 stream을 열어 모두 통과
완료율 99.9% (1,753/1,753) ✅ 우수, 실제 성공률 (vision→rule→answer→reward→done)
보상 수령률 98.6% (1,729/1,753) ✅ 우수
Scan API p95 78ms ✅ 빠름
완료 시간 p95 40.5s ⚠️ 추론 시간 포함 (GPT 5.1 x2)
E2E p95 40.5s ✅ 목표치(90s) 이내
Polling 요청 22,570건 약 13회/job

KEDA Scaling 상태

scan-worker 1 3 1→3 (scan.vision 큐 기반) ✅
scan-api (HPA) 1 3 2→3 (CPU 기반) ✅
event-router 1 3 1
sse-gateway 1 3 1

Redis Streams 상태 (테스트 후)

Stream Length (scan:events:*):
  scan:events:0: 4,410
  scan:events:1: 4,280
  scan:events:2: 4,400
  scan:events:3: 4,440

Pending Messages: 0

4개 샤드에 균등 분배 (±1.8% 편차). Pending 0 = 모든 메시지 ACK 완료.

노드 리소스

k8s-worker-ai 18% (365m) 69% (2,601Mi)

Stage Breakdown

vision  : 1,753
rule    : 1,753
answer  : 1,753
reward  : 1,729
done    : 1,753

성과

  1. 99.9% 성공률 - 250 VU에서도 Scan API 안정성 유지
  2. 99.9% 완료율 - Worker 파이프라인 완벽 동작
  3. 98.6% 보상률 - Reward 로직 정상 동작
  4. KEDA 자동 스케일링 - scan-worker 1→3, scan-api 2→3 정상 작동
  5. 1건 미완료 - 테스트 종료로 인한 interrupted iteration (정상)

VU 300 테스트 결과

https://snapshots.raintank.io/dashboard/snapshot/9LbgErvCS0N94vDc9NwO15kVAk7eLzLM

 

Grafana

If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m

snapshots.raintank.io

 

테스트 환경

 

  • 시간: 2025-12-28 21:42:30 ~ 21:46:30 KST (약 4분 17초)
  • VUs: 300 (동시 사용자)
  • 테스트 방식
    • POST → Polling 방식
    • E2E Latency: POST  ~ done 인까지 전체 시간 측정
    • Completion Rate: status === 'completed'인 job 수 / 성공한 scan 요청 수

핵심 지표

성공률 100% (1,732/1,732) ✅ 즉시 응답 후 stream을 열어 모두 통과
완료율 99.9% (1,719/1,732) ✅ 우수, 실제 성공률 (vision→rule→answer→reward→done)
보상 수령률 98.9% (1,701/1,719) ✅ 우수
Scan API p95 83ms ✅ 빠름
완료 시간 p95 48.5s ⚠️ 추론 시간 포함 (GPT 5.1 x2)
E2E p95 48.6s ✅ 목표치(90s) 이내
Polling 요청 27,411건 약 16회/job

KEDA Scaling 상태

scan-worker 1 3 1→3 (scan.vision 큐 기반) ✅
scan-api (HPA) 1 3 2→3 (CPU 기반) ✅
event-router 1 3 1
sse-gateway 1 3 1

Redis Streams 상태 (테스트 후)

Stream Length (scan:events:*):
  scan:events:0: 4,060
  scan:events:1: 4,300
  scan:events:2: 4,580
  scan:events:3: 4,380

Pending Messages: 0

4개 샤드에 균등 분배 (±6% 편차). Pending 0 = 모든 메시지 ACK 완료.

노드 리소스

k8s-worker-ai 17% (359m) 69% (2,582Mi)
k8s-api-scan 3% (76m) 67% (2,506Mi)

Stage Breakdown

vision  : 1,719
rule    : 1,719
answer  : 1,719
reward  : 1,701
done    : 1,719

결과

  1. 100% 성공률 - 300 VU에서도 Scan API 안정성 유지
  2. 99.9% 완료율 - Worker 파이프라인 정상 동작 (Scan Worker CPU Usage 88.1%)
  3. 98.9% 보상률 - Reward 로직 정상 동작
  4. KEDA 자동 스케일링 - scan-worker 1→3, scan-api 2→3 정상 작동
  5. 17건 미완료 - 테스트 종료로 인한 interrupted iterations (정상)

동시접속자별 테스트 결과 비교 (Event Bus + Pub/Sub Fan-out)

50 685 99.7% 198 req/m 17.7초
200 1,649 99.8% 370 req/m 33.2초
250 1,754 99.9% 418 req/m 40.5초
300 1,732 99.9% 404 req/m 48.6초

 


테스트 시 유의사항

  1. 2vCPU, 4GB 기준 Worker Pods 3개가 HPA 스케일링 한계 지점
  2. 초기화
    • 이전 테스트 잔여 데이터로 인한 성능 저하 방지
    • Redis Streams, RabbitMQ 큐 비우고 테스트 권장
  3. Event Router 성능
    • 8,157개 이벤트 Pub/Sub 발행
    • Pending 0 = 모든 메시지 즉시 처리
  4. Persistence Layer 분리
    • Postgres 접근이 비즈니스 로직에 부하를 주지 않도록 분리
    • 응답을 내린 후 스키마별 배치큐에 WRITE Job을 적재, 50 jobs or timeout 시 일괄 배치 처리
    • 실패한 작업은 DLQ로 이동, 이후 재처리
    • Eventual Consistency로 데이터 간 논리적 일관성 보장
  5. Scan Worker GEVENT 풀 관리 (현 설정: Greenlet 100 x pods)
    • 부하 테스트가 과중첩될 경우, FD 고갈로 Greenlet이 stuck에 빠짐
    • OpenAI API 단일 평균 응답시간이 5-10초로, 저성능 IO와 유사하게 다뤄야 함
    • 현재 Scan API는 두 LLM API가 체인으로 묶인 워크로드, 별도 조치 필요
    • Gevent는 협력적 스케줄링이라 강제 종료 불가, 별도의 튜닝 필요

아키텍처 고도화에 따른 성능 비교

1. Celery Events + SSE 직접 구독 (#77)

Redis Streams 적용 전, Celery Events 직접 구독 구조, 50 VU 테스트

근본 원인 SSE 연결당 다수의 RabbitMQ 연결 생성
연결 비율 SSE 16개 : RabbitMQ 341개 = 1:21

리소스

scan-api 메모리 최대 676Mi (Limit 512Mi) 🔥 초과
RPC Reply Queue 372개 적체 🔥 응답 대기

핵심 문제

SSE가 "파이프라인 실행 감시"와 "결과 전달"을 동시에 하면서
    → 클라이언트 × RabbitMQ 연결 = 곱 폭발 발생

50 VU × 10 연결/SSE = 500개 잠재 연결

이 분석 결과를 바탕으로 Streams & Scaling 기반 SSE 전환 진행

2. KEDA 스케일링 적용 후 (#93)

50 VU 테스트 결과:

Failed 66 (10.0%)
Reward Null 144 (21.9%)

 
스케일링 전 테스트 Success Rate 35% → 86.3%로 개선
그러나 Celery Events 구조로 80-100 VU로 진행할 경우, Success Rate 30%대로 급감

3. Event Bus + Pub/Sub Fan-out (현재 아키텍처)

50 685 99.7% 198 req/m 17.7초
200 1,649 99.8% 370 req/m 33.2초
250 1,754 99.9% 418 req/m 40.5초
300 1,732 99.9% 404 req/m 48.6초

아키텍처 비교

Fan-out 없음 (직접 소비) 없음 Redis Pub/Sub
State 복구 없음 없음 State KV + Streams Catch-up
SSE 라우팅 scan-api Pod 단일 Pod 어느 Pod든 가능
수평 확장 불가 (연결 폭증) 불가 Event Router, SSE Gateway 독립 확장
장애 복구 메시지 유실 메시지 유실 XAUTOCLAIM으로 Pending 복구

개선 효과

  1. SSE Gateway HA: Istio Consistent Hash 의존성 제거
  2. Event Router HA: Consumer Group으로 장애 시 자동 Failover
  3. State 복구: SSE 재접속 시 Streams에서 누락 이벤트 Catch-up
  4. Pub/Sub 효율: Fire-and-forget으로 실시간 전달, 누락은 State로 보완
  5. 수용 가능한 동시접속자 폭증: VU 50 35% -> VU 50-300 99.8%까지 수치 개선
  6. Stateless로 완전 전환, 주요 컴포넌트 수평확장 가능: KEDA로 이벤트 기반 오토스케일링 구현, 주요 컴포넌트가 단일 노드인 환경에서도 동시접속자 300+까지 수용 가능함을 확인 (SSE, GPT 5.1x2, 99.8%), Karpenter 노드 오토스케일링이 적용될 경우 300 x {노드수} 규모로 수용 가능

References

 

테스트 스크립트 및 결과 데이터 (K6,JSON)

k6-sse-load-test.js
0.02MB
k6-load-test-vu200-2025-12-28T11-45-54-248Z.json
0.00MB
k6-load-test-vu250-2025-12-28T12-19-18-348Z.json
0.00MB
k6-load-test-vu300-2025-12-28T12-46-30-548Z.json
0.00MB

 

GitHub

 

GitHub - eco2-team/backend: 🌱 이코에코(Eco²) BE

🌱 이코에코(Eco²) BE. Contribute to eco2-team/backend development by creating an account on GitHub.

github.com

Services

 

이코에코

frontend.dev.growbin.app