Backend/Node.js

Node.js Graceful Shutdown 적용

seung_soos 2024. 10. 30. 15:26

시작하며

Node.js 서버에서 금전적인 행위를 처리하는 배치 작업이 종료(Shutdown)될 때 안전하게 종료되지 않아 특정 유저의 출금이 두 번 발생하는 문제가 종종 발생했다. 원인을 분석해보니, Shutdown 시점에 출금 처리는 되었으나 데이터 처리까지 완료되지 않아, 다음 배치 작업에서 중복 출금이 발생하는 문제였다. 이를 해결하기 위해 Graceful Shutdown을 도입하는 과정에서 배운 점들을 기록하였다.

Graceful Shutdown이란?

직역하면 우아한 종료로, 애플리케이션이 완전히 종료되기 전에 진행 중인 모든 작업을 마무리하고, 리소스를 해제하며 데이터의 무결성을 유지한 상태로 안전하게 종료되는 과정을 의미한다.

Node-schedule 사용

Node.js에서 스케줄업무를 하는 라이브러리는 크게 Node-schedule 과 Node-cron이 있다. 현재 프로젝트에서는 Node-schedule 버전 1.3.3 을 사용중이다. 

 

※ 버전 확인 명령어

npm list node-schedule

 

만약 node-schedule의 버전을 2.x대 를 사용중이라면 기본 제공하는 gracefulShutdown() 메서드를 사용하여 안전한 종료가 가능하다. 하지만 1.x 대 버전을 사용한다면 gracefulShutdown() 메서드를 지원하지 않기때문에 메서드를 만들어 사용할수 있다.

gracefulShutdown 구현

Graceful Shutdown을 위해 작업 관리 리스트를 만들어 scheduledJobs와 runningJobs 배열에 작업을 추가, 삭제하는 구조로 관리한다.

1. 스케줄 관리 함수

const scheduledJobs = [];
let runningJobs = [];

const manageGracefulShutdownJob = (interval, jobFunction) => {
    let job = Schedule.scheduleJob(interval, async () => {
        const promise = jobFunction();
        runningJobs.push(promise);
        try {
            await promise;
        } finally {
            runningJobs = runningJobs.filter(job => job !== promise);
        }
    });
    scheduledJobs.push(job);
};

 

manageGracefulShutdownJob이 실행시 scheduleJobs 변수에 push 하여 관리대상으로 등록하고, 실행시 promise 객체를 반환하여 runningJobs에 push한다. 이후 await으로 해당 실행이 종료된 이후 runningJobs에서 해제를 시켜줌으로서, 현재실행중이 스케줄 잡을 관리한다.

manageGracefulShutdownJob('실행 주기', 메서드명);

2. Graceful Shutdown 적용

SIGINT 신호가 발생하면 모든 스케줄 작업을 취소하고, 실행 중인 작업이 완료될 때까지 대기한다.

process.on('SIGINT', async () => {
    console.log('Graceful shutdown in progress start');
    for (const job of scheduledJobs) {
        job.cancel();
    }
    console.log('All scheduled jobs cancelled');

    await Promise.all(runningJobs);

    console.log('All running jobs completed');
    process.exit(0);
});

 

위 코드를 통해 Graceful Shutdown을 구현할 수 있다. 그러나 개발 서버에서 테스트 시 SIGINT 신호가 발생하면 즉시 종료되는 현상을 확인했다. 이는 PM2의 kill_timeout 기본 설정이 1.6초로, 해당 시간동안 종료되지 않으면 SIGKILL로 변환되어 강제 종료되기 때문이다. kill_timeout을 1시간으로 늘려 테스트한 결과, 정상적으로 Graceful Shutdown이 작동했다.

참고 링크

https://github.com/node-schedule/node-schedule?tab=readme-ov-file#graceful-shutdown

https://pm2.keymetrics.io/docs/usage/signals-clean-restart/