Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

node.js에서 graceful shutdown 적용하기 #31

Open
yuiseo opened this issue Dec 29, 2024 · 3 comments
Open

node.js에서 graceful shutdown 적용하기 #31

yuiseo opened this issue Dec 29, 2024 · 3 comments
Assignees
Labels
프론트엔드 질문 리스트의 직무 구분을 위한 라벨

Comments

@yuiseo
Copy link
Member

yuiseo commented Dec 29, 2024

📝 node.js에서 graceful shutdown 적용하기

📚 주제:

  • graceful shutdown이란 무엇인가
  • 왜 적용해야 할까?
  • 실제 node.js 서버에서의 구현 예시
  • next.js에서의 적용

📖 핵심 내용:

Graceful Shutdown이란?

애플리케이션이나 서버가 종료될 때, 실행 중인 작업을 안전하게 처리하고 리소스를 정리하며 종료하는 과정을 말합니다. 이는 단순히 강제로 프로세스를 종료하는 것과는 달리, 시스템이 정상적인 상태에서 종료되도록 도와줍니다.

❔ Graceful Shutdown의 필요성

  • 데이터 손실 방지: 실행 중인 요청이나 트랜잭션이 갑작스럽게 중단되면 데이터가 손실되거나 손상될 수 있습니다. Graceful Shutdown은 이를 방지합니다.
  • 일관성 유지: 애플리케이션이 예상치 못한 종료로 인해 시스템 상태가 불일치하게 되는 것을 방지합니다.
  • 리소스 정리: 파일, 데이터베이스 연결, 네트워크 소켓 등 사용 중인 리소스를 정리하여 메모리 누수나 리소스 잠금을 방지합니다.
  • 사용자 경험 향상: 서비스가 종료되더라도 사용자 요청을 가능한 한 마무리하여 부정적인 영향을 최소화합니다.

🏗️ Graceful Shutdown의 작동 방식

  1. 종료 신호 수신:
    • 운영 체제에서 종료 신호(SIGTERM, SIGINT 등)를 받습니다.
  2. 새 작업 수락 중단:
    • 새 요청을 수락하지 않고, 기존 요청 처리에 집중합니다.
    • 예를 들어, 웹 서버는 더 이상 클라이언트 요청을 받지 않습니다.
  3. 기존 작업 완료:
    • 현재 진행 중인 작업(예: 데이터 저장, 파일 처리, API 응답 등)을 완료합니다.
    • 제한 시간을 두어 너무 오래 걸리지 않도록 설정할 수도 있습니다.
  4. 리소스 정리:
    • 열려 있는 파일, 데이터베이스 연결, 네트워크 소켓 등을 닫습니다.
    • 캐시나 임시 데이터를 정리합니다.
  5. 프로세스 종료:
    • 모든 작업이 완료되면 프로세스를 안전하게 종료합니다.

Node.js 기반의 Express 서버 구현 예시 코드

const express = require('express');
const app = express();
let server;

app.get('/', (req, res) => {
    res.send('Hello World!');
});

// 서버 시작
server = app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

// Graceful Shutdown 구현
process.on('SIGTERM', () => {
    console.log('SIGTERM signal received: closing HTTP server');
    server.close(() => {
        console.log('HTTP server closed');
        // 추가적인 리소스 정리 작업 (예: DB 연결 종료)
        process.exit(0);
    });
});

process.on('SIGINT', () => {
    console.log('SIGINT signal received: closing HTTP server');
    server.close(() => {
        console.log('HTTP server closed');
        process.exit(0);
    });
});

next.js에서 적용하기

#8 에서 작성한 server.js 에 graceful shutdown 코드를 추가하였다.

//server.js
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const v8 = require('v8');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

const activeConnections = new Set(); // 활성 연결을 추적하기 위한 Set

app.prepare().then(() => {
  const server = createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    handle(req, res, parsedUrl);
  });

  console.log(`App started with PID: ${process.pid}`);

  server.listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on frontend server');
  });

  // 활성 연결 추적
  server.on('connection', (socket) => {
    activeConnections.add(socket);
    socket.on('close', () => {
      activeConnections.delete(socket);
    });
  });

  // Graceful shutdown 처리
  process.on('SIGTERM', async () => {
    console.log('SIGTERM received. Closing server...');

    server.close(() => {
      console.log('Server closed. Closing active connections...');

      // 모든 활성 연결 강제 종료
      activeConnections.forEach((socket) => socket.destroy());

      setTimeout(() => {
        console.log('Shutdown after sleep.');
        process.exit(0); // 정상 종료
      }, 70000);
    });
  });

  // SIGINT 처리 (Ctrl+C)
  process.on('SIGINT', () => {
    console.log('SIGINT received. Performing graceful shutdown...');
    server.close(() => {
      console.log('Server closed.');
      process.exit(0); // 정상 종료
    });
  });
});

// 종료 시 code 출력
process.on('exit', (code) => {
  console.log(`Process exited with code: ${code}`);
});

SIGTERM 발생을 위한 종료 명령어

ps aux | grep node

해당 명령어를 통해 PID를 알아낸다.

kill -SIGTERM [PID]

알아낸 PID을 kill.

🔥 SIGINT 신호는 인식하나, SIGTERM은 인식하지 못하는 현상

image
로컬에서 위의 명령어를 통해 SIGTERM kill을 시도했으나 SIGTERM 로그가 남지 않고 143 코드를 남기며 종료 되었다.
image
SIGINT (ctrl+c)를 통한 kill의 경우에는 의도한 바 대로 로그를 남기며 정상 종료되었다.

👩🏻‍🚒 왜 SIGTERM 신호는 인식하지 못하는가?

  • stack overflownode.js 깃헙 이슈 답에 따르면 window환경에서 unix 스타일의 신호 (SIGTERM, SIGNUP등)를 기본적으로 지원하지 않아, Node.js에서 이런 신호를 수신하거나 처리하는 것이 제한적입니다.

image
image

  • Node.js는 Windows에서 process.kill() 및 ChildProcess.kill() 메서드를 통해 일부 신호를 에뮬레이트합니다. 그러나 이는 완전한 신호 지원이 아니며, 특정 신호(SIGTERM, SIGHUP 등)는 Windows에서 지원되지 않습니다.

👩🏻‍🚒 mac OS에서 테스트 - 성공

image
image


💡 참고 자료:
https://www.ryuollojy.com/articles/nodejs-graceful-shutdown
vercel/next.js#19693
https://velog.io/@hwisaac/NextJS-Depoyment#%EC%88%98%EB%8F%99-graceful-shutdown
vercel/next.js#60059
https://nextjs-ko.org/docs/app/building-your-application/deploying#manual-graceful-shutdowns
https://dtrunin.github.io/2022/04/05/nodejs-graceful-shutdown.html
https://stackoverflow.com/questions/63241807/nodejs-process-kill-doesnt-seem-to-work-on-windows-when-i-try-to-programaticall
https://stackoverflow.com/questions/63241807/nodejs-process-kill-doesnt-seem-to-work-on-windows-when-i-try-to-programaticall

@yuiseo yuiseo added the 프론트엔드 질문 리스트의 직무 구분을 위한 라벨 label Dec 29, 2024
@yuiseo yuiseo added this to the 9주차(12/30 월) milestone Dec 29, 2024
@yuiseo yuiseo self-assigned this Dec 29, 2024
@suna-ji
Copy link
Member

suna-ji commented Dec 30, 2024

오왕 next.js에서 graceful shutdown 적용하는 방법과 스프링부트에서 적용하는 방법이 살짝 다르군욤..!

@jokbalkiller
Copy link
Member

제시해드린 사항이 드디어 프로트엔드에도 적용되어 매우 기쁩니다. 👍🏻

현재 Node.js는 프론트엔드로 사용중이지 백엔드 서버로는 사용하는게 아니라서 저 부분이 적용되고 안되고에 따라 표면적으로는 큰 차이가 나지않겠지만 쿠버네티스 환경의 POD로 배포를 진행할 경우 꼭 생각해야하는 부분이니 한번 보시면서 큰 공부가 되었을 것이라고 생각됩니다.

추가로 궁금한 부분이 있는데요. SIGTERM을 받은 후 진행하는 코드에서 현재 진행중인 것들을 다 처리하고 종료하는게 아니고 강제 종료를 하는것 같다고 느낌이 드는데 맞나요? 프론트엔드에서 graceful shutdown이라하면 어떤 로직들이나 요소들을 무사히 종료시켜야하는건지 한번 더 찾아보면 좋을 것 같습니다!

@yuiseo
Copy link
Member Author

yuiseo commented Feb 8, 2025

제시해드린 사항이 드디어 프로트엔드에도 적용되어 매우 기쁩니다. 👍🏻

현재 Node.js는 프론트엔드로 사용중이지 백엔드 서버로는 사용하는게 아니라서 저 부분이 적용되고 안되고에 따라 표면적으로는 큰 차이가 나지않겠지만 쿠버네티스 환경의 POD로 배포를 진행할 경우 꼭 생각해야하는 부분이니 한번 보시면서 큰 공부가 되었을 것이라고 생각됩니다.

추가로 궁금한 부분이 있는데요. SIGTERM을 받은 후 진행하는 코드에서 현재 진행중인 것들을 다 처리하고 종료하는게 아니고 강제 종료를 하는것 같다고 느낌이 드는데 맞나요? 프론트엔드에서 graceful shutdown이라하면 어떤 로직들이나 요소들을 무사히 종료시켜야하는건지 한번 더 찾아보면 좋을 것 같습니다!

말씀해주신대로 제가 작성해둔 방식은 로직이 활성된 것들에 대해 강제 종료(destroy())되는 명령어입니다. 다시 살펴보니 일정 시간이 지난 후에도 종료가 안되었을 때 강제 종료 시키는 전략으로 위의 코드는 수정이 필요할 거 같습니다!

일단 server.close()의 경우 새로운 연결을 받지 않도록 서버를 닫지만, 이미 존재하는 연결은 유지 시키는 역할을 합니다. 기존 연결이 모두 닫히면 서버가 완전히 종료됩니다.

node.js 공식 문서를 통해 알아본 바 node.js의 socket 연결 종료에 관한 방법은 2가지입니다.

  1. socket.destroy()
  • node.js공식 문서
  • 소켓을 즉시 강제 종료하는 메서드
  • 특징:
    • 진행 중인 요청이 있더라도 강제로 종료됨 (데이터 손실 가능).
    • 현재 I/O 작업이 있든 없든, 소켓이 즉시 닫히고 모든 버퍼가 비워짐.
    • socket.end()와 달리, FIN 패킷을 보내지 않고, TCP 연결을 즉시 끊음
  1. socket.end()
  • node.js 공식 문서
  • 소켓을 정상적으로 닫도록 요청하는 메서드입니다.
  • 특징:
    • 남아 있는 데이터를 전송한 후, FIN 패킷을 보내 클라이언트와 연결을 정상적으로 종료함.
    • 서버 또는 클라이언트가 호출할 수 있음.
    • 클라이언트가 데이터 전송을 마치면 close 이벤트 발생.

결론

본문의 코드는 end()destroy() 두 방법을 채택하여 사용하는 것이 좋을 . 거같습니다.

코드 예시

server.on('connection', (socket) => {
  activeConnections.add(socket);
  
  socket.on('close', () => {
    activeConnections.delete(socket);
  });
});

process.on('SIGTERM', () => {
  console.log('SIGTERM received. Closing server...');

  server.close(() => {
    console.log('Server closed. Closing active connections...');

    // 1️⃣ 정상 종료 시도
    activeConnections.forEach((socket) => {
      socket.end(); //Graceful Shutdown
    });

    // 2️⃣ 일정 시간 후에도 닫히지 않는다면 강제 종료
    setTimeout(() => {
      activeConnections.forEach((socket) => {
        if (!socket.destroyed) {
          socket.destroy(); // 강제 종료
        }
      });
      console.log('All connections closed. Exiting process.');
      process.exit(0);
    }, 10000); // 10초 대기 후 강제 종료
  });
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
프론트엔드 질문 리스트의 직무 구분을 위한 라벨
Projects
None yet
Development

No branches or pull requests

3 participants