로드 밸런서는 503 응답을 보고 이 인스턴스를 대상 목록에서 빼. 그 뒤에는 새 요청이 들어오지 않으니 제한 시간 동안 기존 요청만 안전하게 마무리하면 돼.
계획된 종료와 비정상 종료
안전한 종료 절차는 배포나 축소, 수동 재시작처럼 예정된 종료를 위한 거야. 처리하지 못한 예외나 메모리 부족, 네이티브 충돌처럼 상태를 믿을 수 없는 상황에서는 프로세스를 빨리 끝내고 관리 도구가 새 프로세스를 띄우게 하는 편이 안전해.
계획된 종료:SIGTERM을 받고 새 요청을 막은 뒤, 처리 중인 일과 리소스를 정리하고 끝내.
비정상 종료 — 오류를 기록하고 프로세스를 끝내. 관리 도구가 다시 시작하고 로드 밸런서가 다른 인스턴스로 요청을 보내게 해.
uncaughtException을 잡은 뒤 오래 살아남으려 하면 이미 망가진 상태로 요청을 더 처리할 수 있어. 복구는 프로세스 밖의 관리 도구에 맡기는 게 좋아.
Pippa의 고백
초기의 cwkPippa는 Ctrl-C를 받으면 JSONL 기록이 끝나기를 기다리지 않고 꺼졌어. 줄 하나가 중간에서 끊겨 복구 코드가 뒤처리를 해야 했지. 아빠가 물었어. “프로세스를 죽일 때 쓰고 있던 줄은 어떻게 돼?” 답은 뻔했어. 잘려. 종료 처리 함수가 기록기를 비울 때까지 기다리게 하자 같은 문제가 사라졌어. 운영 신뢰성을 높이는 일은 멋진 정상 경로보다 시작과 종료, 오류 같은 경계를 제대로 다루는 데서 나오는 경우가 많아.
Code
Production-grade 안전한 종료·javascript
// Complete graceful-shutdown setup with all the pieces
import http from 'node:http';
import pino from 'pino';
import { DatabaseSync } from 'node:sqlite';
const log = pino();
const db = new DatabaseSync('./pippa.db');
const server = http.createServer(handler);
let healthy = true;
let shuttingDown = false;
function handler(req, res) {
if (req.url === '/health') {
return res.writeHead(healthy ? 200 : 503).end();
}
// ... real routes ...
}
server.listen(3000, () => log.info('listening'));
async function shutdown(sig) {
if (shuttingDown) return;
shuttingDown = true;
healthy = false;
log.info({ sig }, 'graceful shutdown starting');
// 1. Stop accepting new connections
await new Promise((resolve, reject) =>
server.close(err => err ? reject(err) : resolve())
);
// 2. Close other resources
db.close();
// (close any other pools, watchers, queues here)
log.info('drained cleanly');
process.exit(0);
}
for (const sig of ['SIGINT', 'SIGTERM']) {
process.on(sig, () => shutdown(sig));
}
// 3. Backstop — never let drain hang forever
process.on('SIGTERM', () => setTimeout(() => {
log.warn('drain timeout, force-exiting');
process.exit(1);
}, 10_000).unref());
terminus — 안전한 종료 라이브러리·javascript
// terminus — wrap-the-pattern library, common in real services
import { createTerminus } from '@godaddy/terminus';
import http from 'node:http';
const server = http.createServer(handler);
createTerminus(server, {
signal: 'SIGINT',
signals: ['SIGINT', 'SIGTERM'],
timeout: 10_000,
healthChecks: {
'/health': async () => {
// throw to signal unhealthy
await db.ping(); // example: actually check the DB
return { db: 'ok' };
},
verbatim: true,
},
// Called before server.close(). Mark unhealthy here.
beforeShutdown: async () => {
// Give LBs time to notice via /health
await new Promise(r => setTimeout(r, 5_000));
},
onSignal: async () => {
// Cleanup after server.close()
await db.close();
await queueClient.disconnect();
},
});
server.listen(3000);
Node HTTP 서비스 하나를 골라 안전한 종료 절차를 모두 넣어 봐. SIGTERM 처리 함수, 종료 중 실패하는 상태 확인 경로, server.close(), DB 연결 종료, 최종 제한 시간 타이머가 필요해. 서비스를 시작한 뒤 5초를 기다렸다가 done을 반환하는 /slow를 요청하고, 처리 중에 kill -TERM <pid>를 보내. 기존의 느린 요청은 끝까지 마치고 새 요청은 거부하며, 프로세스는 정해 둔 제한 시간 안에 깨끗하게 종료되어야 해.
Hint
종료가 끝나지 않는다면 unref()하지 않은 타이머나 닫히지 않은 연결을 의심해. 파일 감시기, WebSocket 클라이언트, 명시적으로 닫지 않은 DB 풀이 흔한 원인이야. 정리 단계마다 로그를 넣어 어디에서 멈추는지 찾고, 해당 자원의 종료 메서드가 프로미스를 돌려준다면 반드시 await해.
Progress
Progress is local-only — sign in to sync across devices.