[Node.js]멀티 프로세스 vs 단일 프로세스 (pm2, autocannon)

[Node.js]멀티 프로세스 vs 단일 프로세스 (pm2, autocannon)

(https://github.com/SHINDongHyeo/Node-JS-Performance-Test)[https://github.com/SHINDongHyeo/Node-JS-Performance-Test]

Node JS 부하테스트 툴 autocannon을 활용해 임의로 트래픽을 생성하고, 이를 해결하기 위해 pm2 모듈의 cluster를 활용해 멀티 프로세스 구현을 시도한다.

1. 테스트용 Nest JS api 만들기

node js의 웹 프레임워크 nest js 활용해 간단한 api를 만든다.

나는 GET /test 라는 경로로 cpu를 많이 사용하기 위해 복잡한 연산인 피보나치 수열 계산을 구현했다.

# app.service.ts 가운데

getTest(): number {
    function fibonacci(num: number): number {
      if (num <= 1) return num;
      return fibonacci(num - 1) + fibonacci(num - 2);
    }

    const result = fibonacci(35); // 예: 35번째 피보나치 수 계산
    return result;
  }

2. pm2를 이용해 단일 프로세스 / 멀티 프로세스 구현하기

단일 프로세스

실행 명령어

pm2 start .\dist\main.js

멀티 프로세스

pm2 명령어를 통해 cluster 모드를 실행할 수도 있지만 ecosystem.config.js라는 파일을 이용해 실행하는 것이 더 편리하다고 하여 ecosystem.config.js 파일을 추가했다.

# ecosystem.config.js

module.exports = {
    apps: [
      {
        name: 'performance-test',
        script: 'dist/main.js', // 빌드된 JS 파일 경로
        instances: 'max', // CPU 코어 수만큼 인스턴스 생성
        exec_mode: 'cluster', // 클러스터 모드 사용
        env: {
          NODE_ENV: 'development',
        },
        env_production: {
          NODE_ENV: 'production',
        },
      },
    ],
  };

실행 명령어

pm2 start .\ecosystem.config.js

3. autocannon을 이용해 부하 생성하기

단일 프로세스

실행 명령어

autocannon -c 1000 -d 10 -p 10 http://localhost:3000/test

결과 화면

Running 10s test @ http://localhost:3000/test
1000 connections with 10 pipelining factor


┌─────────┬─────────┬─────────┬─────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat    │ 2.5%    │ 50%     │ 97.5%   │ 99%     │ Avg       │ Stdev     │ Max     │
├─────────┼─────────┼─────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 2901 ms │ 4104 ms │ 7970 ms │ 7970 ms │ 5432.9 ms │ 1499.2 ms │ 7970 ms │
└─────────┴─────────┴─────────┴─────────┴─────────┴───────────┴───────────┴─────────┘
┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬───────┐
│ Stat      │ 1%  │ 2.5% │ 50% │ 97.5%   │ Avg   │ Stdev │ Min   │
├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤
│ Req/Sec   │ 0   │ 0    │ 0   │ 10      │ 2     │ 3.77  │ 1     │
├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤
│ Bytes/Sec │ 0 B │ 0 B  │ 0 B │ 2.33 kB │ 466 B │ 878 B │ 233 B │
└───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴───────┘

Req/Bytes counts sampled once per second.
# of samples: 10

21k requests in 12.8s, 4.66 kB read
10k errors (10k timeouts)

멀티 프로세스

실행 명령어

autocannon -c 1000 -d 10 -p 10 http://localhost:3000/test

결과 화면

Running 10s test @ http://localhost:3000/test
1000 connections with 10 pipelining factor


┌─────────┬─────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐
│ Stat    │ 2.5%    │ 50%     │ 97.5%    │ 99%      │ Avg        │ Stdev      │ Max      │
├─────────┼─────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤
│ Latency │ 2471 ms │ 9870 ms │ 10901 ms │ 10901 ms │ 9683.02 ms │ 1735.59 ms │ 10901 ms │
└─────────┴─────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘
┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬───────┐
│ Stat      │ 1%  │ 2.5% │ 50% │ 97.5%   │ Avg     │ Stdev   │ Min   │
├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼───────┤
│ Req/Sec   │ 0   │ 0    │ 0   │ 30      │ 6.2     │ 11.69   │ 3     │
├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼───────┤
│ Bytes/Sec │ 0 B │ 0 B  │ 0 B │ 6.99 kB │ 1.44 kB │ 2.72 kB │ 699 B │
└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴───────┘

Req/Bytes counts sampled once per second.
# of samples: 10

21k requests in 14.49s, 14.4 kB read
10k errors (10k timeouts)

테스트 결과

이상하게도 멀티 프로세스를 사용한 결과의 latency가 더 길었다. 프로세스를 병렬로 처리하고 있는데 왜 더 느려지는 걸까?

그 이유는 여러 가지로 추측된다.

  1. 시스템 자원 경쟁 병렬적으로 여러 프로세스가 동시에 실행되다보니 메모리 같은 컴퓨팅 리소스를 사용하기 위해 경쟁하다보니 오히려 성능 저하가 발생했을 확률
  2. 컨텍스트 스위칭 오버헤드 CPU가 여러 프로세스를 넘나들며 생기는 오버헤드가 오히려 성능에 문제를 줄 확률
  3. CPU 자원 부족 복잡한 연산같은 경우 CPU 자원을 많이 사용하는데 8개 프로세스가 병렬적으로 실행되다보니 각 프로세스가 사용할 수 있는 CPU 자원이 너무 적어 문제가 되었을 확률

이러한 추측을 기반으로 프로세스 숫자를 조금 줄여주면 어떨까라는 생각을 해봄

프로세스 개수 줄인 후 결과

기존 : max (CPU 코어 수 만큼 최대로 실행) 수정 후 : 2

결과 화면

Running 10s test @ http://localhost:3000/test
1000 connections with 10 pipelining factor


┌─────────┬─────────┬─────────┬─────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat    │ 2.5%    │ 50%     │ 97.5%   │ 99%     │ Avg       │ Stdev     │ Max     │
├─────────┼─────────┼─────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 3363 ms │ 3416 ms │ 4387 ms │ 4387 ms │ 3883.6 ms │ 494.32 ms │ 4387 ms │
└─────────┴─────────┴─────────┴─────────┴─────────┴───────────┴───────────┴─────────┘
┌───────────┬─────┬──────┬─────┬─────────┬───────┬─────────┬───────┐
│ Stat      │ 1%  │ 2.5% │ 50% │ 97.5%   │ Avg   │ Stdev   │ Min   │
├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼───────┤
│ Req/Sec   │ 0   │ 0    │ 0   │ 20      │ 4     │ 7.76    │ 1     │
├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼───────┤
│ Bytes/Sec │ 0 B │ 0 B  │ 0 B │ 4.66 kB │ 932 B │ 1.81 kB │ 233 B │
└───────────┴─────┴──────┴─────┴─────────┴───────┴─────────┴───────┘

Req/Bytes counts sampled once per second.
# of samples: 10

21k requests in 11.89s, 9.32 kB read
10k errors (10k timeouts)

latency가 감소한 결과를 확인할 수 있었다.

최종 결론

멀티 프로세스는 프로세스를 병렬적으로 동시에 실행하며 성능을 향상시킬 수 있는 방법 중 하나다. 하지만 무턱대고 멀티 프로세스 수만 늘린다고 성능이 향상될 수는 컴퓨팅 자원이나 여러 가지 상황에 맞춰서 적절한 숫자의 멀티 프로세스 수를 설정해주는 것이 중요하다.

results matching ""

    No results matching ""