Skip to content

scale out

eeekeee edited this page Mar 5, 2026 · 1 revision

채점 서버 스케일 아웃 성능 분석 보고서


핵심 요약 및 인사이트

본 보고서는 채점 서버의 확장성(Scalability) 검증을 위해 진행된 부하 테스트 결과를 바탕으로, 인프라 자원 한계에 따른 성능 병목 현상을 분석하고 이를 해결하기 위한 최적화 과정을 담고 있습니다.

1. 핵심 분석 결과

  • 다중 인스턴스 환경에서 발생하던 타임아웃 및 지연 현상을 Concurrency 튜닝을 통해 해결
  • 8논리 프로세서 환경에서 15개의 동시 채점 시 발생하는 CPU 자원 경합 및 컨텍스트 스위칭이 주원인
  • 각 워커의 Concurrency를 2로 제한(전체 동시성 6)한 결과:
    • p95 응답 지연 시간: 622ms → 167ms (약 73% 개선)
  • 단일 서버(Replicas 1) 환경에서 관리 오버헤드 최소, 가장 빠른 응답 속도 확인
  • 다중 서버(Replicas 3) 환경에서도 부하가 균등하게 분산됨 → 클라우드 환경 이전 시 강력한 확장성 기대

2. 주요 인사이트

  • 소규모 부하에서는 Replicas 1로 충분하며, 불필요한 분산은 오히려 지연 증가
  • CPU 자원 제한 환경에서는 다중 워커를 동시에 실행하면 컨텍스트 스위칭 오버헤드로 처리 속도 감소
  • Concurrency 제어를 통해 서버 간 오버헤드 최소화 및 안정적인 응답 확보 가능
  • 클라우드 환경에서는 물리적 코어 충분 시 Replicas 확장으로 높은 처리량 확보 가능

3. 배포 환경 시사점

  • Auto-scaling은 물리적 자원이 충분할 때 유효
    • CPU/메모리 여유가 있는 클라우드 환경이라면, 부하 증가 시 Replicas를 자동으로 늘려 처리량 확보 가능
  • 로컬처럼 CPU가 제한된 환경에서는 무작정 늘리는 것보다 Concurrency 제어 + 필요 시 Auto-scaling이 최적

최종 배포 관련 AI 제안

1️⃣ NCP에서의 Replicas vs 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 늘려야 함

2️⃣ 로컬/도커 컴포즈 vs 클라우드 배포

  • 도커 컴포즈
    • 배포 환경에서도 사용할 수 있음
    • 장점: 설정 단순, 환경 일관성 유지
    • 단점: 자동 스케일링 지원 없음
      • CPU가 꽉 차거나 트래픽 폭주 시 수동으로 컨테이너 늘려야 함
      • 로컬 테스트처럼 CPU 경쟁으로 오버헤드 발생 가능
  • 클라우드 환경에서 안정적 확장
    • NCP Container Service(Kubernetes 기반)나 ECS/Fargate 같은 서비스 사용 시
      • HPA나 Auto-Scaling 그룹으로 자동 확장 가능
      • CPU/Mem 기준으로 pod(replica)를 늘리면서 안정적 처리
    • Concurrency 제한 + Auto-Scaling 조합이 가장 효율적

3️⃣ 결론 / 배포 환경 전략

  • 도커 컴포즈로만 운영 → CPU가 한정된 서버에서는 replicas 늘리는 게 항상 성능 향상으로 이어지지 않음
  • Auto-Scaling 활용 가능 환경 → replicas를 자동으로 늘려 부하 대응 가능, 단 클라우드 환경이어야 함
  • 권장 패턴
    • Concurrency 제한 → CPU 경쟁 최소화
    • 필요 시 replicas/Auto-Scaling → 트래픽 증가에 대응

아래는 테스트 결과입니다.

1. 테스트 환경 및 시나리오

  • 도구: k6 (Load Testing Tool)
  • 프리셋: Small (5 VUs, 1분 지속)
  • 대상: 단일 서버(Replicas 1) vs 다중 서버(Replicas 3) 환경 비교
  • BullMQ Concurrency : 5

2. 테스트 결과 요약

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

2.1 Replicas 1개

서버 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%

2.2 Replicas 3개

서버 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%

3. 리소스 모니터링 분석

  • 왜 replicas 3개가 느려보일까?
  1. 물리적 자원(CPU Core)의 포화와 컨텍스트 스위칭
    • 환경: 4코어 / 8논리 프로세서
    • 현상: 스냅샷 로그 기준, submission 컨테이너 9개가 각각 약 30~40%의 CPU를 점유. 여기에 api, judge(3개), mysql, redis 컨테이너 부하를 합산하면 물리적 코어의 한계를 즉시 초과함.
    • 원인: CPU 스레드 수(8개)보다 실행 중인 고부하 컨테이너 수(10개 이상)가 많아지면서, OS 레벨에서 컨텍스트 스위칭(Context Switching) 비용이 폭증함. 이로 인해 각 워커의 실제 연산 속도가 저하됨.
  2. 공유 자원(Docker Daemon)의 병목
    • 원인: 모든 judge 서버가 호스트의 단일 Docker Socket을 공유함.
    • 결과: Replicas 3개 환경에서 BullMQ가 동시에 15개(Concurrency 5 * 3)의 작업을 시도할 때, 도커 엔진이 컨테이너 생성/삭제 명령을 순차적으로 처리하며 병목이 발생. submission 컨테이너가 '대기' 상태에 머무는 시간이 길어지며 전체 타임아웃 발생.
  3. API 서버의 관리 오버헤드 (CPU 146%)
    • 서버가 3대로 늘어남에 따라 API 서버가 관리해야 할 BullMQ 상태 업데이트, 로그 수집, DB 커넥션이 3배로 증가하여 단일 API 서버에 부하가 집중됨.

4. 결과 분석 및 인사이트

✅ 긍정적 결과: 분산 구조 검증

  • 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%까지 상승

    → 다수 워커 서버 관리에 따른 오버헤드 확인


BullMQ Concurrency 2 테스트

앞선 테스트의 CPU 환경 한계를 극복하기 위해 concurrency를 2로 제한하여 진행하였습니다.

5. 테스트 환경 및 시나리오 (Concurrency 2)

  • 도구: k6 (Load Testing Tool)
  • 프리셋: Small (5 VUs, 1분 지속)
  • 대상: 단일 서버(Replicas 1) vs 다중 서버(Replicas 3) 환경 비교
  • BullMQ Concurrency : 2

6. 테스트 결과 요약 (Concurrency 2)

6.1 Concurrency 2, Replicas 1개

서버 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%

6.2 Concurrency 2, Replicas 3개

서버 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%

7. 리소스 모니터링 분석 (Concurrency 2)

  • 왜 replicas 3개가 상대적으로 느릴까?
  1. 물리적 자원(CPU Core) 활용과 컨텍스트 스위칭
    • 환경: 4코어 / 8논리 프로세서
    • 현상: submission 컨테이너 3개 + API, Web, MySQL, Redis 컨테이너 부하 합산 시, CPU 사용량이 스냅샷 기준 20~28%로 나타나지만, 실제 연산 중 컨텍스트 스위칭 비용 발생
    • 결과: replicas 3개에서는 소규모 concurrency(2)에도 불구하고 컨테이너 간 CPU 경쟁으로 p95 지연 증가
  2. 공유 자원 병목
    • Docker Socket 공유로 인해 judge 컨테이너가 동시에 작업 시 컨테이너 생성/삭제 지연 발생
    • 결과적으로 submission 처리 대기 시간이 증가
  3. API 서버 관리 오버헤드
    • replicas 3개 환경에서 API 서버가 관리할 BullMQ 상태, DB 커넥션 수 증가
    • CPU 사용량 평균 2425%, 최대 3536%로 상승

8. 결과 분석 및 인사이트 (Concurrency 2)

✅ 긍정적 결과: 안정적인 처리

  • 모든 제출 요청 정상 처리
  • 제출 성공률 100%
  • p95 지연은 Replicas 1개 기준 80ms, Replicas 3개 기준 167ms로 소규모 concurrency에서는 충분히 안정적

⚠️ 관찰 사항: 소규모 부하 시의 오버헤드

  • 응답 시간 차이
    • Replicas 1: p95 80ms → 물리적 자원 집중으로 응답 속도 최적화
    • Replicas 3: p95 167ms → 자원 경합 및 컨텍스트 스위칭 비용 증가
  • CPU 사용량
    • Replicas 3 환경에서도 각 컨테이너 평균 CPU는 크게 증가하지 않음(18~25%)
    • 하지만 여러 워커와 API 서버 관리 오버헤드로 인해 응답 지연이 발생

Clone this wiki locally