pile·
인프라 / DevOps·쏘카쏘카·

Node.js 컨테이너, 왜 깔끔하게 안 죽을까? (feat. Graceful Shutdown)

Kubernetes 환경에서 Node.js 배치 컨슈머가 SIGTERM을 받아도 깔끔하게 종료되지 않는 문제를 단계별로 해결한 경험이다. PID 1의 시그널 무시 동작, 이벤트 루프와 프로세스 종료의 차이, dumb-init 도입까지 원인을 하나씩 파악하고 최종 해결책을 찾아간 과정을 기술한다.

핵심 포인트
  • Linux 커널은 PID 1에 핸들러 없으면 시그널 무시(일반 프로세스는 기본 동작으로 종료) — Node.js가 PID 1이면 SIGTERM 무시
  • dumb-init을 ENTRYPOINT로 설정해 시그널 전파·좀비 프로세스 정리를 전용 init 시스템에 위임
  • NestJS onModuleDestroy에서 Promise.race로 타임아웃 구현해도 패배한 batchPromise의 비동기 작업이 이벤트 루프에 남아 프로세스 유지
  • AbortController 강제 중단 대신 "시작된 배치는 완료 보장" 전략 채택 — DB 트랜잭션 정합성 우선
  • K8s terminationGracePeriodSeconds를 앱 타임아웃보다 반드시 길게 설정 (앱 120s < K8s 180s)
상세 정리
  • 증상: 배치 일부만 반영된 채 종료, 재현 불가, 배포 타이밍+종료 시그널+graceful shutdown 복합 원인 의심
  • PID 1 원리: 커널 설계 "Global init gets no signals it doesn't want" — 핸들러 없는 PID 1은 SIGTERM 무시
  • NestJS enableShutdownHooks()로 핸들러 등록 시 PID 1도 SIGTERM 수신, 그러나 좀비 프로세스 정리와 자식 프로세스 시그널 전파는 여전히 미해결
  • dumb-init 도입: apt-get install dumb-init 후 ENTRYPOINT ["/usr/bin/dumb-init", "--"], CMD ["node", "dist/main"]
  • npm start 대신 node dist/main 직접 실행 권장: npm이 PID 1이 되어 시그널 가로채는 문제 방지
  • 타임아웃 함정 실제 관찰: 16:07:20 SIGTERM → 16:09:20 2분 타임아웃(훅 return) → 16:11:56 배치 완료 후 실제 종료
  • 원인: Promise.race에서 타임아웃이 이겨도 batchPromise 내 await sleep() 작업이 이벤트 루프에 남아 Node.js 프로세스 유지
  • AbortController 미채택: 모든 await 지점마다 중단 체크 필요, DB 트랜잭션 라이브러리가 abort 안전 처리 못할 가능성
  • 최종 구현: Promise.allSettled(this.runningBatches) + 120s 타임아웃, K8s terminationGracePeriodSeconds=180s
  • 핵심 교훈: "return만으로 프로세스가 종료되지 않는다. 비동기 작업이 이벤트 루프에 남아 있으면 프로세스는 살아있다"
왜 읽나K8s Node.js 서비스의 "종료됐다고 생각했지만 실제로 살아있는" 문제를 마주치기 전에 읽으면 좋다. PID 1 원리부터 이벤트 루프 종료 조건, K8s Grace Period 설정까지 인프라·앱 레이어를 아우르는 체크리스트를 제공한다.
쏘카
쏘카 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 인프라 / DevOps·cloudflare-blogCloudflare Blog·

    대규모 도그푸딩: cdnjs를 Cloudflare 개발자 플랫폼으로 이전하다

    cdnjs는 하루 90억 요청을 처리하는 오픈소스 JS/CSS CDN으로, 전 세계 웹사이트의 12%가 의존한다. 2026년 6월, Cloudflare는 6년간 GCP에서 운영하던 발행 파이프라인을 Workers, Workflows, R2, KV, Queues, Containers, Durable Objects로 완전 전환했다. 성능 문제가 아닌 관측성·유지보수성 한계가 동인이었으며, 마이그레이션 과정에서 플랫폼 한도를 직접 끌어올렸다.

    #durable-objects#workflows#cloudflare-workers+2
  2. 인프라 / DevOps·github-engGitHub Engineering·

    Dependabot 길들이기: 업데이트 묶고, 주기 늦추고, 보안은 빠르게

    GitHub의 Dependabot은 의존성 업데이트를 자동화하지만, 기본 설정에서는 10개 업데이트 = 10개 PR이 쏟아져 메인테이너가 알림을 무시하게 된다. Microsoft의 GCToolkit 사례(578개 커밋 중 92개가 Dependabot 버전 범프)를 바탕으로, 그룹핑·주기 조절·보안 업데이트 분리 3가지 설정으로 노이즈를 줄이면서 보안 속도는 유지하는 방법을 다룬다.

    #github-actions#devops#supply-chain-security+2
  3. 인프라 / DevOps·cloudflare-blogCloudflare Blog·

    Cloudflare, 프라이버시 프로토콜 디버깅 CLI pvcli 오픈소스 공개

    Cloudflare가 OHTTP(Oblivious HTTP) 등 프라이버시 보존 프로토콜 디버깅을 위한 CLI 도구 pvcli를 오픈소스(Apache-2.0)로 공개했다. 기존에는 RFC를 참조하며 바이너리를 수작업으로 파싱해야 했는데, pvcli는 curl과 유사한 문법으로 이 과정을 단일 명령으로 줄인다. Apple Private Relay·Microsoft Edge Secure Network를 구동하는 실전 규모의 경험에서 나온 도구다.

    #rust#cli#ohttp+2