이벤트 기반 Chunk 처리를 위한 Node.js 배치 라이브러리 Arehs

이벤트 기반 Chunk 처리를 위한 Node.js 배치 라이브러리 Arehs

2023, Oct 15    

🔍 개요

개발 환경에서 다수의 파일을 읽거나 쓸 때, 여러 비동기 작업들을 처리하는 상황이 자주 발생합니다.

예를들어 DB 데이터 조회, 파일 쓰기, 파일 생성, S3 파일 업로드, 로그 생성 등의 순서가 진행이 된다고 가정해봅니다.

이때 Promise.all 은 여러 Task를 실행하기에 간단하면서 좋은 솔루션이 될 수 있습니다.

그러나, Promise.all 은 상황에 따라 적당한 크기의 chunk 단위로 분할해서 수행해야합니다.

왜냐하면 데이터베이스의 커넥션 풀이 50개인 상황에서, 1,000개의 쿼리 수행이 필요하다면 보통은 50개 이하의 단위 (chunk) 로 Promise 배열을 만들어 Promise.all 로 수행해야 합니다.

또는, 특정 로직에 의해 청크 단위로 실행을 해야하는 상황이 올 수 있습니다.

🏛️ Arehs

🔗 npm: arehs

Arehs 는 이벤트 중심의 Chunk processing을 지향하는 배치 처리를 수행합니다.

이는 첫 번째 비동기 작업 호출이 완료될 때까지 기다리지 않고 다음 비동기 작업 호출을 즉시 할당하여 조밀하게 프로세스를 진행합니다.

이를 통해 다음과 같은 여러 가지를 달성할 수 있습니다:

  • 프로미스 풀의 동시성을 설정하여 서비스 처리량을 제어할 수 있습니다.
  • 프로미스 풀의 동시성을 설정하여 다운스트림 서비스의 부하를 관리합니다.
  • 애플리케이션의 성능 향상
  • CPU 유휴 시간 감소 등

📚 Getting Started

arehs는 CommonJS와 ES Modules를 지원합니다.

CommonJS

const { Arehs } = require('arehs');

ES Modules

import { Arehs } from 'arehs';

Example

  • create: create 메서드의 목적은 특정한 데이터 배열로부터 Arehs 인스턴스를 생성하기 위함입니다.
  • withConcurrency: 병렬 처리 값을 설정하고 현재 인스턴스를 반환하는 메서드입니다. (default: 10)
  • timeoutLimit:Default 값은 0 입니다. 0보다 크면 옵션이 동작하며, 타임아웃 시간(ms)보다 작업시간이 길면 에러가 발생합니다.
  • mapAsync: mapAsync 함수를 호출하면 입력 데이터를 비동기적으로 처리하고 결과를 반환하는 프로세스가 시작됩니다. 이때 각 작업은 동시에 여러 작업이 실행될 수 있지만, concurrency 설정에 따라 제한됩니다. 이것은 대규모 데이터 처리 작업을 효과적으로 관리하고 제어하기 위한 유용한 도구로 사용될 수 있습니다.
import { Arehs } from 'arehs';

const dataArr = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Alice' },
  { id: 3, name: 'Bob' }
];

const result = await Arehs.create(dataArr)
  .withConcurrency(10)
  .mapAsync(async data => {
    return await someAsyncFunction(data);
  });

⚡️ Performance

테스트 결과 ArehsPromise.all에 비해 약 30% 이상 향상될 수 있는 것으로 나타났습니다.

import { Arehs } from 'arehs';

const delay = i => {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res(i);
    }, 150 + Math.random() * 1000);
  });
};

(async () => {
  const tasks = Array.from({ length: 1000 }).map((d, i) => i);

  const startArehs = performance.now();
  await Arehs.create(tasks).withConcurrency(50).mapAsync(delay);
  const endArehs = performance.now();

  console.log(`Arehs: ${endArehs - startArehs}ms`);

  const startPromiseAll = performance.now();
  while (tasks.length > 0) {
    const chunkedTasks = tasks.splice(0, 50);
    await Promise.all(chunkedTasks.map(delay));
  }
  const endPromiseAll = performance.now();

  console.log(`Promise.all: ${endPromiseAll - startPromiseAll}ms`);
})();
    promiseAllTime: 19.859867874979972(s)
    promisePoolTime: 13.55725229203701(s)

Promise.all

보시다시피, Promise.all은 배치에서 가장 느린 프로미스만큼 오래 실행됩니다.

따라서 메인 스레드는 기본적으로 “아무것도 하지 않는” 상태이며 가장 느린 요청이 완료되기를 기다리고 있습니다.

Promise 배열에서 가장 긴 프로미스인 4번이 청크의 실행 시간이 됩니다.

이로 인해 가장 긴 프로미스가 완료될 때까지 다음 프로미스가 아무 작업도 수행하지 않는 비효율적인 문제가 발생합니다.

promise_all

Arehs

Arehs는 프로미스 풀 패턴을 실행하여 Node.js의 메인 스레드를 최대한 활용하는 것이 핵심입니다.

활용도를 높이려면 API 호출(또는 다른 비동기 작업)을 조밀하게 패킹하여 가장 긴 호출이 완료되는 동안 기다리지 않도록 해야 합니다.

기다리지 않고 첫 번째 호출이 완료되는 즉시 다음 호출을 예약합니다.

promise_pool

🙋‍♀️FAQ

Arehs는 항상 Promise.all보다 좋나요?

아뇨, No Silver Bullet(은탄환은 없다).

많은 API 호출과 비동기 작업을 할 때 애플리케이션의 성능을 향상시킬 수 있습니다.

또한, 각 프로미스의 작업 시간이 거의 동일한 상황에서는 큰 차이를 만들지 못할 수도 있습니다.

사용 중인 환경에서 Promise.all로 더 이상 성능 향상을 얻을 수 없다면 시도해 볼 수는 있지만,

Promise.all로 충분하다면 굳이 사용할 필요는 없습니다.

따라서 성능 개선이 필요한 프로젝트에 Arehs를 사용하려면 충분한 테스트를 거친 후에 사용해 보세요.

감사합니다.

arehs README 한글판: 🔗 바로가기