본문 바로가기
Q&A2026년 7월 6일1분 읽기

쿠버네티스에서 Node 파드가 새벽마다 OOMKilled 됩니다

d
devops_taeho
조회 1533댓글 6

운영 중인 Node API 파드가 새벽 3~4시쯤 되면 재시작돼 있습니다. kubectl describe pod로 보면 Last State: Terminated, Reason: OOMKilled가 찍혀 있어요. 메모리 limit은 512Mi로 잡았습니다. 낮에는 트래픽이 더 많은데 멀쩡하고 왜 하필 한가한 새벽에 죽는지 모르겠습니다.

댓글 6

김김영삼3개월 전

"한가한 새벽에 죽는다"가 결정적 힌트입니다. 트래픽이 아니라 새벽에 도는 무언가(배치·크론)가 메모리를 순간적으로 크게 쓰는 패턴이 거의 확실해요.

먼저 확인할 것
  1. 새벽에 도는 작업이 있나? — 정산·리포트·대용량 조회·로그 정리 같은 크론잡. 이런 게 한 번에 큰 배열을 메모리에 올리면 순간 피크가 512Mi를 넘겨 OOMKilled 됩니다.
  2. 메모리 추이 그래프 — Grafana 등에서 파드 메모리를 보면 새벽에 톱니처럼 치솟는 지점이 보일 겁니다.
흔한 원인 — 대량 데이터를 한꺼번에 로드
// ❌ 수십만 행을 통째로 메모리에
const rows = await db.query('SELECT * FROM orders')
rows.forEach(process)

// ✅ 스트리밍 / 배치로 나눠서
for await (const row of db.queryStream('SELECT * FROM orders')) {
process(row) // 한 번에 하나씩, 메모리 일정
}
당장의 완화
  • limit을 순간 피크에 맞게 올리기(예: 512Mi → 768Mi). 단, 근본 원인 놔두고 올리면 더 큰 배치에서 또 터집니다.
  • Node 힙 상한을 컨테이너 limit보다 낮게: --max-old-space-size=384 (512Mi 컨테이너 기준). 이러면 OOMKilled(강제 종료) 대신 Node가 먼저 GC를 돌려 버티거나 명시적 에러를 냅니다.
ddevops_taeho(guest)3개월 전
맞아요, 새벽 3시에 전날 주문을 CSV로 뽑는 크론잡이 있습니다. 이걸 스트리밍으로 바꾸긴 할 건데, 그 사이 급한 대로 이 배치만 메모리를 더 쓰게 할 수는 없나요? API 파드 전체 limit을 올리긴 부담됩니다.
김김영삼3개월 전

아주 좋은 판단입니다. 배치를 API 파드와 분리하는 게 정답이에요. 같은 파드에서 돌리니 API가 배치 메모리에 휩쓸려 죽는 거니까요.

  • CronJob 리소스로 분리 — 배치를 별도 CronJob으로 빼고, 거기만 메모리를 넉넉히 줍니다. API 파드는 512Mi 그대로 두고요.
    apiVersion: batch/v1
    kind: CronJob
    metadata:
    name: order-export
    spec:
    schedule: "0 3 * * *"
    jobTemplate:
    spec:
    template:
    spec:
    containers:
    - name: export
    image: my-app:latest
    command: ["node", "scripts/export-orders.js"]
    resources:
    limits:
    memory: 1Gi # 배치만 넉넉히
    restartPolicy: OnFailure

이러면 배치가 무거워져도 API 서비스는 안전하고, 배치 파드는 작업 끝나면 사라져서 평소 자원도 안 씁니다. API와 배치의 자원·장애를 분리하는 건 운영에서 아주 중요한 원칙이에요.

ddevops_taeho(guest)3개월 전
CronJob으로 뺐는데, 가끔 이 잡이 실패하면 다음 날 것과 겹쳐서 두 개가 동시에 도는 경우가 생기더라고요. 이건 어떻게 막나요?
김김영삼3개월 전

동시 실행 방지는 concurrencyPolicy로 잡습니다.

spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid # 이전 잡이 안 끝났으면 새 잡 건너뜀
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
  • Forbid — 이전 실행이 아직 돌고 있으면 이번 스케줄을 스킵합니다. 겹침 방지에 딱이에요.
  • Replace로 하면 이전 걸 죽이고 새로 시작합니다(최신 실행이 중요할 때).
  • startingDeadlineSeconds로 너무 늦게 시작되는 잡은 아예 건너뛰게 합니다.

대개 Forbid가 안전합니다. 그리고 잡 자체를 멱등하게(같은 날 두 번 돌아도 결과가 같게) 짜두면 겹쳐 돌아도 데이터가 안 깨져서 더 든든합니다.

ddevops_taeho(guest)3개월 전
CronJob 분리 + Forbid로 완벽하게 해결됐습니다. 새벽 알람에서 해방됐어요 ㅠㅠ 배치랑 API를 같은 파드에 둔 게 애초에 문제였네요. 자세한 매니페스트까지 감사합니다!
Ctrl+Enter로 등록