Node.js 에서 멀티코어를 활용하자 - 3 - 기본 Cluster

728x90

https://sejin-technology.tistory.com/130
앞글 보고 오도록 한다

일단 서버 시작점에서 부터 봐보자

다음처럼 준비하는데, 나는 도커 기반으로 서버를 작업했기 때문에 다음처럼 나온다.

혹시 코드필요한 사람은 아래 댓글 달아주면 링크보내준다

server.listen(port, () => {

const workerId = cluster.worker ? cluster.worker.id : 'single';

const mode = useCluster ? 'cluster' : 'single process';



console.log(`=== Worker ${workerId} Started ===`);

console.log(`Mode: ${mode}`);

console.log(`Process ID: ${process.pid}`);

console.log(`Listening on port: ${port}`);

if (!useCluster) {

console.log(`CPU cores available: ${numCPUs}`);

console.log(`CPU cores in use: 1 (single process mode)`);

}

console.log(`Swagger UI: http://localhost:${port}/docs`);

console.log(`==============================`);

});

이런식으로 클러스터 모드를 끄면 CPU에 대한 Core를 하나만 사용하게 된다.

 docker compose -f docker-compose.dev.yml run -e CLUSTER_MODE=false backend


=== Worker single Started ===
Mode: single process
Process ID: 18
Listening on port: 8080
CPU cores available: 12
CPU cores in use: 1 (single process mode)
Swagger UI: http://localhost:8080/docs
==============================
[Worker single] Client connected: V6GUekbZGMaxaDIXAAAC
[Worker single] Client connected: Z20HVclWwJzP9GHAAAAD

멀티코어는 ?

docker compose -f docker-compose.dev.yml run -e CLUSTER_MODE=true backend


> video-creater@1.0.0 start
> node server.js

=== Master Process Started ===
Process ID: 18
CPU cores available: 12
Forking 12 workers...
==============================
Worker 2 (PID: 26) is online
Worker 3 (PID: 27) is online
Worker 4 (PID: 28) is online
Worker 1 (PID: 25) is online
Worker 5 (PID: 29) is online
Worker 8 (PID: 42) is online
Worker 9 (PID: 48) is online
Worker 10 (PID: 54) is online
Worker 7 (PID: 37) is online
Worker 6 (PID: 30) is online
Worker 12 (PID: 72) is online
Worker 11 (PID: 55) is online
=== Worker 2 Started ===
Mode: cluster
Process ID: 26
Listening on port: 8080
Swagger UI: http://localhost:8080/docs
==============================
=== Worker 4 Started ===
Mode: cluster
Process ID: 28
Listening on port: 8080
Swagger UI: http://localhost:8080/docs
==============================
=== Worker 6 Started ===
Mode: cluster
Process ID: 30
Listening on port: 8080
Swagger UI: http://localhost:8080/docs
==============================
=== Worker 1 Started ===
Mode: cluster
Process ID: 25
Listening on port: 8080
Swagger UI: http://localhost:8080/docs
==============================
=== Worker 10 Started ===
Mode: cluster
Process ID: 54
Listening on port: 8080
Swagger UI: http://localhost:8080/docs
==============================
=== Worker 3 Started ===
=== Worker 9 Started ===
Mode: cluster
Process ID: 48
Listening on port: 8080
Swagger UI: http://localhost:8080/docs
==============================

이렇게 나오게된다. 그러면 실제로 차이가 나는지 봐보자

테스트 코드 준비

  1. 일단 테스트용으로 부하를 줄 수 있는 코드를 작성

테스트 대상 - 파보나치 계산



// ============================================

// CPU 부하 테스트용 엔드포인트

// 싱글 vs 클러스터 성능 비교용

// ============================================

const cluster = require('cluster');



// 피보나치 계산 (CPU 집약적)

function fibonacci(n) {

if (n <= 1) return n;

return fibonacci(n - 1) + fibonacci(n - 2);

}



// 1. CPU 부하 테스트 - 피보나치

// 사용법: GET /api/test/cpu/fibonacci?n=40

router.get('/cpu/fibonacci', (req, res) => {

const n = parseInt(req.query.n) || 40;

const workerId = cluster.worker ? cluster.worker.id : 'single';



console.log(`[Worker ${workerId}] Fibonacci(${n}) 계산 시작...`);

const startTime = Date.now();



const result = fibonacci(n);



const duration = Date.now() - startTime;

console.log(`[Worker ${workerId}] Fibonacci(${n}) 완료: ${duration}ms`);



res.json({

worker: workerId,

pid: process.pid,

operation: `fibonacci(${n})`,

result: result,

duration: `${duration}ms`

});

});

이러면 이제 재귀방식으로 파보나치를 계산하게 되는데

피보나치 재귀 - 빅오                                                           

  function fibonacci(n) {                                                        
      if (n <= 1) return n;                                                      
      return fibonacci(n - 1) + fibonacci(n - 2);                                
  }                                                                              

  시간 복잡도: O(2ⁿ)                                                             

  왜냐면:                                                                        
  fib(5)                                                                         
  ├── fib(4)                                                                     
  │   ├── fib(3)                                                                 
  │   │   ├── fib(2)                                                             
  │   │   └── fib(1)                                                             
  │   └── fib(2)                                                                 
  └── fib(3)                                                                     
      ├── fib(2)                                                                 
      └── fib(1)                                                                 

  매번 2개씩 분기 → 지수적 증가                                                  

  ---                                                                            
  n별 예상 시간                                                                  
  ┌─────┬──────────────────┬────────────┐                                        
  │  n  │ 호출 횟수 (대략) │ 예상 시간  │                                        
  ├─────┼──────────────────┼────────────┤                                        
  │ 30  │ 약 100만         │ ~10ms      │                                        
  ├─────┼──────────────────┼────────────┤                                        
  │ 40  │ 약 10억          │ ~1~2초     │                                        
  ├─────┼──────────────────┼────────────┤                                        
  │ 45  │ 약 300억         │ ~30초      │                                        
  ├─────┼──────────────────┼────────────┤                                        
  │ 50  │ 약 1조           │ ~10분 이상 │                                        
  └─────┴──────────────────┴────────────┘                                        
  ---                                         

이렇게 준비를 했다.

당연히 하나씩만 보내면 코어 하나씩만쓰니까 차이가 없다.
그래서 여러개를 동시에보내서 평균값을 계산할 쉘스크립트를 하나 준비했다

./load-test.sh --concurrent 12

이렇게서 실행해서 12개를 동시에 날리게되니까 테스트 해보면 된다
안에 URL은 맞춰서 알아서 고쳐쓰면되고

#!/bin/bash



# ============================================

# CPU 부하 테스트 스크립트

# 싱글 vs 클러스터 성능 비교용

# ============================================



# 기본값

URL="http://localhost/api/test/cpu/fibonacci"

N=45

CONCURRENT=4



# 사용법 출력

usage() {

echo "사용법: $0 [옵션]"

echo ""

echo "옵션:"

echo " -c, --concurrent NUM 동시 요청 수 (기본: 4)"

echo " -n, --fib NUM 피보나치 n값 (기본: 45)"

echo " -u, --url URL 테스트 URL (기본: http://localhost/api/test/cpu/fibonacci)"

echo " -h, --help 도움말"

echo ""

echo "예시:"

echo " $0 -c 8 -n 40 # 동시 8개 요청, fib(40)"

echo " $0 --concurrent 12 # 동시 12개 요청"

exit 1

}



# 인자 파싱

while [[ $# -gt 0 ]]; do

case $1 in

-c|--concurrent)

CONCURRENT="$2"

shift 2

;;

-n|--fib)

N="$2"

shift 2

;;

-u|--url)

URL="$2"

shift 2

;;

-h|--help)

usage

;;

*)

echo "알 수 없는 옵션: $1"

usage

;;

esac

done



echo "============================================"

echo "CPU 부하 테스트 시작"

echo "============================================"

echo "URL: ${URL}?n=${N}"

echo "동시 요청 수: ${CONCURRENT}"

echo "============================================"

echo ""



# 밀리초 타임스탬프 함수 (macOS 호환)

get_ms() {

if [[ "$OSTYPE" == "darwin"* ]]; then

# macOS: perl 사용

perl -MTime::HiRes=time -e 'printf "%.0f\n", time * 1000'

else

# Linux: date 사용

date +%s%3N

fi

}



# 임시 파일 디렉토리

TEMP_DIR=$(mktemp -d)

trap "rm -rf $TEMP_DIR" EXIT



# 전체 시작 시간

TOTAL_START=$(get_ms)



# 동시 요청 실행

for i in $(seq 1 $CONCURRENT); do

(

START=$(get_ms)

RESPONSE=$(curl -s "${URL}?n=${N}")

END=$(get_ms)



WORKER=$(echo $RESPONSE | grep -o '"worker":[^,]*' | cut -d':' -f2 | tr -d '"')

PID=$(echo $RESPONSE | grep -o '"pid":[^,]*' | cut -d':' -f2)

DURATION=$(echo $RESPONSE | grep -o '"duration":"[^"]*"' | cut -d'"' -f4)



ACTUAL_DURATION=$((END - START))



echo "${i}|${WORKER}|${PID}|${DURATION}|${ACTUAL_DURATION}" > "${TEMP_DIR}/result_${i}.txt"

) &

done



# 모든 백그라운드 작업 대기

wait



# 전체 종료 시간

TOTAL_END=$(get_ms)

TOTAL_DURATION=$((TOTAL_END - TOTAL_START))



echo "결과:"

echo "--------------------------------------------"

printf "%-4s | %-8s | %-8s | %-12s | %-12s\n" "No" "Worker" "PID" "서버 응답" "실제 소요"

echo "--------------------------------------------"



# 결과 수집 및 출력

TOTAL_SERVER_TIME=0

TOTAL_ACTUAL_TIME=0

COUNT=0



for i in $(seq 1 $CONCURRENT); do

if [[ -f "${TEMP_DIR}/result_${i}.txt" ]]; then

IFS='|' read -r NO WORKER PID DURATION ACTUAL < "${TEMP_DIR}/result_${i}.txt"

printf "%-4s | %-8s | %-8s | %-12s | %-12s\n" "$NO" "$WORKER" "$PID" "$DURATION" "${ACTUAL}ms"



# 숫자만 추출해서 합산

SERVER_MS=$(echo $DURATION | tr -dc '0-9')

if [[ -n "$SERVER_MS" ]]; then

TOTAL_SERVER_TIME=$((TOTAL_SERVER_TIME + SERVER_MS))

fi

TOTAL_ACTUAL_TIME=$((TOTAL_ACTUAL_TIME + ACTUAL))

COUNT=$((COUNT + 1))

fi

done



echo "--------------------------------------------"

echo ""

echo "============================================"

echo "통계"

echo "============================================"



if [[ $COUNT -gt 0 ]]; then

AVG_SERVER=$((TOTAL_SERVER_TIME / COUNT))

AVG_ACTUAL=$((TOTAL_ACTUAL_TIME / COUNT))



echo "총 요청 수: ${COUNT}"

echo "전체 소요 시간: ${TOTAL_DURATION}ms"

echo ""

echo "서버 응답 시간 (평균): ${AVG_SERVER}ms"

echo "실제 소요 시간 (평균): ${AVG_ACTUAL}ms"

echo ""

echo "--------------------------------------------"

echo "분석:"

echo "--------------------------------------------"



# 싱글 vs 클러스터 판단

EXPECTED_SINGLE=$((AVG_SERVER * COUNT))

EFFICIENCY=$(echo "scale=2; $EXPECTED_SINGLE / $TOTAL_DURATION" | bc)



echo "싱글 모드 예상 시간: ${EXPECTED_SINGLE}ms (순차 처리)"

echo "실제 전체 시간: ${TOTAL_DURATION}ms"

echo "병렬 효율: ${EFFICIENCY}x"

echo ""



if (( $(echo "$EFFICIENCY > 1.5" | bc -l) )); then

echo "=> 클러스터 모드로 병렬 처리되고 있습니다!"

else

echo "=> 싱글 모드이거나 워커 수가 부족합니다."

fi

fi



echo "============================================"

실제 테스트

싱글코어는 예상 시간: 69864ms (순차 처리) , 실제 전체 시간: 69977ms

yangsejin@yangsejin-ui-MacBookPro video-creater % sh scripts/load-test.sh  --concurrent 12
============================================
CPU 부하 테스트 시작
============================================
URL: http://localhost:8080/api/test/cpu/fibonacci?n=45
동시 요청 수: 12
============================================

결과:
--------------------------------------------
No   | Worker   | PID      | 서버 응답 | 실제 소요
--------------------------------------------
1    | single   | 18       | 5779ms       | 23216ms
2    | single   | 18       | 5795ms       | 11646ms
3    | single   | 18       | 5785ms       | 17436ms
4    | single   | 18       | 5816ms       | 5847ms
5    | single   | 18       | 5786ms       | 40620ms
6    | single   | 18       | 5811ms       | 29026ms
7    | single   | 18       | 5803ms       | 34834ms
8    | single   | 18       | 5838ms       | 58078ms
9    | single   | 18       | 5883ms       | 63972ms
10   | single   | 18       | 5822ms       | 46443ms
11   | single   | 18       | 5791ms       | 52240ms
12   | single   | 18       | 5962ms       | 69932ms
--------------------------------------------

============================================
통계
============================================
총 요청 수: 12
전체 소요 시간: 69977ms

서버 응답 시간 (평균): 5822ms
실제 소요 시간 (평균): 37774ms

--------------------------------------------
분석:
--------------------------------------------
싱글 모드 예상 시간: 69864ms (순차 처리)
실제 전체 시간: 69977ms
병렬 효율: .99x

=> 싱글 모드이거나 워커 수가 부족합니다.
============================================

멀티코어 : 싱글 모드 예상 시간: 97884ms (순차 처리) 실제 전체 시간: 8421ms

============================================
yangsejin@yangsejin-ui-MacBookPro video-creater % sh scripts/load-test.sh  --concurrent 12
============================================
CPU 부하 테스트 시작
============================================
URL: http://localhost:8080/api/test/cpu/fibonacci?n=45
동시 요청 수: 12
============================================

결과:
--------------------------------------------
No   | Worker   | PID      | 서버 응답 | 실제 소요
--------------------------------------------
1    | 1        | 25       | 8311ms       | 8345ms
2    | 6        | 31       | 8220ms       | 8261ms
3    | 3        | 27       | 8147ms       | 8187ms
4    | 12       | 78       | 7783ms       | 7826ms
5    | 4        | 28       | 8169ms       | 8202ms
6    | 7        | 36       | 8125ms       | 8169ms
7    | 5        | 29       | 8356ms       | 8395ms
8    | 9        | 50       | 8339ms       | 8378ms
9    | 2        | 26       | 8099ms       | 8151ms
10   | 11       | 69       | 8176ms       | 8214ms
11   | 8        | 42       | 8100ms       | 8141ms
12   | 10       | 64       | 8063ms       | 8111ms
--------------------------------------------

============================================
통계
============================================
총 요청 수: 12
전체 소요 시간: 8421ms

서버 응답 시간 (평균): 8157ms
실제 소요 시간 (평균): 8198ms

--------------------------------------------
분석:
--------------------------------------------
싱글 모드 예상 시간: 97884ms (순차 처리)
실제 전체 시간: 8421ms
병렬 효율: 11.62x

=> 클러스터 모드로 병렬 처리되고 있습니다!
============================================

계산하는 것을 봤을때 69977ms VS 8421ms 속도차이는 확실히 발생하는 것을 확인할 수 있었다.

다음 글에서는 실제 실무에서는 어떻게 수치들을 관리하고 모니터링하는지를 설명할건데
이렇게 일시적인 부분말고 실제 트래픽처럼 만들어서 오픈소스기반 모니터링툴을 이용해 보이겠다.

728x90