근묵자흑
Kubernetes Pattern: Periodic Job 본문
Periodic Job 패턴이란?
쿠버네티스 패턴 책의 8장에서 다루는 Periodic Job 패턴은 Batch Job 패턴을 확장하여 시간적 요소를 추가한 것입니다. 쉽게 말해, 리눅스의 cron처럼 정해진 시간에 자동으로 작업을 실행하는 패턴입니다. 이를 통해 매일 새벽 3시에 데이터베이스를 백업하거나, 매시간 외부 API에서 데이터를 가져오는 등의 작업을 자동화할 수 있습니다.
왜 주기적 작업이 필요한가?
책에서는 현대 시스템이 실시간 이벤트 기반으로 동작하는 추세임에도 불구하고, 여전히 정기적인 작업 스케줄링이 필요한 이유를 설명합니다.
예를 들어, 여러분이 온라인 쇼핑몰을 운영한다고 생각해보세요. 실시간으로 주문을 처리하는 것도 중요하지만, 다음과 같은 정기적인 작업들도 필요합니다:
- 매일 새벽: 데이터베이스 백업, 로그 파일 정리
- 매주 월요일: 주간 매출 리포트 생성
- 매월 말일: 월별 정산 처리, 오래된 데이터 아카이빙
전통적인 방법들의 문제점을 책에서는 이렇게 지적합니다:
- 전문 스케줄링 소프트웨어: Control-M, AutoSys 같은 도구들은 너무 비싸고 복잡합니다.
- 단일 서버의 cron: 서버가 죽으면 모든 스케줄이 멈춥니다. 백업 서버를 만들기도 어렵습니다.
- 애플리케이션 내장 스케줄러: Java의 Quartz나 Spring Batch를 사용하면 애플리케이션이 복잡해지고, 고가용성을 구현하려면 리더 선출 같은 분산 시스템 문제를 직접 해결해야 합니다.
Kubernetes CronJob
Kubernetes의 CronJob은 이러한 문제들을 플랫폼 레벨에서 해결합니다. 개발자는 "무엇을" 실행할지만 정의하면, "언제" 그리고 "어떻게" 실행할지는 Kubernetes가 알아서 처리합니다.
아래 예제를 살펴보겠습니다:
apiVersion: batch/v1
kind: CronJob
metadata:
name: random-generator
spec:
schedule: "*/3 * * * *" # 3분마다 실행
jobTemplate:
spec:
template:
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
command: ["java", "RandomRunner", "/numbers.txt", "10000"]
restartPolicy: OnFailure
이 간단한 YAML 파일로 3분마다 자동으로 실행되는 작업을 만들 수 있습니다. 서버가 재시작되어도, 노드가 교체되어도 Kubernetes가 알아서 스케줄을 유지합니다.
CronJob의 핵심 필드 이해
.spec.schedule
이 필드는 "언제 실행할지"를 정의합니다. Unix의 crontab과 동일한 형식을 사용합니다. 예를 들어 "0 3 * * *"는 "매일 새벽 3시 정각"을 의미합니다. @daily, @hourly 같은 읽기 쉬운 단축어도 사용할 수 있습니다.
.spec.startingDeadlineSeconds
이 필드는 "늦어도 언제까지는 시작해야 한다"는 데드라인을 설정합니다. 예를 들어, 새벽 3시에 실행해야 하는 백업 작업이 있는데 서버 문제로 3시에 시작하지 못했다고 가정해봅시다. startingDeadlineSeconds: 3600으로 설정했다면, 4시까지는 늦게라도 실행하지만 4시가 넘으면 이번 실행은 건너뜁니다.
책에서는 특히 "10초 미만으로 설정하면 안 된다"고 강조하는데, 이는 Kubernetes가 10초마다 Job 상태를 확인하기 때문입니다.
.spec.concurrencyPolicy
이 필드는 "이전 작업이 아직 실행 중일 때 새 작업을 어떻게 처리할지"를 결정합니다:
- Allow (기본값): 이전 작업과 상관없이 새 작업을 시작합니다. 독립적인 작업에 적합합니다.
- Forbid: 이전 작업이 끝날 때까지 새 작업을 시작하지 않습니다. 데이터베이스 백업처럼 중복 실행이 문제가 되는 경우에 사용합니다.
- Replace: 이전 작업을 취소하고 새 작업을 시작합니다. 주식 가격 조회처럼 최신 데이터만 의미있는 경우에 사용합니다.
.spec.suspend
이 필드를 true로 설정하면 모든 예약된 실행을 일시 중지합니다. 유지보수나 디버깅 중에 유용합니다. 중요한 점은 이미 실행 중인 Job은 영향받지 않는다는 것입니다.
.spec.successfulJobsHistoryLimit / .spec.failedJobsHistoryLimit
이 필드들은 "완료된 Job을 몇 개나 보관할지"를 결정합니다. 성공한 Job은 보통 1-3개, 실패한 Job은 디버깅을 위해 3-5개 정도 보관하는 것이 일반적입니다.
CronJob 언급할 항목
- 자동 배치: 적절한 리소스가 있는 노드에 자동으로 스케줄링됩니다.
- 자동 복구: Pod가 실패하면 자동으로 재시작됩니다.
- 리소스 격리: 각 Job은 독립된 컨테이너에서 실행되어 서로 영향을 주지 않습니다.
- 중복 실행: 네트워크 지연 등으로 같은 작업이 두 번 실행될 수 있습니다.
- 실행 누락: 클러스터 문제로 예약된 실행을 놓칠 수 있습니다.
- 병렬 실행: 이전 작업이 끝나기 전에 다음 작업이 시작될 수 있습니다.
CronJob 코드 분석 및 사용법
1. Schedule
CronJob의 핵심은 스케줄 표현식입니다. 많은 개발자들이 이 부분에서 실수를 하는데, 각 위치가 무엇을 의미하는지 정확히 이해하면 쉽게 작성할 수 있습니다.
spec:
schedule: "30 2 * * 1-5" # 평일 새벽 2시 30분
Cron 표현식의 구조를 분석하면:
┌───────────── 분 (0 - 59)
│ ┌───────────── 시 (0 - 23)
│ │ ┌───────────── 일 (1 - 31)
│ │ │ ┌───────────── 월 (1 - 12)
│ │ │ │ ┌───────────── 요일 (0 - 7, 0과 7은 일요일)
│ │ │ │ │
│ │ │ │ │
* * * * *
편리한 단축 매크로
매번 복잡한 표현식을 작성하는 대신, Kubernetes는 자주 사용하는 패턴에 대한 단축어를 제공합니다:
schedule: "@hourly" # "0 * * * *"와 동일 (매시 정각)
schedule: "@daily" # "0 0 * * *"와 동일 (매일 자정)
schedule: "@weekly" # "0 0 * * 0"와 동일 (매주 일요일 자정)
schedule: "@monthly" # "0 0 1 * *"와 동일 (매월 1일 자정)
schedule: "@yearly" # "0 0 1 1 *"와 동일 (매년 1월 1일 자정)
2. ConcurrencyPolicy
ConcurrencyPolicy는 "이전 작업이 아직 실행 중일 때 새 작업을 어떻게 처리할지"를 결정하는 중요한 설정입니다. 잘못 설정하면 데이터 손실이나 중복 처리 같은 심각한 문제가 발생할 수 있습니다.
1. Allow (기본값) - 병렬 실행 허용
apiVersion: batch/v1
kind: CronJob
metadata:
name: parallel-processor
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: Allow # 기본값
jobTemplate:
spec:
template:
spec:
containers:
- name: processor
image: processor:v1
command: ["process-data.sh"]
이 설정에서 일어나는 일:
- 00:00 - 첫 번째 Job 시작
- 00:05 - 두 번째 Job 시작 (첫 번째 Job은 아직 실행 중)
- 00:10 - 세 번째 Job 시작 (첫 번째, 두 번째 Job 모두 실행 중)
- 00:10 - 첫 번째 Job 완료
- 00:15 - 네 번째 Job 시작 (두 번째, 세 번째 Job 실행 중)
사용법:
- 각 실행이 완전히 독립적일 때
- 예: 서로 다른 사용자 그룹에게 이메일 발송
2. Forbid - 중복 실행 방지
apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: Forbid # 중복 방지
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:14
command: ["pg_dump", "-h", "db-host", "mydb"]
이 설정에서 일어나는 일:
- 00:00 - 첫 번째 Job 시작
- 00:05 - 두 번째 Job 스킵! (첫 번째 Job이 아직 실행 중이므로)
- 00:10 - 첫 번째 Job 완료
- 00:10 - 세 번째 Job 시작 (이제 실행 가능)
사용법:
- 데이터베이스 백업처럼 중복 실행이 문제가 되는 경우
- 파일 처리처럼 순서가 중요한 경우
- 리소스를 많이 사용하는 작업
데이터베이스 백업을 예로 들면, 백업이 동시에 두 개 실행되면 데이터베이스에 부하가 걸릴 뿐 아니라 백업 파일이 손상될 수도 있습니다.
3. Replace - 기존 작업 교체
apiVersion: batch/v1
kind: CronJob
metadata:
name: stock-price-fetcher
spec:
schedule: "*/1 * * * *" # 매분마다
concurrencyPolicy: Replace # 이전 작업 취소
jobTemplate:
spec:
template:
spec:
containers:
- name: fetcher
image: stock-fetcher:v1
command: ["fetch-latest-prices.sh"]
이 설정에서 일어나는 일:
- 00:00 - 첫 번째 Job 시작
- 00:01 - 첫 번째 Job 취소, 두 번째 Job 시작
- 00:02 - 두 번째 Job 취소, 세 번째 Job 시작
사용법:
- 최신 데이터만 의미있는 경우
- 예: 실시간 주식 가격, 현재 날씨 정보
주식 가격을 가져오는 작업이 지연되고 있다면, 1분 전 가격보다는 현재 가격을 가져오는 것이 더 중요합니다.
3. StartingDeadlineSeconds
StartingDeadlineSeconds는 "작업이 예정된 시간보다 늦게 시작되어도 괜찮은 최대 지연 시간"을 의미합니다. 이 설정은 특히 시간에 민감한 작업에서 중요합니다.
apiVersion: batch/v1
kind: CronJob
metadata:
name: time-sensitive-report
spec:
schedule: "0 9 * * *" # 매일 오전 9시
startingDeadlineSeconds: 600 # 10분까지는 지연 허용
jobTemplate:
spec:
template:
spec:
containers:
- name: reporter
image: reporter:v1
실제 시나리오로 이해하기:
오전 9시에 일일 리포트를 생성해야 한다고 가정합시다. 그런데 클러스터에 일시적인 문제가 있어서 9시에 Job이 시작되지 못했습니다.
- startingDeadlineSeconds: 600 설정이 있는 경우:
- 9:00 - 예정된 실행 시간
- 9:05 - 아직 리소스 부족, 대기 중
- 9:08 - 리소스 확보, Job 실행 시작
- 리포트가 8분 늦었지만 10분 이내이므로 실행됩니다.
- 만약 9:11에 리소스가 확보되었다면:
- Job은 실행되지 않고 스킵됩니다
- 다음 날 9시까지 기다립니다.
왜 이 설정이 필요한가?
실시간 데이터를 처리하는 경우를 생각해보세요. 1시간 전 데이터를 처리하는 것은 의미가 없을 수 있습니다. 예를 들어:
- 주식 시장 데이터: 장 마감 후의 데이터는 의미 없음
- 실시간 트래픽 분석: 너무 오래된 데이터는 이미 쓸모없음
4. Job History 관리 전략
Kubernetes는 완료된 Job들을 자동으로 삭제하지 않습니다. 이는 디버깅과 감사를 위해서인데, 너무 많이 쌓이면 리소스를 낭비하게 됩니다.
apiVersion: batch/v1
kind: CronJob
metadata:
name: optimized-cronjob
spec:
schedule: "0 * * * *"
successfulJobsHistoryLimit: 3 # 성공한 Job 3개만 유지
failedJobsHistoryLimit: 5 # 실패한 Job 5개 유지
jobTemplate:
spec:
ttlSecondsAfterFinished: 86400 # 완료 후 24시간 뒤 자동 삭제
각 설정의 의미:
successfulJobsHistoryLimit: 3 성공한 Job을 3개만 보관합니다. 매시간 실행되는 Job이라면 최근 3시간의 성공 기록만 유지됩니다. 성공한 Job은 보통 문제가 없으므로 많이 보관할 필요가 없습니다.
failedJobsHistoryLimit: 5 실패한 Job은 5개까지 보관합니다. 실패한 Job은 디버깅을 위해 더 많이 보관하는 것이 좋습니다. 로그를 확인하고 왜 실패했는지 분석해야 하기 때문입니다.
ttlSecondsAfterFinished: 86400 Job이 완료(성공 또는 실패)된 후 24시간(86400초) 뒤에 자동으로 삭제됩니다. 이는 History Limit과는 별개로 작동합니다. 예를 들어, 한 달에 한 번만 실행되는 Job이라면 History Limit보다 TTL이 먼저 적용될 수 있습니다.
환경별 권장 설정:
개발 환경에서는 디버깅을 위해 많은 히스토리를 보관합니다:
successfulJobsHistoryLimit: 10
failedJobsHistoryLimit: 10
ttlSecondsAfterFinished: 604800 # 7일
프로덕션 환경에서는 리소스 절약을 위해 최소한만 보관합니다:
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
ttlSecondsAfterFinished: 86400 # 1일
5. TimeZone 설정과 글로벌 서비스
Kubernetes 1.27부터 CronJob에 타임존을 명시적으로 설정할 수 있게 되었습니다. 이전에는 모든 스케줄이 UTC 기준이었는데, 이제는 각 지역의 시간대로 설정할 수 있습니다.
apiVersion: batch/v1
kind: CronJob
metadata:
name: korea-business-report
spec:
schedule: "0 9 * * 1-5" # 평일 오전 9시
timeZone: "Asia/Seoul" # 한국 시간 기준
jobTemplate:
spec:
template:
spec:
containers:
- name: reporter
image: reporter:v1
env:
- name: TZ
value: "Asia/Seoul" # 컨테이너 내부도 같은 시간대로
왜 TimeZone 설정이 중요한가요?
글로벌 서비스를 운영한다고 생각해보세요. 한국 사용자를 위한 리포트는 한국 시간 오전 9시에, 미국 사용자를 위한 리포트는 미국 동부 시간 오전 9시에 생성해야 합니다.
TimeZone 설정이 없다면:
- 한국 오전 9시 = UTC 00:00 (한국은 UTC+9)
- 미국 동부 오전 9시 = UTC 14:00 (동부는 UTC-5, 서머타임 시 UTC-4)
매번 이런 계산을 하는 것은 실수하기 쉽고, 특히 서머타임이 적용되는 지역은 더 복잡합니다.
다중 지역 운영 예제:
# 아시아 지역 작업
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: asia-daily-process
spec:
schedule: "0 2 * * *" # 새벽 2시 (트래픽이 적은 시간)
timeZone: "Asia/Tokyo"
jobTemplate:
spec:
template:
spec:
nodeSelector:
region: asia-northeast-1 # 도쿄 리전에서 실행
containers:
- name: processor
env:
- name: TARGET_REGION
value: "ASIA"
- name: DATABASE_ENDPOINT
value: "asia-db.example.com" # 아시아 DB 사용
---
# 유럽 지역 작업
apiVersion: batch/v1
kind: CronJob
metadata:
name: europe-daily-process
spec:
schedule: "0 2 * * *" # 똑같이 새벽 2시지만
timeZone: "Europe/London" # 런던 시간 기준
jobTemplate:
spec:
template:
spec:
nodeSelector:
region: eu-west-1 # 런던 리전에서 실행
containers:
- name: processor
env:
- name: TARGET_REGION
value: "EUROPE"
- name: DATABASE_ENDPOINT
value: "eu-db.example.com" # 유럽 DB 사용
각 지역의 새벽 2시에 실행되므로, 해당 지역의 트래픽이 가장 적을 때 무거운 작업을 수행할 수 있습니다.
Kubernetes 1.29: 개선된 가시성
Kubernetes 1.29에서는 CronJob 관리가 훨씬 편리해졌습니다. 이제 kubectl get cronjob 명령을 실행하면 TIMEZONE 컬럼이 표시됩니다.
$ kubectl get cronjob
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE
backup-job 0 3 * * * Asia/Seoul False 0 8h
report-generator 0 9 * * 1-5 UTC False 1 2m
이전에는 각 CronJob을 일일이 describe해서 타임존을 확인해야 했는데, 이제 한눈에 볼 수 있습니다. 특히 여러 타임존의 CronJob을 관리할 때 매우 유용합니다.
또한 Job의 Ready Pod 추적 기능이 안정화되었습니다. 이게 무슨 의미인지 예를 들어 설명하면:
apiVersion: batch/v1
kind: Job
metadata:
name: parallel-job
spec:
completions: 5 # 총 5개 완료 필요
parallelism: 2 # 동시에 2개씩 실행
이런 Job을 실행하면:
$ kubectl get job parallel-job
NAME COMPLETIONS READY AGE
parallel-job 2/5 2 3m
READY 컬럼이 현재 정상적으로 실행 중인 Pod 수를 보여줍니다. 이를 통해 Job이 제대로 진행되고 있는지 쉽게 확인할 수 있습니다.
Kubernetes 1.30: 외부 컨트롤러 통합
Kubernetes 1.30의 가장 큰 변화는 managedBy 필드의 추가입니다. 이 필드를 통해 Kubernetes Job을 외부 시스템이 관리할 수 있게 되었습니다.
apiVersion: batch/v1
kind: Job
metadata:
name: workflow-job
spec:
managedBy: "argo-workflows.argoproj.io" # 외부 시스템이 관리
completions: 1
template:
spec:
containers:
- name: task
image: task:v1
이게 왜 중요한가요? 예를 들어, Argo Workflows나 Tekton 같은 워크플로우 엔진이 Job을 생성하고 관리할 때, Kubernetes는 이 Job을 직접 관리하지 않고 해당 워크플로우 엔진에게 맡깁니다. 이렇게 하면 두 시스템이 충돌하지 않고 협력할 수 있습니다.
또한 kubectl get job 명령의 출력이 개선되어 Job 상태를 바로 확인할 수 있습니다:
$ kubectl get job
NAME STATUS COMPLETIONS DURATION AGE
backup-20240113 Complete 1/1 2m15s 10m
cleanup-20240113 Failed 0/1 30s 5m
process-20240113 Running 3/5 5m 5m
STATUS 컬럼이 추가되어 Complete, Failed, Running 등의 상태를 한눈에 볼 수 있습니다.
Kubernetes 1.31: 안정적인 종료 처리
Kubernetes 1.31에서는 Job의 종료 처리가 크게 개선되었습니다. 이전 버전에서는 Job이 "완료"되었다고 표시되어도 실제로는 일부 Pod가 아직 종료 중인 경우가 있었습니다. 이로 인해 혼란이 발생하곤 했습니다.
이제는 모든 Pod가 완전히 종료된 후에만 Job이 Complete 또는 Failed 상태가 됩니다:
apiVersion: batch/v1
kind: Job
metadata:
name: reliable-job
spec:
completions: 3
backoffLimit: 2
podReplacementPolicy: Failed # 실패한 Pod만 교체
template:
spec:
containers:
- name: worker
image: worker:v1
새로운 상태 조건들을 확인해보면:
$ kubectl get job reliable-job -o json | jq '.status.conditions'
[
{
"type": "SuccessCriteriaMet",
"status": "True",
"reason": "CompletionsReached",
"message": "Job has successfully completed 3 pods"
},
{
"type": "Complete",
"status": "True",
"reason": "AllPodsTerminated",
"message": "All pods have terminated and job is complete"
}
]
SuccessCriteriaMet는 "필요한 수만큼 성공했다"를 의미하고, Complete는 "모든 Pod가 종료되고 Job이 완료되었다"를 의미합니다. 이 두 조건을 구분함으로써 Job의 상태를 더 정확하게 파악할 수 있습니다.
'k8s > kubernetes-pattern' 카테고리의 다른 글
| Kubernetes Pattern: Singleton Service (2) | 2025.09.27 |
|---|---|
| Kubernetest Pattern: DaemonService (4) | 2025.09.20 |
| Kubernetes Pattern: Batch Job (3) | 2025.09.06 |
| Kubernetes Pattern: Automated Placement (1) | 2025.08.30 |
| Kubernetes Pattern: Managed Lifecycle (2) | 2025.08.23 |