Kubernetes Job, backoffLimit만 쓰면 OOM 한 번에 재시도 6번이 따라온다

오늘 알게 된 건데, 의외로 Job spec에서 podFailurePolicy 안 쓰는 팀이 꽤 많더라. 같이 일하는 분이 "야 우리 배치가 새벽에 6번 OOM 나고 죽었는데 알람이 한 번에 6번 왔어"라고 메시지를 보내서 들여다봤다. 코드는 멀쩡한데 메모리 한도가 빡빡했고, backoffLimit: 6만 박혀 있었다. 그게 다였다.
근데 이게 별거 아닌 것 같아도 비용/알람/멘탈 다 갉아먹는다. 잠깐만 짚고 가자.
backoffLimit만 있을 때 무슨 일이 벌어지나
Job spec이 이렇게만 돼 있다고 치자.
apiVersion: batch/v1
kind: Job
metadata:
name: nightly-report
spec:
backoffLimit: 6
template:
spec:
restartPolicy: Never
containers:
- name: report
image: reporter:1.4
resources:
limits:
memory: 512Mi
이러면 컨테이너가 어떤 이유로 죽든 — OOMKill이든, 노드 preemption이든, ImagePullBackOff에서 끝까지 못 받아오든 — Pod 실패 카운터가 1씩 올라간다. 7번째 실패에서야 Job이 Failed 상태로 굳는다. 사실 OOM 같은 결정론적 실패는 한 번 보고 멈췄어야 했다. 메모리 부족하면 재시도해도 또 부족하다. 그게 정의다.
반대로 노드가 spot으로 evict 당해서 죽은 거라면 그건 인프라 사정이지 코드 사정이 아니다. 이런 건 카운터에서 빼주는 게 맞다. backoffLimit 하나로 이 둘을 구분할 방법이 없다.
podFailurePolicy로 쪼개기
K8s 1.31부터 GA된 podFailurePolicy 필드를 같이 박아주면 이렇게 분리할 수 있다.
spec:
backoffLimit: 6
podFailurePolicy:
rules:
- action: FailJob
onExitCodes:
operator: In
values: [137] # SIGKILL — OOM 포함
- action: Ignore
onPodConditions:
- type: DisruptionTarget # preemption/eviction
template:
spec:
restartPolicy: Never
...
규칙은 두 줄이지만 의미는 크다. 첫째 규칙은 "OOM처럼 137로 죽으면 더 시도하지 말고 Job을 즉시 실패시켜라". 둘째는 "노드 disruption 때문에 죽었으면 카운터에서 빼라, 그건 네 잘못 아니다". 이 둘만 박아도 야간 알람이 절반으로 줄어든다. 진짜로.
조금 더 욕심내면 exit code 별로 동작을 다르게 줄 수도 있다. 예를 들어 1번은 코드 버그라 즉시 FailJob, 42번은 일시적 외부 API 장애라 Count(재시도)로 보내는 식.
헷갈리기 쉬운 포인트 하나
restartPolicy: OnFailure로 두면 podFailurePolicy 규칙이 평가되기 전에 컨테이너가 그냥 같은 Pod에서 재시작돼 버린다. 그러면 OOM이 카운트에도 안 잡힌다. 실제로는 죽고 살고를 반복하는데 Job은 멀쩡해 보인다. 더 무섭다. podFailurePolicy 쓰려면 무조건 restartPolicy: Never다. 이건 옵션이 아니라 요구사항.
다음에는 podReplacementPolicy 쪽도 한 번 짚어보려고 한다. Failed 상태 Pod가 Terminating 중인데 새 Pod가 동시에 떠서 동시 실행이 일어나는 케이스 — 이게 또 사람 잡는다.