이코에코(Eco²) Streams & Scaling for SSE #10: Scan API 부하 테스트 (1)
테스트 시나리오 (K6)
┌─────────────────────────────────────────────────────────────────────────────┐
│ VU n Load Test Timeline │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ VU │
│ n ├──────────────────────────────────────────────────────┤ │
│ │ ↑ 2분 유지 │ │
│ 25 │ ╱ ╲ │
│ │ ╱ ╲ │
│ 0 ├───────────╱ ╲───────── │
│ 0s 30s 2m30s 3m │
│ └─ ramp-up ┘└───────── steady state ─────────────────┘└─ ramp-down │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
- VU: 동시접속자, 각 VU는 Scan API 요청 후 task_id 수령
- Cycle: POST api/v1/scan: task_id 수령- > GET result/task_id polling으로 결과 조회 -> 결과 수령 후 재요청
- ramp-up: 30초동안 최대 VU까지 점진적 도달
- Steady: 2분간 최대 VU 유지
- 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 |
성과
- 100% 성공률 - Scan API 안정성 검증 (Event Bus 전, 75-85%)
- 99.7% 완료율 - Worker 파이프라인 안정적
- 99.3% 보상률 - Reward 로직 정상 동작
- 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
성과
- 99.9% 성공률 - 250 VU에서도 Scan API 안정성 유지
- 99.9% 완료율 - Worker 파이프라인 완벽 동작
- 98.6% 보상률 - Reward 로직 정상 동작
- KEDA 자동 스케일링 - scan-worker 1→3, scan-api 2→3 정상 작동
- 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
결과
- 100% 성공률 - 300 VU에서도 Scan API 안정성 유지
- 99.9% 완료율 - Worker 파이프라인 정상 동작 (Scan Worker CPU Usage 88.1%)
- 98.9% 보상률 - Reward 로직 정상 동작
- KEDA 자동 스케일링 - scan-worker 1→3, scan-api 2→3 정상 작동
- 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초 |
테스트 시 유의사항
- 2vCPU, 4GB 기준 Worker Pods 3개가 HPA 스케일링 한계 지점
- 초기화
- 이전 테스트 잔여 데이터로 인한 성능 저하 방지
- Redis Streams, RabbitMQ 큐 비우고 테스트 권장
- Event Router 성능
- 8,157개 이벤트 Pub/Sub 발행
- Pending 0 = 모든 메시지 즉시 처리
- Persistence Layer 분리
- Postgres 접근이 비즈니스 로직에 부하를 주지 않도록 분리
- 응답을 내린 후 스키마별 배치큐에 WRITE Job을 적재, 50 jobs or timeout 시 일괄 배치 처리
- 실패한 작업은 DLQ로 이동, 이후 재처리
- Eventual Consistency로 데이터 간 논리적 일관성 보장
- 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 복구 |
개선 효과
- SSE Gateway HA: Istio Consistent Hash 의존성 제거
- Event Router HA: Consumer Group으로 장애 시 자동 Failover
- State 복구: SSE 재접속 시 Streams에서 누락 이벤트 Catch-up
- Pub/Sub 효율: Fire-and-forget으로 실시간 전달, 누락은 State로 보완
- 수용 가능한 동시접속자 폭증: VU 50 35% -> VU 50-300 99.8%까지 수치 개선
- Stateless로 완전 전환, 주요 컴포넌트 수평확장 가능: KEDA로 이벤트 기반 오토스케일링 구현, 주요 컴포넌트가 단일 노드인 환경에서도 동시접속자 300+까지 수용 가능함을 확인 (SSE, GPT 5.1x2, 99.8%), Karpenter 노드 오토스케일링이 적용될 경우 300 x {노드수} 규모로 수용 가능
References
- Redis Streams Consumer Groups
- Redis Pub/Sub
- 이전 글: 초기 부하 테스트 병목 분석 (#78)
- 이전 글: Application Layer 업데이트 (#91)
- 이전 글: KEDA 기반 스케일링 (#93)
- 이전 글: Consistent Hash + StatefulSet의 한계 (#98)
- 이전 글: Event Bus Layer 구현 (#103)
테스트 스크립트 및 결과 데이터 (K6,JSON)
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