~/infra-dev.md
INFRA_DEV

배치 작업은 왜 “전체 재시도 횟수”만으로 부족할까: Kubernetes Indexed Job의 실패 예산 설계

10분 읽기·2026.06.25·출처 4·00
오늘의 질문

수백 개 샤드로 나뉜 배치 작업에서 한 샤드의 빠른 실패가 전체 작업의 재시도 예산을 다 써버리지 않게 하려면 무엇을 설계해야 할까?

infra-dev.md
JOB BUDGET

배치 작업은 왜 “전체 재시도 횟수”만으로 부족할까: Kubernetes Indexed Job의 실패 예산 설계

1. 왜 지금 봐야 하나

작은 팀도 이제 “한 번 실행하고 끝나는 작업”을 많이 운영한다. 매일 새벽 리포트 생성, 고객별 임베딩 재생성, 이미지 썸네일 생성, 데이터 마이그레이션 검증, 대량 이메일 발송, 통합 테스트 매트릭스 같은 일이다. 겉으로는 전부 배치 작업이지만 운영 관점에서는 질문이 다르다.

  • 실패한 한 고객 때문에 전체 작업을 실패 처리해야 할까?
  • 500개 샤드 중 3개만 실패했을 때 나머지 결과를 버려야 할까?
  • 특정 오류 코드는 재시도해도 의미가 없는데 왜 계속 Pod를 다시 만들까?
  • “성공”은 모든 샤드의 성공일까, 필요한 최소 개수의 성공일까?

Kubernetes v1.33에서 Indexed Job의 backoffLimitPerIndex와 Job successPolicy가 GA가 된 흐름은 이 질문에 대한 좋은 학습 재료다. v1.33 릴리스 노트는 두 기능을 모두 Stable로 소개한다. backoffLimitPerIndex는 인덱스별 재시도 한도를 주어 한 인덱스의 실패가 전체 Job을 조기 실패시키지 않도록 하고, successPolicy는 인덱스 기반 Job에서 “어떤 성공이면 전체 작업을 성공으로 볼 것인가”를 명시하게 한다.

오늘의 핵심 문장은 이것이다.

배치 작업의 신뢰성은 재시도를 많이 하는 것이 아니라, 실패 예산과 성공 조건을 작업 단위에 맞게 나누는 데서 시작한다.

2. 핵심 개념

Kubernetes Job은 완료될 때까지 Pod를 만들고 재시도하는 컨트롤러다. 일반적인 Job에서는 .spec.backoffLimit이 전체 Job의 실패 허용 횟수처럼 동작한다. 단일 작업이라면 충분하다. 하지만 Indexed Job에서는 문제가 생긴다.

Indexed Job은 .spec.completionMode: Indexed를 사용해 각 Pod에 0부터 completions - 1까지의 인덱스를 부여한다. 이 인덱스는 “샤드 번호”처럼 쓸 수 있다. 예를 들어 고객 100명에 대해 고객별 리포트를 만들거나, 테스트 스위트 40개를 병렬 실행하거나, 이미지 1만 장을 100개 범위로 나누어 처리할 수 있다.

문제는 모든 인덱스가 같은 실패 원인을 갖지 않는다는 점이다.

  • 7번 인덱스는 입력 데이터가 깨져서 항상 실패한다.
  • 12번 인덱스는 노드 eviction 때문에 일시적으로 실패했다.
  • 18번 인덱스는 권한 오류라서 재시도해도 실패한다.
  • 나머지 인덱스는 정상적으로 끝날 수 있다.

전체 backoffLimit만 있으면 빠르게 실패하는 인덱스 하나가 전체 재시도 예산을 소모할 수 있다. Kubernetes의 Backoff Limit Per Index GA 글도 통합 테스트 suite를 인덱스로 나눈 예를 들며, fast-failing index가 전체 Pod failure 예산을 소비해 다른 인덱스를 실행하지 못하게 할 수 있다고 설명한다.

backoffLimitPerIndex는 이 문제를 인덱스 단위로 나눈다.

zsh — 생존확인.sh
apiVersion: batch/v1
kind: Job
metadata:
  name: report-shards
spec:
  completions: 10
  parallelism: 10
  completionMode: Indexed
  backoffLimitPerIndex: 1
  maxFailedIndexes: 3
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: worker
        image: example/report-worker:2026-06-25

이 설정의 의미는 단순하다.

  • 각 인덱스는 실패를 1번까지 허용한다.
  • 특정 인덱스가 한도를 넘으면 그 인덱스만 실패로 표시된다.
  • 실패 인덱스 수가 maxFailedIndexes를 넘으면 전체 Job을 종료한다.
  • 나머지 인덱스는 기본적으로 계속 실행될 수 있다.

여기에 podFailurePolicy를 붙이면 “재시도할 실패”와 “바로 실패 처리할 실패”를 나눌 수 있다. Kubernetes 블로그 예시는 DisruptionTarget 조건은 무시하고, exit code 42는 FailIndex로 즉시 해당 인덱스를 실패 처리하는 패턴을 보여준다.

3. 최신 이슈와 연결

v1.33에서 같이 봐야 할 두 번째 기능은 successPolicy다. 배치 작업은 항상 “모든 인덱스가 성공해야 성공”인 것은 아니다.

예를 들어 AI/ML 실험에서 100개 파라미터 조합 중 상위 후보 몇 개만 필요할 수 있다. HPC나 simulation 작업에서는 최소 성공 개수만 채우면 다음 단계로 넘어갈 수 있다. leader-follower 패턴에서는 follower 전체의 성공보다 leader 인덱스의 성공이 전체 작업의 의미를 결정할 수 있다.

successPolicy는 이런 성공 조건을 .spec.successPolicy에 적는다.

zsh — 생존확인.sh
apiVersion: batch/v1
kind: Job
metadata:
  name: leader-result-job
spec:
  parallelism: 10
  completions: 10
  completionMode: Indexed
  successPolicy:
    rules:
    - succeededIndexes: 0
      succeededCount: 1
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: worker
        image: example/leader-worker:2026-06-25

Kubernetes SuccessPolicy GA 글은 이 예시처럼 0번 인덱스가 성공하면 Job이 성공 조건을 만족할 수 있다고 설명한다. Success Policy가 충족되면 Job에는 SuccessCriteriaMet 조건이 추가되고, Job controller가 실행 중인 Pod를 정리한 뒤 최종적으로 Complete 조건을 얻는다.

이 지점이 운영적으로 중요하다. 성공 조건을 코드 안에만 숨겨두면, 플랫폼은 작업을 언제 멈춰야 하는지 모른다. 반대로 성공 조건을 Job spec에 드러내면 컨트롤러가 불필요한 Pod를 줄이고, 관측 시스템도 “왜 완료로 처리됐는지”를 더 잘 설명할 수 있다.

정리하면 v1.33의 Indexed Job 관련 변화는 하나의 방향을 가리킨다.

질문예전의 단순 답더 나은 답
실패를 얼마나 재시도할까?Job 전체 backoffLimit인덱스별 backoffLimitPerIndex
실패가 전체를 망쳐도 될까?실패 횟수 초과 시 전체 실패maxFailedIndexes로 전체 실패 임계값 설정
재시도해도 의미 없는 실패는?똑같이 재시도podFailurePolicyFailIndex
성공은 무엇인가?모든 completion 성공successPolicy로 필요한 성공 조건 명시

4. 개발자 관점 해석

개발자는 배치 작업을 작성할 때 보통 “worker 코드를 어떻게 짤까?”부터 생각한다. 하지만 운영 사고는 worker 코드보다 작업의 경계에서 많이 난다. 특히 아래 세 가지가 자주 빠진다.

첫째, 작업 단위의 독립성이다. 인덱스 3이 실패해도 인덱스 4의 결과를 사용할 수 있다면, 이 둘은 같은 실패 예산을 공유하면 안 된다. 고객별 처리, 파일별 처리, 테스트 suite별 처리처럼 독립적인 작업은 전체 재시도 횟수보다 샤드별 재시도 횟수가 더 자연스럽다.

둘째, 오류의 분류다. 모든 실패가 transient failure는 아니다. 네트워크 timeout, node eviction, rate limit은 재시도 가치가 있을 수 있다. 반면 잘못된 입력 스키마, 권한 없음, 존재하지 않는 리소스, 검증 불가능한 파일은 재시도해도 같은 결과가 나올 가능성이 높다. 이런 오류를 exit code나 Pod condition으로 표현하고 podFailurePolicy와 연결해야 불필요한 재시도 비용을 줄일 수 있다.

셋째, 성공 조건의 선언성이다. “상위 1개 결과만 있으면 된다”, “90% 샤드가 성공하면 다음 단계로 간다”, “leader 인덱스가 성공하면 follower는 정리해도 된다” 같은 조건은 README에만 있으면 안 된다. 플랫폼이 이해할 수 있는 spec으로 옮겨야 실제 리소스 절감과 장애 판단으로 이어진다.

이 관점은 AI 시대의 인프라에도 잘 맞는다. 임베딩 재생성, 평가 데이터셋 실행, 모델별 벤치마크, 로그 백필처럼 많은 AI 운영 작업은 대량 병렬 배치다. 이때 한 샘플 묶음이 깨졌다고 전체 eval을 실패시키면 비용과 시간이 낭비된다. 반대로 실패를 너무 관대하게 보면 조용한 데이터 누락이 생긴다. 그래서 필요한 것은 “무한 재시도”가 아니라 명시적인 실패 예산이다.

5. 내 프로젝트에 적용할 체크포인트

아래 질문을 배치 작업마다 적어보자.

인덱스로 나눌 수 있는가

  • 고객, tenant, 파일, 날짜, 테스트 suite, shard id처럼 자연스러운 분할 키가 있는가?
  • 한 인덱스의 실패가 다른 인덱스의 결과 신뢰성을 깨뜨리지 않는가?
  • 인덱스별 입력과 출력 위치가 분리되어 있는가?
  • 같은 인덱스를 재실행해도 같은 출력 경로를 안전하게 덮어쓰거나 멱등 처리하는가?

실패를 분류했는가

  • timeout, eviction, temporary 5xx, rate limit처럼 재시도 가능한 실패를 구분했는가?
  • validation error, permission denied, missing source처럼 재시도해도 의미 없는 실패를 구분했는가?
  • 재시도 불가능한 실패가 별도 exit code로 종료되는가?
  • podFailurePolicy로 특정 condition이나 exit code를 Ignore, FailJob, FailIndex 중 무엇으로 처리할지 정했는가?

실패 예산을 숫자로 정했는가

  • 인덱스 하나가 몇 번까지 실패해도 되는가?
  • 전체 인덱스 중 몇 개까지 실패해도 다음 단계로 갈 수 있는가?
  • 실패 인덱스를 다시 처리하는 별도 catch-up Job이 있는가?
  • 실패 인덱스 목록을 status.failedIndexes나 로그에서 회수할 수 있는가?

성공 조건을 선언했는가

  • 모든 인덱스 성공이 정말 필요한가?
  • 최소 성공 개수나 특정 leader 인덱스 성공만으로 충분한가?
  • 성공 조건이 애플리케이션 코드 안에만 있지 않은가?
  • 성공 조건 충족 후 남은 Pod가 정리되어도 안전한가?

6. 오늘 10분 액션

10분만 있다면 새 기능을 바로 도입하지 않아도 된다. 먼저 기존 배치 작업 하나를 골라 아래 표를 채운다.

항목내 작업의 답
자연스러운 인덱스 키예: customer_id, date, shard_number
인덱스별 최대 재시도예: 1회 또는 2회
전체 허용 실패 인덱스예: 100개 중 3개
즉시 실패 처리할 오류예: schema validation, permission denied
성공 조건예: 전체 성공, 95% 성공, leader index 성공
실패 인덱스 재처리 방식예: 다음 CronJob, 수동 rerun, DLQ 테이블

그 다음 Kubernetes를 쓰고 있다면 아래 manifest 초안을 만들어본다.

zsh — 생존확인.sh
spec:
  completionMode: Indexed
  completions: 100
  parallelism: 10
  backoffLimitPerIndex: 1
  maxFailedIndexes: 3
  podFailurePolicy:
    rules:
    - action: Ignore
      onPodConditions:
      - type: DisruptionTarget
    - action: FailIndex
      onExitCodes:
        operator: In
        values: [42]

오늘의 목표는 YAML을 배포하는 것이 아니다. “우리 배치 작업의 실패 예산과 성공 조건을 말로 설명할 수 있는가?”를 확인하는 것이다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 infra_dev 글을 확인했다. 최근 글은 AI endpoint 비용 방화벽, OIDC audience/replay 방지, GitHub Actions VNET failover, AI incident response guardrail, EventBridge 로깅, Lambda durable functions, CI SaaS 장애, Cloudflare Queues backlog/DLQ, PostgreSQL 관측, Docker 보안, GKE capacity buffer, OpenTelemetry Collector, Cloud Run worker backpressure, Kubernetes Job suspend와 리소스 재조정을 다뤘다. 이번 글은 Kubernetes 배치 작업을 다시 다루지만, suspend/리소스 조정이나 큐·워커 backpressure가 아니라 Indexed Job의 인덱스별 실패 예산, 재시도 불가능 오류의 FailIndex, 그리고 successPolicy로 성공 조건을 선언하는 운영 설계에 집중해 중복을 피했다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)