CKAD — 쿠버네티스 애플리케이션 개발자 · 배치는 성공했는데 택배는 두 번 나갔다 · 실습
배송 중복 사건 — Job 성공과 업무 성공은 다르다
목표
한 주문을 처리하는 Job이 성공했는데 배송은 두 번 만들어지는 사건을 재현하고, 재시도·멱등 키·실행 기한을 근거로 설명합니다.
왜 중요한가
completions와 parallelism이 1이어도 외부 업무가 한 번만 처리된다는 보장은 아닙니다. 실패한 프로세스가 이미 저장을 끝냈을 수 있기 때문입니다. Kubernetes가 관리하는 실행 수명과 원장이 관리하는 업무 수명을 분리해서 관측해야 재실행 여부를 판단할 수 있습니다.
개인 VM의 실제 k3s와 HTTP·SQLite 합성 원장을 사용합니다. 실제 배송·결제·개인정보는 없습니다. 원장의 safe 모드는 서버의 쓰기 트랜잭션과 업무 키 유일성으로 중복 행을 합칩니다. 이 실습은 그 계약의 적용과 검증을 다루며 분산 시스템 전체의 정확히 한 번 처리를 보장하지 않습니다. 원장 Pod를 지우면 데이터도 사라집니다.
처음 준비에 최대 6분이 허용됩니다. 모든 파일은 /root/ckad-job-effects 아래에 둡니다. 세션 종료 시 작업은 사라지므로 필요한 기록은 미리 내려받으세요. 실습은 55분 기준이며 더 필요하면 만료 전에 시간을 연장하세요.
사용하는 틀과 실행 도우미
job-template.json은 실제 worker 명령과 고정 이미지, 권한 제한이 포함된 Job JSON입니다. 단계마다 복사하여 metadata.name, spec.backoffLimit, 컨테이너 env의 CASE_ID·MODE·FAULT 값을 바꿉니다. Pod 틀의 spec.template.metadata.labels에 있는 labhub.io/case도 해당 CASE_ID와 같은 값으로 바꿉니다. WORK_ID는 order-001, POD_UID는 Downward API로 유지합니다. parallelism·completions는 1, restartPolicy는 Never를 유지합니다. 다른 보안·앱 설정도 변경하지 않습니다. 기한이 필요한 단계에만 spec.activeDeadlineSeconds를 추가합니다.
직접 kubectl create로 실행하지 말고 파일 작성 후 python3 /opt/fixtures/ckad_job_effects_lab.py run <단계>를 실행합니다. 도우미는 학생 파일을 그대로 API로 제출하고 실행 중 Pod 관측을 보존합니다. 이미 완료된 같은 단계를 다시 실행하면 새 Job을 만들지 않고 채점만 합니다. 관측 대기가 중단되면 같은 명령으로 이어가며 Job을 삭제·재생성하지 않습니다. 생성 응답 자체를 잃은 경우에는 중복 생성 대신 새 실습을 요구합니다.
단계
1. baseline.json: name=baseline, CASE_ID=baseline, MODE=unsafe, FAULT=none, backoffLimit=0. run 1로 정상 비교군을 관측합니다.
2. unsafe-retry.json: name=unsafe-retry, CASE_ID=unsafe-retry, MODE=unsafe, FAULT=after-commit, backoffLimit=1. run 2로 저장 후 실패와 대체 Pod를 관측합니다.
3. observation-2.json을 읽어 retry-analysis.json에 job_uid, failed_pod_uid, succeeded_pod_uid, request_count, shipment_count, retry_layer를 적습니다. retry_layer는 job-controller 또는 kubelet-container 중 관측에 맞는 값입니다. run 3으로 분석을 기록합니다.
4. safe-retry.json: name=safe-retry, CASE_ID=safe-retry, MODE=safe, FAULT=after-commit, backoffLimit=1. run 4로 같은 실패에서 멱등 결과를 확인합니다.
5. safe-replay.json: name=safe-replay, CASE_ID=safe-retry, MODE=safe, FAULT=none, backoffLimit=0. run 5로 새 Job에서도 같은 업무 키가 이어지는지 봅니다.
6. no-retry.json: name=no-retry, CASE_ID=no-retry, MODE=unsafe, FAULT=after-commit, backoffLimit=0. run 6으로 실패해도 배송이 남는지 봅니다.
7. deadline.json: name=deadline, CASE_ID=deadline, MODE=safe, FAULT=deadline, backoffLimit=0, activeDeadlineSeconds=20. run 7로 기한 초과와 원장을 관측합니다. 현재 Pod 목록이 비어도 observed_pods의 과거 관측을 지우지 않습니다.
8. 여섯 실행을 비교해 final-analysis.json을 씁니다. request_count·shipment_count는 원장 전체 합계, safe_replay_shipment_id는 재실행에서 유지된 배송 번호입니다. timeout_cancels_effect와 ledger_survives_ledger_pod_replacement는 각각 기한 초과가 업무를 취소하는지, 원장 Pod 교체에도 데이터가 보존되는지의 불리언입니다. idempotency_scope는 실제 중복 판정에 쓰는 필드 이름을 +로 이어 적습니다. outcomes에는 여섯 Job 이름을 키로 두고 condition(Complete/Failed), shipments(각 실행 직후 해당 업무의 배송 수)를 적습니다. run 8로 종합 분석을 기록합니다.
참고
- 현재 API와 원장은
python3 /opt/fixtures/ckad_job_effects_lab.py observe로 조회할 수 있습니다. 정답 보고서를 생성하지는 않습니다. - observation-N.json은 도우미가 기록한 자료입니다. pods는 수집 시점 목록, observed_pods는 실행 중 보존한 마지막 Pod 관측입니다. 기한 초과로 삭제된 Pod의 과거 Running을 현재 상태나 종료 코드로 해석하지 마세요.
- 잘못 작성한 학생 입력 파일은 실행 전에 고칠 수 있습니다. 완료된 단계의 파일과 관측은 보존하세요. 같은 이름의 Job을 삭제하면 원장 이력과 실행의 연결이 끊어집니다.
- 단계 준비 기능은 서로 다른 업무의 선행 Job을 동시에 실행할 수 있습니다. 기록 순서는 학습 순서이며 실제 시작 순서는 아닙니다. 관측 원장에 다른 사례의 행이 먼저 나타날 수 있으므로 CASE_ID로 나누어 비교하세요. 같은 업무의 safe-replay는 safe-retry 관측이 저장된 다음에만 실행합니다. 준비가 중단되면 안내된 같은 prepare 명령을 재개하며 현재 단계의 답안은 만들지 않습니다.
- backoffLimit=0이나 Job 삭제는 업무 취소가 아닙니다. 원장 행을 손으로 지워 숫자를 맞추지 마세요.
단계 8개
- 정상 배송 비교군
- 성공했는데 두 건이다
- 범인은 어느 재시도인가
- 같은 실패, 한 건의 배송
- 다른 Job으로 다시 실행
- 재시도를 꺼도 남는 것
- 시간이 끝나도 남는 것
- 배송 사고 종합 보고서