본문 바로가기

Infrastructure/Prometheus

Node.js Cluster mode 모니터링

이번엔 지난 시간에 이어 싱글 프로세스가 아닌, cluster 로 여러 프로세스를 띄운 상태에서의 메트릭 수집 및 모니터링을 해보도록 하겠습니다.

 

- Prometheus Client Libraries

 

이전의 포스트에서 prom-client 라는 Node.js 에서 사용 가능한 Prometheus 클라이언트 라이브러리를 소개해드렸습니다.

이번에도 마찬가지로 해당 라이브러리를 사용할 것입니다. (prom-client 는 cluster 환경에서의 메트릭 수집을 지원하는 객체 및 함수가 있습니다)

 

- 샘플 프로젝트

 

프로젝트 설정이나 지표를 수집하는 부분은 이전 내용과 어느정도 동일하지만 처음부터 만든다고 가정하고 시작해보겠습니다.

 

• 먼저 Node.js 프로젝트를 하나 생성합니다.

 

> mkdir prom-client-sample
> cd prom-client-sample
> npm init

 

사용할 웹 프레임워크인 koa 및 prom-client 등의 라이브러리를 설치합니다.

 

> npm install koa koa-router prom-client

 

• 간략하게 구조는 cluster mode 로 실행할 app.js, 메트릭 객체 및 수집할 함수를 모은 metrics.js, 그리고 master 에서 띄울 metric-server.js 와 worker 에서 띄울 web-server.js 로 나뉘어져 있습니다.

 

먼저 metrics.js 입니다. 저번과 코드는 동일합니다.

 

const { Registry, Counter, Gauge, Histogram, Summary, register } = require('prom-client');

const prometheus = () => {
  const registry = new Registry();
  const instances = {};

  const create = ({ type, name, help }) => {
    let instance;

    if (type === 'counter') {
      instance = new Counter({ name, help });
    } else if (type === 'gauge') {
      instance = new Gauge({ name, help });
    } else if (type === 'histogram') {
      instance = new Histogram({ name, help });
    } else if (type === 'summary') {
      instance = new Summary({ name, help });
    }

    if (instance) {
      registry.registerMetric(instance);
      instances[name] = { type, instance };
    }
  };

  const add = ({ name, data }) => {
    if (instances[name]) {
      const { type, instance } = instances[name];

      if (type === 'counter') {
        instance.inc(data);
      } else if (type === 'gauge') {
        instance.set(data);
      } else if (type === 'histogram') {
        instance.observe(data);
      } else if (type === 'summary') {
        instance.observe(data);
      }
    }
  };

  const get = async () => {
    return {
      metrics: await registry.metrics(),
      contentType: register.contentType,
    };
  };

  return { create, add, get };
};

const Prometheus = prometheus();
Prometheus.create({
  type: 'counter',
  name: 'counter',
  help: 'random counter for test',
});
Prometheus.create({
  type: 'gauge',
  name: 'gauge',
  help: 'random gauge for test',
});
Prometheus.create({
  type: 'histogram',
  name: 'histogram',
  help: 'random histogram for test',
});
Prometheus.create({
  type: 'summary',
  name: 'summary',
  help: 'random summary for test',

});

module.exports = {
  Prometheus,
};

 

prom-client 가 제공하는 api 들을 사용하였고 코드 작성 편의를 위해 Prometheus 라는 함수를 만들어 create, add, get 이라는 내부 함수를 두었습니다.

 

이제 이 함수를 사용해 특정 route 로 들어올 시 메트릭을 수집하는 서버 (web-server.js) 를 작성해보도록 하겠습니다 (이 서버는 각 worker 에서 띄워지게 될 것입니다)

 

const Koa = require('koa');
const Router = require('koa-router');
const cluster = require('cluster');

const app = new Koa();
const router = new Router();
const port = 4000;

const { Prometheus } = require('./metrics');

const run = () => {
  router.get('/metrics', async (ctx) => {
    console.info(cluster.worker.id);
    const { metrics, contentType } = await Prometheus.get();

    ctx.set('Content-Type', contentType);
    ctx.body = metrics;
  });

  router.get('/metrics/:name', (ctx) => {
    console.info(cluster.worker.id);
    const { name } = ctx.params;
    const randomNumber = Math.round(Math.random() * 10);
    console.info(name, randomNumber);
    Prometheus.add({ name, data: randomNumber });
    ctx.body = 'done';
  });

  app.use(router.routes());
  app.use(router.allowedMethods());

  app.listen(port, () => {
    console.info(`Web server-${cluster.worker.id} is listening on port #${port}`);
  });
};

module.exports = {
  run,
};

 

router.get('/metrics') 이 route 는 사실 필요없지만, 왜 master 에서 수집한 메트릭을 보여주기 위해 따로 서버를 띄워야하는지 보여주기 위해 남겨두었고 worker 의 id 만 console 로 찍는 코드를 추가했습니다.

 

• 이번엔 master 에서 띄울 metric-server.js 입니다.

 

const Koa = require('koa');
const Router = require('koa-router');
const { AggregatorRegistry } = require('prom-client');

const app = new Koa();
const router = new Router();
const port = 5000;
const aggregatorRegistry = new AggregatorRegistry();

const run = () => {
  router.get('/metrics', async (ctx) => {
    const metrics = await aggregatorRegistry.clusterMetrics();
    const contentType = aggregatorRegistry.contentType;

    ctx.set('Content-Type', contentType);
    ctx.body = metrics;
  });

  app.use(router.routes());
  app.use(router.allowedMethods());

  app.listen(port, () => {
    console.info(`Metrics server is listening on port ${port}`);
  });
};

module.exports = {
  run,
};

 

prom-client 의 AggregatorRegistry 라는 클래스를 사용한 부분이 눈에 띄는 점입니다.

해당 클래스의 clusterMetrics 함수는 각 worker 에서 쌓인 메트릭을 한번에 aggregate 해서 확인할 수 있습니다.

prom-client 의 내부 구조까지는 여기서 파고들지 않도록 하겠습니다.

 

• 마지막으로 작업한 코드들을 cluster mode 로 실행할 app.js 입니다.

 

const os = require('os');
const cluster = require('cluster');

const cpuCount = os.cpus().length;

const clusters = {
  master() {
    require('./metric-server').run();

    for (let i = 0 ; i < cpuCount ; i++) {
      cluster.fork();
    }
  },
  webServer() {
    require('./web-server').run();
  },
};

const run = () => {
  if (cluster.isMaster) {
    clusters.master();
  } else {
    clusters.webServer();
  }
}

run();

 

비교적 간단합니다. master 에선 metric-server 를 띄우는 것과 worker 들을 fork 하는게 전부고, worker 에선 web-server 를 실행하는 부분이 끝입니다.

 

이렇게 작성한 코드를 실행해보겠습니다.

 

> node app.js

 

 

master 에서 metric-server 가 5000 포트로 하나 띄워졌고, 각 worker 에서 web-server 가 4000 포트로 실행된 걸 볼 수 있습니다.

(현재 os 의 cpu 숫자에 맞춰 worker fork 가 되기에 저는 12개의 worker 가 뜨게 되었고 이는 실행환경마다 다를 수 있습니다)

 

이제 브라우저에서 localhost:4000/metrics/:name 을 여러번 호출한 후에, localhost:4000/metrics 로 접근해보도록 하겠습니다.

코드상에서 콘솔로 찍고있는 cluster.worker.id 를 같이 확인해 왜 따로 메트릭을 aggregate 해줘야하는지도 볼 것입니다.

 

 

localhost:4000/metrics/counter 를 4번 호출했고 콘솔에 찍힌대로 운좋게 4번 모두 2번 worker 가 처리했습니다.

이대로면 counter 의 메트릭은 17이 되어있을 것입니다.

하지만 마지막 localhost:4000/metrics 는 3번의 worker 가 처리하게 되었습니다.

 

 

위처럼 메트릭은 2번 worker 에 쌓여있는데, 메트릭 조회는 3번 worker 에서 처리하니 메트릭이 원하는대로 보이지 않게 됩니다.

이전에 메트릭을 쌓는 요청을 1 ~ 12 worker 중 어느곳에서 받아서 처리하게 될지도 랜덤이기때문에 (이번엔 운좋게 2번 worker 가 전부 처리했지만, 라이브 환경에선 절대 이럴 수 없습니다) 이와 상관없이 모든 worker 가 갖고있는 메트릭을 aggregate 해서 보여줄 metric-server 가 필요한 것입니다.

 

이제 localhost:5000/metrics 에 접근해보도록 하겠습니다.

 


메트릭이 aggregate 되어 counter 가 17 인것을 확인할 수 있습니다.

이렇게 작업이 되었으니, 이젠 Prometheus 설정에서 타겟을 localhost:5000 으로 변경하기만 하면 끝입니다.

 

여기까지 prom-client 라이브러리를 사용해 cluster mode 에서의 메트릭 수집을 진행해봤습니다.

(예시용이므로 코드는 다소 지저분해 보일 수 있습니다)

 

실제로 production 환경에서는 Node.js 앱을 거의 단일로 운영하는 경우는 없고, 위처럼 cluster 모듈을 사용하거나 pm2 라는 프로세스 매니저를 사용해 복수의 프로세스를 띄워 운영하게 됩니다.

만약 그러한 환경에서 Node.js 앱을 모니터링 하기 위해선 이러한 작업이 꼭 필요하고, 저도 현재 공용 라이브러리를 만들어 cluster 혹은 pm2 를 사용하는 환경에 대응해 모니터링을 하고 있습니다.

 

pm2 에선 코드가 조금 더 복잡한데, 기회가 되면 소개해보도록 하겠습니다. (최종 목표는 사내에서 쓸 수 있게 만든 공용 라이브러리를 외부에 공개하는 것입니다)

 

다음엔 마지막으로 push-gateway 에 대해 다뤄보도록 하겠습니다.

'Infrastructure > Prometheus' 카테고리의 다른 글

Prometheus Pushgateway with Node.Js  (2) 2021.04.10
Node.js 어플리케이션 모니터링  (0) 2021.03.27
Prometheus Node Exporter 모니터링  (0) 2021.03.20
Prometheus 란?  (0) 2021.03.14