웰제오의 개발 블로그

Node.js 환경에서의 반복작업 수행 본문

개발

Node.js 환경에서의 반복작업 수행

웰치스제로오렌지 2022. 10. 19. 23:24

Node.js 는 현대 웹 백엔드 서버가 처리하는 작업의 대부분이 Network IO 작업에 국한된다는 문제를 해결하기 위해 나온 Javascript 런타임이다.

 

이러한 Node.js 는, 싱글스레드로 동작하는 이벤트 루프를 기반으로 Network IO 를 포함한 여러 비동기 작업들을 Syntax-sugar 가 가미된 편리한 문법을 통해, 복잡한 비즈니스 로직을 포함한 웹 백엔드 프로그램을 개발자가 보다 쉽게 개발할 수 있게 도와준다.

 

프론트엔드를 독점한 Javascript 와의 시너지에 더불어, 하나의 언어를 익힘으로서 프론트와 백을 포함한 풀스택 개발을 가능하게 해준 Node.js 는, 어느 덧 단순한 요청작업을 처리하는 서버 프로그램을 넘어, 복잡한 비즈니스 로직을 처리하는 프로그램에 까지 사용되면서 그 사용처가 넓어지게 되었다.

 

필자도 Node.js 를 활용해 단순한 REST API 서버 뿐만이 아닌 다양한 작업을 수행하는 프로그램을 개발했고, 그 와중에 경험했던 많은 이슈들 중, 싱글스레드로 동작하는 Node.js 의 독특한 설계방식으로 인해, 반복작업 ( Cron Job ) 을 실행하던 도중 경험했던 내용들과, 이에 대한 해결책 및 주의점을 공유하려 한다.

 

 


Cron

3초마다 특정 작업을 실행시키고 싶을 때는 Node.js 에서 제공하는 setInterval 함수를 활용하거나,

Node.js 가 아닌 다른 환경에선 loop 에서 sleep 과 같은 메소드를 활용해 몇초간 스레드를 멈췄다가 이어지는 context 에서 작업을 수행하는 방법등이 있다.

하지만 반복적인 작업의 시작 시점이 프로그램의 실행시점이 아닌 절대적인 시간에 의해 결정되면 이는 어떻게 구현할 수 있을까?

 

이는 Cron 을 활용해 해결할 수 있다.

 

Cron 은 유닉스 계열 컴퓨터 운영 체제의 시간 기반 작업 스케줄러를 뜻하며, 이는 고정된 시간, 날짜, 간격에 주기적으로 특정 작업을 실행할 수 있도록 스케줄링할 때 사용되며,

 

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# │ │ │ │ │                                   7 is also Sunday on some systems)
# │ │ │ │ │
# │ │ │ │ │
# * * * * * <command to execute>


// 출처: https://en.wikipedia.org/wiki/Cron

 

위와 같은 Cron Expression (* * * * *) 으로 반복되는 작업의 수행 시점을 정할 수 있다.

 

Cron 은 api 같은것이 아닌 유닉스 계열 운영체제에서 제공하는 하나의 프로그램 ( 커맨드 ) 이며, 프로그래밍 언어단에서 해당 기능을 활용하기 위해서는 별도의 작업이 필요하다.

대부분의 언어에서는 해당 기능을 구현한 라이브러리가 존재하며, Spring 이나 Nest.js 같은 대형 프레임워크에서는 별도의 라이브러리 없이 해당 기능이 같이 제공되고 있다.

 

Node.js 에서는 cron, node-cron 이름의 npm 을 통해 제공되는 두가지 대표적인 라이브러리가 있으며,

약간의 차이는 있지만 모두 내부적으로 Cron Expression 을 파싱해 setInterval, setTimeout 함수를 호출하고, Date 개체와 heartbeat 를 통해 주기를 싱크함으로서 Cron 기능을 구현했다.

 

한가지 특징이라면, 모두 자식 프로세스를 fork 한다는 것인데,

 

출처:&nbsp;https://github.com/node-cron/node-cron/blob/master/src/background-scheduled-task/index.js

 

 

Node.js 의 Cron 라이브러리들이 왜 fork 를 활용해야만 했는지, 이어지는 내용을 통해 알아보자

 

 


setInterval 의 주기는 보장되는가

 

3초마다 표준출력에 Hello 를 출력하는 프로그램을 만들어 보자.

Javascript 로 이를 작성하면

 

setInterval(() => console.log("Hello"), 3000);

// Hello
// Hello
// ...

 

위와 같은 코드가 될것이다.

Node.js 로 코드를 실행하면 3초마다 Hello 가 출력되지만, 내부를 들여다 보면 이는 절대 3초마다 콜백함수가 실행되는 형식이 아니다.

 

setInterval 호출 시, 등록한 콜백은 이벤트 루프 내부의 timer phase 에서 최소힙으로 관리되는데,

마지막 setInterval 콜백함수의 실행 후 일정 시간이 지났음에도 불구하고, 다른 작업으로 인해 Node.js 가 timer phase 에 도달하지 못하게 될경우 setInterval 의 콜백함수는 실행시점이 불규칙적이게 되거나 실행되지 못하게 된다.

 

setInterval(() => console.log("Hello"), 3000);

while(true){}

// while 문이 콜스택을 점유하며 Hello 를 출력하는 함수는 실행되지 못한다

 

위의 코드를 실행하면 Node.js 는 이벤트 루프에 진입을 못하고 스크립트 실행 단계에서 머물게 되고,
이로 인해 timer phase 에 영원히 도달하지 못하게 되어, Hello 는 콘솔에 출력되지 못하게 된다.

 

혹은,

 

const someAsyncJob = async (ms) => await new Promise((res, _rej) => {
    setTimeout(() => res(0), ms);
});

setInterval(async () => {
    await someAsyncJob(Math.random() * 10000);
    console.log("Hello")
}, 3000);

// hello 가 3초마다 출력되지 않고 주기가 제각각이 된다

 

위와 같이 매 tick 마다 microtask queue 를 polling 해 resolve 된 promise 의 콜백함수를 실행하면서, Hello 가 출력되는 주기가 제각각이 될 수도 있다.

 

따라서 Node.js 의 Cron 라이브러리들은, 위와 같은 이슈들을 피하기 위해 독립적인 이벤트 루프가 필요했고,

자식프로세스를 fork 함으로서, 이벤트 루프의 다른 phase 에서의 작업을 idle 하게 유지해 timer phase 의 접근빈도를 최대한 늘려 setInterval 의 주기가 어긋날 수 있는 문제를 해결했다.

( 독립적인 이벤트 루프라면 워커 스레드로도 해결이 가능하지만, 해당 라이브러리가 10년도 더 되었고, 워커스레드가 안정화된게 2020에 릴리즈된 Node 14 버전이라 자식 프로세스를 활용한 것 같다 )

 

 


Node.js 의 Cron 라이브러리 사용시 주의할점 하나

 

마지막으로 해당 라이브러리를 사용하려면 반드시 염두해두어야 하는 점 하나를 환기하고 싶다.

 

https://github.com/kelektiv/node-cron/issues/232

 

cron job stops after certain hours. · Issue #232 · kelektiv/node-cron

var job = new CronJob({ cronTime: '/1 * * * * *', onTick:function() { console.log(new Date, 'tick triggered'); }, onComplete: function(){/*/}, start: true, runOnInit: true }); job.s...

github.com

 

 

2016 년부터 열려있는 유서깊은 issue 인데

 

프로그램 실행환경에서 cpu 나 메모리 같은 하드웨어 리소스가 부족하다면 이유없이 fork 한 자식 프로세스가 죽어버린다

 

필자도 운영환경에서 해당 이슈를 경험했으며, 정말 무서운건 에러하나 안던지고 조용히 죽는바람에 이게 죽었는지 살았는지 정말 알수가 없다 ( 슈뢰딩거의 cron job... )

 

만약 Node.js 환경에서 해당 라이브러리를 사용할 경우, 해당 라이브러리의 클래스에서 제공하는 running 프로퍼티를 통해 자식 프로세스의 생존유무를 확인할 수 있으니, health check 를 반드시 구성해 예기치 못한 상황에 대비했으면 한다.

Comments