최대 실행 스레드 수 제한(max_workers) - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 테스트 목적

a. 가정

  • CPU 코어 수보다 너무 많은 스레드가 실행되면 문맥 전환 비용이 증가하고 전체 처리 효율이 저하될 수 있다.
  • ThreadPoolExecutormax_workers는 병렬로 실행할 수 있는 태스크 수를 제한하며, CPU 코어 수에 비례해 증가시키면 더 많은 작업을 동시에 처리할 수 있다.
  • max_workers의 증가에 따른 성능 변화(처리 시간, 리소스 사용률)를 관찰함으로써, 최적의 동시 처리 수를 찾을 수 있다.

b. 예상 결과

  • max_workers가 CPU 코어 수(4)에 가까울 땐 안정적이지만, 처리량이 제한될 수 있다.
  • max_workers를 2~4배 수준까지 확장하면 처리량이 증가하고, 일정 지점 이후엔 지연 시간 증가 및 CPU/메모리 과부하 현상이 나타날 것으로 예상된다.
  • 태스크별 duration 분석을 통해 이후 세마포어 제한 기준도 설정 가능할 것이다.

2. 테스트 설계

a. 도구

  • 부하 테스트 도구: Grafana k6
  • 서버 실행 환경: GCP VM (n2-standard-4: 4 vCPUs, 16 GB RAM)
  • 요청 처리 구조: loop.run_in_executor() 사용, ThreadPoolExecutor(max_workers=N)으로 스레드 수 제어

b. 방식

  • max_workers 값을 CPU 코어 수의 배수로 설정하며 실험 진행

    예: max_workers = 4, 8, 12, 16

  • 각 실험에서 고정된 테스트 시나리오 실행:

    • VU 수: 10, 15, 20, 25, 30
    • 요청 흐름:
      1. /embedding 요청 (10장 이미지)
      2. 성공 시 /categories, /quality, /duplicates 병렬 요청
      3. /categories 응답으로 /score 요청
  • 메트릭 수집:

    • 각 태스크별 duration (avg, p95, max)
    • 전체 처리 시간 (total_duration)
    • HTTP 실패율
    • GCP Monitoring 통한 CPU/RAM 사용률

3. 테스트 결과

  • 설정 전

    VUs Embedding(avg / p95 / max) Categories (avg / p95 / max) Duplicates (avg / p95 / max) Quality (avg / p95 / max) Score (avg / p95 / max) Total (avg / p95 / max) http_req_failed
    10 3.71s / 5.06s / 5.95s 336ms / 1.15s / 2.61s 342ms / 1.51s / 2.68s 362ms / 1.51s / 2.64s 163ms / 472ms / 523ms 5.15s / 6.97s / 10.75s 1.33%
    15 6.16s / 9.11s / 9.21s 2.05s / 4.56s / 5.07s 2.03s / 4.51s / 5.09s 2.04s / 4.53s / 5.05s 233ms / 613ms / 753ms 12.59s / 18.24s / 19.33s 0.24%
    20 7.30s / 9.86s / 11.41s 2.80s / 5.80s / 6.61s 2.79s / 5.75s / 6.57s 2.79s / 5.76s / 6.57s 343ms / 623ms / 699ms 16.09s / 22.48s / 24.48s 0.19%
    25 9.18s / 13.65s / 14.80s 4.24s / 8.35s / 8.69s 4.15s / 8.32s / 8.69s 4.17s / 8.34s / 8.73s 293ms / 475ms / 514ms 22.04s / 30.89s / 31.52s 0%
    30 10.39s / 16.54s / 17.14s 5.78s / 11.06s / 11.39s 5.72s / 11.03s / 11.38s 5.74s / 11.04s / 11.42s 299ms / 445ms / 497ms 28.17s / 39.47s / 40.23s 0.44%
  • max_workers = 4

    VUs Embedding (avg / p95 / max) Categories (avg / p95 / max) Duplicates (avg / p95 / max) Quality (avg / p95 / max) Score (avg / p95 / max) Total (avg / p95 / max) http_req_failed
    10 3.34s / 4.82s / 4.85s 0.81s / 1.15s / 1.91s 0.80s / 1.51s / 1.94s 0.82s / 1.91s / 1.92s 86ms / 161ms / 171ms 5.86s / 7.83s / 7.86s 0.00%
    15 4.63s / 7.17s / 7.21s 1.68s / 3.92s / 3.94s 1.66s / 3.89s / 3.90s 1.67s / 3.89s / 3.91s 207ms / 351ms / 355ms 9.85s / 14.06s / 14.06s 0.00%
    20 6.17s / 9.72s / 9.81s 3.04s / 6.50s / 6.51s 3.03s / 6.50s / 6.55s 3.04s / 6.52s / 6.52s 261ms / 503ms / 518ms 15.53s / 22.36s / 22.40s 0.00%
    25 7.22s / 11.79s / 11.91s 4.06s / 8.57s / 8.61s 4.06s / 8.58s / 8.60s 4.06s / 8.59s / 8.60s 373ms / 698ms / 709ms 19.77s / 28.70s / 28.70s 0.00%
    30 8.49s / 13.98s / 14.09s 5.75s / 11.44s / 11.56s 5.73s / 11.43s / 11.59s 5.70s / 11.44s / 11.55s 420ms / 619ms / 667ms 26.10s / 37.40s / 37.61s 0.00%
  • max_workers = 8

    VUs Embedding (avg / p95 / max) Categories (avg / p95 / max) Duplicates (avg / p95 / max) Quality (avg / p95 / max) Score (avg / p95 / max) Total (avg / p95 / max) http_req_failed
    10 4.30s / 5.10s / 5.10s 193ms / 347ms / 360ms 192ms / 347ms / 351ms 191ms / 360ms / 376ms 98ms / 193ms / 202ms 4.97s / 5.23s / 5.24s 0.00%
    15 5.39s / 7.21s / 7.30s 1.13s / 2.27s / 2.27s 1.14s / 2.31s / 2.33s 1.13s / 2.31s / 2.31s 347ms / 934ms / 1.01s 9.15s / 11.11s / 11.11s 0.00%
    20 6.68s / 9.67s / 9.68s 2.38s / 4.64s / 4.76s 2.38s / 4.63s / 4.75s 2.38s / 4.65s / 4.76s 409ms / 992ms / 1.11s 14.23s / 18.55s / 18.83s 0.00%
    25 8.00s / 11.87s / 12.16s 3.66s / 7.07s / 7.21s 3.65s / 7.13s / 7.18s 3.66s / 7.08s / 7.20s 323ms / 886ms / 997ms 19.29s / 25.99s / 26.17s 0.00%
    30 9.42s / 14.48s / 14.62s 4.74s / 8.84s / 9.37s 4.73s / 8.88s / 9.38s 4.73s / 8.85s / 9.37s 465ms / 1.72s / 1.76s 24.09s / 32.10s / 33.36s 0.00%
  • max_workers = 12

    VUs Embedding (avg / p95 / max) Categories (avg / p95 / max) Duplicates (avg / p95 / max) Quality (avg / p95 / max) Score (avg / p95 / max) Total (avg / p95 / max) http_req_failed
    10 4.88s / 5.12s / 5.12s 219ms / 398ms / 412ms 209ms / 393ms / 402ms 191ms / 403ms / 421ms 94ms / 228ms / 248ms 5.59s / 5.89s / 5.92s 0.00%
    15 6.25s / 7.41s / 7.49s 618ms / 1.62s / 1.67s 592ms / 1.57s / 1.58s 618ms / 1.59s / 1.67s 225ms / 504ms / 507ms 8.30s / 10.02s / 10.08s 0.00%
    20 7.52s / 9.80s / 9.81s 1.08s / 2.32s / 2.33s 1.06s / 2.28s / 2.29s 1.09s / 2.33s / 2.35s 511ms / 1.17s / 1.26s 11.26s / 13.37s / 13.44s 0.00%
    25 8.88s / 12.07s / 12.37s 2.79s / 5.89s / 6.28s 2.80s / 5.84s / 5.85s 2.84s / 5.87s / 6.16s 319ms / 962ms / 1.03s 17.63s / 23.52s / 23.68s 0.00%
    30 10.33s / 14.72s / 14.75s 3.82s / 7.53s / 7.57s 3.80s / 7.60s / 7.72s 3.82s / 7.56s / 7.62s 577ms / 1.87s / 1.89s 22.35s / 29.69s / 29.91s 0.00%
  • max_workers = 16

    VUs Embedding (avg / p95 / max) Categories (avg / p95 / max) Duplicates (avg / p95 / max) Quality (avg / p95 / max) Score (avg / p95 / max) Total (avg / p95 / max) http_req_failed
    10 4.89s / 5.08s / 5.09s 220ms / 374ms / 381ms 198ms / 365ms / 379ms 195ms / 373ms / 374ms 99ms / 255ms / 377ms 5.60s / 5.87s / 5.88s 0.00%
    15 6.71s / 7.43s / 7.44s 332ms / 572ms / 636ms 300ms / 568ms / 604ms 317ms / 676ms / 715ms 199ms / 446ms / 481ms 7.86s / 8.23s / 8.30s 0.00%
    20 8.19s / 9.88s / 9.90s 874ms / 1.67s / 2.10s 909ms / 1.92s / 2.14s 885ms / 1.81s / 2.10s 422ms / 979ms / 986ms 11.28s / 12.25s / 12.55s 0.00%
    25 9.61s / 12.26s / 12.29s 1.48s / 3.18s / 3.25s 1.49s / 3.18s / 3.28s 1.48s / 3.17s / 3.31s 799ms / 2.00s / 2.02s 14.87s / 17.52s / 17.71s 0.00%
    30 11.17s / 14.86s / 14.95s 2.02s / 4.18s / 4.56s 2.01s / 4.16s / 4.45s 1.99s / 4.06s / 4.30s 1.15s / 3.34s / 3.39s 18.34s / 21.65s / 22.85s 0.00%

output.png


4. 결과 분석

a. embeddingmax_workers=4일 때 가장 빠르지만, 다른 작업들은 월등히 느림

  • max_workers = 4 대비 처리 시간 증가율(VUs = 10 / 20/ 30 기준)
작업 항목 max_workers=4 max_workers=8 max_workers=12 max_workers=16
Embedding 3.34 / 4.63 / 6.17 4.30 (+29%) / 5.39 (+16%) / 6.68 (+8%) 4.88 (+46%) / 6.25 (+35%) / 7.52 (+22%) 4.89 (+46%) / 6.71 (+45%) / 8.19 (+33%)
Categories 0.81 / 1.68 / 3.04 0.19 (−77%) / 1.13 (−33%) / 2.38 (−22%) 0.22 (−73%) / 0.62 (−63%) / 1.48 (−51%) 0.22 (−73%) / 0.33 (−80%) / 0.87 (−71%)
Duplicate 0.80 / 1.67 / 3.03 0.19 (−76%) / 1.14 (−32%) / 2.38 (−21%) 0.21 (−74%) / 0.59 (−65%) / 1.49 (−51%) 0.20 (−75%) / 0.30 (−82%) / 0.91 (−70%)
Quality 0.82 / 1.67 / 3.04 0.19 (−77%) / 1.13 (−32%) / 2.38 (−22%) 0.19 (−77%) / 0.62 (−63%) / 1.48 (−51%) 0.19 (−77%) / 0.33 (−80%) / 0.88 (−71%)
Score 0.16 / 0.23 / 0.26 0.10 (−38%) / 0.35 (+52%) / 0.41 (+58%) 0.09 (−44%) / 0.22 (−4%) / 0.42 (+62%) 0.10 (−38%) / 0.20 (−13%) / 0.42 (+62%)
  • embedding은 무거운 연산(CPU-bound)이고, max_workers=4는 정확히 CPU 코어 수에 맞추어 연산이 이루어지므로 스레드 경쟁이 가장 적은 것으로 추측
  • 하지만 max_workers=4에서는 embedding이 모든 스레드를 독점하므로, 후속 작업(categories, quality 등)은 처리되지 못하고 대기 → 다음 작업들의 응답이 지연
  • 다만 VU가 증가할수록 Score 작업은 다시 증가하는 경향이 나타남 (예: max_workers=12에서 Score가 0.42s → +62%).

b. VUs가 max_workers보다 많을 때 급격한 성능 저하가 발생

  • max_workers=16 기준 변화량

    작업 VU=10 → 15 변화량 VU=15 → 20 변화량
    embedding +37% (4.89 → 6.71s) +22% (6.71 → 8.19s)
    categories +50% (0.22 → 0.33s) +164% (0.33 → 0.87s)
    quality +74% (0.19 → 0.33s) +167% (0.33 → 0.88s)
    score +100% (0.10 → 0.20s) +110% (0.20 → 0.42s)
  • embedding을 제외하고는 VU = 10 → 15 지점보다, VU = 15 → 20 지점에서 더 큰 증가량 보임

    • 특히 categories, quality, scoreVU=15 → 20에서 급격히 2~3배 이상 증가병목 지점 확인 가능.
  • VUs가 max_workers를 초과하면:

    • 스레드 풀에서 실행 대기중인 작업이 Queue에 적재되며 대기 시간이 발생합니다.
    • 각 작업의 처리 시간 증가폭이 커지고, 특히 score처럼 가벼운 작업조차 느려짐 → 전반적인 응답 시간 악화.

5. 인사이트 요약

a. embedding은 max_workers=4에서 가장 빠르지만, 전체 처리 효율은 낮다

  • embedding 평균 소요 시간(VU=10 기준)은 3.34s (max_workers=4)로 가장 낮음.
  • 그러나 같은 조건에서 categories, duplicates, quality는 모두 0.8s 이상, score0.16s 소요됨.
  • 반면 max_workers=8~16에서는 이들 작업이 대부분 0.2s 이하로 줄어듦.
  • 해석 : max_workers=4는 모든 스레드를 embedding에만 할당하면서 후속 작업이 블로킹됨 → 전체적인 응답 시간 지연.

b. max_workers가 8 이상부터 embedding 외 작업의 병목이 크게 해소됨

  • 예: categories (VU=10 기준)
    • max_workers=4: 0.81s
    • max_workers=8~16: 0.19~0.22s (최대 77% 감소)
  • duplicates, quality에서도 동일한 패턴 → embedding 이후 병렬 작업이 동시에 처리 가능해짐
  • 이로 인해 전체 평균 total_durationmax_workers=8 이상에서 더 안정화됨.

c. VU > max_workers일 때, 후속 작업들에 급격한 병목 발생

  • 예: max_workers=16, VU=15 → 20
    • categories: +164% 증가 (0.33s → 0.87s)
    • quality: +167% 증가 (0.33s → 0.88s)
    • score: +110% 증가 (0.20s → 0.42s)
  • 이는 스레드 수보다 많은 요청이 동시에 들어왔을 때, 남은 작업이 queue에 밀려 대기시간이 급증한다는 것을 의미함.
  • 특히 가벼운 작업(score)조차 2배 이상 느려지는 현상은 병목이 명확히 나타나는 증거.

6. 적절한 max_workers와 세마포어 설정에 대한 제안

a. max_workers는 8 또는 12가 적절

  • 실험 결과, max_workers=812 구간에서 전체적인 처리 시간이 안정적이며, 병목 현상이 상대적으로 적게 발생함.
  • max_workers=4는 embedding 처리 시간은 가장 빠르지만, 후속 작업들이 스레드를 대기하면서 전체 처리 시간이 길어짐.
  • 반면 max_workers=16은 과도한 동시 실행으로 인해 각 작업의 처리 시간이 급증하며 효율 저하가 발생함.
  • 따라서 전체적인 처리량과 안정성의 균형을 고려할 때 max_workers=8 또는 12가 가장 적합하다고 판단됨.

b. embedding 세마포어는 4로 설정하는 것이 가장 효율적

  • embedding은 연산 집약적인 CPU-bound 작업으로, 내부적으로 다중 스레드를 사용하는 구조이기 때문에 너무 많은 개수를 동시에 실행하면 오히려 성능 저하가 발생.

  • 실험 결과, embedding 작업은 최대 4개까지 동시에 실행할 때 가장 빠름 (예: max_workers=4에서 평균 3.34초).

  • 코어 수(4개)를 초과해서 embedding을 병렬 실행할 경우, context switching 등으로 인해 오히려 처리 속도가 느려짐.

    → 따라서 embedding 작업에 대해서는 세마포어를 4로 제한하는 것이 가장 이상적이며, 이는 CPU 리소스를 적절히 활용하면서도 다른 작업들의 처리를 방해하지 않게 함.