웹 개발
Node.js 작업 큐가 조용히 멈췄을 때 heartbeat를 추가한 방법
작업 큐의 프로세스는 살아 있지만 소비가 멈춘 장애를 heartbeat와 지표로 발견한 경험입니다.
프로세스가 살아 있다는 거짓 안도감
이미지 변환 작업이 몇 시간씩 밀렸는데 systemd 상태는 여전히 active (running)이었다. 워커 프로세스가 죽은 것은 아니고, 외부 스토리지 요청이 끝나지 않는 Promise를 기다리면서 소비 루프가 진행되지 않은 상태였다. CPU와 메모리도 평소와 비슷해 일반적인 서버 모니터링으로는 알아채기 어려웠다.
큐 길이만 보면 요청이 없는 날과 멈춘 날을 구분하기 어렵다는 점이 헷갈렸다.
작업 수와 생존 신호를 따로 기록하기
워커가 작업을 받거나 완료할 때 카운터를 올리고, 소비 루프가 실제로 순환할 때 heartbeat 시간을 갱신했다. 핵심은 프로세스의 PID가 아니라 “마지막으로 일을 진전시킨 시각”이었다.
let lastProgressAt = Date.now()
async function consume() {
while (true) {
const job = await queue.take()
lastProgressAt = Date.now()
await processJob(job)
metrics.completed.inc()
lastProgressAt = Date.now()
}
}
setInterval(() => {
metrics.workerHeartbeat.set(Date.now() - lastProgressAt)
}, 5000)
실제 코드에서는 queue.take()가 무한 대기하더라도 heartbeat를 별도 주기로 갱신해 “대기 중”과 “처리 중 정지”를 구분했다. 작업 처리에는 외부 요청 타임아웃도 추가했다.
알림 조건을 큐 길이 하나로 두지 않기
다음 두 조건을 함께 보도록 알림을 만들었다.
- 대기 작업이 일정 개수 이상이다.
- 최근 완료 시간이 임계값보다 오래됐다.
queue_waiting > 20 AND worker_last_completed_age > 10m
작업이 없는 새벽에는 오탐이 나지 않고, 큐가 짧아도 한 작업이 멈춘 경우를 잡을 수 있다. 재시작은 마지막 수단으로 두고, 먼저 타임아웃과 실패 원인을 로그에 남기도록 했다.
운영 전에 확인할 항목
- 외부 HTTP, DB 호출마다 유한한 timeout이 있는가
- 처리 중인 작업을 재시도해도 중복 부작용이 없는가
- heartbeat가 단순 프로세스 생존과 구별되는가
- 알림이 “대기량”과 “최근 진전”을 함께 보는가
워커를 감시할 때 active라는 한 줄 상태만 믿으면 늦는다. 실제로 작업이 앞으로 나아가고 있는지를 나타내는 작은 지표 하나가 장애 발견 시간을 크게 줄여 줬다.