CKAD — 쿠버네티스 애플리케이션 개발자 · 배치는 성공했는데 택배는 두 번 나갔다 · 이론
배치는 성공했는데 택배는 두 번 나갔다
한 줄 요약
Job이 Complete라는 것은 지정한 실행 완료 조건을 충족했다는 뜻이지, 외부 시스템의 업무가 정확히 한 번 처리됐다는 증명은 아닙니다.
왜 이게 필요했나
주문 한 건을 받아 배송 접수를 만드는 배치가 있습니다. 개발자는 completions와 parallelism을 모두 1로 설정하고 restartPolicy도 Never로 골랐습니다. 실행 화면에는 성공한 Pod 하나와 Complete가 보입니다. 그런데 원장에는 같은 주문의 배송이 두 건입니다. 숫자 1을 세 군데 적었는데 어떻게 이런 일이 생겼을까요?
프로그램의 마지막 줄과 외부 시스템의 저장 시점은 같은 사건이 아닙니다. 첫 Pod가 배송 API에 요청을 보냈고 서버는 새 배송을 저장했습니다. 응답을 받은 뒤 프로그램이 종료 코드 0을 남기기 전에 오류로 끝납니다. Job 컨트롤러는 실패한 실행을 보고 새 Pod를 만듭니다. 새 Pod는 같은 주문을 다시 처리하고 이번에는 정상 종료합니다. 컨트롤러 입장에서는 해야 할 일을 끝냈지만 외부 원장에는 두 번의 저장이 남을 수 있습니다.
이 실습의 배송은 SQLite에 행을 추가하는 합성 서비스입니다. 실제 주소·개인정보·배송사 API를 쓰지 않습니다. 작고 통제 가능한 부작용을 사용해야 실패 원인을 읽는 연습에 집중할 수 있습니다.
어떻게 동작하나
먼저 실행 주체를 구분합니다. Job은 필요한 완료 수를 관리하는 컨트롤러의 대상입니다. Pod는 작업 프로그램을 담는 실행 단위입니다. 컨테이너 안의 프로세스가 끝나면 종료 코드가 남습니다. 외부 업무 시스템은 자기 저장소에 요청의 결과를 남깁니다. 어느 한 층의 성공을 다른 층의 성공으로 바꿔 읽으면 원인을 놓칩니다.
parallelism은 동시에 실행하도록 원하는 Pod 수와 관계가 있고, completions는 필요한 성공 수와 관계가 있습니다. 둘 다 외부 API의 트랜잭션 횟수를 세지 않습니다. restartPolicy: Never는 같은 Pod 안의 실패 컨테이너를 kubelet이 다시 실행하지 않는 선택입니다. Job 컨트롤러가 대체 Pod를 만드는 것까지 금지하는 스위치가 아닙니다.
관측할 때 이름만 보지 말고 UID와 소유 관계를 확인합니다. 두 Pod가 같은 Job UID를 ownerReferences로 가리키고, 각각의 restartCount는 0이며 하나는 Failed, 다른 하나는 Succeeded라면 Pod 내부 재시작과 Job 재시도를 구별할 근거가 됩니다. 실패한 Pod의 종료 코드와 로그도 보존해야 저장 전 실패인지 저장 후 실패인지 좁힐 수 있습니다.
kubectl get jobs,pods -n ckad-job-effectskubectl get pod -n ckad-job-effects <파드이름> -o jsonkubectl logs -n ckad-job-effects <파드이름>하지만 로그의 성공 문장 하나만으로 배송을 세지는 않습니다. 서버에서 요청 시도 목록과 배송 행 목록을 따로 조회합니다. 요청 시도에는 어떤 Pod가 어떤 업무 키를 보냈는지가 남고, 배송 행에는 실제 부작용이 남습니다. 요청 두 번이 배송 두 건을 만들었는지, 같은 배송 번호를 돌려받았는지를 비교할 수 있어야 합니다.
현장에서 만나는 모습
재시도는 오류를 숨기기 위한 기능이 아닙니다. 일시적인 실패를 견디려면 필요하지만, 다시 실행해도 안전한 작업인지 판단하는 책임은 사라지지 않습니다. 이메일 발송, 파일 변환 결과 등록, 재고 차감, 외부 주문 접수도 같은 경계 문제를 만납니다. 상대 시스템의 저장 여부를 확인하기 어려운 구간이 특히 중요합니다.
backoffLimit을 0으로 바꾸면 재시도 수는 줄일 수 있습니다. 그렇다고 이미 기록된 배송이 취소되는 것은 아닙니다. 첫 Pod가 저장 후 실패하면 Job은 실패인데 배송은 한 건 존재할 수 있습니다. 운영자가 실패한 Job을 보고 새 Job을 수동 생성한다면 같은 부작용을 다시 만들 가능성도 남습니다. 실패 상태를 보고 무조건 재실행하기 전에 업무 키로 원장을 조회하는 이유입니다.
activeDeadlineSeconds 역시 실행에 허용한 시간이지 외부 저장의 유효 기간이 아닙니다. 저장 후 대기하는 프로그램이 시간 제한으로 종료돼도 이미 만들어진 행은 자동으로 사라지지 않습니다. 실행 취소, 업무 취소, 보상 처리는 서로 다른 설계입니다. 실습에서는 이 차이를 관측만 하며 임의의 보상 삭제를 수행하지 않습니다.
관측 시점도 중요합니다. 기한 초과 뒤 컨트롤러가 작업 Pod를 삭제하면 나중에 get pods만 실행해서는 그 실행을 찾지 못할 수 있습니다. 실험 중 수집한 Pod UID·소유 Job·컨테이너 실행 정보와 원장의 요청을 연결하고, 현재 Job의 실패 이유를 별도로 확인합니다. 삭제 전 Running 관측을 종료 코드로 바꾸어 기록하거나, Pod가 안 보인다는 이유로 요청도 없었다고 판단하지 않습니다. 재채점에는 수집 시점을 구분한 보존 자료가 필요합니다.
다음 교재에서 할 것
이어지는 교재에서는 재실행을 막는 대신 같은 업무를 안전하게 반복하는 계약을 설계합니다. 실행 식별자와 업무 식별자를 먼저 구분하세요. 이후 실습에서는 정상 Job을 비교군으로 남기고 저장 직후 실패하는 Job을 실행하며, Job 조건·실제 Pod UID·종료 코드·요청 횟수·배송 행 수를 함께 기록합니다. Complete인데 두 건인 경우와 Failed인데 한 건인 경우를 각각 설명하는 것이 목표입니다.
공식 문서와 적용 범위
- [Job의 Pod 실패와 재시도](https://kubernetes.io/docs/concepts/workloads/controllers/job/#handling-pod-and-container-failures)
- [Job 종료와 정리](https://kubernetes.io/docs/concepts/workloads/controllers/job/#job-termination-and-cleanup)
이 교재의 원장 구조와 장애 주입은 독립적인 학습 예제입니다. 모든 외부 시스템에 같은 저장 모델이 있다고 가정하지 않으며, 시험 문제의 복제나 CKAD 전체 범위의 대체물이 아닙니다.