-
Notifications
You must be signed in to change notification settings - Fork 0
scale out
eeekeee edited this page Mar 5, 2026
·
1 revision
본 보고서는 채점 서버의 확장성(Scalability) 검증을 위해 진행된 부하 테스트 결과를 바탕으로, 인프라 자원 한계에 따른 성능 병목 현상을 분석하고 이를 해결하기 위한 최적화 과정을 담고 있습니다.
- 다중 인스턴스 환경에서 발생하던 타임아웃 및 지연 현상을 Concurrency 튜닝을 통해 해결
- 8논리 프로세서 환경에서 15개의 동시 채점 시 발생하는 CPU 자원 경합 및 컨텍스트 스위칭이 주원인
- 각 워커의 Concurrency를 2로 제한(전체 동시성 6)한 결과:
- p95 응답 지연 시간: 622ms → 167ms (약 73% 개선)
- 단일 서버(Replicas 1) 환경에서 관리 오버헤드 최소, 가장 빠른 응답 속도 확인
- 다중 서버(Replicas 3) 환경에서도 부하가 균등하게 분산됨 → 클라우드 환경 이전 시 강력한 확장성 기대
- 소규모 부하에서는 Replicas 1로 충분하며, 불필요한 분산은 오히려 지연 증가
- CPU 자원 제한 환경에서는 다중 워커를 동시에 실행하면 컨텍스트 스위칭 오버헤드로 처리 속도 감소
- Concurrency 제어를 통해 서버 간 오버헤드 최소화 및 안정적인 응답 확보 가능
- 클라우드 환경에서는 물리적 코어 충분 시 Replicas 확장으로 높은 처리량 확보 가능
-
Auto-scaling은 물리적 자원이 충분할 때 유효
- CPU/메모리 여유가 있는 클라우드 환경이라면, 부하 증가 시 Replicas를 자동으로 늘려 처리량 확보 가능
- 로컬처럼 CPU가 제한된 환경에서는 무작정 늘리는 것보다 Concurrency 제어 + 필요 시 Auto-scaling이 최적
-
Replicas
- 단순히 컨테이너를 몇 개 띄우겠다고 설정하는 것
- 예:
docker-compose scale judge=3또는 Kubernetes에서replicas: 3 - 서버 수를 수동으로 지정 → 부하에 따라 자동 조정 안 됨
-
Auto-Scaling
- 트래픽/CPU 사용량/메모리 사용량을 기준으로 자동으로 서버를 늘리거나 줄이는 기능
- NCP에서는 Auto-Scaling 그룹이나 **Container Service(Kubernetes 기반)**에서 설정 가능
- Kubernetes를 쓰면
Horizontal Pod Autoscaler(HPA)가 자동으로 pod 수를 늘려줌 - Docker Compose만 쓰면 자동 스케일링은 안 됨 → 수동으로 replicas 늘려야 함
-
도커 컴포즈
- 배포 환경에서도 사용할 수 있음
- 장점: 설정 단순, 환경 일관성 유지
- 단점: 자동 스케일링 지원 없음
- CPU가 꽉 차거나 트래픽 폭주 시 수동으로 컨테이너 늘려야 함
- 로컬 테스트처럼 CPU 경쟁으로 오버헤드 발생 가능
-
클라우드 환경에서 안정적 확장
- NCP Container Service(Kubernetes 기반)나 ECS/Fargate 같은 서비스 사용 시
- HPA나 Auto-Scaling 그룹으로 자동 확장 가능
- CPU/Mem 기준으로 pod(replica)를 늘리면서 안정적 처리
- Concurrency 제한 + Auto-Scaling 조합이 가장 효율적
- NCP Container Service(Kubernetes 기반)나 ECS/Fargate 같은 서비스 사용 시
- 도커 컴포즈로만 운영 → CPU가 한정된 서버에서는 replicas 늘리는 게 항상 성능 향상으로 이어지지 않음
- Auto-Scaling 활용 가능 환경 → replicas를 자동으로 늘려 부하 대응 가능, 단 클라우드 환경이어야 함
-
권장 패턴
- Concurrency 제한 → CPU 경쟁 최소화
- 필요 시 replicas/Auto-Scaling → 트래픽 증가에 대응
아래는 테스트 결과입니다.
- 도구: k6 (Load Testing Tool)
- 프리셋: Small (5 VUs, 1분 지속)
- 대상: 단일 서버(Replicas 1) vs 다중 서버(Replicas 3) 환경 비교
- BullMQ Concurrency : 5
k6 raw 결과 보기
## Replicas 3개일 때(채점 서버 3개) k6 run -e PRESET=small submission-load-test.js
INFO[0000] ======================================== source=console
INFO[0000] 채점 시스템 부하 테스트 source=console
INFO[0000] ======================================== source=console
INFO[0000] Preset: small source=console
INFO[0000] Description: 소규모 테스트: 기본 부하 (5 VUs, 1분) source=console
INFO[0000] Target: http://localhost:3000 source=console
INFO[0000] Problem ID: beta_easy_1 source=console
INFO[0000] ======================================== source=console
INFO[0060] ======================================== source=console
INFO[0060] 테스트 완료 source=console
INFO[0060] 총 소요 시간: 60.50초 source=console
INFO[0060] ======================================== source=console
█ THRESHOLDS
http_req_duration
✓ 'p(95)<10000' p(95)=622.22ms
http_req_failed
✓ 'rate<0.05' rate=0.00%
submission_success_rate
✓ 'rate>0.9' rate=100.00%
█ TOTAL RESULTS
checks_total.......: 309 5.101362/s
checks_succeeded...: 100.00% 309 out of 309
checks_failed......: 0.00% 0 out of 309
✓ status is 201
✓ has submissionId
✓ status is PENDING
CUSTOM
submission_duration............: avg=308.062624 min=33.8915 med=279.9424 max=887.1157 p(90)=569.28904 p(95)=622.5194
submission_success_rate........: 100.00% 103 out of 103
HTTP
http_req_duration..............: avg=305.32ms min=22.97ms med=278.82ms max=887.11ms p(90)=567.87ms p(95)=622.22ms
{ expected_response:true }...: avg=305.32ms min=22.97ms med=278.82ms max=887.11ms p(90)=567.87ms p(95)=622.22ms
http_req_failed................: 0.00% 0 out of 104
http_reqs......................: 104 1.716963/s
EXECUTION
iteration_duration.............: avg=2.33s min=1.18s med=2.29s max=3.85s p(90)=3.15s p(95)=3.29s
iterations.....................: 103 1.700454/s
vus............................: 1 min=1 max=5
vus_max........................: 5 min=5 max=5
NETWORK
data_received..................: 35 kB 575 B/s
data_sent......................: 70 kB 1.1 kB/s
running (1m00.6s), 0/5 VUs, 103 complete and 0 interrupted iterations
default ✓ [======================================] 0/5 VUs 1m0s
## Replicas 3개 모니터링
[Before Test - Last Values]
web30-mysql-1 | CPU: 1.16% | MEM: 5.83%
web30-redis-1 | CPU: 0.57% | MEM: 0.14%
web30-judge-1 | CPU: 11.08% | MEM: 1.73%
web30-judge-2 | CPU: 10.27% | MEM: 1.61%
web30-judge-3 | CPU: 18.15% | MEM: 2.11%
web30-api-1 | CPU: 22.69% | MEM: 2.56%
web30-web-1 | CPU: 22.63% | MEM: 1.25%
[During Test - Statistics]
CONTAINER | CPU_MIN | CPU_MAX | CPU_AVG | MEM_MIN | MEM_MAX | MEM_AVG
---------------------------------------------------------------------------------------------------------
web30-mysql-1 | 0.55% | 15.39% | 7.23% | 5.83% | 6.36% | 6.24%
web30-redis-1 | 0.38% | 3.94% | 1.86% | 0.14% | 0.15% | 0.15%
web30-api-1 | 11.66% | 146.28% | 32.47% | 2.16% | 2.78% | 2.31%
web30-judge-1 | 4.68% | 54.25% | 18.64% | 1.71% | 2.93% | 2.21%
web30-judge-2 | 5.24% | 46.13% | 21.74% | 1.62% | 3.21% | 2.23%
web30-judge-3 | 4.18% | 43.77% | 20.4% | 1.86% | 2.76% | 2.31%
web30-web-1 | 14.41% | 54.34% | 26.11% | 1.25% | 1.26% | 1.26%
채점 첫번째 시작 시간 : 02/02/2026, 2:01:34 PM
채점 마지막 끝난 시간 : 02/02/2026, 2:03:36 PM
--------------
## Replicas 1개일 때(채점 서버 1개) k6 run -e PRESET=small submission-load-test.js
INFO[0000] ======================================== source=console
INFO[0000] 채점 시스템 부하 테스트 source=console
INFO[0000] ======================================== source=console
INFO[0000] Preset: small source=console
INFO[0000] Description: 소규모 테스트: 기본 부하 (5 VUs, 1분) source=console
INFO[0000] Target: http://localhost:3000 source=console
INFO[0000] Problem ID: beta_easy_1 source=console
INFO[0000] ======================================== source=console
INFO[0060] ======================================== source=console
INFO[0060] 테스트 완료 source=console
INFO[0060] 총 소요 시간: 60.26초 source=console
INFO[0060] ======================================== source=console
█ THRESHOLDS
http_req_duration
✓ 'p(95)<10000' p(95)=208.17ms
http_req_failed
✓ 'rate<0.05' rate=0.00%
submission_success_rate
✓ 'rate>0.9' rate=100.00%
█ TOTAL RESULTS
checks_total.......: 339 5.621049/s
checks_succeeded...: 100.00% 339 out of 339
checks_failed......: 0.00% 0 out of 339
✓ status is 201
✓ has submissionId
✓ status is PENDING
CUSTOM
submission_duration............: avg=93.838248 min=14.8234 med=72.5801 max=335.5332 p(90)=173.01356 p(95)=208.21632
submission_success_rate........: 100.00% 113 out of 113
HTTP
http_req_duration..............: avg=93.09ms min=8.82ms med=71.69ms max=335.53ms p(90)=172.47ms p(95)=208.17ms
{ expected_response:true }...: avg=93.09ms min=8.82ms med=71.69ms max=335.53ms p(90)=172.47ms p(95)=208.17ms
http_req_failed................: 0.00% 0 out of 114
http_reqs......................: 114 1.890264/s
EXECUTION
iteration_duration.............: avg=2.09s min=1.07s med=2.03s max=3.12s p(90)=2.9s p(95)=2.98s
iterations.....................: 113 1.873683/s
vus............................: 1 min=1 max=5
vus_max........................: 5 min=5 max=5
NETWORK
data_received..................: 38 kB 633 B/s
data_sent......................: 76 kB 1.3 kB/s
running (1m00.3s), 0/5 VUs, 113 complete and 0 interrupted iterations
default ✓ [======================================] 0/5 VUs 1m0s
## Replicas 1개 모니터링
[Before Test - Last Values]
web30-mysql-1 | CPU: 0.49% | MEM: 5.58%
web30-redis-1 | CPU: 0.72% | MEM: 0.13%
web30-judge-1 | CPU: 4.17% | MEM: 1.75%
web30-api-1 | CPU: 8.76% | MEM: 1.94%
web30-web-1 | CPU: 17.17% | MEM: 1.24%
[During Test - Statistics]
CONTAINER | CPU_MIN | CPU_MAX | CPU_AVG | MEM_MIN | MEM_MAX | MEM_AVG
---------------------------------------------------------------------------------------------------------
web30-mysql-1 | 0.44% | 12.8% | 4.9% | 5.58% | 6.08% | 5.94%
web30-redis-1 | 0.37% | 2.84% | 1.54% | 0.13% | 0.14% | 0.14%
web30-judge-1 | 3.65% | 63.75% | 25.13% | 1.76% | 3.38% | 2.64%
web30-api-1 | 8.14% | 42.91% | 25.02% | 1.95% | 2.39% | 2.23%
web30-web-1 | 8.76% | 43.07% | 23.9% | 1.24% | 1.25% | 1.25%
채점 첫번째 시작 시간 : 02/02/2026, 2:08:44 PM
채점 마지막 끝난 시간 : 02/02/2026, 2:10:30 PM
| 서버 | CPU 최소 | CPU 최대 | CPU 평균 | MEM 최소 | MEM 최대 | MEM 평균 |
|---|---|---|---|---|---|---|
| web30-mysql-1 | 0.44% | 12.8% | 4.9% | 5.58% | 6.08% | 5.94% |
| web30-redis-1 | 0.37% | 2.84% | 1.54% | 0.13% | 0.14% | 0.14% |
| web30-judge-1 | 3.65% | 63.75% | 25.13% | 1.76% | 3.38% | 2.64% |
| web30-api-1 | 8.14% | 42.91% | 25.02% | 1.95% | 2.39% | 2.23% |
| web30-web-1 | 8.76% | 43.07% | 23.9% | 1.24% | 1.25% | 1.25% |
- 평균 제출 처리 시간: 2.09s
- HTTP 요청 p95: 208ms
- 제출 성공률: 100%
| 서버 | CPU 최소 | CPU 최대 | CPU 평균 | MEM 최소 | MEM 최대 | MEM 평균 |
|---|---|---|---|---|---|---|
| web30-mysql-1 | 0.55% | 15.39% | 7.23% | 5.83% | 6.36% | 6.24% |
| web30-redis-1 | 0.38% | 3.94% | 1.86% | 0.14% | 0.15% | 0.15% |
| web30-judge-1 | 4.68% | 54.25% | 18.64% | 1.71% | 2.93% | 2.21% |
| web30-judge-2 | 5.24% | 46.13% | 21.74% | 1.62% | 3.21% | 2.23% |
| web30-judge-3 | 4.18% | 43.77% | 20.4% | 1.86% | 2.76% | 2.31% |
| web30-api-1 | 11.66% | 146.28% | 32.47% | 2.16% | 2.78% | 2.31% |
| web30-web-1 | 14.41% | 54.34% | 26.11% | 1.25% | 1.26% | 1.26% |
- 평균 제출 처리 시간: 2.33s
- HTTP 요청 p95: 622ms
- 제출 성공률: 100%
- 왜 replicas 3개가 느려보일까?
-
물리적 자원(CPU Core)의 포화와 컨텍스트 스위칭
- 환경: 4코어 / 8논리 프로세서
-
현상: 스냅샷 로그 기준,
submission컨테이너 9개가 각각 약 30~40%의 CPU를 점유. 여기에api,judge(3개),mysql,redis컨테이너 부하를 합산하면 물리적 코어의 한계를 즉시 초과함. - 원인: CPU 스레드 수(8개)보다 실행 중인 고부하 컨테이너 수(10개 이상)가 많아지면서, OS 레벨에서 컨텍스트 스위칭(Context Switching) 비용이 폭증함. 이로 인해 각 워커의 실제 연산 속도가 저하됨.
-
공유 자원(Docker Daemon)의 병목
-
원인: 모든
judge서버가 호스트의 단일 Docker Socket을 공유함. -
결과: Replicas 3개 환경에서 BullMQ가 동시에 15개(Concurrency 5 * 3)의 작업을 시도할 때, 도커 엔진이 컨테이너 생성/삭제 명령을 순차적으로 처리하며 병목이 발생.
submission컨테이너가 '대기' 상태에 머무는 시간이 길어지며 전체 타임아웃 발생.
-
원인: 모든
-
API 서버의 관리 오버헤드 (CPU 146%)
- 서버가 3대로 늘어남에 따라 API 서버가 관리해야 할 BullMQ 상태 업데이트, 로그 수집, DB 커넥션이 3배로 증가하여 단일 API 서버에 부하가 집중됨.
- Replicas 3개 환경에서도 시스템이 정상적으로 동작하며 부하 분산 구조 검증 완료
- 소규모 부하(5 VUs, 1분)에서는 replica 1개만으로도 충분
- 부하가 증가할 경우에만 replica 수를 늘려 확장 가능
-
응답 시간 증가: Replicas 3 환경에서 p95 지연이 약 3배 증가
→ 소규모 부하에서는 서버 간 통신 비용, 분산 락 경쟁, API 서버 컨텍스트 스위칭 비용이 실제 처리 이득보다 큼
- Replicas 1: 물리적 코어를 집중적으로 사용하여 응답 속도 최적화(p95: 208ms).
- Replicas 3: 자원 경합으로 인해 지연 시간 증가(p95: 622ms).
-
API 서버 부하 집중: API 서버의 최대 CPU 사용량이 146%까지 상승
→ 다수 워커 서버 관리에 따른 오버헤드 확인
앞선 테스트의 CPU 환경 한계를 극복하기 위해 concurrency를 2로 제한하여 진행하였습니다.
- 도구: k6 (Load Testing Tool)
- 프리셋: Small (5 VUs, 1분 지속)
- 대상: 단일 서버(Replicas 1) vs 다중 서버(Replicas 3) 환경 비교
- BullMQ Concurrency : 2
| 서버 | CPU 최소 | CPU 최대 | CPU 평균 | MEM 최소 | MEM 최대 | MEM 평균 |
|---|---|---|---|---|---|---|
| web30-mysql-1 | 0.51% | 7.45% | 2.48% | 6.34% | 6.34% | 6.34% |
| web30-redis-1 | 0.36% | 2.92% | 1.05% | 0.15% | 0.15% | 0.15% |
| web30-judge-1 | 4.25% | 35.73% | 14.25% | 1.77% | 2.22% | 2.05% |
| web30-api-1 | 9.28% | 42.34% | 20.15% | 2.39% | 2.42% | 2.41% |
| web30-web-1 | 16.78% | 47.62% | 28.16% | 1.31% | 1.32% | 1.31% |
- 평균 제출 처리 시간: 2.09s
- HTTP 요청 p95: 80ms
- 제출 성공률: 100%
| 서버 | CPU 최소 | CPU 최대 | CPU 평균 | MEM 최소 | MEM 최대 | MEM 평균 |
|---|---|---|---|---|---|---|
| web30-mysql-1 | 0.40% | 16.82% | 5.52% | 5.35% | 5.38% | 5.38% |
| web30-redis-1 | 0.42% | 2.95% | 1.49% | 0.16% | 0.17% | 0.17% |
| web30-judge-1 | 3.05% | 30.93% | 18.42% | 1.68% | 2.03% | 1.93% |
| web30-judge-2 | 3.13% | 49.92% | 21.35% | 1.66% | 1.99% | 1.86% |
| web30-judge-3 | 3.36% | 34.64% | 18.90% | 1.64% | 2.12% | 2.02% |
| web30-api-1 | 8.85% | 35.97% | 24.93% | 1.98% | 2.20% | 2.14% |
| web30-web-1 | 16.78% | 39.04% | 24.18% | 1.69% | 1.71% | 1.70% |
- 평균 제출 처리 시간: 2.11s
- HTTP 요청 p95: 167ms
- 제출 성공률: 100%
- 왜 replicas 3개가 상대적으로 느릴까?
-
물리적 자원(CPU Core) 활용과 컨텍스트 스위칭
- 환경: 4코어 / 8논리 프로세서
-
현상:
submission컨테이너 3개 + API, Web, MySQL, Redis 컨테이너 부하 합산 시, CPU 사용량이 스냅샷 기준 20~28%로 나타나지만, 실제 연산 중 컨텍스트 스위칭 비용 발생 - 결과: replicas 3개에서는 소규모 concurrency(2)에도 불구하고 컨테이너 간 CPU 경쟁으로 p95 지연 증가
-
공유 자원 병목
- Docker Socket 공유로 인해
judge컨테이너가 동시에 작업 시 컨테이너 생성/삭제 지연 발생 - 결과적으로
submission처리 대기 시간이 증가
- Docker Socket 공유로 인해
-
API 서버 관리 오버헤드
- replicas 3개 환경에서 API 서버가 관리할 BullMQ 상태, DB 커넥션 수 증가
- CPU 사용량 평균 24
25%, 최대 3536%로 상승
- 모든 제출 요청 정상 처리
- 제출 성공률 100%
- p95 지연은 Replicas 1개 기준 80ms, Replicas 3개 기준 167ms로 소규모 concurrency에서는 충분히 안정적
-
응답 시간 차이
- Replicas 1: p95 80ms → 물리적 자원 집중으로 응답 속도 최적화
- Replicas 3: p95 167ms → 자원 경합 및 컨텍스트 스위칭 비용 증가
-
CPU 사용량
- Replicas 3 환경에서도 각 컨테이너 평균 CPU는 크게 증가하지 않음(18~25%)
- 하지만 여러 워커와 API 서버 관리 오버헤드로 인해 응답 지연이 발생
- OAuth 로그인 구현
- 매칭 시스템
- 실시간 관전 채팅 & 시스템 알림
- Docker 기반의 코드 실행 환경 개발
- NCP 배포 및 CI/CD, HTTPS 적용
- SSH 무차별 대입(Bruteforce) 공격에 대한 보안 강화
- 트래픽 확장을 고려한 서버 역할 분리
- Docker 기반 코드 실행 보안
- 이벤트 기반 비동기 통신
- 채점 서버 스케일 아웃 성능 분석