pm2로 무중단 배포 안전하게 하기

00_8_3·2022년 3월 27일
0

작성이유

JAVA와 다르게 NodeJS의 경우 싱글스레드이기 때문에 CPU의 모든 성능을 끌어다 쓴다고 할 수 없습니다.
이것을 해결하기 위해 Node의 cluster를 사용 할 수 있지만 pm2를 사용하면 더욱 간편합니다.

기존의 Node 서버에서 terminus을 사용한 health 체크와 graceful shut down기능을 추가하였습니다. 이정도로 충분할 수 있지만 종료가 진행되는 사이
완료가 되지않은 요청을 제대로 처리 해주지 않았다는 것을 알게되었습니다.

구현

프로세스가 실행되었는가?

//코드10. ready 이벤트 설정 변경
//ecosystem.config.js
module.exports = {
  apps: [{
    ...
  wait_ready: true, //  ready 이벤트를 기다리라는 의미
  listen_timeout: 50000 // ready  이벤트를 기다릴 시간 
  }]
}

프로세스 종료후 요청 거절

//코드11. 클라이언트 요청 처리 설정
//ecosystem.config.js
module.exports = {
  apps: [{
  ...
  wait_ready: true,
  listen_timeout: 50000,
  kill_timeout: 5000 // 프로세스에 SIGINT 시그널이 전달되면,
    //새로운 요청을 더 이상 받지 않고 연결되어 있는 요청이 완료된 후
    // 해당 프로세스를 강제로 종료하도록 처리
  }]
}

Keep-Alive 해제

app.close를 이용해 새로운 요청을 거절하고 이미 연결되어 있는 건 유지할 때,
HTTP 1.1 Keep-Alive를 사용하고 있었다면, 요청이 처리된 후에도 기존 연결이 계속 유지되기 때문에 앞의 방법만으로는 해결되지 않습니다.

따라서 ‘Connection: close’를 설정해 클라이언트 요청을 종료하는 방법을 활용, 타임아웃으로 서비스가 중단되는 문제를 해결할 수 있습니다.

//app.js
const express = require('express')
const app = express()
const port = 3000

//추가할 부분 1
let isDisableKeepAlive = false
app.use(function(req, res, next) {
  if (isDisableKeepAlive) {
    res.set(‘Connection’, ‘close’)
  }
  next()
})
app.get('/', function(req, res) { 
  res.send('Hello World!')
})
app.listen(port, function() {
  process.send(‘ready’)
  console.log(`application is listening on port ${port}...`)
})
process.on(SIGINT, function () {
  //추가할 부분
  isDisableKeepAlive = true
  app.close(function () {
  console.log(‘server closed’)
  process.exit(0)
  })
})

대략적으로 위의 코드로 사용할 수 있지만
저는 terminus를 사용하고 있기 때문에 조금 수정하여 사용하겠습니다.

// setOffKeepAlive.ts
// const emitter = Container.get(Emitter).getInstance();
export function setOffKeepAlive(app: Application) {
  
  let isDisableKeepAlive = false;
  emitter.on("offKeepAlive", () => {
    isDisableKeepAlive = true;
    logger.info("Set off 'keep-alive' header");
  });

  app.use((_, res, next) => {
    if (isDisableKeepAlive) {
      res.set("Connection", "close");
    }
    next();
  });
}
//app.ts
...
setOffKeepAlive(app);
...
//terminus.ts

function onSignal() {
  emitter.emit("offKeepAlive");
  return Promise.all([mysql.closePool(), redis.closeRedis()]);
}

...

export const terminusOption: TerminusOptions = {
  logger: logger.info,
  signal: "SIGINT",
  healthChecks: {
    "/health": onHealthCheck,
    __unsafeExposeStackTraces: true,
  },
  onShutdown,
  onSignal,
  timeout: 5000,
};

terminus에서 SIGINT 시그널을 받으면 onSignal를 실행시키고
eventEmitter를 emit하여 connection을 close로 수정합니다.

참고

위의 글을 참고하여 제 코드에 맞게 변형하여 작성하였습니다.

0개의 댓글